Python port of the core modelling from my Master's thesis: predicting how
long a customer order takes to become an invoice. The original data was
real ERP data from a manufacturing company, which obviously can't go in a
public repo, so everything here runs on a synthetic dataset built to mirror
the same variables and roughly the same hazard ratios the original Cox
model found. Business line and item-type values are generic labels
(LINE A, ITEM 1, and so on) rather than the real category names, so the
company stays unidentifiable.
Some of the orders in the dataset haven't been invoiced yet by the time the data was pulled. A regular regression on "days until invoiced" doesn't know what to do with that: drop those rows and you bias the sample toward fast orders, treat them as already resolved and you're making up an outcome that hasn't happened. Survival analysis is built for exactly this situation through censoring — an order still open just tells you it survived at least this long, nothing more, nothing less. That's why the models here are Kaplan-Meier curves and hazard-based models (Cox, Random Survival Forest, Gradient Boosting) instead of a regression.
Three models, same 5-fold cross-validation:
| Model | Cross-validated C-index (5 folds) |
|---|---|
| Cox proportional hazards | 0.741 |
| Random Survival Forest | 0.726 |
| Gradient Boosting (tree-based, Cox loss) | 0.742 |
They land close to each other, within about a point and a half. In the original thesis the Random Survival Forest scored noticeably worse than the other two, but that was mostly an artifact of the R package available at the time not handling categorical variables directly — it forced an ordinal encoding that penalized the forest. One-hot encoding in Python gets rid of most of that gap. Worth flagging though: this doesn't prove the real invoicing process is additive, since the synthetic data is itself generated from an additive log-hazard, so there's no interaction structure for the tree models to find in the first place. More on that in the notebook 2 conclusion.
- Builds a synthetic order dataset with the same structure as the original ERP data (business line, requested lead time, delivery status, etc.)
- Calibrates the synthetic time-to-invoice distribution against the real Kaplan-Meier curve from the thesis at day 30
- Fits and cross-validates Cox, Random Survival Forest, and Gradient Boosting, all with the same 5-fold split
- Revisits the RSF encoding issue from the thesis with proper one-hot encoding instead of the ordinal encoding R forced
01_synthetic_data.ipynb— builds and calibrates the synthetic dataset, savessynthetic_orders.csv02_survival_models.ipynb— Kaplan-Meier, Cox, Random Survival Forest, Gradient Boosting, and the comparison
The dataset is synthetic — generated from an exponential time-to-event distribution calibrated to match the real Kaplan-Meier curve at a single point (day 30). It reproduces the general shape and the modelling pipeline, not the real data itself; the actual observed times have an early peak and a long tail that a simple distribution doesn't capture.
The additivity argument in the results section is also weaker than it might look at first glance: since the synthetic log-hazard is a linear sum of effects by construction, three models converging on a similar C-index mostly confirms the encoding fix works, not that the real process is additive.
Python, lifelines for Kaplan-Meier and Cox, scikit-survival for the Random Survival Forest and Gradient Boosting, plus pandas, numpy, scipy and matplotlib.
pip install -r requirements.txt
jupyter labRun 01_synthetic_data.ipynb first, then 02_survival_models.ipynb.




