Skip to content

Commit 132f86e

Browse files
refactor(downstream): source features from philanthropy.ingest package
Drop the local unischema_features.py shim (and its Python contract tests) in favor of importing read_constituent_events / constituent_events_to_features directly from the PhilanthroPy package, the single source of truth for the feature contract. Update pipelines, docs, and dbt notes to the new column names (gift_count, event_attendance_count, total_gift_amount, distinct_source_systems). - downstream-ml.yml: replace the deleted-pytest gate with a dependency-free py_compile check; repoint the optional smoke to philanthropy.ingest - repositoryGuard: skip .venv/venv/__pycache__ in the secret scan
1 parent 621039c commit 132f86e

14 files changed

Lines changed: 174 additions & 481 deletions

.github/workflows/downstream-ml.yml

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ on:
1313
- '.github/workflows/downstream-ml.yml'
1414

1515
jobs:
16-
feature-contract:
17-
name: Python feature contract tests
16+
pipeline-compile:
17+
name: Downstream pipeline scripts compile
1818
runs-on: ubuntu-latest
1919

2020
steps:
@@ -26,20 +26,19 @@ jobs:
2626
with:
2727
python-version: '3.11'
2828

29-
- name: Install dependencies
30-
run: |
31-
pip install pandas pytest
32-
pip install -r examples/downstream/requirements-philanthropy.txt || pip install pandas pytest
33-
34-
- name: Run feature contract tests
29+
# The feature contract now lives in philanthropy.ingest (PhilanthroPy repo)
30+
# and is pinned TS-side by tests/unit/schemaContract.test.ts, so there is no
31+
# local Python contract test. This gate just catches syntax breakage in the
32+
# example scripts (py_compile parses only — no third-party imports executed).
33+
- name: Byte-compile downstream pipeline scripts
3534
working-directory: examples/downstream
36-
run: pytest tests/test_feature_contract.py -q
35+
run: python3 -m py_compile *.py
3736

3837
philanthropy-smoke:
3938
name: PhilanthroPy pipeline smoke (optional)
4039
runs-on: ubuntu-latest
4140
needs:
42-
- feature-contract
41+
- pipeline-compile
4342
continue-on-error: true
4443

4544
steps:
@@ -54,6 +53,6 @@ jobs:
5453
- name: Install PhilanthroPy stack
5554
run: pip install -r examples/downstream/requirements-philanthropy.txt
5655

57-
- name: Smoke test unischema_features import
56+
- name: Smoke test philanthropy.ingest import
5857
working-directory: examples/downstream
59-
run: python3 -c "from unischema_features import REQUIRED_FEATURE_COLUMNS; print(len(REQUIRED_FEATURE_COLUMNS), 'columns')"
58+
run: python3 -c "from philanthropy.ingest import constituent_events_to_features, read_constituent_events; print('philanthropy.ingest OK')"

