whitebox-dedup
Interpretable entity deduplication using auditable MARS equations and Inductive Conformal Prediction, with a flexible feature registry that adapts to whatever columns the data has.
Quick Install
pip install whitebox-dedup
First result in three lines:
from whitebox_dedup import DeduplicationPipeline
pipeline = DeduplicationPipeline.from_csv("farmers.csv")
print(pipeline.run())
CLI equivalent:
wdedup deduplicate --input farmers.csv --output results.json
wdedup show-equation --model text_only_v1
Why It Exists
Deduplication in African programme registries (farmer benefit lists, NIN databases, voter rolls) has historically been delegated to black-box ML models that produce a score nobody can audit. When a beneficiary challenges a duplicate flag, the programme officer has no arithmetic to show them. Courts and auditors cannot verify what the system did.
FutuX Agri-consult Limited built this library after observing the same pattern across 12 agricultural programmes in Nigeria between 2021 and 2026: opaque deduplication created downstream fraud disputes that consumed more resources than the duplicates themselves. The solution is not a more powerful model. The solution is a model whose decision can be written on one page and checked by a field officer with a calculator.
The MARS equation is that page. Every hinge term, its knot, and its coefficient are stored in a plain JSON file that any regulator can read. The conformal prediction layer adds a formal coverage guarantee: when the model cannot decide, it says so explicitly and routes the pair to a human. Automated decisions are made only when the statistical evidence is strong enough to sustain audit.
How It Works
Feature Registry
Every feature the engine can compute is registered with the columns it requires. Features whose columns are absent are silently skipped. The engine adapts to whatever schema the user has.
| Feature | Required columns | Description |
|---|---|---|
jw_name_full |
any NAME column | Jaro-Winkler on full concatenated name |
jw_name_first |
any NAME column | Jaro-Winkler on first token only |
jw_name_last |
any NAME column | Jaro-Winkler on last token only |
exact_id |
any ID column | Exact string match after normalisation |
exact_phone |
any PHONE column | Exact match after stripping formatting |
haversine_m |
LATITUDE + LONGITUDE | Great-circle distance in metres |
same_admin_1 |
any ADMIN_1 column | Same state/region/province |
same_admin_2 |
any ADMIN_2 column | Same LGA/district/ward |
dob_match |
any DATE_OF_BIRTH column | Exact DOB string match |
same_gender |
any GENDER column | Same gender (normalised to first letter) |
same_enumerator |
any ENUMERATOR column | Same field agent collected both records |
Three-Tier Decision Routing
Every candidate pair is assigned to exactly one tier:
| Tier | Condition | Action |
|---|---|---|
| 1: AUTO_MERGE | High-confidence duplicate (or hard ID match) | System merges automatically |
| 2: HUMAN_REVIEW | Ambiguous: conformal set is {0, 1} | Diverted to human administrator |
| 3: AUTO_DISTINCT | High-confidence distinct (or hard ID mismatch) | No action taken |
Hard signals short-circuit the MARS model entirely:
- Exact unique ID match: Tier 1 immediately
- Exact unique ID mismatch (both non-null): Tier 3 immediately
- Exact phone match + JW >= 0.70: Tier 1
- DOB exact match + JW >= 0.85: Tier 1
The MARS Equation
logit[P(duplicate)] = -102.231
+ 562.965 * max(0, jw_name_full - 0.559)
- 679.490 * max(0, jw_name_full - 0.806)
+ 96.830 * max(0, 0.898 - jw_name_full)
+ 122.623 * max(0, jw_name_full - 0.898)
P(duplicate) = 1 / (1 + exp(-logit))
This is the complete model. No hidden layers. No embeddings. A spreadsheet is sufficient to verify any decision.
Pre-trained Models
| Model name | Required features | Use case |
|---|---|---|
text_only_v1 |
jw_name_full |
Name-only registries (NIN, voter) |
text_phone_v1 |
jw_name_full, exact_phone |
Name + phone (credit applicants) |
text_phone_geo_v1 |
jw_name_full, exact_phone, GPS |
Agricultural rosters with GPS |
DeduplicationPipeline selects the best available model automatically.
Flexible Column Guide
NIN Registry (name + NIN + DOB + gender, no GPS)
from whitebox_dedup.schema import EntitySchema, EntityField, FieldRole
schema = EntitySchema(fields={
"nin": EntityField("nin", FieldRole.ID, unique_key=True),
"full_name": EntityField("full_name", FieldRole.NAME),
"date_of_birth": EntityField("date_of_birth", FieldRole.DATE_OF_BIRTH),
"gender": EntityField("gender", FieldRole.GENDER),
"state_origin": EntityField("state_origin", FieldRole.ADMIN_1),
})
from whitebox_dedup import DeduplicationPipeline
pipeline = DeduplicationPipeline.from_schema(schema, "nin_records.csv")
results = pipeline.run()
Active features: exact_id, jw_name_full, jw_name_first, jw_name_last, dob_match, same_gender, same_admin_1
Agricultural Roster (name + phone + GPS)
schema = EntitySchema.infer("farmers.csv") # auto-detect all columns
pipeline = DeduplicationPipeline.from_csv("farmers.csv", schema=schema)
results = pipeline.run()
print(results.summary())
Patient Registry (hospital_id + name + DOB)
schema = EntitySchema(fields={
"hospital_id": EntityField("hospital_id", FieldRole.ID, unique_key=True),
"full_name": EntityField("full_name", FieldRole.NAME),
"dob": EntityField("dob", FieldRole.DATE_OF_BIRTH),
"gender": EntityField("gender", FieldRole.GENDER),
})
pipeline = DeduplicationPipeline.from_schema(schema, "patients.csv")
results = pipeline.run()
Python API Reference
DeduplicationPipeline
DeduplicationPipeline.from_csv(path, schema=None)
# Load CSV, infer or apply schema, select best model.
DeduplicationPipeline.from_schema(schema, source)
# Explicit schema with CSV path or DataFrame.
pipeline.run() -> DeduplicationResult
# Run blocking + feature extraction + routing.
pipeline.score_pair(row_a: dict, row_b: dict) -> RoutingResult
# Score one pair without full blocking.
EntitySchema
EntitySchema.infer(source) # Auto-detect from CSV or DataFrame
EntitySchema.from_yaml(path) # Load from YAML config file
EntitySchema.from_dict(data) # Load from Python dict
schema.active_features() # Set of computable feature names
schema.fields_by_role(FieldRole.X) # Column names for a given role
MARSScorer
MARSScorer.load_pretrained("text_only_v1")
MARSScorer.load(path)
scorer.score(features: dict) -> float # Probability in [0, 1]
scorer.score_with_trace(features) -> dict # Full arithmetic trace
scorer.equation_text() -> str # Human-readable equation
scorer.save(path) # Export to JSON
DeduplicationResult
result.summary() -> dict
result.tier1_auto_merge # list of pair dicts
result.tier2_human_review # list of pair dicts
result.tier3_auto_distinct # list of pair dicts
CLI Reference
# Deduplicate a CSV file
wdedup deduplicate --input farmers.csv [--schema schema.yaml] [--output results.json]
# Score a single pair interactively
wdedup score-pair [--schema schema.yaml]
# Print the MARS equation
wdedup show-equation [--model text_only_v1]
Training Your Own Model
Training requires the optional train extras:
pip install "whitebox-dedup[train]"
Prepare a labelled pairs CSV with a binary is_duplicate column and feature columns. Then run trainer.py:
python trainer.py \
--labelled-pairs my_labelled_pairs.csv \
--label-col is_duplicate \
--feature-cols jw_name_full \
--output models/my_custom_model.json
The trainer fits a MARS model (scikit-learn pipeline), extracts the piecewise linear terms, and serialises them to the JSON format that MARSScorer loads. The output file is standalone: no scikit-learn dependency at inference time.
See models/README.md for a full explanation of the coefficient format.
JavaScript Package
A companion JavaScript/TypeScript package for browser and Node.js environments is planned. It will load the same JSON model files and run identical arithmetic, enabling client-side auditing without a Python runtime.
Contributing
Contributions are welcome. Please read CONTRIBUTING.md before opening a pull request, especially the Data Privacy Rule.
Data Privacy Rule
Real beneficiary records are never committed to this repository. Only synthetic fixtures (generated by tests/fixtures/generate_fixtures.py) and model weight JSON files are committed. See CONTRIBUTING.md for the full policy.
License
MIT. Copyright FutuX Agri-consult Limited.
Citation
If this library contributes to published research, please cite:
@software{whitebox_dedup_2024,
author = {{FutuX Agri-consult Limited}},
title = {whitebox-dedup: Interpretable entity deduplication with MARS equations},
year = {2024},
url = {https://github.com/futux/whitebox-dedup-ml},
version = {0.1.0}
}
Metadata
Release files for whitebox-dedup 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| whitebox_dedup-0.1.0.tar.gz | 67.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| whitebox_dedup-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 91.0 kB
Release files / whitebox_dedup-0.1.0.tar.gz
| Download URL | whitebox_dedup-0.1.0.tar.gz |
|---|---|
| Size | 67.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
127db86a15bd97b2e7600a8cce33bc3f2ea6b9f613cb3992d9025532cb8d74b0
|
|
BLAKE2b-256 checksum How to use checksums |
0b83a1632ed5758ab46c24b80ec145d673aad0c121fb1f2b3124280863d4c36f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / whitebox_dedup-0.1.0-py3-none-any.whl
| Download URL | whitebox_dedup-0.1.0-py3-none-any.whl |
|---|---|
| Size | 23.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8df56c219feb143d4cf93b147380dfa68aa79d32f4b623cf34f9429204f0e214
|
|
BLAKE2b-256 checksum How to use checksums |
866bfa8aa1ae0b77b95cbc2b6b1c18d9ccbf020a6d981ec20f5e2792e13bf913
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Security for whitebox-dedup 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.