Predictive Modeling API Reference
Feature Analytics
Use feature-analytics helpers (early-performance rates, WoE/IV, feature
engineering, lift/gain, score calibration and monitoring) to prepare and
validate signal before fitting a final estimator. No single run() here —
these are independent analytics tools rather than one pipeline.
cranalytics.feature_analytics
Feature analytics compatibility re-export surface.
This package groups early-performance rate/separation analytics
(_early_performance), feature engineering and WoE binning
(_model_development), and score calibration/monitoring
(_score_monitoring) — independent analytics tools rather than one
end-to-end pipeline, so there is no single run() entry point here. Reach
for the specific helper you need.
This module carries no logic of its own — it only re-exports the focused
submodules so that existing from cranalytics.feature_analytics import ...
call sites keep working. Add new behavior to the appropriate submodule, not
here.
calculate_early_performance_rates(df: pd.DataFrame, flag_columns: list[str], weight_col: str | None = None, confidence: float = 0.95, reliability_thresholds: tuple[int, int] = (10, 30)) -> pd.DataFrame
Compute portfolio-level event rates and Wilson confidence intervals.
Returns one row per flag with n_mature, n_events, n_event_loans,
rate, ci_lower, ci_upper and reliability ("low",
"medium" or "high" from n_event_loans; see
compute_segment_rates for the thresholds).
compute_conditional_loss_table(df: pd.DataFrame, segment_cols: list[str], flag_col: str, outcome_col: str, n_bins_score: int = 5, score_col: str | None = None, weight_col: str | None = None, confidence: float = 0.95) -> pd.DataFrame
Compute conditional lifetime-loss summaries by segment, score bucket, and flag.
compute_marginal_impact(df: pd.DataFrame, feature_col: str, flag_col: str, control_col: str, n_bins_control: int = 5, n_bins_feature: int | None = None, weight_col: str | None = None) -> pd.DataFrame
Estimate feature effect within control strata via within-bucket rate deltas.
compute_segment_rates(df: pd.DataFrame, flag_col: str, group_by: str | list[str], weight_col: str | None = None, n_bins: int | None = None, confidence: float = 0.95, reliability_thresholds: tuple[int, int] = (10, 30), adjusted: bool = False) -> pd.DataFrame
Compute mature-event rates by segment(s) with volume and contribution shares.
Beyond the raw rate and its Wilson interval, two plain-language columns
say how far to trust each segment:
vs_portfolio:"higher"or"lower"when the segment's interval sits entirely above or below the portfolio rate, else"unclear".reliability:"low","medium"or"high"from the number of loans with an event (n_event_loans), not the number of loans.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
Loan-level data with the outcome flag and segment columns. |
required |
flag_col
|
str
|
Binary event column. Null outcomes (immature loans) are excluded. |
required |
group_by
|
str | list[str]
|
Segment column or columns. |
required |
weight_col
|
str | None
|
Optional exposure weight, e.g. original balance. Intervals then use the effective sample size of the weights. |
None
|
n_bins
|
int | None
|
Quantile-bin numeric |
None
|
confidence
|
float
|
Interval confidence level. |
0.95
|
reliability_thresholds
|
tuple[int, int]
|
|
(10, 30)
|
adjusted
|
bool
|
Add |
False
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
One row per segment with |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
Examples:
>>> import pandas as pd
>>> from cranalytics.feature_analytics import compute_segment_rates
>>> frame = pd.DataFrame(
... {"channel": ["a"] * 400 + ["b"] * 400,
... "fpf": [1] * 8 + [0] * 392 + [1] * 60 + [0] * 340}
... )
>>> out = compute_segment_rates(frame, "fpf", "channel")
>>> out[["channel", "vs_portfolio", "reliability"]].values.tolist()
[['a', 'lower', 'low'], ['b', 'higher', 'high']]
compute_woe_iv(df: pd.DataFrame, feature_col: str, flag_col: str, n_bins: int = 10, weight_col: str | None = None) -> tuple[pd.DataFrame, float]
Compute a Weight of Evidence table and total Information Value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
Loan-level feature and outcome data. |
required |
feature_col
|
str
|
Candidate feature to bin and evaluate. |
required |
flag_col
|
str
|
Binary event column. Null outcomes are excluded. |
required |
n_bins
|
int
|
Maximum number of bins for numeric features. |
10
|
weight_col
|
str | None
|
Optional exposure or observation-weight column. |
None
|
Returns:
| Type | Description |
|---|---|
tuple[DataFrame, float]
|
Tuple of WoE detail table and total IV. |
Examples:
>>> import pandas as pd
>>> from cranalytics.feature_analytics import compute_woe_iv
>>> frame = pd.DataFrame({"fico": [600, 650, 750, 800], "bad": [1, 1, 0, 0]})
>>> table, total_iv = compute_woe_iv(frame, "fico", "bad", n_bins=2)
>>> {"bin", "woe", "iv_contribution"} <= set(table.columns)
True
>>> total_iv >= 0
True
estimate_vintage_lifetime_profit(df: pd.DataFrame, expected_loss_col: str, avg_life_col: str | None = None, avg_life: float | None = None, coupon_rate: float = 0.0, servicing_cost_rate: float = 0.0, funding_cost_rate: float = 0.0) -> pd.DataFrame
Estimate vintage-level net margin from pricing assumptions and expected loss.
Formula::
net_margin = coupon_rate * avg_life
- expected_loss_rate
- servicing_cost_rate
- funding_cost_rate
All rate inputs are decimals (e.g. 0.18 for 18%). avg_life is in years.
coupon_rate is an annualised yield multiplied by avg_life to give the
lifetime interest income. servicing_cost_rate and funding_cost_rate are
lifetime rates (not annualised) and are subtracted directly without scaling
by avg_life. expected_loss_rate is likewise a lifetime rate (e.g. from
compute_conditional_loss_table).
Parameters
df : DataFrame with at least expected_loss_col (and optionally avg_life_col).
expected_loss_col : Column of lifetime loss rates (decimals, e.g. 0.05 = 5%).
avg_life_col : Per-row average life column (years). Takes precedence over avg_life.
avg_life : Scalar average life (years) applied to all rows. Required if avg_life_col is None.
coupon_rate : Annualised all-in yield (decimal); multiplied by avg_life in the formula.
servicing_cost_rate : Lifetime servicing cost rate (decimal); not scaled by avg_life.
funding_cost_rate : Lifetime cost-of-funds rate (decimal); not scaled by avg_life.
Returns
Copy of df with an added net_margin column.
rank_features_by_separation(df: pd.DataFrame, feature_cols: list[str], flag_col: str, n_bins: int = 10, weight_col: str | None = None) -> pd.DataFrame
Rank candidate features by IV, Gini, and KS against a binary target.
validate_performance_flags(df: pd.DataFrame, flag_columns: list[str]) -> tuple[pd.DataFrame, pd.DataFrame]
Validate binary flag columns and return maturity coverage statistics.
engineer_loan_features(df: pd.DataFrame, *, as_of_date: pd.Timestamp | None = None, reference_date: pd.Timestamp | None = None) -> pd.DataFrame
Add derived credit-risk features to a loan DataFrame.
Source columns that are absent are silently skipped. Original columns are always preserved.
Parameters
df : pd.DataFrame
Loan-level DataFrame.
as_of_date : pd.Timestamp
Snapshot or model development date used to compute loan_age_months.
Must be provided explicitly — never default to max(start_date).
reference_date : pd.Timestamp, optional
Deprecated alias for as_of_date. Will be removed in v2.0.
Returns
pd.DataFrame
Copy of df with derived columns appended.
fit_woe_binning(df: pd.DataFrame, feature_cols: list[str], target_col: str, *, special_codes: list | None = None, **binning_kwargs)
Fit an optimal WoE binning process on the provided features.
Wraps optbinning.BinningProcess. Requires optbinning.
Parameters
df : pd.DataFrame
feature_cols : list[str]
target_col : str
Binary target column (0/1).
special_codes : list, optional
Sentinel values to group into a special bin.
**binning_kwargs
Passed to BinningProcess.
Returns
optbinning.BinningProcess
Fitted process. Call .transform(X, metric="woe") to encode.
lift_gain_table(y_true: pd.Series | np.ndarray, y_prob: pd.Series | np.ndarray, *, n_bins: int = 10) -> pd.DataFrame
Compute lift and gain table from binary labels and predicted probabilities.
Sorts observations by descending score, splits into n_bins equal-sized
buckets, and returns per-bin statistics.
Parameters
y_true : array-like Binary labels (0/1). y_prob : array-like Predicted probabilities in [0, 1]. n_bins : int Number of equal-size buckets (default 10).
Returns
pd.DataFrame Columns: bin, n, n_events, event_rate, cumulative_n, cumulative_events, cumulative_gain, lift, score_min, score_max, baseline_rate.
calibrate_score_to_event_rate(df: pd.DataFrame, score_col: str, flag_col: str, n_bins: int = 10, weight_col: str | None = None, method: str = 'binned') -> tuple[pd.DataFrame, dict[str, float | str]]
Calibrate a score to observed target event rates.
compute_actual_vs_expected(df: pd.DataFrame, score_col: str, flag_col: str, group_by: str | list[str], calibration_table: pd.DataFrame | None = None, weight_col: str | None = None, confidence: float = 0.95, n_bins_group: int | None = None, reliability_thresholds: tuple[int, int] = (10, 30)) -> pd.DataFrame
Compare observed event rates to expected rates by segment.
Each segment gets a z-test of actual against expected. The significant
flag uses a Bonferroni correction for the number of segments, so slicing
many ways does not by itself produce flagged segments. Two plain-language
columns summarise the result:
vs_expected:"worse"(more events than expected),"better"or"in line", followingsignificantand the sign ofz_score.reliability:"low","medium"or"high"from the number of loans with an event (n_event_loans); seereliability_thresholds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
Loan-level data with score, outcome flag and segment columns. |
required |
score_col
|
str
|
Model score or predicted probability. |
required |
flag_col
|
str
|
Binary event column. Null outcomes (immature loans) are excluded. |
required |
group_by
|
str | list[str]
|
Segment column or columns. |
required |
calibration_table
|
DataFrame | None
|
Output of |
None
|
weight_col
|
str | None
|
Optional exposure weight. Intervals then use the effective sample size of the weights. |
None
|
confidence
|
float
|
Confidence level for the intervals and the z-test. |
0.95
|
n_bins_group
|
int | None
|
Quantile-bin numeric |
None
|
reliability_thresholds
|
tuple[int, int]
|
|
(10, 30)
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
One row per segment, sorted by |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
by the expected rate. |
compute_psi(expected: pd.Series, actual: pd.Series, n_bins: int = 10, bin_edges: list[float] | None = None, expected_weights: pd.Series | None = None, actual_weights: pd.Series | None = None) -> tuple[pd.DataFrame, float]
Compute population stability index between expected and actual distributions.
score_performance_monitoring_report(df: pd.DataFrame, score_col: str, flag_col: str, group_by: str | list[str] | None = None, baseline_df: pd.DataFrame | None = None, n_bins: int = 10, weight_col: str | None = None) -> dict
Bundled score monitoring report: discrimination, calibration, A/E, PSI.
Parameters
df : Scored portfolio DataFrame with mature flags. score_col : Numeric score column (higher = higher risk). flag_col : Binary 0/1 event flag (NaN = immature). group_by : Column(s) for actual-vs-expected breakdown. Omitted if None. baseline_df : Reference period DataFrame for PSI. Omitted if None. n_bins : Score bins for calibration table. weight_col : Optional loan-balance weight.
Returns
dict with keys:
"discrimination": dict ofauc,gini,ks,spearman_rho"calibration_table": DataFrame fromcalibrate_score_to_event_rate"calibration_stats": summary stats dict"actual_vs_expected": DataFrame or None (requiresgroup_by)"psi":{"table": DataFrame, "total_psi": float}or None (requiresbaseline_df)
simulate_policy_cutoff(df: pd.DataFrame, flag_col: str, cutoff_col: str, cutoffs: list[float], direction: str = 'min', weight_col: str | None = None) -> pd.DataFrame
Simulate the volume/loss trade-off across candidate policy cutoffs.
For each cutoff value, computes approval rate, projected event rate, volume lost, and loss reduction relative to no cutoff (all mature loans).
Parameters
df : DataFrame with mature-flag and cutoff columns.
flag_col : Binary 0/1 event flag (NaN = immature, excluded).
cutoff_col : Numeric column to threshold (e.g. fico_score, dti).
cutoffs : Candidate threshold values to evaluate.
direction : "min" approves rows where cutoff_col >= cutoff (FICO
floor); "max" approves rows where cutoff_col <= cutoff (DTI
ceiling).
weight_col : Optional loan-balance weight.
Returns
DataFrame with columns: cutoff, n_approved, n_total_mature,
approval_rate, projected_event_rate, volume_loss_pct,
loss_reduction_pct.
.. warning:: Selection bias: this analysis is backward-looking. You only observe performance for loans that were approved. The performance of declined loans is unknown (rejection inference problem). Use for directional analysis only — not for precise P&L projection.
Predictive Modeling
Use predictive.run() for the end-to-end path: target construction, training,
scoring, and temporal out-of-time backtesting behind one call, returning a
result with .summary() and .plot(). Use train_binary_model() and
score_model() directly for narrow training and scoring steps, or the
forecasting bridge to turn model scores into calendar-month portfolio
projections.
cranalytics.predictive
Predictive modeling compatibility re-export surface.
The deep entry point for the predictive workflow is cranalytics.predictive.run
(defined in :mod:cranalytics.predictive._session), which trains a binary
model, scores it, and runs a temporal out-of-time backtest behind one call.
Prefer it for end-to-end modeling; it returns a result with .summary() and
.plot(). Reach for the narrower predictive.* helpers directly only
when you need one concern such as training, scoring, or backtesting.
This module carries no logic of its own — it only re-exports the focused
submodules (targets, modeling, backtest, forecasting bridge, session) so that
existing from cranalytics.predictive import ... call sites keep working.
Add new behavior to the appropriate submodule, not here.
PredictiveModelingSessionResult
dataclass
summary() -> pd.DataFrame
Backtest metrics aggregated across folds (mean AUC/Gini/KS).
plot(**kwargs: Any) -> Any
Gini stability across backtest folds. Requires matplotlib.
run_predictive_backtest(df: pd.DataFrame, feature_cols: list[str], target_col: str, split_col: str, *, model_family: str, window_type: str = 'expanding', n_splits: int | np.integer = 5, rolling_window_size: int | np.integer | None = None, model_params: dict | None = None, random_state: int = 42, strict: bool = False, label_available_col: str | None = None, feature_as_of_col: str | None = None, target_horizon_months: int | np.integer | None = None, evaluation_as_of: object = None, loan_id_col: str | None = None, preprocessor: Any | None = None) -> pd.DataFrame
Rolling or expanding temporal OOT backtest.
Splits df by unique values of split_col (e.g., origination_month)
in ascending order. No data shuffling — all splits are strictly temporal.
Parameters
df : pd.DataFrame
feature_cols : list[str]
target_col : str
Binary target (0/1/NaN). NaN rows excluded from training and scoring.
split_col : str
Column defining temporal ordering (e.g., origination_month, vintage).
model_family : str
Passed to _build_binary_estimator.
window_type : str
"expanding" — all prior periods in train.
"rolling" — fixed-width window of rolling_window_size prior periods.
n_splits : int
Number of OOT folds. Requires len(unique_periods) >= n_splits + 1.
rolling_window_size : int, optional
Number of prior periods in each rolling train window. Only used when
window_type="rolling". Defaults to n_splits when not provided.
model_params : dict, optional
random_state : int
label_available_col : str, optional
Date when each training label became available. If supplied, both this
column and split_col must contain dates. A fold excludes training rows
whose labels became available after its validation-period start and
reports the excluded count as n_purged. Missing availability dates on
labeled rows are rejected. Without it, the caller must ensure labels
were known at each historical cutoff.
str, optional
Feature snapshot date, no later than its split decision date. If absent, the split date is the assumed feature date; stored values must actually represent information known then.
target_horizon_months : int, optional Months after the split decision date. Derives conservative label availability when no explicit availability column is given; explicit negative labels cannot be available before this horizon ends. evaluation_as_of : date, optional Only evaluate validation outcomes available by this historical date. Requires explicit availability or a target horizon. loan_id_col : str, optional Origination-model identity; defaults to loan_id when present. Requires one non-null row per loan. Repeated-observation models are unsupported. preprocessor : sklearn transformer, optional Cloned and fit inside each training fold (including supervised selection using training labels). Accepts numeric arrays in feature_cols order.
Returns
pd.DataFrame One row per requested fold: split, train_periods, val_periods, n_train, n_val, n_purged, auc, gini, ks, status, reason. Skipped folds retain their counts and have NaN metrics. Strict mode rejects any skipped fold.
summarize_predictive_backtest(backtest_df: pd.DataFrame, *, by: str | list[str] | None = None) -> pd.DataFrame
Aggregate backtest metrics across folds.
Parameters
backtest_df : pd.DataFrame
Output of run_predictive_backtest.
by : str or list[str], optional
Column(s) to group by (e.g., "model"). If None, aggregates all rows.
Returns
pd.DataFrame One row per group with mean_auc, mean_gini, mean_ks, n_folds, n_requested_folds, n_skipped_folds and status (ok, partial, or insufficient_data). Only evaluated folds contribute to metric means. Frames produced before fold statuses were added remain supported.
forecast_calendar_chargeoff_from_predictions(scored_df: pd.DataFrame, *, score_col: str, as_of_col: str, current_mob_col: str, hazard_curves: pd.DataFrame, principal_col: str = 'original_principal', segment_col: str | None = None, strict: bool = True, prediction_kind: str | None = None, prediction_horizon_months: int | str | None = None, exposure_basis: str | None = None, chargeoff_severity: float | None = None, loss_basis: str = 'gross_chargeoff') -> pd.DataFrame
Allocate compatible remaining-horizon predictions to monthly gross chargeoffs.
Predictions must explicitly be expected_chargeoff_fraction (expected
future gross chargeoffs divided by the selected exposure) or
default_probability. Default probabilities additionally require an
explicit chargeoff_severity in [0, 1]: gross chargeoffs conditional on
default divided by the selected exposure. Early-delinquency probabilities
and net-loss fractions are incompatible and must be modeled or converted
outside this function. No implicit PD-to-loss or delinquency-to-PD conversion
is performed.
prediction_horizon_months is a positive integer or a column containing
each loan's remaining horizon measured from its snapshot. Forecasts use
exactly the same horizon; historical/cumulative lifetime scores must first
be converted to remaining-horizon predictions. exposure_basis explicitly
declares original_principal or current_balance for principal_col.
The caller must ensure predictions and severity use that same denominator.
Exposure is held fixed for this allocation; amortization is not modeled.
The timing profile in hazard_curves must contain one finite nonnegative
rate per segment/MOB for every future month in each requested horizon, with
a positive sum. Rates are normalized into timing weights, not interpreted
as an independent default probability. Missing months, zero profiles,
duplicate observations and invalid scores/exposures raise rather than drop
expected dollars. strict=False allows an unweighted global mean profile
for missing segments, with a warning and metadata identifying fallback use.
Returns a DataFrame with forecast_month, expected_chargeoff_amount,
loan_count and expected_chargeoff_rate. The rate denominator is total
input exposure in principal_col. Counts are loans contributing to each
month, not default counts. Currency units match principal_col; recoveries
are not deducted. attrs["prediction_contract"] records the semantic
choices, exposure denominator and total expected/allocated dollars.
score_model(df: pd.DataFrame, estimator: Any, feature_cols: list[str], *, output_col: str, prediction_type: str) -> pd.DataFrame
Score a fitted estimator against a DataFrame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
DataFrame to score. It is the first argument for |
required |
estimator
|
Any
|
Fitted scikit-learn-compatible estimator. |
required |
feature_cols
|
list[str]
|
Columns passed to the estimator. |
required |
output_col
|
str
|
Name for the appended prediction column. |
required |
prediction_type
|
str
|
|
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
Copy of |
Examples:
>>> import pandas as pd
>>> from sklearn.linear_model import LogisticRegression
>>> from cranalytics.predictive import score_model
>>> train = pd.DataFrame({"fico": [600, 650, 750, 800], "bad": [1, 1, 0, 0]})
>>> estimator = LogisticRegression().fit(train[["fico"]].to_numpy(), train["bad"])
>>> scored = score_model(
... train, estimator, ["fico"], output_col="pd", prediction_type="probability"
... )
>>> bool(scored["pd"].between(0, 1).all())
True
train_binary_model(df: pd.DataFrame, feature_cols: list[str], target_col: str, *, model_family: str, model_params: dict[str, Any] | None = None, calibrate: bool = False, random_state: int = 42, strict: bool = False, preprocessor: Any | None = None) -> tuple
Train a binary classification model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
Modeling frame. |
required |
feature_cols
|
list[str]
|
Columns passed to the estimator. |
required |
target_col
|
str
|
Binary 0/1 target. Null rows are dropped before training. |
required |
model_family
|
str
|
|
required |
model_params
|
dict[str, Any] | None
|
Optional parameters merged over estimator defaults. |
None
|
calibrate
|
bool
|
Reserved calibration switch. Currently raises when enabled. |
False
|
random_state
|
int
|
Seed forwarded to supported estimators. |
42
|
strict
|
bool
|
Whether predictive contract warnings should fail validation. |
False
|
preprocessor
|
Any | None
|
Optional sklearn-compatible transformer or pipeline, cloned and fitted only on training rows. Receives a numeric feature array; column selectors must use indices. Returned estimator retains it for scoring. Do not pre-fit preprocessing on the full backtest dataset. |
None
|
Returns:
| Type | Description |
|---|---|
tuple
|
Tuple of fitted estimator, metadata dictionary, and diagnostics |
tuple
|
DataFrame. |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
If |
ValueError
|
If the modeling frame or family is invalid. |
Examples:
>>> import pandas as pd
>>> from cranalytics.predictive import train_binary_model
>>> frame = pd.DataFrame({"fico": [600, 650, 750, 800], "bad": [1, 1, 0, 0]})
>>> estimator, metadata, diagnostics = train_binary_model(
... frame, ["fico"], "bad", model_family="logistic"
... )
>>> metadata["model_family"]
'logistic'
>>> diagnostics["split"].tolist()
['train']
train_regression_model(df: pd.DataFrame, feature_cols: list[str], target_col: str, *, model_family: str, model_params: dict[str, Any] | None = None, random_state: int = 42, strict: bool = False, preprocessor: Any | None = None) -> tuple
Train a regression model.
Parameters
model_family : str
"tweedie", "hgb_regressor", "quantile_hgb",
"xgboost_regressor".
Returns
tuple[estimator, dict, pd.DataFrame]
run(df: pd.DataFrame, feature_cols: list[str], target_col: str, split_col: str, *, model_family: str, model_params: dict | None = None, window_type: str = 'expanding', n_splits: int | np.integer = 5, rolling_window_size: int | np.integer | None = None, score_output_col: str = 'prediction', scoring_df: pd.DataFrame | None = None, random_state: int = 42, strict: bool = False, label_available_col: str | None = None, feature_as_of_col: str | None = None, target_horizon_months: int | np.integer | None = None, evaluation_as_of: object = None, training_as_of: object = None, loan_id_col: str | None = None, preprocessor: Any | None = None) -> PredictiveModelingSessionResult
Run one end-to-end predictive modeling session.
The representative session trains a binary model on df, scores either
df or scoring_df, executes a temporal out-of-time backtest on
df, and returns a typed session result with fold-level and summarized
outputs.
Pass label_available_col to purge labels unavailable at each historical
backtest cutoff (requires date-valued split_col), or supply
target_horizon_months to derive conservative availability. Feature
snapshots must predate their split decision dates. evaluation_as_of
limits validation outcomes; training_as_of limits the final fit to
decisions and labels known on that date. With no final cutoff the final
estimator fits the supplied frame, independently of historical backtests.
preprocessor is cloned separately for each fold and the final fit.
Loan IDs, when present or explicitly selected, must be unique.
assemble_modeling_frame(features_df: pd.DataFrame, targets_df: pd.DataFrame, *, on: str | list[str], leakage_guard: bool = True) -> pd.DataFrame
Join features and targets with optional leakage detection.
Parameters
features_df : pd.DataFrame
targets_df : pd.DataFrame
on : str or list[str]
Join key(s), typically "loan_id" or ["loan_id", "as_of_date"].
leakage_guard : bool
If True, raises if any non-key target column appears in features_df.
Returns
pd.DataFrame Inner join of features and targets.
build_targets(panel_df: pd.DataFrame, *, mode: str, targets: list[str], target_overrides: dict | None = None, include_diagnostics: bool = False) -> pd.DataFrame
Derive or validate binary/regression targets from a monthly performance panel.
Parameters
panel_df : pd.DataFrame
Monthly performance panel. Required columns (panel mode):
loan_id, mob, as_of_date, origination_date, original_principal,
dpd, chargeoff_amount.
mode : str
"panel" — derive targets from dpd/chargeoff columns.
"prelabeled" — validate and pass through pre-built target columns.
targets : list[str]
Target column names to derive or validate.
target_overrides : dict, optional
Per-target config overrides, e.g.
{"fpf30_flag": {"dpd_threshold": 60, "mob_horizon": 6}}.
bool
Append per-target __status, __missing_mobs, __n_observed,
__n_required and __label_available_date columns in panel mode.
The same records are always available in attrs["target_diagnostics"].
Returns
pd.DataFrame
Loan-level DataFrame with loan_id + one column per target.
NaN means unknown: immature, incomplete coverage, unavailable outcome,
invalid denominator or no confirmed lifetime completion.
Notes
MOB is an integer monthly age. Window targets require every MOB from 1 to
the horizon; MOB 0 is optional and excluded. Point-in-time flags require
only their specified MOB. Missing DPD/chargeoffs do not count as observed.
An observed early delinquency establishes a positive even in an incomplete
window. No terminal status implicitly fills future months or proves a
fixed-horizon negative. Lifetime chargeoff additionally requires a boolean
terminal_col override: True certifies that the final gross chargeoff is
known, including any post-closure adjustments. min_mob alone is not
evidence of lifetime completion. Chargeoffs must be monthly increments,
not cumulative amounts; percentage denominators are original principal.
Prediction-to-chargeoff contract
forecast_calendar_chargeoff_from_predictions now requires an explicit prediction
kind, residual horizon and exposure basis. Earlier calls that supplied only a
score are rejected: multiplying a first-payment delinquency probability by
principal is not an estimate of gross chargeoffs.
| Prediction kind | Expected future gross chargeoffs |
|---|---|
expected_chargeoff_fraction |
Score × the declared exposure denominator. The score already includes severity. |
default_probability |
PD × explicit chargeoff_severity × the declared exposure denominator. |
| Early-delinquency probability, net-loss fraction | Rejected. A separate fitted conversion/recovery model is required. |
The prediction must describe the remaining prediction_horizon_months from
each row's snapshot. Supply a positive integer for a common horizon or a column
name for per-loan horizons. A cumulative lifetime score, a three-month early flag,
and a twelve-month default score are not interchangeable. The bridge cannot
verify how a supplied score was trained; these arguments declare a contract that
the analyst must establish from the target definition and model validation.
exposure_basis is original_principal or current_balance; principal_col
contains that denominator. Both the prediction and conditional severity must
use the same basis. Gross chargeoff severity is before recoveries, unlike
many net LGD estimates. Only loss_basis="gross_chargeoff" is supported. The
allocation holds exposure fixed; it does not model amortization, prepayment or
future balance changes.
import pandas as pd
from cranalytics.predictive import forecast_calendar_chargeoff_from_predictions
scored = pd.DataFrame({
"loan_id": ["A"], "pd_2m": [0.20], "balance": [600.0],
"snapshot": pd.to_datetime(["2024-06-15"]), "mob": [2],
})
profile = pd.DataFrame({
"segment_id": ["all", "all"], "month_on_book": [3, 4],
"chargeoff_hazard_rate": [0.2, 0.3],
})
forecast = forecast_calendar_chargeoff_from_predictions(
scored, score_col="pd_2m", as_of_col="snapshot", current_mob_col="mob",
hazard_curves=profile, principal_col="balance",
prediction_kind="default_probability", prediction_horizon_months=2,
exposure_basis="current_balance", chargeoff_severity=0.5,
)
# 0.20 * 0.50 * 600 = 60, allocated 40%/60% over the next two months.
assert abs(forecast["expected_chargeoff_amount"].sum() - 60.0) < 1e-9
assert forecast["expected_chargeoff_amount"].tolist() == [24.0, 36.0]
print(forecast.attrs["prediction_contract"])
The chargeoff hazard profile supplies timing weights, normalized over exactly
the prediction horizon; it does not supply another default probability. Every
requested month must be present, and the profile must have positive total weight.
Missing months, zero-weight profiles, duplicate segment/MOB rows and invalid
numeric inputs raise. A loan outside the profile horizon is rejected instead of
silently losing its expected dollars, even when all loans would be skipped.
strict=False still permits a missing-segment fallback to the unweighted mean
profile across segments, with a warning and the affected segments in metadata.
Output amounts are in the exposure's currency units. Each monthly rate divides
by total input exposure in principal_col. loan_count counts contributing
loans per month, not predicted defaults. attrs["prediction_contract"] records
the prediction type, horizon declaration, denominator, severity, gross-loss
basis, fallback use and expected/allocated totals. Export it separately from CSV.