Skip to content
Finowl

Django · Gender prediction

Batch gender enrichment with a Django command

The enrich_gender management command loads customers whose gender_status is still empty, groups them into chunks of 50 and sends each chunk to /v1/gender/batch. Item ids are the customers' primary keys as strings, so each result maps straight back to a model instance. When a customer has a country code, it is added to the item as a hint, which helps with names that lean differently by country.

Every identified or unknown prediction is saved with update_fields, and unknown results keep an empty predicted_gender so they are never looked up again. Item-level errors are skipped and picked up by the next run. The send method retries only a 429 with code rate_limited, sleeping for Retry-After, and raises CommandError for anything else so a failed batch is never resent blindly.

Steps

  1. 1

    Add gender_status, predicted_gender and gender_confidence fields to your Customer model and run migrate.

  2. 2

    Save the command as customers/management/commands/enrich_gender.py.

  3. 3

    Run python manage.py enrich_gender --limit 200 to try a small batch first.

  4. 4

    Schedule the command with cron, Celery beat or your platform's scheduler to cover new customers.

  5. 5

    Report results in aggregate and let customers set their own gender in their profile.

Install
pip install django requests
Batch gender enrichment with a Django command
# customers/management/commands/enrich_gender.py
import time

from django.core.management.base import BaseCommand, CommandError

from accounts.finowl import FinowlError, finowl_post
from customers.models import Customer

BATCH_SIZE = 50  # the batch endpoint accepts 1 to 50 items


class Command(BaseCommand):
    help = "Estimate gender from first names for aggregate analytics."

    def add_arguments(self, parser):
        parser.add_argument("--limit", type=int, default=1000)

    def handle(self, *args, **options):
        queryset = Customer.objects.filter(gender_status="").exclude(first_name="")
        customers = list(queryset.order_by("pk")[: options["limit"]])

        for start in range(0, len(customers), BATCH_SIZE):
            chunk = {str(c.pk): c for c in customers[start : start + BATCH_SIZE]}
            items = [
                {"id": pk, "type": "name", "value": c.first_name, **({"country": c.country} if c.country else {})}
                for pk, c in chunk.items()
            ]
            for row in self.send(items):
                customer = chunk[row["id"]]
                if "error" in row:
                    self.stderr.write(f"{customer.pk}: {row['error']['code']}, left for a later run")
                    continue
                prediction = row["data"]
                customer.gender_status = prediction["result_status"]  # identified or unknown
                customer.predicted_gender = prediction["gender"] or ""  # None when unknown
                customer.gender_confidence = prediction["confidence"]
                customer.save(update_fields=["gender_status", "predicted_gender", "gender_confidence"])
            self.stdout.write(f"Processed {start + len(chunk)} of {len(customers)}")

    def send(self, items):
        while True:
            try:
                return finowl_post("/v1/gender/batch", {"items": items}, timeout=30)["data"]
            except FinowlError as exc:
                if exc.status == 429 and exc.code == "rate_limited":
                    time.sleep(exc.retry_after or 5)  # the batch never ran, so resending is safe
                    continue
                if exc.status == 400:
                    raise CommandError("Batch failed validation; no item was processed") from exc
                if exc.status == 401:
                    raise CommandError("Check FINOWL_API_KEY") from exc
                raise CommandError(f"Finowl {exc.status} {exc.code}; not resending this batch") from exc

IN PRODUCTION

Production tips

  • Store gender_confidence as a nullable FloatField. Unknown results may not include a confidence, and null keeps averages and filters honest in your analytics queries.

  • Batch items run with ai_mode off by default, so results come only from the dataset at 1 credit each. Expect more unknown results for handles and unusual names than single lookups give.

  • Mention gender estimation in your privacy notice, because names are personal data. Keep the predicted field separate from any gender a customer provides, and let the provided value take precedence.

Each lookup costs 1 credit (3 when it requests AI), with 100 free credits every month. See pricing and error codes.

A FEW GOOD QUESTIONS

Common questions

Batch gender enrichment with a Django command

Grab an API key, paste the example and run it. 100 free credits every month.