🎯 Hands-on Portfolio Deliverable. Regression is the most-used model in DS — and the most misread. This project proves you check assumptions instead of quoting R².
🏢 Business Scenario & Executive Prompt
CloudNine SaaS (50,000 accounts, 4.8% quarterly churn) wants to know why customers leave.
The PM's draft model regresses churn on 12 features and reports R² = 0.31 with "all p-values significant."
You spot sessions_per_week, logins_per_week, and active_days_per_week in the same model — plus a suspicion that tenure effects differ by plan tier.
The VP Sales asks: "Which three levers actually move churn, and how much do you trust each number?"
📋 Source Data & Input Schemas
cloudnine_churn.csv — 50,000 rows (mock shape; scaffold generates it):
| column | dtype | description |
|---|---|---|
acct_id | str | unique account |
churned | 0/1 | churned this quarter (4.8% base rate) |
tenure_months | int | account age |
plan | category | basic | pro | enterprise |
sessions_pw | float | sessions/week (collinear trio #1) |
logins_pw | float | logins/week (collinear trio #2) |
active_days_pw | float | active days/week (collinear trio #3) |
support_tickets | int | tickets this quarter |
contract_value_inr | float | ARR in ₹ |
🎯 Required Deliverables
churn_regression.ipynb— diagnostics → VIF triage → interactions → validation notebook.churn_model.py— final OLS specification (formula + fit +get_summary()printing VIF table).vif_report.csv—feature, vif_before, vif_after, action (keep \| drop \| combine).regression_memo.md— three levers with per-lever effect sizes + CIs, assumption verdicts, and what you would NOT claim.
🛠️ Step-by-Step Implementation Guide
- Baseline + residual diagnostics. Fit OLS on all 12 features (statsmodels). Residual-vs-fitted, Q-Q, scale-location plots. Name each violated assumption in plain language.
- VIF triage. Compute VIF for all features; the engagement trio will print VIF > 10. Decide per feature: drop, combine (e.g.,
engagement_index = z-mean), or keep-with-HC-SEs. Record invif_report.csv; refit and show coefficient stability (before/after table). - Interactions. Test
tenure_months × plan(enterprise tenure behaves differently). F-test for the interaction block; interpret one interaction coefficient as "for enterprise, each extra month changes churn log-odds by…". - Partial-effect interpretation. Every reported coefficient stated as "holding others constant" with 95% CI. One paragraph on why the PM's "all p-values significant" claim is wrong under multicollinearity.
- Memo. Three levers ranked by standardized effect; honest limits (observational, no causation).
💻 Production Starter Scaffold
import numpy as np, pandas as pd
import statsmodels.api as sm
from statsmodels.stats.outliers_influence import variance_inflation_factor
rng = np.random.default_rng(21)
N = 50_000
tenure = rng.integers(1, 60, N)
plan = rng.choice(["basic","pro","enterprise"], N, p=[.55,.3,.15])
sess = rng.gamma(4, 1.2, N) + (plan=="enterprise")*2
df = pd.DataFrame({
"acct_id": [f"CN-{i:05d}" for i in range(N)],
"tenure_months": tenure, "plan": plan,
"sessions_pw": sess,
"logins_pw": sess*1.4 + rng.normal(0,.5,N), # collinear by construction
"active_days_pw": (sess/2).clip(0,7) + rng.normal(0,.3,N),
"support_tickets": rng.poisson(1.2, N),
"contract_value_inr": np.round(rng.lognormal(10.5,.8,N),2),
})
logit = -3 + -0.02*tenure + (plan=="pro")*-0.4 + (plan=="enterprise")*-0.9 \
+ 0.15*df.support_tickets - 0.1*sess + (plan=="enterprise")*tenure*-0.01
df["churned"] = (rng.uniform(size=N) < 1/(1+np.exp(-logit))).astype(int)
print("churn rate:", df.churned.mean().round(4))
# --- VIF triage ---
X = pd.get_dummies(df[["tenure_months","sessions_pw","logins_pw","active_days_pw",
"support_tickets","contract_value_inr","plan"]], drop_first=True).astype(float)
vif = pd.DataFrame({"feature": X.columns,
"vif": [variance_inflation_factor(X.to_numpy(), i) for i in range(X.shape[1])]})
print(vif.sort_values("vif", ascending=False).head(8))
🧪 Acceptance Criteria & Verification Checks
-
vif_report.csvlists all features with before/after VIF and a justified action per feature. - Final model has max VIF < 5 and a before/after coefficient-stability table in the notebook.
-
tenure × planinteraction tested with an F-test; one interaction coefficient interpreted correctly. - Residual diagnostic trio (residual-vs-fitted, Q-Q, scale-location) present with verdicts.
- Memo names three levers with CIs and explicitly disclaims causal reading.
📊 Evaluation Rubric (100 points — passing ≥ 80)
| # | Criterion | Points | What earns full marks |
|---|---|---|---|
| 1 | Statistical correctness | 25 | Right distributions/tests/CIs; assumptions checked, not assumed; math is reproducible |
| 2 | Code quality & reproducibility | 25 | Seeded RNG, clean re-runnable notebook, typed schemas, no hardcoded magic numbers |
| 3 | Business reasoning & memo | 25 | Decision-oriented memo with costed trade-offs; honest about uncertainty and limits |
| 4 | Visualization & communication | 25 | Labeled, honest axes (log-scale where skewed); figures referenced in the memo |
📤 Submission Guidelines
- Upload your files (
.ipynb,.csv,.md,.png) via the file dropzone on this lesson. - Paste your public GitHub repo URL in the submission notes (repo must contain the same files + a 5-line
READMEwith the run command). - Name files exactly as listed under Required Deliverables — autograding checks filenames first.
- Passing score is 80/100. You may resubmit; the highest score counts.