demografix (Ruby)

Predict gender, age, and nationality from first names. One Ruby client covers all three Demografix APIs — genderize.io (gender), agify.io (age), and nationalize.io (nationality) — with single-name lookups and batches of up to 100 names per request.

Gem Version CI License: MIT

Install

Add the gem to your Gemfile:

gem "demografix"

Then run bundle install. To install directly:

gem install demografix

The client uses the Ruby standard library (net/http and json) and has no runtime dependencies. It requires Ruby 3.2 or later.

Quickstart

Construct a client, run a batch over a list of names, read the predictions, and read the remaining quota.

require "demografix"

client = Demografix::Client.new(api_key: ENV.fetch("DEMOGRAFIX_API_KEY"))

names = %w[michael matthew jane emily peter lois]

ages = client.agify_batch(names)

# Aggregate the predictions into an age distribution for the list.
known = ages.results.map(&:age).compact
average_age = known.sum.to_f / known.length

ages.quota.remaining # => 24987

Each call returns prediction fields plus a quota. Batch calls return results (one prediction per input name, in input order) plus one quota for the response. Aggregate the results into a distribution.

genderize

Predict gender from a name.

result = client.genderize("peter")
result.gender      # => "male"
result.probability # => 1.0
result.count       # => 1352696

Batch a list and reduce it to a gender split:

batch = client.genderize_batch(%w[peter lois meg chris])
split = batch.results.each_with_object(Hash.new(0)) do |pred, counts|
  counts[pred.gender || "unknown"] += 1
end
# => { "male" => 2, "female" => 2 }

gender is "male", "female", or nil. A name with no match returns nil gender, 0.0 probability, and 0 count. That is a successful response, not an error.

agify

Predict age from a name.

result = client.agify("michael")
result.age   # => 57
result.count # => 311558

Batch a list and reduce it to an age distribution:

batch = client.agify_batch(%w[michael matthew jane])
ages = batch.results.map(&:age).compact
buckets = ages.group_by { |age| (age / 10) * 10 }
# => { 50 => [57], 40 => [48], ... }

age is an integer or nil. A name with no match returns nil age and 0 count.

nationalize

Predict nationality from a name.

result = client.nationalize("nguyen")
result.country.first.country_id   # => "VN"
result.country.first.probability  # => 0.891132

Batch a list and reduce it to a nationality mix:

batch = client.nationalize_batch(%w[nguyen schmidt rossi])
mix = batch.results.each_with_object(Hash.new(0)) do |pred, counts|
  top = pred.country.first
  counts[top ? top.country_id : "unknown"] += 1
end
# => { "VN" => 1, "DE" => 1, "IT" => 1 }

country holds up to five candidates in descending probability order. A name with no match returns an empty country array.

Batch limit

Each batch accepts at most 100 names. A batch of more than 100 raises ValidationError before any HTTP call is made. Chunk a longer list and aggregate across the chunks.

split = roster.each_slice(100).each_with_object(Hash.new(0)) do |chunk, counts|
  client.genderize_batch(chunk).results.each do |pred|
    counts[pred.gender || "unknown"] += 1
  end
end

country_id

genderize and agify accept an optional country_id (ISO 3166-1 alpha-2) to scope the prediction to a country. nationalize does not accept it. The value is echoed back uppercase in country_id on every prediction.

result = client.genderize("kim", country_id: "US")
result.country_id # => "US"
result.gender     # => "female"

client.agify_batch(%w[andrea giulia], country_id: "IT")

Scoping changes the prediction: andrea reads female with probability 0.99 in the United States and male with probability 0.79 in Italy.

client.genderize("andrea", country_id: "US").gender # => "female"
client.genderize("andrea", country_id: "IT").gender # => "male"

When the request sends no country_id, the field is nil.

Quota

Every result and every raised error carries a quota read from the response rate-limit headers:

Field Meaning
limit names allowed in the current window
remaining names left in the current window
reset seconds until the window resets
result = client.genderize("peter")
result.quota.limit     # => 25000
result.quota.remaining # => 24987
result.quota.reset     # => 1314000

Read quota off the returned value or a raised error. The client does not cache it.

Errors

Every error subclasses Demografix::Error and carries status, message, and quota (when the response included rate-limit headers).

Error Raised on
Demografix::AuthError 401, invalid or missing API key
Demografix::SubscriptionError 402, subscription not active
Demografix::ValidationError 422, invalid parameters; also client-side for a batch over 100 names
Demografix::RateLimitError 429, request limit reached (quota always populated)
Demografix::TransportError network failure, timeout, or non-JSON body
Demografix::Error any other non-2xx status

On a RateLimitError, read quota.reset for the seconds to wait before retrying:

begin
  client.genderize_batch(names)
rescue Demografix::RateLimitError => e
  sleep(e.quota.reset)
  retry
end

Methods

Method Returns country_id
genderize(name, country_id:) GenderizeResult yes
genderize_batch(names, country_id:) Batch of GenderizePrediction yes
agify(name, country_id:) AgifyResult yes
agify_batch(names, country_id:) Batch of AgifyPrediction yes
nationalize(name) NationalizeResult no
nationalize_batch(names) Batch of NationalizePrediction no

Demografix::Client.new requires api_key: and accepts timeout: (optional, default 10 seconds). The host URLs and the User-Agent are fixed constants, not options.

API keys

An API key is required. Creating one is free and includes 2,500 names per month.

Quota counts names, not requests. A single-name call costs 1. A batch of 100 names costs 100. The free tier therefore covers 2,500 names in a month however they are split across calls.

Generate a key in your dashboard at genderize.io, agify.io, or nationalize.io. One key works across all three services. Full reference: genderize.io/documentation/api.

License

MIT. See LICENSE.