Skip to content

Add activities_to_features for multi-source engagement data - #232

Merged
shivamlalakiya merged 1 commit into
mainfrom
e2-activities-to-features
Sep 25, 2026
Merged

shivamlalakiya merged 1 commit into
mainfrom
e2-activities-to-features

Conversation

@shivamlalakiya

Copy link
Copy Markdown
Contributor

Summary

The no-code upload flow (map columns) needs a way to fold arbitrarily many activity files (event attendance, volunteer shifts, email clicks, ...) into per-donor features, without new model code every time a source is added.

philanthropy.ingest.activities_to_features(activities, *, as_of, donors=None) takes a long table (contact_id, activity_date, activity_type, optional amount/hours/name) and, for every activity type actually present in the data after the as_of cutoff, produces <type>_count_12m, <type>_count_36m, <type>_days_since_last, <type>_distinct, plus <type>_hours_12m / <type>_amount_12m when those columns exist. A donor with no rows of a present type gets zeros; a type with zero rows anywhere in the (cutoff) data contributes no columns at all, rather than a column of zeros for every donor.

as_of is required, not optional, and is the leakage boundary: everything is filtered to activity_date <= as_of before any count, window, or recency figure is computed, so a row dated after the cutoff cannot change the output.

donors is optional and used only to report the match rate (the share of distinct activity contact_ids also found in donors), warning when it's under 80%, which is usually a sign the activity export is keyed on a different id (email vs. an internal CRM id) than the file it's being joined against. It does not add rows for donors absent from the activity log.

This generalises the aggregation pattern already used for the UniSchema constituent-event bridge (philanthropy/ingest/_constituent_events.py) to an open-ended, data-discovered set of activity types instead of a fixed handful.

Test plan

  • python -c "from philanthropy.ingest import activities_to_features; print('OK')"
  • tests/test_activities_to_features.py (23 tests): windowed counts, days-since-last, zero-vs-absent columns, amount/hours columns, multiple types, leakage (future rows ignored, idempotent on repeat), match-rate warning (DataFrame/Series/plain-iterable donors, low vs. high match rate), donors doesn't inject extra rows, missing required column, empty input, every row filtered by cutoff, blank type / unparseable date dropped, DataFrame input, sort order
  • make ci (flake8, mypy, doctest, full suite, 92% coverage floor) - all green, _activities.py at 100% coverage
  • make riskcov (risk-tier floor over preprocessing/, models/, ingest/, cli.py) - green

A no-code upload flow lets a user hand over as many activity files as
they have (event attendance, volunteer shifts, email clicks), each
tagged with a type and mapped onto a shared long-table shape. This adds
the aggregator every one of those types runs through: per activity type
found in the data, count_12m, count_36m, days_since_last and distinct,
plus hours_12m / amount_12m when those columns are present. as_of is
required and is the leakage boundary; rows dated after it are dropped
before anything is counted, and a type or donor with no qualifying rows
gets zero columns or zero values rather than inventing a value from
future data.

Generalises the aggregation already used for the UniSchema constituent
event bridge, so a new activity type never needs new model code.
@shivamlalakiya
shivamlalakiya merged commit 7a949e9 into main Sep 25, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant