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.
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.