Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 0 additions & 29 deletions .github/workflows/adjacent_repo_recommender.yaml

This file was deleted.

6 changes: 0 additions & 6 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,4 @@ jobs:
pages: write
id-token: write
with:
# naampy predates the docs/ flat-layout convention: real Sphinx source
# (myst-nb notebook rendering, custom furo sidebars) lives in docs/source.
docs-dir: docs/source
# Docstring "Example:" blocks use >>> prompts for illustration (some
# require live network/data access) and aren't meant to run as doctests.
run-doctests: false
deploy: ${{ github.event_name == 'push' }}
35 changes: 34 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,46 @@ name: Release

on:
push:
tags: ["v*"]
tags: ["v*.*.*"]
workflow_dispatch:
inputs:
publish:
description: >-
Upload to PyPI. Only takes effect when the run is started from a tag;
from a branch this is a build-and-check rehearsal.
type: boolean
default: false

permissions:
contents: read

# A manual run and a tag push can otherwise race for the same ref: two builds,
# and two `dist` artifacts uploaded under one name.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
uses: gojiplus/py-canon/.github/workflows/reusable-release.yml@v1
permissions:
contents: write
id-token: write
with:
# `gh release create` fails when the release already exists, so create it
# only on the original tag push — not on a manual re-run of a released tag.
github-release: ${{ github.event_name == 'push' }}

# PyPI trusted publishing rejects reusable workflows (the OIDC claims
# must reference this repo's workflow file), so the publish step lives
# here rather than in py-canon's reusable-release.
publish:
needs: release
# A PyPI upload cannot be withdrawn, so publish only from a tag. The reusable
# build rejects the tag unless it matches the version in pyproject.toml.
if: >-
startsWith(github.ref, 'refs/tags/') &&
(github.event_name == 'push' || inputs.publish)
runs-on: ubuntu-latest
timeout-minutes: 15
environment: pypi
Expand All @@ -33,3 +56,13 @@ jobs:
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
attestations: true
# The `if:` above decides *which builds may publish*. This decides what
# happens when one version is published twice, which that guard cannot
# see. The concurrency group serialises a manual dispatch and a tag
# push aimed at the same version, it does not deduplicate them, and
# cancel-in-progress is false -- so the queued run starts once the
# first finishes and uploads identical files. A completed run is not
# covered by concurrency at all, so a plain rerun does the same. PyPI
# answers with a 400 and the release goes red having already
# succeeded, which is the worst of both readings.
skip-existing: true
8 changes: 5 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@
*.log
./data
build/
naampy.egg-info/
*.egg-info/
lit/
dist/
venv/
docs/_build/
in_rolls_*.csv.gz

