🎯 Capstone Portfolio Deliverable. Randomization was impossible — the CEO launched city-wise. This capstone proves you can still do credible causal inference.
🏢 Business Scenario & Executive Prompt
RideWynk (ride-hailing, 12 cities) launched "Priority Pickup" in 4 cities (Mumbai, Delhi, Bengaluru, Hyderabad) in July; the other 8 cities launch in October. Leadership claims "+18% bookings in launch cities — the feature works!" But July is monsoon season, and launch cities are the largest metros. The board asks: "What did the feature itself cause — and should we roll it to all cities?" No randomization is possible retroactively.
📋 Source Data & Input Schemas
ridewynk_city_panel.csv — city × week panel, Jan–Sep 2026, 12 cities × 39 weeks = 468 rows:
| column | dtype | description |
|---|---|---|
city | str | city code |
week | date | week start (Monday) |
treated | 0/1 | 1 for the 4 launch cities (all weeks) |
post | 0/1 | 1 for weeks ≥ 2026-07-01 |
bookings | int | weekly completed bookings |
rainfall_mm | float | weekly rainfall (confounder: monsoon) |
active_drivers | int | supply proxy |
Stakes: all-city rollout costs ~₹2.1Cr in driver incentives. A wrong call burns the quarter's growth budget.
🎯 Required Deliverables
did_analysis.ipynb— trends → DiD estimate → placebos → recommendation.did_estimates.csv—specification, estimate, se, ci_lo, ci_hi(naive diff, DiD, DiD + rainfall + drivers, event-study avg post).did_memo.md— causal estimate with CI, parallel-trends verdict, placebo results, rollout recommendation.event_study.png— leads-and-lags coefficients with CIs (pre-period should straddle 0).
🛠️ Step-by-Step Implementation Guide
- Show the naive claim failing. Plot launch vs non-launch booking trends Jan–Sep. The +18% post gap is visible — and so is the monsoon divergence. Compute the naive post-only difference.
- DiD estimator.
Y = α + γ·treated + λ·post + δ·(treated×post) + ε; δ is the causal estimate under parallel trends. Then addrainfall_mm + active_driversand watch δ move — sign of confounding absorbed. - Defend parallel trends. Event-study spec: interact
treatedwith each pre-period week dummy; plot leads — they must be ≈0 (joint F-test reported). This is the load-bearing assumption; one paragraph on why it could still fail (Ashenfelter-dip-style selection: biggest cities chosen first). - Placebos. Fake treatment date (April); fake treatment group (2 control cities). Both δ̂ should be ≈0. If not, say so and downgrade the recommendation.
- Recommend. Roll out / hold / run a randomized holdout in 2 cities — with the rupee math.
💻 Production Starter Scaffold
import numpy as np, pandas as pd
import statsmodels.formula.api as smf
import matplotlib.pyplot as plt
rng = np.random.default_rng(31)
cities = ["MUM","DEL","BLR","HYD","CHN","KOL","PUN","AMD","JAI","LKO","KOCHI","IND"]
treated_cities = {"MUM","DEL","BLR","HYD"}
weeks = pd.date_range("2026-01-05", "2026-09-28", freq="W-MON")
rows=[]
for ci, c in enumerate(cities):
base = 8000 + ci*1500
for w in weeks:
monsoon = 1 + 0.35*np.exp(-((w.month-7)**2)/1.5) * (c in treated_cities)
post = int(w >= "2026-07-01"); tr = int(c in treated_cities)
y = base*(1 + 0.02*(w.weekofyear)) * monsoon + tr*post*900 + rng.normal(0, 400)
rows.append([c, w, tr, post, int(y),
round(float(40*monsoon + rng.normal(0,8)),1), int(base/4 + rng.normal(0,150))])
df = pd.DataFrame(rows, columns=["city","week","treated","post","bookings","rainfall_mm","active_drivers"])
m1 = smf.ols("bookings ~ treated*post", data=df).fit(cov_type="cluster", cov_kwds={"groups": df.city})
m2 = smf.ols("bookings ~ treated*post + rainfall_mm + active_drivers", data=df).fit(
cov_type="cluster", cov_kwds={"groups": df.city})
print(m1.summary().tables[1]); print(m2.summary().tables[1])
# Event study: replace post with week dummies x treated, plot leads/lags (see notebook).
🧪 Acceptance Criteria & Verification Checks
- Naive post-only gap (+18%-ish) computed and explicitly rejected with the monsoon confounder shown.
- DiD δ̂ reported for 3+ specs with cluster-robust (city) SEs; rainfall-adjusted δ̂ differs from naive.
-
event_study.pngshows pre-period leads straddling 0 + joint F-test p-value printed. - Two placebos (fake date, fake group) with ≈0 estimates documented.
- Memo gives a costed rollout recommendation conditional on the placebo verdicts.
📊 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.