Skip to content

Build grateful patient features

Academic medical centers (AMCs) track hospital visits across different service lines. Those visits don't count equally for philanthropy: an oncology encounter correlates more strongly with giving than an urgent care visit. This guide turns encounter histories into model-ready features.

Using GratefulPatientFeaturizer

GratefulPatientFeaturizer turns clinical encounter histories into predictive signals, drawn from encounter metadata alone: service line, attending physician, and dates. It reads only those columns and returns four numeric aggregates, so no identifier from the encounter table reaches the model. That is a narrow read surface, not formal HIPAA de-identification; if you need identifier-like columns dropped from a wider encounter frame, use EncounterTransformer and its pii_patterns. Review Compliance Considerations before production use.

import pandas as pd
from philanthropy.preprocessing import GratefulPatientFeaturizer

# Clinical history (no Patient Identifiers needed for output features)
encounters = pd.DataFrame({
    "donor_id": [1, 1, 2],
    "discharge_date": ["2022-01-01", "2023-06-15", "2022-09-30"],
    "service_line": ["cardiac", "cardiac", "oncology"],
    "attending_physician_id": ["P1", "P2", "P3"],
})

# Donors in our current dataset
X = pd.DataFrame({"donor_id": [1, 2, 3]})

gpf = GratefulPatientFeaturizer(encounter_df=encounters)
out = gpf.fit_transform(X)
print(pd.DataFrame(out, columns=gpf.get_feature_names_out()))

transform returns a plain (n_samples, 4) float array, never a DataFrame, with these columns in this order:

Column Meaning
clinical_gravity_score Encounter count weighted by service-line multiplier.
distinct_service_lines Number of distinct service lines the donor was seen in.
distinct_physicians Number of distinct attending physicians.
total_drg_weight Sum of drg_weight_col, or 0.0 when that column is unset.

Donors with no encounters (donor 3 above) get 0.0 across all four.

Clinical gravity scores

GratefulPatientFeaturizer can weight each encounter by service line, but does not by default: use_capacity_weights=False. The built-in multipliers are illustrative and have no published source, so they are opt-in rather than silently applied to the headline score. To use service-line weighting, set use_capacity_weights=True and pass a capacity_weights dictionary of {service_line: multiplier} your institution has reviewed.

The solicitation window

Patients in a 90-to-365 day window post-discharge are often the warmest prospects. DischargeToSolicitationWindowTransformer scores each donor's proximity to that window. It reads days_since_discharge_col by name and raises if a DataFrame does not carry it, so route it with a ColumnTransformer rather than chaining it behind a transformer that renames columns:

from philanthropy.preprocessing import DischargeToSolicitationWindowTransformer

recency = pd.DataFrame({"days_since_last_discharge": [10.0, 200.0, 400.0]})

window = DischargeToSolicitationWindowTransformer()
scored = window.fit_transform(recency)
print(pd.DataFrame(scored, columns=window.get_feature_names_out()))

in_solicitation_window is 1 only for the 200-day row; window_position_score peaks at the window midpoint (227.5 days) and falls to 0 at either edge.