docs/downstream-pipeline.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ End-to-end path from UniSchema pilot to warehouse analytics and ML with [Philant
88
flowchart LR
99
Vendors[Vendor webhooks] --> UniSchema[UniSchema API]
1010
UniSchema --> Egress[Local or S3 egress]
11-
Egress --> Features[unischema_features.py]
11+
Egress --> Features[philanthropy.ingest]
1212
Egress --> dbt[dbt staging and marts]
1313
Features --> PhilanthroPy[PhilanthroPy pipelines]
1414
dbt --> PhilanthroPy
@@ -104,7 +104,6 @@ events group onto one key across vendors before RFM aggregation.
104104
|--------|--------|
105105
| `philanthropy_crm_pipeline.py` | **Primary** — CRM labels + `DonorPropensityModel` |
106106
| `philanthropy_pipeline.py` | Egress-only demo with proxy labels |
107-
| `unischema_features.py` | Feature table library |
108107
| `crm_join_example.py` | CRM join (`externalConstituentId` or email) |
109108

110109
CRM join prefers `externalConstituentId` → CRM `constituent_id`, then email fallback.

docs/philanthropy-integration.md

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
```
88
ConstituentEvent (egress JSON / S3 NDJSON)
9-
unischema_features.py (per-email feature table)
9+
philanthropy.ingest.constituent_events_to_features (per-donor feature table)
1010
→ optional CRM join (engagement_tier, lifetime_giving)
1111
→ PhilanthroPy DonorPropensityModel / LapsePredictor
1212
→ affinity or lapse scores (0–100)
1313
```
1414

15+
Both reading and aggregation live in PhilanthroPy's `philanthropy.ingest` — the single source of truth. `read_constituent_events` walks UniSchema's date-partitioned egress tree (recursively; skips `.manifest.json` sidecars); `constituent_events_to_features` is **leakage-safe** (recency anchored to the batch, never a moving "now") and **deduplicates** redelivered webhooks by `eventId`.
16+
1517
Warehouse path: dbt `mart_constituent_rfm_features` → export CSV → same PhilanthroPy pipeline.
1618

1719
## Install
@@ -38,31 +40,29 @@ npm run downstream-demo
3840

3941
## Feature column contract
4042

41-
`examples/downstream/unischema_features.py` aggregates events to one row per `constituent_email`:
43+
`philanthropy.ingest.constituent_events_to_features` aggregates events to one row per donor, indexed by `constituent_id` (the `externalConstituentId` when present, else `constituentEmail`). This contract is owned and tested in the PhilanthroPy repo (`tests/test_ingest.py`):
4244

4345
| Column | Type | Source |
4446
|--------|------|--------|
45-
| `constituent_email` | string | Group key |
46-
| `donation_count` | int | `eventType == DONATION` |
47-
| `registration_count` | int | `eventType == EVENT_REGISTRATION` |
48-
| `email_click_count` | int | `eventType == EMAIL_CLICK` |
49-
| `total_amount` | float | Sum of donation `amount` |
50-
| `total_gift_amount` | float | Alias of `total_amount` (PhilanthroPy naming) |
51-
| `event_attendance_count` | int | Alias of `registration_count` |
52-
| `last_donation_date` | ISO string | Latest donation `createdAt` |
53-
| `last_event_date` | ISO string | Latest any-event `createdAt` |
54-
| `first_event_date` | ISO string | Earliest any-event `createdAt` |
55-
| `years_active` | float | Years from `first_event_date` to reference date |
56-
| `metadata_key_count` | float | Sum of `normalizedMetadata` key counts |
57-
58-
Contract tests: `examples/downstream/tests/test_feature_contract.py`
47+
| `constituent_email` | string | First email seen for the donor |
48+
| `total_gift_amount` | float | Sum of `DONATION` `amount` (single-currency; warns on mixed) |
49+
| `gift_count` | int | Count of `DONATION` events |
50+
| `event_attendance_count` | int | Count of `EVENT_REGISTRATION` events |
51+
| `email_click_count` | int | Count of `EMAIL_CLICK` events |
52+
| `first_gift_date` | datetime | Earliest donation `createdAt` |
53+
| `last_gift_date` | datetime | Latest donation `createdAt` |
54+
| `years_active` | float | First event → reference date |
55+
| `recency_days` | int | Last event → reference date |
56+
| `distinct_source_systems` | int | Channel breadth |
57+
58+
Index: `constituent_id` (`externalConstituentId` ?? `constituentEmail`).
5959

6060
### PhilanthroPy model inputs
6161

6262
| PhilanthroPy component | UniSchema-derived columns |
6363
|------------------------|---------------------------|
6464
| `DonorPropensityModel` | `total_gift_amount`, `years_active`, `event_attendance_count` |
65-
| `RFMTransformer` | Use `last_donation_date`, `donation_count`, `total_amount` after date parsing |
65+
| `RFMTransformer` | `recency_days`, `gift_count`, `total_gift_amount` |
6666
| `WealthScreeningImputer` | CRM `lifetime_giving``estimated_net_worth` |
6767
| `LapsePredictor` | CRM-derived lapse labels + RFM features from warehouse |
6868

@@ -86,7 +86,6 @@ Example CRM columns (see `samples/crm-golden-record.csv`):
8686

8787
| Script | Purpose |
8888
|--------|---------|
89-
| `unischema_features.py` | Library — egress → DataFrame |
9089
| `philanthropy_pipeline.py` | Egress only, proxy labels (demo) |
9190
| `philanthropy_crm_pipeline.py` | **Recommended** — CRM labels + `DonorPropensityModel` |
9291

@@ -101,11 +100,10 @@ Example CRM columns (see `samples/crm-golden-record.csv`):
101100
## sklearn pipeline example
102101

103102
```python
104-
from pathlib import Path
103+
from philanthropy.ingest import constituent_events_to_features, read_constituent_events
105104
from philanthropy.models import DonorPropensityModel
106-
from unischema_features import egress_dir_to_features
107105

108-
features = egress_dir_to_features(Path("data/egress"))
106+
features = constituent_events_to_features(read_constituent_events("data/egress"))
109107
X = features[["total_gift_amount", "years_active", "event_attendance_count"]].to_numpy()
110108

111109
# y from CRM engagement_tier after join — see philanthropy_crm_pipeline.py

docs/schema-governance.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Add `VOLUNTEER_SHIFT` to `EventTypeSchema` in `src/schema/master.ts`.
6565
|-----------|--------|
6666
| dbt `mart_constituent_engagement_daily` | Add `volunteer_shift_count` column |
6767
| dbt `mart_constituent_rfm_features` | Optional engagement feature |
68-
| `unischema_features.py` | Add `volunteer_shift_count` to contract |
68+
| `philanthropy.ingest` (PhilanthroPy repo) | Add `volunteer_shift_count` to the feature contract |
6969
| PhilanthroPy pipelines | Map to engagement features or separate model |
7070

7171
### Do not implement until

examples/downstream/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ npm run downstream-demo
3333

3434
## Feature column contract
3535

36-
`unischema_features.py` aggregates egress JSON to a per-constituent DataFrame. Required columns are tested in `tests/test_feature_contract.py`.
36+
`philanthropy.ingest` is the single source of truth: `read_constituent_events` walks the date-partitioned local egress tree (recursive; skips `.manifest.json`), and `constituent_events_to_features` aggregates it (leakage-safe, dedup by `eventId`). Owned and tested in the PhilanthroPy repo.
3737

3838
| Column | PhilanthroPy use |
3939
|--------|------------------|
@@ -47,7 +47,6 @@ Full contract → [philanthropy-integration.md](../../docs/philanthropy-integrat
4747

4848
| Script | Purpose |
4949
|--------|---------|
50-
| `unischema_features.py` | Egress → feature DataFrame |
5150
| `philanthropy_crm_pipeline.py` | **Recommended** ML path with CRM labels |
5251
| `philanthropy_pipeline.py` | Demo with proxy labels |
5352
| `crm_join_example.py` | CRM join (`externalConstituentId` or email) |

examples/downstream/dbt/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ dbt run
3333
Export `mart_constituent_rfm_features` to CSV or query via Python:
3434

3535
```python
36-
# Warehouse export columns align with unischema_features.py contract
36+
# Warehouse mart produces model-ready aliases (total_gift_amount, event_attendance_count)
3737
# See docs/philanthropy-integration.md
3838
```
3939

examples/downstream/dbt/models/marts/schema.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ models:
1313

1414
- name: mart_constituent_rfm_features
1515
description: >
16-
Per-constituent RFM-style rollup for PhilanthroPy batch scoring.
17-
Column contract matches examples/downstream/unischema_features.py
18-
and docs/philanthropy-integration.md.
16+
Per-constituent RFM-style rollup for PhilanthroPy batch scoring. A
17+
warehouse-side alternative to the philanthropy.ingest event aggregation:
18+
it produces model-ready aliases (total_gift_amount, event_attendance_count)
19+
consumed directly by PhilanthroPy models. See docs/philanthropy-integration.md.
1920
columns:
2021
- name: constituent_email
2122
tests:

0 commit comments

Comments
 (0)