feat(datasets): add make_donor_panel, a gift-level multi-year donor panel - #162
Merged
Conversation
…anel generate_synthetic_donor_data returns one aggregated row per donor. That is enough to fit a classifier and nothing else: it cannot demonstrate RFMTransformer, which needs a gift log; FiscalYearGroupedSplitter, which needs repeated donor-years; an as_of cutoff, which needs something to cut off; or the grateful-patient transformers, which need encounters. Those are the ideas this library exists for, and the only generator that could show them lived privately inside scripts/leakage_experiment.py. make_donor_panel returns the tables a shop actually exports: a gift log, a donor table, and optionally encounters. Fiscal years run 1 July to 30 June, labelled by the year they end in, at most one gift per donor-year. It ships no label column, deliberately. A label is a claim about a point in time, and shipping one pre-computed would hand every user the exact mistake this package exists to prevent; the docstring shows the one-line derivation instead. wealth_estimate is ~30% missing, because a wealth screen that came back for every record is not one anyone has received. Encounter dates are drawn independently of giving, because a generator that made grateful-patient features predictive by construction would be a convincing demo of nothing. scripts/leakage_experiment.py now imports it rather than defining its own copy, so the published experiment and the tutorials share a generator. The aggregated frames are asserted byte-identical to the private generator's on all five published seeds, so 0.639 / 0.625 / 0.608 / 0.750 and the +0.126 inflation figure stand unchanged. Gift amounts are not rounded to cents for exactly that reason: rounding shifted the reported min-max ranges by 0.001 AUC, and a cosmetic decimal does not outrank a number already in the docs.
shivamlalakiya
enabled auto-merge (squash)
September 5, 2026 20:51
This was referenced Sep 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
generate_synthetic_donor_datareturns one already-aggregated row per donor: five columns, no giftlog, no repeated years, no encounters. That is enough to fit a classifier and nothing else. It
cannot demonstrate
RFMTransformer(needs one row per gift),FiscalYearGroupedSplitter(needs repeated donor-years),as_ofcutoff (needs something to cut off),EncounterTransformer/GratefulPatientFeaturizer(need an encounter table).Those are the ideas this library exists for, and the only generator that could show them lived
privately inside
scripts/leakage_experiment.py. Every tutorial and notebook that wants to teachthe library's actual argument has had to hand-roll a panel first.
What it returns
Column names match what the transformers already require, so nothing has to be renamed on the way
in. Fiscal years run 1 July to 30 June, labelled by the year they end in; at most one gift per
donor-year, which is what makes "recent" well defined.
Three design decisions worth arguing with
No label column, deliberately. A label is a claim about a point in time. Shipping one
pre-computed would hand every user the exact mistake this package exists to prevent, and the ready
availability of a
ycolumn is precisely how whole-history features get built. The docstring showsthe one-line derivation instead.
wealth_estimateis ~30% missing. A wealth screen that came back for every record is not awealth screen anyone has ever received, and
WealthScreeningImputerhas nothing to do on a fullcolumn.
Encounter dates are drawn independently of giving. A generator that made grateful-patient
features predictive by construction would be a very convincing demo of nothing.
The part that needed proving
scripts/leakage_experiment.pynow imports this instead of defining its own_panel, so thepublished experiment and the tutorials share a generator. That script's numbers are in
docs/explanation/benchmarks.mdand are referenced frompaper.md, so "close enough" was notacceptable.
The aggregated frames are asserted byte-identical to the private generator's output on all five
published seeds:
and the script's own output is unchanged line for line:
Gift amounts are not rounded to cents, on purpose. An earlier revision rounded them, which looks
more like a real gift log and moved the reported min-max ranges by 0.001 AUC
(0.653 → 0.652, 0.757 → 0.758). A cosmetic decimal does not outrank a number that is already in the
docs; there is a comment in the source saying so, because it will otherwise look like an oversight.
Tests
tests/test_donor_panel.py, 26 tests, in four groups: schema, panel invariants (one gift perdonor-year, dates inside their own fiscal year,
first_gift_fyagrees with the gift table, wealthmissingness in range), seeding (same seed reproduces every frame; asking for encounters does not
perturb the giving draws), and validation. The last group is the point of the whole thing: the
transformers a flat frame cannot reach actually consume the output, unmodified.
One of those tests documents a wart it found rather than papering over it:
EncounterTransformerraisesDTypePromotionErroron a realdatetime64gift-date column and onlyaccepts date strings, as its own docstring example passes them. That is a defect in the
transformer, not in this generator, and it gets its own issue.
Verification
Tier 2 (Beta) row added to
docs/reference/index.md, which the tier-table test requires.