Models Reference
philanthropy.models
Donor propensity, lapse prediction, share-of-wallet capacity, and repeat-giving (BG/NBD, gamma-gamma) models.
PropensityScorer
Bases: ClassifierMixin, BaseEstimator
Constant-probability baseline that predicts P=0.5 for every donor.
A deliberately trivial, sklearn-compliant reference point: it fits nothing
and returns 0.5 for all rows, which makes it equivalent in effect to
:class:sklearn.dummy.DummyClassifier with strategy="uniform". It
exists so a domain benchmark has a named floor to beat, not because it
scores anything. For real
propensity scoring reach for
:class:~philanthropy.models.DonorPropensityModel or
:class:~philanthropy.models.MajorGiftClassifier.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
threshold
|
float
|
Decision threshold on |
0.5
|
Raises:
| Type | Description |
|---|---|
ValueError
|
In :meth: |
Source code in philanthropy/models/_propensity_baseline.py
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
fit(X, y)
Validate input and record the target classes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. |
required |
y
|
array-like of shape (n_samples,)
|
Binary target labels. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
self |
PropensityScorer
|
Fitted estimator. Sets |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in philanthropy/models/_propensity_baseline.py
predict(X)
Predict binary labels using the constant probability baseline.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
y_pred |
ndarray of shape (n_samples,)
|
Predicted labels. With the default threshold, the constant 0.5
probability is not above the threshold and |
Raises:
| Type | Description |
|---|---|
NotFittedError
|
If :meth: |
Source code in philanthropy/models/_propensity_baseline.py
predict_proba(X)
Return the constant probability baseline.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
proba |
ndarray of shape (n_samples, 2)
|
|
Raises:
| Type | Description |
|---|---|
NotFittedError
|
If :meth: |
Source code in philanthropy/models/_propensity_baseline.py
AskAmountRecommender
Bases: RegressorMixin, BaseEstimator
Recommend a donor's base ask amount and derive a gift array.
AskAmountRecommender is a scikit-learn–compatible regressor that wraps
:class:~sklearn.ensemble.HistGradientBoostingRegressor to predict the
base ask amount: the single dollar figure a gift officer anchors a
solicitation on for a given prospect.
By using HistGradientBoostingRegressor internally, the model handles
missing CRM and wealth-screening values natively without requiring an
upstream imputation step, reducing pipeline complexity and eliminating one
source of potential leakage.
Structurally this is the same wrapper as
:class:~philanthropy.models.ShareOfWalletRegressor: same estimator, same
NaN handling, different target and different domain method name. Both exist
because the two quantities are separate columns in a real advancement
workflow, not because the modelling differs. If you want a plain regressor,
use HistGradientBoostingRegressor directly.
The companion method :meth:ask_ladder expands the base ask into a
discrete gift array (or ask ladder): the low / target / stretch
rungs presented in a real solicitation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
learning_rate
|
float
|
Step size shrinkage applied to each tree. Smaller values require
more |
0.1
|
max_iter
|
int
|
Number of boosting iterations (trees). Increase to 300–500 for production models trained on large prospect pools. |
100
|
max_depth
|
int or None
|
Maximum depth of each individual decision tree. |
None
|
l2_regularization
|
float
|
L2 regularisation term on leaf weights. Increase (e.g., to 1.0) to combat overfitting when the feature-to-sample ratio is high, a common scenario in small-shop advancement analytics. |
0.0
|
min_samples_leaf
|
int
|
Minimum number of samples per leaf. Larger values prevent overfitting on sparse major-donor training sets. |
20
|
loss
|
('squared_error', 'absolute_error')
|
Loss function for the backend |
"squared_error"
|
random_state
|
int or None
|
Seed for the internal random-number generator. Set to an integer for reproducible model artefacts suitable for audit trails. |
None
|
ask_floor
|
float
|
Minimum recommended ask (in dollars). Predictions are clipped to
this floor via |
1.0
|
target_mode
|
('absolute', 'relative')
|
|
"absolute"
|
last_gift_idx
|
(int, str or None)
|
Column of the donor's last gift amount in |
None
|
avg_gift_idx
|
(int, str or None)
|
Column of the donor's average gift amount in When both |
None
|
categorical_features
|
list of int or str, bool mask, or None
|
Columns the backend should split on as unordered categories (a
region, constituency or appeal code) instead of as numbers, so they
need no one-hot step. Positions, a boolean mask, or column names
when |
None
|
monotonic_cst
|
array-like of int or dict
|
Monotonic constraint on each feature, passed to the underlying
:class: |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
estimator_ |
HistGradientBoostingRegressor
|
The fitted backend estimator, trained on all of |
n_features_in_ |
int
|
Number of features seen during :meth: |
last_gift_idx_, avg_gift_idx_ |
int or None
|
Column positions |
beats_rule_ |
bool or None
|
Whether the model's mean absolute error beat the simple rule
max(last gift, average gift) on a 20% holdout of the training data.
The holdout model is a separate fit on the other 80%; the returned
model is then refit on everything. |
rule_mae_, model_mae_ |
float or None
|
The two holdout mean absolute errors behind |
Examples:
Predict a base ask and expand it into a gift array:
>>> import numpy as np
>>> from philanthropy.models import AskAmountRecommender
>>> rng = np.random.default_rng(42)
>>> X = rng.uniform(0, 1e6, (200, 6))
>>> y = rng.uniform(1e3, 250_000, 200)
>>> model = AskAmountRecommender(random_state=42).fit(X, y)
>>> model.predict(X[:3]).shape
(3,)
>>> ladder = model.ask_ladder(X[:3])
>>> ladder.shape
(3, 3)
>>> bool((ladder[:, 2] >= ladder[:, 1]).all())
True
Pipeline usage:
>>> from sklearn.pipeline import Pipeline
>>> pipe = Pipeline([("model", AskAmountRecommender(random_state=0))])
>>> _ = pipe.fit(X, y)
Notes
Why HistGradientBoosting?
Wealth-screening datasets consistently contain 30–70 % missing values.
HistGradientBoostingRegressor implements a native missing-value
splitting strategy that treats NaN as an informative category rather
than an erroneous artefact, avoiding the information loss of mean/median
imputation.
Gift Array Interpretation:
The default multipliers=(1.0, 1.5, 2.5) map the base ask onto three
rungs a gift officer works from:
======= ================================================== Rung Meaning ======= ================================================== Low The base ask, a comfortable, likely-accepted gift. Target 1.5× the base, the amount the ask is anchored on. Stretch 2.5× the base, the aspirational upgrade ask. ======= ==================================================
See Also
philanthropy.models.ShareOfWalletRegressor : Continuous capacity model; pair with this recommender to bound the top of the gift array by estimated philanthropic capacity.
Source code in philanthropy/models/_ask.py
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 | |
n_iter_
property
Number of iterations run by the backend estimator.
fit(X, y, sample_weight=None)
Fit the ask-amount recommender to labelled prospect data.
With last_gift_idx and avg_gift_idx set, first compares the
model to the simple rule on a holdout (see beats_rule_).
sample_weight is passed to the backend, and the holdout MAEs are
weighted by it too.
Source code in philanthropy/models/_ask.py
predict(X)
Predict the base ask amount for each prospect.
Source code in philanthropy/models/_ask.py
ask_ladder(X, multipliers=(1.0, 1.5, 2.5))
Return a discrete gift array (ask ladder) for each prospect.
The base ask (from :meth:predict) multiplied by each entry of
multipliers gives the low / target / stretch rungs a gift officer
works from when structuring a solicitation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix passed to :meth: |
required |
multipliers
|
sequence of float
|
Ascending positive factors applied to the base ask to build each rung of the gift array. Must be non-empty and strictly positive. |
(1.0, 1.5, 2.5)
|
Returns:
| Name | Type | Description |
|---|---|---|
ask_array |
ndarray of shape (n_samples, len(multipliers))
|
Element |
Raises:
| Type | Description |
|---|---|
NotFittedError
|
If :meth: |
ValueError
|
If |
Examples:
>>> import numpy as np
>>> from philanthropy.models import AskAmountRecommender
>>> rng = np.random.default_rng(7)
>>> X = rng.uniform(0, 1e6, (50, 4))
>>> y = rng.uniform(1e3, 1e5, 50)
>>> model = AskAmountRecommender(random_state=7).fit(X, y)
>>> ladder = model.ask_ladder(X, multipliers=(1.0, 2.0, 4.0))
>>> ladder.shape
(50, 3)
>>> bool((ladder[:, 2] >= ladder[:, 0]).all())
True
Source code in philanthropy/models/_ask.py
DonorPropensityModel
Bases: ClassifierMixin, BaseEstimator
Predict whether a hospital prospect is a major-gift donor.
DonorPropensityModel wraps a :class:sklearn.ensemble.RandomForestClassifier
and is designed specifically for hospital advancement and major-gift
fundraising teams. Given a feature matrix describing donors (e.g. recency,
frequency, monetary value, event attendance, giving capacity estimates), the
model outputs:
- Binary predictions (
predict): 0 for standard donors, 1 for major-gift prospects above the team's threshold. - Probability estimates (
predict_proba): class probabilities in the standard sklearn two-column format. These come straight from the underlyingRandomForestClassifierand are not calibrated; see :meth:predict_affinity_scorefor details. - Affinity scores (
predict_affinity_score): the positive-class probability mapped to a 0–100 integer scale, enabling gift officers to quickly rank prospects in wealth-screening reports or CRM dashboards (e.g. Salesforce NPSP, Raiser's Edge NXT, Veeva CRM).
The model is pipeline-safe and passes sklearn.utils.estimator_checks.
check_estimator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_estimators
|
int
|
Number of trees in the underlying :class: |
100
|
max_depth
|
int or None
|
Maximum depth of each decision tree. |
None
|
min_samples_split
|
int or float
|
Minimum number of samples (or fraction) required to split an internal node. Larger values act as a regulariser, improving generalisation on sparse hospital datasets. |
2
|
min_samples_leaf
|
int or float
|
Minimum number of samples required to be at a leaf node. An int is
an absolute count; a float is a fraction of the training rows
( |
0.008
|
min_weight_fraction_leaf
|
float
|
Minimum weighted fraction of the sum of weights required to be at a
leaf node. When |
0.0
|
class_weight
|
(dict, 'balanced', 'balanced_subsample' or None)
|
Weight scheme for the two classes. Pass |
None
|
random_state
|
int or None
|
Seed for the internal random-number generator. Pass an integer to make model training fully reproducible, important for audit trails in gift-officer accountability dashboards. |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
estimator_ |
RandomForestClassifier
|
The fitted backend estimator. Inspect via
|
classes_ |
ndarray of shape (n_classes,)
|
The unique class labels seen during :meth: |
n_features_in_ |
int
|
Number of features seen during :meth: |
Examples:
Basic usage with synthetic data:
>>> from philanthropy.datasets import generate_synthetic_donor_data
>>> from philanthropy.models import DonorPropensityModel
>>> df = generate_synthetic_donor_data(n_samples=500, random_state=0)
>>> feature_cols = [
... "total_gift_amount", "years_active", "event_attendance_count"
... ]
>>> X = df[feature_cols].to_numpy()
>>> y = df["is_major_donor"].to_numpy()
>>> model = DonorPropensityModel(random_state=42)
>>> model.fit(X, y)
DonorPropensityModel(random_state=42)
>>> scores = model.predict_affinity_score(X)
>>> bool(scores.min() >= 0 and scores.max() <= 100)
True
Pipeline integration:
>>> from sklearn.pipeline import Pipeline
>>> from sklearn.preprocessing import StandardScaler
>>> pipe = Pipeline([
... ("scaler", StandardScaler()),
... ("model", DonorPropensityModel(n_estimators=200, random_state=0)),
... ])
>>> pipe.fit(X, y)
Pipeline(...)
Notes
Why RandomForest? Random forests are a natural fit for philanthropic data science because:
- They handle the diverse mix of numerical and ordinal features common in CRM exports (recency in days, monetary amounts spanning four orders of magnitude, event counts) without feature scaling.
- Their ensemble nature provides stable, rank-ordered probability
estimates suitable for affinity scoring (though not calibrated
probabilities; see the note under :meth:
predict_affinity_score). - Feature importances are easily explained to non-technical gift officers and development committees.
Affinity Score Interpretation (0–100 scale):
====== ================================= Range Recommended action ====== ================================= 80–100 Premium prospect: assign major gift officer immediately. 60–79 Strong prospect: include in next biannual solicitation cycle. 40–59 Moderate prospect: steward via annual fund or planned giving. 0–39 Low propensity: retain in broad annual-appeal pool. ====== =================================
Isotonic calibration was checked and is not recommended. Wrapping
the model in CalibratedClassifierCV(method="isotonic", cv=5) on the
KDD Cup 1998 validation fold (five seeds) lowered the decile
calibration gap on only three seeds (it is already 0.003 to 0.005
there) and lowered the top-10% hit rate on all five (e.g. 9.43 to 9.15
of 100). The wrapper is still a valid sklearn pipeline step if you need
probabilities on your own file; check it on held-out data first.
See Also
philanthropy.datasets.generate_synthetic_donor_data : Generate a synthetic prospect pool to prototype this model. philanthropy.metrics.donor_retention_rate : Measure year-over-year donor retention alongside propensity scoring.
Source code in philanthropy/models/_propensity.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 | |
__sklearn_tags__()
Declare sklearn-compatible metadata tags for this estimator.
Overrides the default :class:ClassifierMixin tags to indicate that
DonorPropensityModel supports multi-class targets (inherited from
the backend :class:RandomForestClassifier).
Returns:
| Name | Type | Description |
|---|---|---|
tags |
Tags
|
Populated sklearn Tags object. |
fit(X, y)
Fit the DonorPropensityModel to labelled donor data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. Accepts NumPy arrays or Pandas DataFrames. Common features include RFM metrics, event attendance counts, and wealth-screening capacity estimates. |
required |
y
|
array-like of shape (n_samples,)
|
Binary target vector. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
self |
DonorPropensityModel
|
Fitted estimator (enables method chaining). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in philanthropy/models/_propensity.py
predict(X)
Predict binary major-donor labels for each prospect.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. Must have the same number of columns as
the data passed to :meth: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
y_pred |
ndarray of shape (n_samples,)
|
Predicted class labels ( |
Raises:
| Type | Description |
|---|---|
NotFittedError
|
If :meth: |
Source code in philanthropy/models/_propensity.py
predict_proba(X)
Return class-probability estimates for each prospect.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
proba |
ndarray of shape (n_samples, 2)
|
Columns are |
Raises:
| Type | Description |
|---|---|
NotFittedError
|
If :meth: |
Source code in philanthropy/models/_propensity.py
decision_function(X)
Raw P(major_donor) scores. Used by sklearn scoring and calibration.
Returns:
| Type | Description |
|---|---|
np.ndarray of shape (n_samples,), dtype float64
|
Scores for each sample. Centered at 0 for binary case to match predict threshold. |
Source code in philanthropy/models/_propensity.py
predict_affinity_score(X)
Map major-donor probability to a 0–100 affinity score.
This method is the primary interface for gift officers and CRM
integrations. The raw predict_proba positive-class probability is
linearly rescaled from [0.0, 1.0] to [0, 100] and rounded to two
decimal places, making scores directly comparable across fiscal years
and prospect cohorts.
Affinity scores are monotonically equivalent to model probabilities, so any rank-ordering derived from probabilities is preserved. Scores do not represent calibrated probabilities and should not be interpreted as the literal odds of a major gift.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. Accepts NumPy arrays or Pandas DataFrames. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
affinity_scores |
ndarray of shape (n_samples,)
|
Float values in the closed interval [0.0, 100.0]. Higher scores indicate stronger major-gift propensity. |
Raises:
| Type | Description |
|---|---|
NotFittedError
|
If :meth: |
Examples:
>>> import numpy as np
>>> from philanthropy.datasets import generate_synthetic_donor_data
>>> from philanthropy.models import DonorPropensityModel
>>> df = generate_synthetic_donor_data(500, random_state=7)
>>> X = df[["total_gift_amount", "years_active",
... "event_attendance_count"]].to_numpy()
>>> y = df["is_major_donor"].to_numpy()
>>> model = DonorPropensityModel(random_state=0).fit(X, y)
>>> scores = model.predict_affinity_score(X)
>>> scores.shape
(500,)
>>> bool((scores >= 0).all() and (scores <= 100).all())
True
Source code in philanthropy/models/_propensity.py
MajorGiftClassifier
Bases: ClassifierMixin, BaseEstimator
Classifies whether a donor is likely to make a major gift using calibrated probabilities.
This uses HistGradientBoostingClassifier to handle missing data natively, and wraps
it in a CalibratedClassifierCV so the output probabilities are true calibrated probabilities.
Calibration uses sklearn's default sigmoid (Platt) method over 5 folds, the
stable choice on small files; isotonic calibration needs far more rows per
fold and is not used. Boosting stops early on its own once a file has more
than 10,000 rows (sklearn's early_stopping="auto"), so raising
max_iter or lowering learning_rate rarely changes the ranking: on a
KDD Cup 1998 validation fold (55/15/30 split, five seeds, response target),
learning_rate=0.05, max_iter=300 scored the same as the defaults.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_iter
|
int
|
Maximum number of boosting iterations for the underlying
:class: |
100
|
learning_rate
|
float
|
Shrinkage applied to each boosting iteration. |
0.1
|
min_samples_leaf
|
int
|
Minimum number of samples per leaf, passed straight to the underlying
:class: |
20
|
max_leaf_nodes
|
int or None
|
Maximum number of leaves per tree, passed straight to the underlying
:class: |
31
|
monotonic_cst
|
array-like of int or dict
|
Monotonic constraint on each feature, passed straight to the
underlying :class: |
None
|
class_weight
|
(dict, 'balanced' or None)
|
Weight scheme for the two classes. Useful when the major-gift class
is rare (e.g. a 2-3% base rate), where |
None
|
random_state
|
int or None
|
Seed for reproducibility. |
None
|
Source code in philanthropy/models/_propensity.py
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 | |
fit(X, y)
Fit the classifier to labelled donor data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. Missing values are accepted. |
required |
y
|
array-like of shape (n_samples,)
|
Binary target vector. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
self |
MajorGiftClassifier
|
Fitted estimator. Sets |
Source code in philanthropy/models/_propensity.py
predict(X)
Predict binary major-donor labels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix with the same columns used in :meth: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
y_pred |
ndarray of shape (n_samples,)
|
Predicted class labels ( |
Raises:
| Type | Description |
|---|---|
NotFittedError
|
If :meth: |
Source code in philanthropy/models/_propensity.py
predict_proba(X)
Return calibrated class probabilities.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
proba |
ndarray of shape (n_samples, 2)
|
Columns are |
Raises:
| Type | Description |
|---|---|
NotFittedError
|
If :meth: |
Source code in philanthropy/models/_propensity.py
predict_affinity_score(X)
Map the calibrated major-donor probability to a 0-100 score.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
scores |
ndarray of shape (n_samples,)
|
Positive-class probability multiplied by 100 and rounded to two
decimal places. Class |
Raises:
| Type | Description |
|---|---|
NotFittedError
|
If :meth: |
Source code in philanthropy/models/_propensity.py
ShareOfWalletRegressor
Bases: RegressorMixin, BaseEstimator
Predict a donor's total philanthropic capacity (share-of-wallet).
ShareOfWalletRegressor is a scikit-learn–compatible regressor that
wraps :class:~sklearn.ensemble.HistGradientBoostingRegressor to estimate
a prospect's total philanthropic capacity, i.e. the maximum lifetime
gift they could make given their wealth profile, giving history, and
engagement signals.
By using HistGradientBoostingRegressor internally, the model handles
missing CRM and wealth-screening values natively without requiring an
upstream imputation step, reducing pipeline complexity and eliminating one
source of potential leakage.
Structurally this is the same wrapper as
:class:~philanthropy.models.AskAmountRecommender: same estimator, same
NaN handling, different target and different domain method name. Both exist
because the two quantities are separate columns in a real advancement
workflow, not because the modelling differs. If you want a plain regressor,
use HistGradientBoostingRegressor directly.
The companion method :meth:capacity_ratio exposes the
untapped-capacity ratio (predicted capacity ÷ historical cumulative
giving), the primary metric gift officers use to prioritise discovery
calls and major-gift portfolios.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
learning_rate
|
float
|
Step size shrinkage applied to each tree. Smaller values require
more |
0.1
|
max_iter
|
int
|
Number of boosting iterations (trees). Increase to 300–500 for production models trained on large prospect pools. |
100
|
max_depth
|
int or None
|
Maximum depth of each individual decision tree. |
None
|
l2_regularization
|
float
|
L2 regularisation term on leaf weights. Increase (e.g., to 1.0) to combat overfitting when the feature-to-sample ratio is high, a common scenario in small-shop advancement analytics. |
0.0
|
min_samples_leaf
|
int
|
Minimum number of samples per leaf. Larger values prevent overfitting on sparse major-donor training sets. |
20
|
random_state
|
int or None
|
Seed for the internal random-number generator. Set to an integer for reproducible model artefacts suitable for audit trails. |
None
|
capacity_floor
|
float
|
Minimum predicted capacity (in dollars). Predictions are clipped
to this floor via |
1.0
|
categorical_features
|
list of int or str, bool mask, or None
|
Columns the backend should split on as unordered categories (a
region, constituency or appeal code) instead of as numbers, so they
need no one-hot step. Positions, a boolean mask, or column names
when |
None
|
monotonic_cst
|
array-like of int or dict
|
Monotonic constraint on each feature, passed to the underlying
:class: |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
estimator_ |
HistGradientBoostingRegressor
|
The fitted backend estimator. |
n_features_in_ |
int
|
Number of features seen during :meth: |
Examples:
Predict raw capacity and untapped-capacity ratio:
>>> import numpy as np
>>> from philanthropy.models import ShareOfWalletRegressor
>>> rng = np.random.default_rng(42)
>>> X = rng.uniform(0, 1e6, (200, 6))
>>> y = rng.uniform(5e4, 5e6, 200)
>>> historical = rng.uniform(1e3, 5e5, 200)
>>> model = ShareOfWalletRegressor(random_state=42).fit(X, y)
>>> model.predict(X[:3]).shape
(3,)
>>> ratios = model.capacity_ratio(X[:3], historical_giving=historical[:3])
>>> bool((ratios >= 0).all())
True
Pipeline usage:
>>> from sklearn.pipeline import Pipeline
>>> from philanthropy.preprocessing import WealthScreeningImputer
>>> # WealthScreeningImputer only used here for non-NaN-native context;
>>> # ShareOfWalletRegressor can handle NaN inputs natively.
>>> pipe = Pipeline([("model", ShareOfWalletRegressor(random_state=0))])
>>> _ = pipe.fit(X, y)
Notes
Why HistGradientBoosting?
Wealth-screening datasets consistently contain 30–70 % missing values.
HistGradientBoostingRegressor implements a native missing-value
splitting strategy that treats NaN as an informative category rather
than an erroneous artefact, avoiding the information loss of mean/median
imputation.
Capacity Ratio Interpretation:
====== ===================================================== Ratio Recommended action ====== ===================================================== ≥ 10× Dramatically under-asked; schedule discovery call. 5–9× Significant untapped potential; major-gift candidate. 2–4× Moderate upside; consider upgrade ask. < 2× Near capacity; focus on retention and stewardship. ====== =====================================================
See Also
philanthropy.models.DonorPropensityModel : Binary propensity model; use alongside this regressor for a two-stage (propensity × capacity) portfolio ranking. philanthropy.preprocessing.WealthScreeningImputer : Optional upstream imputer for non-NaN-native downstream models.
Source code in philanthropy/models/_wallet.py
n_iter_
property
Number of iterations run by the backend estimator.
fit(X, y, sample_weight=None)
predict(X)
capacity_ratio(X, historical_giving)
Return the predicted capacity-to-historical-giving ratio.
This ratio is the primary metric for gift officers prioritising discovery calls. A ratio of 5.0 means the model estimates the donor could give five times more than they have historically, a strong signal of untapped major-gift potential.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix passed to :meth: |
required |
historical_giving
|
array-like of shape (n_samples,)
|
Each donor's cumulative historical giving in dollars. Values
of zero or negative are replaced with |
required |
Returns:
| Name | Type | Description |
|---|---|---|
capacity_ratio |
ndarray of shape (n_samples,)
|
Element-wise ratio |
Raises:
| Type | Description |
|---|---|
NotFittedError
|
If :meth: |
ValueError
|
If |
Examples:
>>> import numpy as np
>>> from philanthropy.models import ShareOfWalletRegressor
>>> rng = np.random.default_rng(7)
>>> X = rng.uniform(0, 1e6, (50, 4))
>>> y = rng.uniform(1e4, 1e6, 50)
>>> hist = rng.uniform(500, 1e5, 50)
>>> model = ShareOfWalletRegressor(random_state=7).fit(X, y)
>>> ratios = model.capacity_ratio(X, historical_giving=hist)
>>> ratios.shape
(50,)
>>> bool((ratios > 0).all())
True
Source code in philanthropy/models/_wallet.py
MovesManagementClassifier
Bases: ClassifierMixin, BaseEstimator
Predicts the next best moves management stage for a donor.
Wraps a :class:~sklearn.ensemble.HistGradientBoostingClassifier and
labels its predictions with the moves-management stage names the donor
was trained on, rather than requiring the caller to encode stages
themselves. class_weight="balanced" is the default because moves
stages are typically imbalanced (many donors sit in early stages, few in
"STEWARD"); pass class_weight=None to disable the reweighting.
predict_proba and :meth:action_priority pass NaN straight through
to the HistGradientBoostingClassifier backend, which handles missing
values natively, so features do not need to be imputed first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
learning_rate
|
float
|
Learning rate of the underlying HistGradientBoostingClassifier. |
0.1
|
max_iter
|
int
|
Maximum number of boosting iterations. |
200
|
class_weight
|
(str, dict or None)
|
Class weights passed to the backend. |
"balanced"
|
random_state
|
int or None
|
Random seed for reproducibility. |
None
|
categorical_features
|
list of int or str, bool mask, or None
|
Columns the backend should split on as unordered categories (a
region, constituency or appeal code) instead of as numbers, so they
need no one-hot step. Positions, a boolean mask, or column names
when |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
classes_ |
ndarray of shape (n_classes,)
|
Moves-stage labels seen during |
label_encoder_ |
LabelEncoder
|
Encoder mapping stage labels to the integer classes the backend estimator was fit on. |
estimator_ |
HistGradientBoostingClassifier
|
The fitted backend estimator. |
n_features_in_ |
int
|
Number of features seen during |
n_iter_ |
int
|
Number of boosting iterations performed by |
Examples:
>>> import numpy as np
>>> from philanthropy.models import MovesManagementClassifier
>>> rng = np.random.default_rng(0)
>>> X = rng.random((12, 3))
>>> y = ["IDENTIFY", "QUALIFY", "CULTIVATE"] * 4
>>> clf = MovesManagementClassifier(max_iter=10, random_state=0).fit(X, y)
>>> sorted(clf.classes_.tolist())
['CULTIVATE', 'IDENTIFY', 'QUALIFY']
Notes
action_priority's "confidence" is the raw max class probability
from the backend estimator, not a calibrated probability: on held-out
data, rows with a reported confidence of 0.73-0.99 were observed correct
only 56-70% of the time. Treat it as a ranking signal for prioritizing
donors, not as a calibrated likelihood.
Source code in philanthropy/models/_moves.py
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | |
fit(X, y, sample_weight=None)
Fit the classifier to labelled moves-stage data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. |
required |
y
|
array-like of shape (n_samples,)
|
Moves-stage target labels. |
required |
sample_weight
|
array-like of shape (n_samples,)
|
Per-donor weights, passed to the backend and multiplied there
with |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
self |
MovesManagementClassifier
|
Fitted estimator. Sets |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in philanthropy/models/_moves.py
predict(X)
Predict the next moves-management stage for each donor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
y_pred |
ndarray of shape (n_samples,)
|
Predicted stage labels. |
Raises:
| Type | Description |
|---|---|
NotFittedError
|
If :meth: |
Source code in philanthropy/models/_moves.py
predict_proba(X)
Return class probabilities for each moves-management stage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
proba |
ndarray of shape (n_samples, n_classes)
|
Predicted probabilities for each stage. |
Raises:
| Type | Description |
|---|---|
NotFittedError
|
If :meth: |
Source code in philanthropy/models/_moves.py
action_priority(X)
Predict the next-best stage per donor plus a portfolio rollup.
Unlike predict/predict_proba (which return ndarrays), this
returns a dict with keys "stage" (ndarray of predicted stage
labels), "confidence" (ndarray of max class probabilities), and
"portfolio_summary" (dict mapping each stage to its donor count).
Source code in philanthropy/models/_moves.py
LapsePredictor
Bases: ClassifierMixin, BaseEstimator
Predicts whether a donor will lapse within a configurable window. Uses RandomForestClassifier backend by default.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_estimators
|
int
|
Number of trees in the RandomForestClassifier. Ignored when
|
100
|
max_depth
|
int or None
|
Maximum depth of trees. None means nodes expand until pure
(unbounded for |
None
|
class_weight
|
(dict, 'balanced', 'balanced_subsample' or None)
|
Class weights for imbalanced lapse prediction. Ignored when
|
None
|
random_state
|
int or None
|
Random seed for reproducibility. |
None
|
backend
|
('random_forest', 'hist_gradient_boosting')
|
|
"random_forest"
|
categorical_features
|
list of int or str, bool mask, or None
|
Columns the backend should split on as unordered categories (a
region, constituency or appeal code) instead of as numbers, so they
need no one-hot step. Positions, a boolean mask, or column names
when |
None
|
monotonic_cst
|
array-like of int or dict
|
Monotonic constraint on each feature, passed to the underlying
:class: |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
training_lapse_rate_ |
float
|
Fraction of training rows labelled "lapsed", set at fit time. |
retention_read_ |
bool
|
|
Source code in philanthropy/models/_lapse.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | |
fit(X, y, sample_weight=None)
Fit the LapsePredictor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. |
required |
y
|
array-like of shape (n_samples,)
|
Binary target: 1 = lapse, 0 = no lapse. |
required |
sample_weight
|
array-like of shape (n_samples,)
|
Per-donor weights, passed to the backend (and multiplied there
with |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
self |
LapsePredictor
|
|
Source code in philanthropy/models/_lapse.py
predict(X)
predict_proba(X)
Return class probabilities of shape (n_samples, 2).
Source code in philanthropy/models/_lapse.py
predict_lapse_score(X)
Return P(lapse) × 100 rounded to 2 decimal places (0–100 scale).
Source code in philanthropy/models/_lapse.py
predict_retention_score(X)
Return P(retained) x 100, the complement of
:meth:predict_lapse_score.
Ranking donors by this score, highest first, is the useful list on a
file where :attr:retention_read_ is True: ranking by lapse
score barely beats random when almost everyone lapses, but the tail
least likely to lapse still beats the rule (E.12d).
Source code in philanthropy/models/_lapse.py
PlannedGivingIntentScorer
Bases: ClassifierMixin, BaseEstimator
Predicts bequest/planned giving intent. Wraps HistGradientBoostingClassifier with CalibratedClassifierCV.
Exposes .predict_intent_score(X) returning a 0-100 float array.
Missing values are accepted and handled natively by the boosting
backend, so a file with blank age or wealth-screening columns needs no
imputation first.
Calibration uses cv=2, so every class in y must have at least 2
examples; fit raises a ValueError up front if that is not the
case rather than surfacing scikit-learn's cross-validation error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_estimators
|
int
|
The maximum number of boosting iterations, passed as |
100
|
random_state
|
int, RandomState instance or None
|
Controls the randomness of the estimator. |
None
|
class_weight
|
(dict, 'balanced' or None)
|
Weight scheme for the classes, useful when bequest intent is rare.
Applied as |
None
|
categorical_features
|
list of int or str, bool mask, or None
|
Columns the backend should split on as unordered categories (a
region, constituency or appeal code) instead of as numbers, so they
need no one-hot step. Positions, a boolean mask, or column names
when |
None
|
monotonic_cst
|
array-like of int or dict
|
Monotonic constraint on each feature, passed to the underlying
:class: |
None
|
Source code in philanthropy/models/_planned_giving.py
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | |
fit(X, y, sample_weight=None)
Fit the calibrated classifier to planned-giving intent labels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. |
required |
y
|
array-like of shape (n_samples,)
|
Binary target labels. |
required |
sample_weight
|
array-like of shape (n_samples,)
|
Per-donor weights, used by both the boosting backend and the calibration step. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
self |
PlannedGivingIntentScorer
|
Fitted estimator. Sets |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in philanthropy/models/_planned_giving.py
predict(X)
Predict bequest/planned-giving intent labels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
y_pred |
ndarray of shape (n_samples,)
|
Predicted class labels. |
Raises:
| Type | Description |
|---|---|
NotFittedError
|
If :meth: |
Source code in philanthropy/models/_planned_giving.py
predict_proba(X)
Return calibrated class probabilities.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
proba |
ndarray of shape (n_samples, n_classes)
|
Predicted probabilities for each class. |
Raises:
| Type | Description |
|---|---|
NotFittedError
|
If :meth: |
Source code in philanthropy/models/_planned_giving.py
predict_intent_score(X)
Return P(planned giving intent) × 100, rounded to 2 decimal places.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
|
required |
Returns:
| Name | Type | Description |
|---|---|---|
scores |
ndarray of shape (n_samples,)
|
Values in range [0.0, 100.0]. |
Source code in philanthropy/models/_planned_giving.py
FinancialForecastModel
Bases: RegressorMixin, BaseEstimator
Hybrid LSTM-ARIMA forecaster for nonprofit revenue / giving series.
FinancialForecastModel is a scikit-learn–compatible regressor that
closes the loop with the LSTM-ARIMA hybrid forecasting literature. It fits
two complementary sub-models on the training data:
- a linear (ARIMA-surrogate) component: a
:class:
~sklearn.linear_model.LinearRegressionmapping the feature matrix to giving revenue, capturing the linear / trend structure; and - a nonlinear (LSTM-surrogate) component: a
:class:
~sklearn.neural_network.MLPRegressorfitted on the residuals of the linear component, capturing the nonlinear structure a linear model leaves behind.
Point predictions (:meth:predict) are the additive hybrid
linear(X) + nonlinear_residual(X). Forward-looking, multi-period
forecasts (:meth:predict_revenue_forecast) are produced by seeding a
frozen autoregressive model with the most recent hybrid predictions and
rolling it forward over the requested horizon.
The model handles missing values natively: at :meth:fit time it freezes a
per-column median fill (falling back to 0.0 for all-NaN columns) and
applies it before either sub-model sees the data, so no upstream imputer is
required and no test-set statistic can leak backwards into training.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ar_order
|
int
|
Order |
3
|
hidden_layer_sizes
|
tuple of int
|
Hidden-layer architecture of the nonlinear residual network, passed
straight through to :class: |
(64,)
|
max_iter
|
int
|
Maximum optimisation iterations for the residual network. |
300
|
alpha
|
float
|
L2 regularisation strength of the residual network. Increase to combat overfitting on short giving histories. |
1e-4
|
random_state
|
int or None
|
Seed for the residual network's weight initialisation. Pass an integer for fully reproducible forecasts suitable for board-level audit trails. |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
linear_model_ |
LinearRegression
|
The fitted linear (ARIMA-surrogate) component. |
nonlinear_model_ |
MLPRegressor or None
|
The fitted nonlinear (LSTM-surrogate) residual component. |
fill_values_ |
ndarray of shape (n_features_in_,)
|
Per-column median fill values frozen at :meth: |
ar_coef_ |
ndarray of shape (ar_order,)
|
Frozen autoregressive coefficients used for the forecast roll-forward. |
ar_intercept_ |
float
|
Frozen autoregressive intercept. |
y_mean_ |
float
|
Mean of the training target, used to pad short forecast seeds. |
n_features_in_ |
int
|
Number of features seen during :meth: |
Examples:
>>> import numpy as np
>>> from philanthropy.models import FinancialForecastModel
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(120, 4))
>>> # revenue with linear + mild nonlinear structure
>>> y = 5_000 + 800 * X[:, 0] + 300 * X[:, 1] ** 2 + rng.normal(0, 50, 120)
>>> model = FinancialForecastModel(random_state=0).fit(X, y)
>>> preds = model.predict(X)
>>> preds.shape
(120,)
>>> forecast = model.predict_revenue_forecast(X, horizon=4)
>>> forecast.shape
(4,)
See Also
philanthropy.models.ShareOfWalletRegressor : Cross-sectional capacity regressor; pair with this forecaster to move from per-donor capacity to portfolio-level revenue projections. philanthropy.preprocessing.WealthScreeningImputer : The leakage-safe fill contract this model mirrors internally.
References
.. [1] Zhang, G. P. (2003). Time series forecasting using a hybrid ARIMA and neural network model. Neurocomputing, 50, 159-175.
Source code in philanthropy/models/_forecast.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 | |
fit(X, y)
Fit the hybrid forecaster on labelled revenue data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix describing each period (e.g. fiscal-year index,
appeal counts, prior-period giving, macro indicators). May contain
|
required |
y
|
array-like of shape (n_samples,)
|
Giving revenue for each period. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
self |
FinancialForecastModel
|
Fitted estimator (enables method chaining). |
Source code in philanthropy/models/_forecast.py
predict(X)
Predict revenue for each period in X (cross-sectional hybrid).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix with the same number of columns as seen at
:meth: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
y_pred |
ndarray of shape (n_samples,)
|
Additive hybrid predictions |
Source code in philanthropy/models/_forecast.py
predict_revenue_forecast(X, horizon)
Forecast giving revenue for the next horizon periods.
The supplied X provides the most recent observed context: its hybrid
predictions seed a frozen autoregressive roll-forward that projects
horizon periods into the future. Because the autoregressive
coefficients are frozen at :meth:fit time, no information from X
can contaminate the learned dynamics.
The roll-forward sees only the target's own recent values, never the
future periods' features, so it cannot follow a seasonal pattern.
When the future periods' features are known in advance (fiscal
month, planned appeal count), call :meth:predict on those rows
instead. On the synthetic monthly benchmark
(scripts/benchmark_models_vs_baselines.py, bench_forecast)
that beats a 12-month mean, and this method does not.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Feature matrix for the most recent periods, ordered oldest to
newest. May contain |
required |
horizon
|
int
|
Number of future periods to forecast. Must be a positive integer. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
forecast |
ndarray of shape (horizon,)
|
Forecasted revenue for each of the next |
Raises:
| Type | Description |
|---|---|
NotFittedError
|
If :meth: |
ValueError
|
If |
Source code in philanthropy/models/_forecast.py
GiftInterval
dataclass
A calibrated interval on a dollar amount, with the level it attained.
Attributes:
| Name | Type | Description |
|---|---|---|
lower, upper |
ndarray of shape (n_samples,)
|
The interval, in the target's units. |
rank |
ndarray of int, shape (n_samples,)
|
The order statistic |
attained_level |
ndarray of shape (n_samples,)
|
|
requested_level |
float
|
|
Source code in philanthropy/models/_conformal_interval.py
GiftIntervalCalibrator
Bases: RegressorMixin, BaseEstimator
Turn a fitted dollar-valued regressor into calibrated intervals.
Split conformal prediction over one order statistic. fit calibrates on
held-out rows and never touches estimator, which must already be fitted;
:meth:predict forwards to it unchanged, so the point prediction a gift
officer sees does not move when an interval is added around it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
estimator
|
object
|
A fitted regressor whose |
required |
alpha
|
(float, Fraction, Decimal or int)
|
Miscoverage. The interval targets |
0.05
|
score
|
('absolute', 'difficulty', 'log')
|
The conformity score, all three of them one-rank:
Equal-tailed two-rank intervals are deliberately absent: two order
statistics at |
"absolute"
|
difficulty_estimator
|
object or callable
|
Required when |
None
|
lower_bound
|
float or None
|
Intersect the interval with |
0.0
|
Attributes:
| Name | Type | Description |
|---|---|---|
quantile_ |
float or dict
|
The calibrated score at rank |
rank_ |
int or dict
|
|
attained_level_ |
float or dict
|
|
n_calibration_ |
int or dict
|
Calibration rows used. |
requested_level_ |
float
|
|
groups_ |
ndarray or None
|
The distinct group labels calibrated for, or |
n_features_in_ |
int
|
Features seen during :meth: |
Raises:
| Type | Description |
|---|---|
NotFittedError
|
If |
ValueError
|
If the calibration set, or any group in it, is below the floor for the
requested level; if |
Examples:
Pooled calibration:
>>> import numpy as np
>>> from philanthropy.models import AskAmountRecommender, GiftIntervalCalibrator
>>> rng = np.random.default_rng(3)
>>> X = rng.uniform(0, 1, (240, 4))
>>> y = 20_000 + 40_000 * X[:, 0] + rng.normal(0, 3_000, 240)
>>> ask = AskAmountRecommender(max_iter=40, random_state=3).fit(X[:140], y[:140])
>>> cal = GiftIntervalCalibrator(ask, alpha=0.1).fit(X[140:200], y[140:200])
>>> cal.rank_, cal.n_calibration_
(55, 60)
>>> round(cal.attained_level_, 4)
0.9016
Per-segment calibration. Segments are calibrated independently, so the rank and the attained level differ between them:
>>> seg = np.where(X[140:200, 1] > 0.5, "principal", "annual")
>>> cal = GiftIntervalCalibrator(ask, alpha=0.1).fit(X[140:200], y[140:200], groups=seg)
>>> sorted(cal.n_calibration_.items())
[('annual', 33), ('principal', 27)]
>>> interval = cal.predict_gift_interval(X[200:], groups=np.where(
... X[200:, 1] > 0.5, "principal", "annual"))
>>> bool((interval.upper >= interval.lower).all())
True
>>> sorted(set(interval.rank.tolist()))
[26, 31]
Notes
Why this is not a check_estimator-compliant estimator. The battery
clones with default parameters and calls fit(X, y). This class cannot
satisfy that: calibrating and training on the same rows is the one thing it
exists to prevent. It is exempted in tests/test_sklearn_compliance.py
with that reason, rather than gaining a parameter that carves training rows
out of the calibration set to satisfy a test.
What exchangeability buys and what it does not. The guarantee is
marginal over the calibration draw: coverage holds on average across
calibration sets, not conditionally on a donor's features. Per-group
calibration is the practical answer to that gap, and is why groups
exists.
See Also
philanthropy.metrics.interval_report : Whether the interval is narrow enough to act on.
Source code in philanthropy/models/_conformal_interval.py
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 | |
fit(X, y, groups=None)
Calibrate on held-out rows.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Calibration features. Held out of |
required |
y
|
array-like of shape (n_samples,)
|
Realised dollar amounts for those rows. |
required |
groups
|
array-like of shape (n_samples,)
|
Segment label to calibrate within: capacity tier, sector, giving society, business unit. Not the donor identifier and not the fiscal year. Every distinct label is calibrated on its own rows and must clear the floor on its own; passing donor ids here gives one row per group and is refused. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
self |
GiftIntervalCalibrator
|
|
Source code in philanthropy/models/_conformal_interval.py
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 | |
predict(X)
Return estimator's point prediction, unchanged.
Source code in philanthropy/models/_conformal_interval.py
predict_gift_interval(X, groups=None)
Return a calibrated interval on the dollar amount for each row.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_samples, n_features)
|
Features for the rows to be scored. |
required |
groups
|
array-like of shape (n_samples,)
|
Required, and only accepted, when :meth: |
None
|
Returns:
| Type | Description |
|---|---|
GiftInterval
|
|
Source code in philanthropy/models/_conformal_interval.py
BetaGeoModel
Bases: BaseEstimator
BG/NBD model of repeat giving: who is still giving, and how often.
Fit by maximum likelihood on one row per donor. No labels needed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_iter
|
int
|
Maximum optimiser iterations. |
2000
|
Attributes:
| Name | Type | Description |
|---|---|---|
params_ |
dict
|
Fitted |
log_likelihood_ |
float
|
Total log-likelihood at |
n_iter_ |
int
|
Optimiser iterations used. |
n_features_in_ |
int
|
Always 3. |
Notes
X is (frequency, recency, T) in that column order, as built by
:func:repeat_giving_summary: repeat-gift count, time from first to last
gift, and time from first gift to the end of the window, all in one time
unit. recency must not exceed T.
Examples:
>>> import numpy as np
>>> from philanthropy.models import BetaGeoModel
>>> X = np.array([[0, 0, 5], [3, 4, 5], [1, 1, 5], [6, 4.8, 5]] * 10, float)
>>> model = BetaGeoModel().fit(X)
>>> model.predict_alive_score(X[:2]).shape
(2,)
Source code in philanthropy/models/_repeat_giving.py
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | |
fit(X, y=None)
Fit r, alpha, a, b by maximum likelihood.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_donors, 3)
|
|
required |
y
|
ignored
|
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
self |
BetaGeoModel
|
|
Source code in philanthropy/models/_repeat_giving.py
predict_alive_score(X)
P(the donor is still active) at the end of the window, in [0, 1].
A donor with no repeat gift is always scored 1: the model only lets a donor drop out right after a gift.
Source code in philanthropy/models/_repeat_giving.py
predict_gifts_forecast(X, horizon=1.0)
Expected number of gifts each donor makes over the next horizon.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_donors, 3)
|
|
required |
horizon
|
float
|
Length of the forecast window, in the time unit of |
1.0
|
Returns:
| Name | Type | Description |
|---|---|---|
gifts |
ndarray of shape (n_donors,)
|
|
Source code in philanthropy/models/_repeat_giving.py
GammaGammaModel
Bases: BaseEstimator
Gamma-gamma model of gift size: each donor's expected gift amount.
Fit by maximum likelihood on donors with at least one repeat gift; their mean repeat gift is shrunk toward the population mean, more so the fewer gifts they have. Assumes gift size is independent of how often a donor gives.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_iter
|
int
|
Maximum optimiser iterations. |
2000
|
Attributes:
| Name | Type | Description |
|---|---|---|
params_ |
dict
|
Fitted |
log_likelihood_ |
float
|
Total log-likelihood at |
n_iter_ |
int
|
Optimiser iterations used. |
n_features_in_ |
int
|
Always 2. |
Notes
X is (frequency, monetary_value) in that column order, as built by
:func:repeat_giving_summary. Rows with frequency 0 are ignored in
:meth:fit and scored at the population mean.
Examples:
>>> import numpy as np
>>> from philanthropy.models import GammaGammaModel
>>> rng = np.random.default_rng(0)
>>> X = np.column_stack([rng.integers(1, 6, 200), rng.gamma(3, 30, 200)])
>>> GammaGammaModel().fit(X).predict_gift_size_forecast(X[:3]).shape
(3,)
Source code in philanthropy/models/_repeat_giving.py
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 | |
fit(X, y=None)
Fit p, q, v by maximum likelihood.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array-like of shape (n_donors, 2)
|
|
required |
y
|
ignored
|
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
self |
GammaGammaModel
|
|
Source code in philanthropy/models/_repeat_giving.py
predict_gift_size_forecast(X)
Expected size of each donor's future gifts.
p (v + x m) / (p x + q - 1): a weighted average of the population
mean p v / (q - 1) and the donor's own mean m. Infinite when
q <= 1, where the population mean does not exist.
Source code in philanthropy/models/_repeat_giving.py
suggest_ask(last_gift, avg_gift, stretch=0.1, round_to=25)
Suggest an ask from giving history alone: the simple rule, no model.
Takes the larger of the donor's last gift and average gift, raises it by
stretch, and rounds up to the next multiple of round_to. With
stretch=0 and round_to=None this is the rule the Results pages
test; on no real file there does :class:AskAmountRecommender land
within 25% of the next gift reliably more often than this rule, so it is
the place to start.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
last_gift
|
array-like of shape (n_samples,)
|
Each donor's most recent and average gift amount. A |
required |
avg_gift
|
array-like of shape (n_samples,)
|
Each donor's most recent and average gift amount. A |
required |
stretch
|
float
|
Fractional increase over the larger amount (0.10 asks for 10% more). A policy choice, not an estimate: no observational file can say what stretch works best. |
0.10
|
round_to
|
float or None
|
Round up to a multiple of this amount. |
25
|
Returns:
| Type | Description |
|---|---|
ndarray of shape (n_samples,)
|
The suggested ask per donor, in the same currency as the input. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> from philanthropy.models import suggest_ask
>>> suggest_ask([100, 40, 260], [80, 55, 200])
array([125., 75., 300.])
>>> suggest_ask([100], [80], stretch=0.0, round_to=None)
array([100.])
Source code in philanthropy/models/_ask.py
score_leadership_prospects(gifts, *, activities=None, donors=None, threshold=1000.0, band=(100.0, 999.0), fiscal_year_start=7, as_of=None, top_n=None, baseline_giving_threshold=None, random_state=None, include_momentum=False)
Fit an upgrade model on history, then score today's band-qualifying donors.
Every fiscal year T where both T and T+1 are fully resolved as
of as_of is a labelled training row (via
:func:~philanthropy.ingest.build_leadership_snapshots); a
:class:~philanthropy.models.MajorGiftClassifier is fit on the stack of
those years and evaluated on the most recent
:class:~philanthropy.model_selection.FiscalYearGroupedSplitter fold, an
honest, held-out read. A second copy of the model, refit on every
historical row (more data, no held-out fold to protect), scores the
CURRENT band-qualifying donors: one unlabelled row per donor, built the
same way but for the fiscal year containing as_of and cut at as_of
rather than at that year's end, since it may still be in progress.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gifts
|
iterable of mapping, or DataFrame
|
Gift-level rows with |
required |
activities
|
iterable of mapping, or DataFrame
|
A long activity log, forwarded to
:func: |
None
|
donors
|
DataFrame
|
Static donor attributes, indexed by donor id. Forwarded the same way. Only its numeric columns become model features (see Notes); it is still joined and returned for a non-numeric attribute a caller wants to inspect alongside the score. |
None
|
threshold
|
float or str
|
The leadership-giving level an upgrade crosses into. A string
|
1000.0
|
band
|
(float, float)
|
Inclusive bounds on FY giving that define the upgrade-candidate
population, exactly as in |
(100.0, 999.0)
|
fiscal_year_start
|
int
|
Month (1-12) the fiscal year begins. |
7
|
as_of
|
str or datetime - like
|
The scoring cutoff: nothing dated after it is ever read, in either
the historical training rows or the current scored row. Defaults to
the latest gift date in |
None
|
top_n
|
int
|
How many of the held-out validation fold's highest-scored donors
count as "the top" for |
None
|
baseline_giving_threshold
|
float
|
The FY T giving level the "gave >= X last FY" naive baseline uses.
Defaults to |
None
|
random_state
|
int
|
Seed forwarded to the classifier fits and to the permutation importance call, for reproducible scores and reasons. |
None
|
include_momentum
|
bool
|
Forwarded to :func: |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
scores |
DataFrame
|
One row per currently band-qualifying donor, indexed by |
report |
dict
|
Always present: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
KeyError
|
If |
Notes
Feature columns. Only the numeric columns of the snapshot (the
gift-derived features, any numeric activities_to_features columns,
and any numeric donors columns) become model features; a non-numeric
donors column (e.g. a wealth-rating letter grade) is still joined and
returned for reference but dropped before fitting, the simplest rule
that needs no per-column encoding policy for a feature nobody asked this
function to build. fiscal_year is excluded from the model features
even though it is numeric: it is not a donor-specific signal, and the
current row's fiscal year always sits outside the range the model was
trained on (every historical row is a strictly earlier FY), so it can
only ever generalise as noise or, worse, an ordering artefact. It is
still returned as an output column and left out of top_reasons for
the same reason. A current-row feature column absent from history (or
vice versa), e.g. an activity type that only shows up in one window, is
reindexed to 0.0 rather than dropped, matching
activities_to_features's own "no rows of that type -> 0" rule.
Top reasons. There is no per-instance explainer in this package
(SHAP is out of scope for this whole series); this reuses the existing
GLOBAL permutation importances (:func:~philanthropy.inspection.donor_feature_importance,
computed once on the held-out fold, or in-sample with a warning if there
was no fold to hold out) as fixed feature weights, then for each scored
donor ranks their OWN feature columns by |z-score within the current
scored population| * global_importance_weight (importance clipped at 0,
since a negative permutation importance means shuffling that feature
helped, not a reason to report), and keeps the top 3 as
(feature_name, donor_value) pairs. This is a heuristic, not a causal
attribution: it says which features are both unusual for this donor and
generally predictive, not "if X were different the score would change by
Y". It is deliberately simple rather than a general explainability
framework.
Suggested ask. AskAmountRecommender needs its own ask-amount
label (what a gift officer actually asked for, or a realistic proxy for
it); nothing in build_leadership_snapshots's output is that. Training it
on, say, the FY T+1 amount actually given would train on the very
quantity the upgrade label is derived from, an honesty problem, not a
convenience one. suggested_ask is left NaN, out of scope for this
function until a real ask-amount training signal exists.
Examples:
Four donor archetypes across five fiscal years: a flat high-band donor,
an early riser that crosses the threshold in FY2022, a later riser that
crosses in FY2024, and a flat low-band donor. Only the two that never
cross threshold are still band-qualifying "today" (FY2025):
>>> import pandas as pd
>>> from philanthropy.models import score_leadership_prospects
>>> years = ["2020-08-01", "2021-08-01", "2022-08-01", "2023-08-01", "2024-08-01"]
>>> archetypes = {
... "flat_high": [900, 900, 900, 900, 900],
... "early_riser": [600, 1050, 1200, 1400, 1600],
... "late_riser": [300, 500, 700, 1100, 1300],
... "flat_low": [400, 400, 400, 400, 400],
... }
>>> rows = [
... {"donor_id": f"{name}_{i}", "gift_date": year, "gift_amount": amount}
... for name, amounts in archetypes.items()
... for i in range(20)
... for year, amount in zip(years, amounts)
... ]
>>> gifts = pd.DataFrame(rows)
>>> scores, report = score_leadership_prospects(gifts, random_state=0)
>>> list(scores.columns)
['fiscal_year', 'affinity_score', 'rank', 'decile', 'top_reasons', 'suggested_ask']
>>> len(scores)
40
>>> bool((scores["affinity_score"] >= 0).all() and (scores["affinity_score"] <= 100).all())
True
>>> report["n_training_rows"]
200
>>> report["validated"]
True
Source code in philanthropy/models/_upgrade.py
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 | |
repeat_giving_summary(gifts, *, as_of=None, time_unit_days=365.25)
Build per-donor BG/NBD and gamma-gamma inputs from a gift table.
Gifts on the same day are merged into one gift (amounts summed), as in [1]_, so a split payment does not count as a repeat gift.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gifts
|
DataFrame
|
One row per gift with |
required |
as_of
|
(str, datetime - like or None)
|
End of the observation window. Gifts after it are dropped. |
None
|
time_unit_days
|
float
|
Days per time unit for |
365.25
|
Returns:
| Name | Type | Description |
|---|---|---|
summary |
pandas.DataFrame, indexed by ``donor_id``
|
|