Statistics for Data Science
Back to Live DS Statistical Cases

SUBMISSION: Capstone 1 — Quasi-Experimentation with Difference-in-Differences (DiD) on a Product Launch

Estimate the causal impact of a staggered city-wise feature rollout without randomization: build the DiD estimator, defend parallel trends with pre-period event-study plots, run placebo/falsification checks, and deliver a launch recommendation.

134 minutesProject submission
🎯 Free Guest Mode: You are learning for free. Sign in to save your completion progress and quiz answers.

Assignment Instructions

🎯 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:

columndtypedescription
citystrcity code
weekdateweek start (Monday)
treated0/11 for the 4 launch cities (all weeks)
post0/11 for weeks ≥ 2026-07-01
bookingsintweekly completed bookings
rainfall_mmfloatweekly rainfall (confounder: monsoon)
active_driversintsupply proxy

Stakes: all-city rollout costs ~₹2.1Cr in driver incentives. A wrong call burns the quarter's growth budget.

🎯 Required Deliverables

  1. did_analysis.ipynb — trends → DiD estimate → placebos → recommendation.
  2. did_estimates.csv — specification, estimate, se, ci_lo, ci_hi (naive diff, DiD, DiD + rainfall + drivers, event-study avg post).
  3. did_memo.md — causal estimate with CI, parallel-trends verdict, placebo results, rollout recommendation.
  4. event_study.png — leads-and-lags coefficients with CIs (pre-period should straddle 0).

🛠️ Step-by-Step Implementation Guide

  1. 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.
  2. DiD estimator. Y = α + γ·treated + λ·post + δ·(treated×post) + ε; δ is the causal estimate under parallel trends. Then add rainfall_mm + active_drivers and watch δ move — sign of confounding absorbed.
  3. Defend parallel trends. Event-study spec: interact treated with 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).
  4. Placebos. Fake treatment date (April); fake treatment group (2 control cities). Both δ̂ should be ≈0. If not, say so and downgrade the recommendation.
  5. 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.png shows 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)

#CriterionPointsWhat earns full marks
1Statistical correctness25Right distributions/tests/CIs; assumptions checked, not assumed; math is reproducible
2Code quality & reproducibility25Seeded RNG, clean re-runnable notebook, typed schemas, no hardcoded magic numbers
3Business reasoning & memo25Decision-oriented memo with costed trade-offs; honest about uncertainty and limits
4Visualization & communication25Labeled, honest axes (log-scale where skewed); figures referenced in the memo

📤 Submission Guidelines

  1. Upload your files (.ipynb, .csv, .md, .png) via the file dropzone on this lesson.
  2. Paste your public GitHub repo URL in the submission notes (repo must contain the same files + a 5-line README with the run command).
  3. Name files exactly as listed under Required Deliverables — autograding checks filenames first.
  4. Passing score is 80/100. You may resubmit; the highest score counts.
Checking your access…