Brings the Python service under version control for the first time. It has
been running and serving the demo since 23 August; until now it existed only
on one developer's disk.
Contents:
training/build_dataset.py point-in-time join over the feature store, with
leakage assertions. dscr_p10 and dscr_p50 are
excluded deliberately — both are derived from
the forecast, which is derived from the
instalment, so the model would read its own
answer.
training/train_stress.py LightGBM vs XGBoost vs two single-feature
baselines, expanding-window validation over
as-of dates, publishes to the model_metric table
training/train_forecast.py three quantile heads (P10/P50/P90), horizon as a
feature rather than one model per horizon
serving/explain.py SHAP TreeExplainer folded into the five risk
families the console already speaks. The family
map mirrors RuleBasedScoringStrategy in the API
repo; where they disagree the rule engine wins.
serving/app.py /score, /score/batch, /forecast, /health, /model
The trained models, their metrics and the training dataset are committed
alongside the code. Without them the published AUC of 0.760 is a claim nobody
else can check; at ~2.2 MB together they are worth the space.
Measured, on 5,523 rows / 263 enterprises / 242 positives / 100% synthetic
labels: LightGBM AUC-ROC 0.7599, and the emi_to_inflow baseline 0.7685 on the
same folds. The baseline wins. That is recorded rather than tuned away — on
generated labels, beating it would mostly prove the model had learned the
generator.
The quantile forecast beats the deterministic strategy on all three quantiles
(pinball loss, 12-33% better) but flags every borrower as breaching, so the
deployment runs udyam.forecast.engine=DETERMINISTIC and this model is held
back pending calibration against real repayment outcomes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
61 lines
1.4 KiB
JSON
61 lines
1.4 KiB
JSON
{
|
|
"rows": 30711,
|
|
"enterprises": 263,
|
|
"as_of_months": 22,
|
|
"horizons": 6,
|
|
"validation": "expanding-window time-series split over as-of months",
|
|
"feature_order": [
|
|
"feed_cost_change",
|
|
"output_price_change",
|
|
"margin_squeeze",
|
|
"price_z_score",
|
|
"rainfall_anomaly",
|
|
"heat_anomaly",
|
|
"cash_flow_change",
|
|
"emi_delay",
|
|
"inventory_days",
|
|
"feed_staleness_days",
|
|
"days_since_last_entry",
|
|
"emi_to_inflow",
|
|
"feed_cost_change__missing",
|
|
"output_price_change__missing",
|
|
"margin_squeeze__missing",
|
|
"price_z_score__missing",
|
|
"rainfall_anomaly__missing",
|
|
"heat_anomaly__missing",
|
|
"cash_flow_change__missing",
|
|
"emi_delay__missing",
|
|
"inventory_days__missing",
|
|
"feed_staleness_days__missing",
|
|
"days_since_last_entry__missing",
|
|
"emi_to_inflow__missing",
|
|
"trail_mean",
|
|
"trail_sd",
|
|
"trail_min",
|
|
"trail_inflow",
|
|
"trail_months",
|
|
"emi",
|
|
"horizon",
|
|
"sector_id",
|
|
"district_id"
|
|
],
|
|
"quantiles": {
|
|
"p10": {
|
|
"alpha": 0.1,
|
|
"pinball_model": 3150.78,
|
|
"pinball_deterministic": 4182.0
|
|
},
|
|
"p50": {
|
|
"alpha": 0.5,
|
|
"pinball_model": 6194.72,
|
|
"pinball_deterministic": 7061.24
|
|
},
|
|
"p90": {
|
|
"alpha": 0.9,
|
|
"pinball_model": 2770.14,
|
|
"pinball_deterministic": 4112.53
|
|
}
|
|
},
|
|
"verdict": "3/3 quantiles beat the deterministic strategy on held-out months",
|
|
"trainedAt": "2026-08-24T11:07:50.265843"
|
|
} |