# Build intermediates (regenerable / Dataverse-hosted): downloaded rolls + the rebuilt
# first-name->gender table. Notebooks in model_training/data/ stay tracked; data files don't.
# Lifecycle: code committed; naampy/model/gender_lstm.pt bundled (committed); naampy_v3.csv.gz
# uploaded to Dataverse + downloaded at runtime (URL in in_rolls_fn.IN_ROLLS_DATA).
# Lifecycle: code committed; gender_lstm.pt is pinned on Hugging Face; naampy_v3.csv.gz
# is uploaded to Dataverse and downloaded at runtime (URL in in_rolls_fn.IN_ROLLS_DATA).
model_training/data/*.csv.gz
model_training/data/*.csv
37 changes: 35 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.10.0] - 2026-08-17

### Fixed

- Preserve meaningful input indices, including duplicate index values, while
appending electoral-roll results.
- Reject unknown datasets, unavailable states, and unavailable state/year
combinations instead of silently returning an inconsistent schema or
treating every name as an electoral miss and returning neural predictions.
- Return the documented empty output schema without downloading a dataset when
the input DataFrame has no rows.
- Map the Streamlit app's “all states” and Maharashtra selections to the
national lookup and the source table's legacy state key, respectively.
- Preserve caller-owned `pred_gender` and `pred_prob` columns when using the
native dataset, which does not produce those fields.
- Keep the model-training entry point working after the package moved under
`src/`.

### Changed

- Store the runtime electoral-roll cache as Zstandard-compressed Parquet with an
explicit Arrow schema. Dataverse CSV.gz files are now temporary transport
inputs that are validated, converted atomically, and removed.
- Move the gender checkpoint out of the wheel and into
`gojiberries/naampy` on Hugging Face at an immutable revision. Controlled
deployments can continue to use `NAAMPY_MODEL_DIR`.
- Adopt the current py-canon structure: `src/` package layout, `uv_build`,
static package version, reusable workflows, and flat Sphinx documentation.
- Replace the executed notebook documentation stack with ordinary Markdown and
remove unsupported hand-written prediction and model-performance numbers.

## [0.9.0] - 2026-08-02

### Fixed
Expand All @@ -19,7 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
the model, which scored the literal string `"nan"` and returned a
confident-looking gender for a row that had no name at all.
- Names with no `a-z` characters (Devanagari, Gujarati, Kannada, …) now return
`None`/`NaN` instead of being labelled `"male"` at 0.5 confidence, which is
`None`/`NaN` instead of being labeled `"male"` at 0.5 confidence, which is
what the previous neutral-probability default produced.
- Re-running `in_rolls_fn_gender` on its own output no longer raises. Existing
naampy columns are dropped before the merge (previously they became
Expand Down Expand Up @@ -95,6 +126,8 @@ Versions prior to 0.5.0 predate this changelog; see the
[commit history](https://github.com/appeler/naampy/commits/master) for
details.

[Unreleased]: https://github.com/appeler/naampy/compare/v0.6.0...master
[Unreleased]: https://github.com/appeler/naampy/commits/master
[0.10.0]: https://github.com/appeler/naampy/compare/v0.9.0...master
[0.9.0]: https://github.com/appeler/naampy/compare/v0.6.0...v0.9.0
[0.6.0]: https://github.com/appeler/naampy/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/appeler/naampy/compare/v0.3.0...v0.5.0
44 changes: 44 additions & 0 deletions MODEL_CARD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
tags:
- names
- india
- pytorch
---

# naampy gender checkpoint

`gender_lstm.pt` powers `naampy.predict_fn_gender`. The package downloads this
repository at an immutable commit, then caches the file locally.

The artifact is a PyTorch state dictionary for the character-level
bidirectional LSTM defined in `naampy.nnets`. It was trained from first-name and
gender counts derived from Indian electoral rolls. The training program and
architecture constants are maintained in the
[`naampy` repository](https://github.com/appeler/naampy) under
`model_training/` and `naampy/nnets.py`.

## Usage

```python
from naampy import predict_fn_gender

predictions = predict_fn_gender(["Priya", "Rahul"])
```

Set `NAAMPY_MODEL_DIR` to a directory containing `gender_lstm.pt` to bypass the
Hub download in controlled or offline deployments.

## Limitations

The output is a statistical estimate from historical administrative records,
not a statement about a person's gender identity. Binary labels reflect the
available training target and do not represent the full range of identities.
Romanization, spelling, regional coverage, shared names, and changes over time
can produce systematic errors. Names without Latin `a` to `z` characters are
left unscored. Do not use these predictions to make decisions about a person or
their access to services.

## Licensing

The `naampy` source code is MIT licensed. Consult the source dataset terms and
your intended use before redistributing or deploying the learned weights.
18 changes: 8 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ help:
@echo " dev Install package in development mode with all dependencies"
@echo " test Run tests without coverage"
@echo " test-cov Run tests with coverage"
@echo " lint Run linting checks (ruff, mypy, pydoclint)"
@echo " lint Run linting checks (ruff, pyright, pydoclint)"
@echo " format Format code with ruff"
@echo " clean Remove build artifacts and cache files"
@echo " docs Build documentation"
Expand All @@ -16,20 +16,19 @@ install:
uv pip install .

dev:
uv sync --group dev --group test
uv pip install -e ".[docs]"
uv sync --all-groups
uv run pre-commit install

test:
uv run --group test pytest tests/ -v
uv run pytest

test-cov:
uv run --group test pytest --cov=naampy --cov-report=term-missing --cov-report=html -v
uv run pytest --cov=naampy --cov-report=term-missing --cov-report=html

lint:
uv run ruff check .
uv run mypy naampy --ignore-missing-imports
uv run pydoclint naampy/
uv run pyright
uv run pydoclint src/

format:
uv run ruff format .
Expand All @@ -50,8 +49,7 @@ clean:
find . -type f -name "*~" -delete

docs:
cd docs && make clean && make html
@echo "Documentation built at docs/_build/html/index.html"
uv run sphinx-build -W --keep-going -b html docs docs/_build/html

build: clean
build:
uv build
27 changes: 2 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

[![CI](https://github.com/appeler/naampy/actions/workflows/ci.yml/badge.svg)](https://github.com/appeler/naampy/actions/workflows/ci.yml)
[![Documentation](https://github.com/appeler/naampy/actions/workflows/docs.yml/badge.svg)](https://github.com/appeler/naampy/actions/workflows/docs.yml)
[![Model](https://img.shields.io/badge/%F0%9F%A4%97-model-yellow)](https://huggingface.co/gojiberries/naampy)
[![image](https://img.shields.io/pypi/v/naampy.svg)](https://pypi.python.org/pypi/naampy)
[![image](https://static.pepy.tech/badge/naampy)](https://pepy.tech/project/naampy)

Expand Down Expand Up @@ -127,14 +128,6 @@ result = in_rolls_fn_gender(df, "name")
print(result[["name", "n_male", "n_female", "prop_female", "prop_male"]])
```

**Output:**
```
name n_male n_female prop_female prop_male
0 gaurav 25625.0 47.0 0.001831 0.998169
1 yasmin 58.0 6079.0 0.990549 0.009451
2 deepti 35.0 5784.0 0.993985 0.006015
```

### Machine Learning Predictions

```python
Expand All @@ -146,20 +139,12 @@ predictions = predict_fn_gender(names)
print(predictions)
```

**Output:**
```
name pred_gender pred_prob
0 nabha female 0.755028
1 hrithik male 0.922181
2 kiara female 0.614125
3 reyansh male 0.891234
```
<!-- END:detailed_usage -->

<!-- START:functionality -->
## How it Works

When you first run `in_rolls_fn_gender`, it downloads data from [Harvard Dataverse](https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/WZGJBM) to a local cache folder. Subsequent runs use the cached data for faster performance.
When you first run `in_rolls_fn_gender`, it downloads the selected raw table from [Harvard Dataverse](https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/WZGJBM), validates its schema, and stores a typed Parquet cache under `~/.naampy`. The downloaded CSV.gz is only a temporary transport file and is removed after conversion. Subsequent runs use the Parquet cache.

The package provides two complementary approaches:

Expand All @@ -185,11 +170,3 @@ Suriyan Laohaprapanon, Gaurav Sood, and Rajashekar Chintalapati
- [appeler/outkast](https://github.com/appeler/outkast) — Map last names to caste categories
- [appeler/parsernaam](https://github.com/appeler/parsernaam) — AI-powered name parsing
<!-- END:links -->

## 🔗 Adjacent Repositories

- [appeler/pranaam](https://github.com/appeler/pranaam) — pranaam: predict religion based on name
- [appeler/outkast](https://github.com/appeler/outkast) — Using data from over 140M+ Indians from the SECC 2011, we map last names to caste (SC, ST, Other)
- [appeler/parsernaam](https://github.com/appeler/parsernaam) — AI name parsing. Predict first or last name using a DL model.
- [appeler/namesexdata](https://github.com/appeler/namesexdata) — Data on international first names and sex of people with that name
- [appeler/graphic_names](https://github.com/appeler/graphic_names) — Infer the gender of person with a particular first name using Google image search and Clarifai
20 changes: 0 additions & 20 deletions docs/Makefile

This file was deleted.

14 changes: 1 addition & 13 deletions docs/source/about.md → docs/about.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# About naampy

```{include} ../../README.md
```{include} ../README.md
:start-after: <!-- START:description -->
:end-before: <!-- END:description -->
```
Expand Down Expand Up @@ -44,22 +44,10 @@ When a name doesn't exist in the electoral roll database, naampy uses a machine
- **Training Data**: Indian electoral roll names
- **Classification**: Names with predicted proportion < 0.5 are classified as male, otherwise female

### Model Performance

On test data:
- **MSE (Mean Squared Error)**: 0.05
- **RMSE (Root Mean Squared Error)**: 0.22

The model handles the fact that some names are shared between men and women, as shown in the distribution of female proportions:

![Female Proportion Distribution](images/female_prop.png)

### Inference Results

The model shows strong performance across different name types:

![Out-of-Sample Inference Results](images/infer_oos.png)

## Important Considerations

### Data Limitations
Expand Down
File renamed without changes.
5 changes: 5 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Sphinx configuration — fleet standard via py-canon."""

from py_canon.sphinx import configure

configure(globals())
Loading