Source code · Python
run.py
"""Run the credit risk lab end to end and write the figures the website shows. SQL data_layer.py + sql/ load, check and segment the 30,000 accounts Python pd_model.py WoE scorecard and validation Python portfolio_risk.py expected loss, IRB capital, Monte Carlo, stress R crosscheck.R independent refit and capital check Usage: python3 run.pyWrites app/lib/creditRisk.json in the website project."""import jsonimport subprocessfrom pathlib import Path import data_layerimport pd_modelimport portfolio_risk HERE = Path(__file__).resolve().parentOUT = HERE / "output"SITE_JSON = HERE.parents[1] / "app" / "lib" / "creditRisk.json" def check(condition: bool, message: str) -> None: if not condition: raise SystemExit(f"check failed: {message}") def rounded(value, digits=8): """Round to significant figures, so tiny cross-check gaps stay visible.""" if isinstance(value, float): return float(f"{value:.{digits}g}") if isinstance(value, dict): return {k: rounded(v, digits) for k, v in value.items()} if isinstance(value, list): return [rounded(v, digits) for v in value] return value def main() -> None: OUT.mkdir(exist_ok=True) db = OUT / "credit.db" db.unlink(missing_ok=True) con, quality, segments, features = data_layer.load(db) check(len(features) == 30_000 and int(features["default_next_month"].sum()) == 6_636, "row and default counts match the UCI documentation") model = pd_model.run(features) portfolio = portfolio_risk.run(features, model["pd"]) # Hand the same inputs to R and compare its answers with Python's. model["model_data"].to_csv(OUT / "model_data.csv", index=False) portfolio["account_level"].to_csv(OUT / "portfolio_accounts.csv", index=False) subprocess.run(["Rscript", str(HERE / "crosscheck.R"), str(OUT)], check=True, capture_output=True) r = json.loads((OUT / "r_crosscheck.json").read_text()) py_coefs = {c["term"]: c["coefficient"] for c in model["result"]["coefficients"]} coef_gap = max(abs(py_coefs[t] - r["coefficients"][t]) for t in py_coefs) auc_gap = abs(model["result"]["validation"]["test"]["auc"] - r["test_auc"]) el_gap = abs(portfolio["base"]["expected_loss"] - r["expected_loss"]) / r["expected_loss"] capital_gap = abs(portfolio["base"]["irb_capital"] - r["irb_capital"]) / r["irb_capital"] check(coef_gap < 1e-4 and auc_gap < 1e-6 and el_gap < 1e-9 and capital_gap < 1e-9, "Python and R agree on coefficients, AUC, expected loss and capital") portfolio.pop("account_level") site = { "source": { "dataset": "Default of Credit Card Clients, UCI Machine Learning Repository (id 350)", "citation": "Yeh, I. C., & Lien, C. H. (2009). Expert Systems with Applications, 36(2), 2473-2480.", "doi": "https://doi.org/10.24432/C55S3H", "license": "CC BY 4.0", "period": "Repayment history April to September 2005; default measured in October 2005", "region": "Taiwan", }, "quality": quality.to_dict(orient="records"), "segments": segments.to_dict(orient="records"), "model": model["result"], "portfolio": portfolio, "r_crosscheck": { "r_version": r["r_version"], "max_coefficient_gap": coef_gap, "test_auc_gap": auc_gap, "max_pd_gap": r["max_pd_gap"], "expected_loss_gap": el_gap, "irb_capital_gap": capital_gap, }, } SITE_JSON.write_text(json.dumps(rounded(site), indent=1) + "\n") print(f"wrote {SITE_JSON}") print(f"test AUC {model['result']['validation']['test']['auc']:.4f} | R gap {auc_gap:.2e} | " f"coefficient gap {coef_gap:.2e} | capital gap {capital_gap:.2e}") if __name__ == "__main__": main()