SPB Git
3.9 KB · 89 lines python
1"""Survival analysis of project lifecycles.23Among multi-filing projects (tracked across >=2 filings), we measure the span4from first to last appearance. Event = the project reaches 'completed' status;5projects last observed as planned/in-progress are right-censored. Kaplan-Meier6curves by project family and a Cox proportional-hazards model.7"""8from __future__ import annotations910import warnings11import numpy as np12import pandas as pd13import statsmodels.api as sm14from statsmodels.duration.survfunc import SurvfuncRight15from statsmodels.duration.hazard_regression import PHReg1617from common import DATA, set_style, savefig, write_table, save_results18import matplotlib.pyplot as plt1920warnings.filterwarnings("ignore")21set_style()2223p = pd.read_parquet(DATA / "projects.parquet")24d = p[(p["n_filings"] >= 2) & (p["duration_days"] > 0)].copy()25d["months"] = d["duration_days"] / 30.4426d["event"] = (d["status"] == "completed").astype(int)27d["log_amount_f"] = np.log(d["total_amount_usd"].fillna(d["total_amount_usd"].median()).clip(lower=1))28d["is_ai_i"] = d["is_ai"].astype(int)2930# ---- Kaplan-Meier by project family --------------------------------------31FAMILIES = {32    "M&A integration": ["ma_integration"],33    "Capex / construction": ["capex_program", "plant_construction", "infrastructure"],34    "R&D / product": ["rd_program", "product_launch", "drug_pipeline"],35    "Digital / AI": ["ai_initiative", "digital_transformation", "cloud_migration",36                     "data_center", "automation", "technology_deployment"],37}38fig, ax = plt.subplots(figsize=(7.4, 4.4))39median_surv = {}40for (lbl, types), color in zip(FAMILIES.items(), ["#1b3a5b", "#c44e52", "#2e8b57", "#8172b3"]):41    sub = d[d["project_type"].isin(types)]42    if len(sub) < 20:43        continue44    sf = SurvfuncRight(sub["months"], sub["event"])45    ax.step(sf.surv_times, sf.surv_prob, where="post", label=f"{lbl} (n={len(sub)})", color=color, lw=1.8)46    # median survival47    below = sf.surv_times[sf.surv_prob <= 0.5]48    median_surv[lbl] = float(below[0]) if len(below) else float("nan")49ax.set_xlabel("Months since first appearance in filings")50ax.set_ylabel("Share not yet completed")51ax.set_title("Kaplan-Meier project-survival curves by family")52ax.legend(fontsize=8)53ax.set_xlim(0, d["months"].quantile(0.98))54savefig(fig, "fig_survival_km.pdf")5556# ---- Cox proportional hazards --------------------------------------------57base = "ma_integration"58X = pd.get_dummies(d["project_type"].astype(str), prefix="pt", drop_first=False, dtype=float)59X = X.drop(columns=[f"pt_{base}"], errors="ignore")          # baseline category60X = X.loc[:, X.std() > 0]                                     # drop all-zero/constant dummies61X["is_ai"] = d["is_ai_i"].values62X["log_amount"] = d["log_amount_f"].values63X["n_tech"] = d["n_tech"].values64X = X.loc[:, X.std() > 0]65cox = PHReg(d["months"].values, X.values.astype(float), status=d["event"].values).fit()66params = pd.Series(cox.params, index=X.columns)67bse = pd.Series(cox.bse, index=X.columns)68pvals = pd.Series(cox.pvalues, index=X.columns)6970SHOW = ["is_ai", "log_amount", "n_tech"]71lt = [r"\begin{tabular}{lcc}", r"\toprule",72      r"Covariate & Hazard ratio & $p$-value \\", r"\midrule"]73for k in SHOW:74    hr = np.exp(params[k]); pv = pvals[k]75    star = "***" if pv < .01 else "**" if pv < .05 else "*" if pv < .1 else ""76    lt.append(f"{k.replace('_',' ')} & {hr:.3f}{star} & {pv:.3f} \\\\")77lt += [r"\midrule", r"Project-type controls & Yes & \\",78       f"Projects (events) & {int(d['event'].sum()):,} of {len(d):,} & \\\\",79       r"\bottomrule", r"\end{tabular}"]80write_table("tab_cox", "\n".join(lt))8182save_results("survival", {83    "n": int(len(d)), "events": int(d["event"].sum()),84    "median_survival_months": median_surv,85    "cox_is_ai_hr": float(np.exp(params["is_ai"])), "cox_is_ai_p": float(pvals["is_ai"]),86    "cox_logamount_hr": float(np.exp(params["log_amount"])), "cox_logamount_p": float(pvals["log_amount"]),87})88print(f"survival done | n={len(d):,}, events={int(d['event'].sum()):,}")89
Files · main
Menu

Theme