Portfolio laboratory

Python projects

Exploratory analysis, prediction and operational modelling translated into commercial actions.

All datasets and commercial scenarios on this page are synthetic and created for portfolio demonstration.

Choose a case.

Each case includes the business question, source structure, working method, selected code or decision logic, measured result, recommendation and limitations.

Synthetic case studyPython

Customer churn risk prioritisation

Which customers need intervention before the renewal date?

A practical classification workflow that balances predictive performance with the commercial need to explain who is at risk and why.

0.694
ROC-AUC
15.2%
base churn rate
75.8%
churn in highest-risk decile
View full case study
Synthetic case studyPython

Demand forecast & reorder policy

How much stock protects service without locking up cash?

A six-SKU forecasting and safety-stock model with a transparent holdout test and a simulated operational impact check.

22.3%
holdout MAPE
90.9%
simulated stockout reduction
318
highest reorder point
View full case study
Case 01
Synthetic case study1,200 customer recordsPython · pandas · scikit-learn · Model evaluation

Customer churn risk prioritisation

Which customers need intervention before the renewal date?

A practical classification workflow that balances predictive performance with the commercial need to explain who is at risk and why.

0.694
ROC-AUC
15.2%
base churn rate
75.8%
churn in highest-risk decile

Business context

A subscription business has limited retention capacity. The goal is not merely to predict churn, but to rank a practical intervention queue and explain the operational signals behind it.

Questions tested

  1. Can behaviour, service and contract data distinguish likely churners?
  2. How concentrated is observed churn in the highest-risk decile?
  3. Which interpretable signals should trigger a retention conversation?

Working method

From raw information to a decision.

  1. 01

    Validated identifiers, missingness and the 15.2% target prevalence.

  2. 02

    Used a stratified 75/25 split and fitted a preprocessing/model pipeline to prevent leakage.

  3. 03

    Evaluated both discrimination (ROC-AUC) and threshold accuracy.

  4. 04

    Scored the full population and converted probability into operational risk bands.

Dataset

Visible grain, fields and sample.

contract_typeMonthly, annual or two-year
tenure_monthsCustomer age
support_tickets_90dRecent service friction
usage_scoreProduct engagement, 0–100
payment_failures_12mRecent payment instability
churnedObserved binary outcome
CustomerRegionContractTenureTicketsUsageChurned
CU-00001NorthMonthly28253No
CU-00002SouthMonthly72123No
CU-00003LondonTwo-year21079No

Preview shows three records; the complete synthetic dataset is available above.

Selected workingPython
model = Pipeline([
    ("prep", ColumnTransformer([
        ("num", StandardScaler(), numeric),
        ("cat", OneHotEncoder(handle_unknown="ignore"), categorical),
    ])),
    ("model", RandomForestClassifier(
        n_estimators=220, min_samples_leaf=5, random_state=1210
    )),
])
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=.25, stratify=y, random_state=1210
)
model.fit(X_train, y_train)
risk_probability = model.predict_proba(X_test)[:, 1]

Result

Highest-risk decile75.8% churn
Monthly contract19.8% churn
Portfolio average15.2% churn
Two-year contract7.3% churn
  • The holdout ROC-AUC was 0.694. A 0.50 cut-off produced 85.0% headline accuracy but no positive predictions, exposing why accuracy is unsafe for this imbalanced problem.
  • Observed churn reached 75.8% in the highest-risk decile, materially concentrating intervention effort.
  • Monthly contracts churned at 19.8% versus 7.3% for two-year contracts in the synthetic population.

Recommendation

Contact the highest-risk decile first, pair the score with recent support and usage context, and test at least two retention treatments against a control group.

Limits & next evidence

The synthetic target was generated from known relationships and does not prove causal drivers. Production use would require fairness checks, calibration, drift monitoring and treatment-uplift measurement.

Case 02
Synthetic case study4,380 SKU-day recordsPython · Time series · Regression · Inventory policy

Demand forecast & reorder policy

How much stock protects service without locking up cash?

A six-SKU forecasting and safety-stock model with a transparent holdout test and a simulated operational impact check.

22.3%
holdout MAPE
90.9%
simulated stockout reduction
318
highest reorder point

Business context

A small distributor wants fewer stockouts without treating every SKU as equally predictable. The case joins demand forecasting with a transparent service-level reorder rule.

Questions tested

  1. How well can a simple, explainable model forecast the last 60 days?
  2. How should demand variability and lead time change safety stock?
  3. What is the simulated service impact versus a naive five-day rule?

Working method

From raw information to a decision.

  1. 01

    Created trend, day-of-week, month and promotion features for each SKU.

  2. 02

    Held out the final 60 days, preserving time order.

  3. 03

    Calculated reorder points as lead-time demand plus 1.65 standard deviations of safety stock.

  4. 04

    Compared rolling stockout events against a naive replenishment rule.

Dataset

Visible grain, fields and sample.

date / skuDaily product grain
units_soldObserved daily demand
lead_time_daysSupplier replenishment delay
unit_holding_cost_gbpIllustrative carrying cost
promotion_flagKnown demand intervention
DateSKUUnitsLead timeHolding costPromotion
01 Jan 2024SKU-A296 days£6.20Yes
02 Jan 2024SKU-A146 days£6.20No
03 Jan 2024SKU-A206 days£6.20No

Preview shows three records; the complete synthetic dataset is available above.

Selected workingPython
train, holdout = history.iloc[:-60], history.iloc[-60:]
model = LinearRegression().fit(train[features], train["units_sold"])
prediction = np.clip(model.predict(holdout[features]), 1, None)

recent = history.tail(90)["units_sold"]
expected_lead_demand = recent.mean() * lead_time
safety_stock = 1.65 * recent.std() * sqrt(lead_time)
reorder_point = round(expected_lead_demand + safety_stock)

Result

  • The six-SKU portfolio produced 22.3% mean holdout MAPE—good enough for policy testing, not precision planning.
  • The safety-stock rule reduced simulated stockout events by 90.9% versus the naive benchmark.
  • SKU-D required the highest reorder point at 318 units because of its volume and lead time.

Recommendation

Pilot the new rule on high-volume SKUs, track stockout rate and excess inventory together, and review service factors by item criticality rather than assuming one service level.

Limits & next evidence

The simulation excludes supplier minimums, shelf life, substitution, capacity constraints and forecast bias. It estimates policy direction, not a production purchase order.