From c40a15442a0dfba4208d2c1ff6baac8d232c6621 Mon Sep 17 00:00:00 2001 From: igerber Date: Thu, 9 Apr 2026 20:11:28 -0400 Subject: [PATCH] Add practitioner decision tree and getting started guide (B1b-d) Phase B1 foundation docs making diff-diff discoverable for data science practitioners. Business-framed estimator selection, end-to-end marketing campaign walkthrough, and README entry point. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 10 + ROADMAP.md | 6 +- docs/doc-deps.yaml | 29 +++ docs/index.rst | 17 ++ docs/practitioner_decision_tree.rst | 291 ++++++++++++++++++++++++ docs/practitioner_getting_started.rst | 308 ++++++++++++++++++++++++++ 6 files changed, 658 insertions(+), 3 deletions(-) create mode 100644 docs/practitioner_decision_tree.rst create mode 100644 docs/practitioner_getting_started.rst diff --git a/README.md b/README.md index 1bc95209..82fa025a 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,16 @@ After estimation, call `practitioner_next_steps(results)` for context-aware guid Detailed guide: [`docs/llms-practitioner.txt`](docs/llms-practitioner.txt) +## For Data Scientists + +Measuring campaign lift? Evaluating a product launch? diff-diff handles the causal inference so you can focus on the business question. + +- **[Which method fits my problem?](docs/practitioner_decision_tree.rst)** - Start from your business scenario (campaign in some markets, staggered rollout, survey data) and find the right estimator +- **[Getting started for practitioners](docs/practitioner_getting_started.rst)** - End-to-end walkthrough: marketing campaign -> causal estimate -> stakeholder-ready result +- **[Brand awareness survey tutorial](docs/tutorials/17_brand_awareness_survey.ipynb)** - Full example with complex survey design, brand funnel analysis, and staggered rollouts + +Already know DiD? The [academic quickstart](docs/quickstart.rst) and [estimator guide](docs/choosing_estimator.rst) cover the full technical details. + ## Features - **sklearn-like API**: Familiar `fit()` interface with `get_params()` and `set_params()` diff --git a/ROADMAP.md b/ROADMAP.md index 5f9626a3..d4f5e5a6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -76,9 +76,9 @@ Parallel track targeting data science practitioners — marketing, product, oper | Item | Priority | Status | |------|----------|--------| | **B1a.** Brand Awareness Survey DiD tutorial — lead use case showcasing unique survey support | HIGH | Done (Tutorial 17) | -| **B1b.** README "For Data Scientists" section alongside "For Academics" and "For AI Agents" | HIGH | Not started | -| **B1c.** Practitioner decision tree — "which method should I use?" framed for business contexts | HIGH | Not started | -| **B1d.** "Getting Started" guide for practitioners with business ↔ academic terminology bridge | MEDIUM | Not started | +| **B1b.** README "For Data Scientists" section alongside "For Academics" and "For AI Agents" | HIGH | Done | +| **B1c.** Practitioner decision tree — "which method should I use?" framed for business contexts | HIGH | Done | +| **B1d.** "Getting Started" guide for practitioners with business ↔ academic terminology bridge | MEDIUM | Done | ### Phase B2: Practitioner Content diff --git a/docs/doc-deps.yaml b/docs/doc-deps.yaml index 1528000f..473ee1e5 100644 --- a/docs/doc-deps.yaml +++ b/docs/doc-deps.yaml @@ -101,6 +101,10 @@ sources: type: user_guide - path: docs/benchmarks.rst type: performance + - path: docs/practitioner_getting_started.rst + type: user_guide + - path: docs/practitioner_decision_tree.rst + type: user_guide diff_diff/twfe.py: drift_risk: low @@ -141,6 +145,12 @@ sources: - path: docs/survey-roadmap.md type: roadmap note: "Phase 4/6/7 CS survey sections" + - path: docs/practitioner_getting_started.rst + section: "What If Your Campaign Rolled Out in Waves?" + type: user_guide + - path: docs/practitioner_decision_tree.rst + section: "Staggered Rollout" + type: user_guide # ── StaggeredTripleDifference (staggered_triple_diff group) ───────── @@ -265,6 +275,9 @@ sources: type: user_guide - path: docs/choosing_estimator.rst type: user_guide + - path: docs/practitioner_decision_tree.rst + section: "Varying Spending Levels" + type: user_guide # ── SyntheticDiD ───────────��─────────────────────────────────────── @@ -284,6 +297,9 @@ sources: - path: docs/llms-full.txt section: "SyntheticDiD" type: user_guide + - path: docs/practitioner_decision_tree.rst + section: "Few Test Markets" + type: user_guide - path: docs/choosing_estimator.rst type: user_guide - path: docs/benchmarks.rst @@ -471,6 +487,12 @@ sources: - path: docs/choosing_estimator.rst section: "Survey Design Support" type: user_guide + - path: docs/practitioner_decision_tree.rst + section: "Survey Data" + type: user_guide + - path: docs/practitioner_getting_started.rst + section: "What If You Have Survey Data?" + type: user_guide # ── Infrastructure ───────────────────────────────────────────────── @@ -493,6 +515,9 @@ sources: - path: docs/methodology/REGISTRY.md section: "Inference, safe_inference NaN gating" type: methodology + - path: docs/practitioner_getting_started.rst + section: "Step 4: Check Whether the Result Is Trustworthy" + type: user_guide - path: CLAUDE.md section: "safe_inference pattern" type: internal @@ -531,6 +556,10 @@ sources: docs: - path: docs/api/prep.rst type: api_reference + - path: docs/practitioner_getting_started.rst + type: user_guide + - path: docs/practitioner_decision_tree.rst + type: user_guide diff_diff/datasets.py: drift_risk: low diff --git a/docs/index.rst b/docs/index.rst index e64205d1..de65253e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,6 +44,8 @@ For development: Quick Links ----------- +- :doc:`practitioner_getting_started` - Measuring campaign impact? Start here +- :doc:`practitioner_decision_tree` - Which method fits your business problem? - :doc:`quickstart` - Installation and your first DiD analysis - :doc:`choosing_estimator` - Which estimator should I use? - :doc:`tutorials/01_basic_did` - Hands-on basic tutorial @@ -51,6 +53,14 @@ Quick Links - :doc:`r_comparison` - Coming from R? - :doc:`api/index` - Full API reference +.. toctree:: + :maxdepth: 2 + :caption: For Data Scientists + :hidden: + + practitioner_getting_started + practitioner_decision_tree + .. toctree:: :maxdepth: 2 :caption: Getting Started @@ -60,6 +70,13 @@ Quick Links choosing_estimator troubleshooting +.. toctree:: + :maxdepth: 1 + :caption: Tutorials: Business Applications + :hidden: + + tutorials/17_brand_awareness_survey + .. toctree:: :maxdepth: 1 :caption: Tutorials: Fundamentals diff --git a/docs/practitioner_decision_tree.rst b/docs/practitioner_decision_tree.rst new file mode 100644 index 00000000..484083de --- /dev/null +++ b/docs/practitioner_decision_tree.rst @@ -0,0 +1,291 @@ +.. meta:: + :description: Which Difference-in-Differences method fits your business problem? Match your campaign, product launch, or pricing scenario to the right analysis method in diff-diff. + :keywords: which DiD method, python marketing campaign analysis, geo experiment method, campaign lift measurement, business causal inference, staggered rollout analysis + +Which Analysis Method Fits Your Problem? +======================================== + +You ran a campaign, launched a product, or changed pricing in some markets. You need +to know whether it worked - and by how much. This guide matches your situation to the +right analysis method. No econometrics background required. + +Start Here +---------- + +Which of these best describes your situation? + +1. **My campaign launched in all test markets at the same time** + + Your treatment started on the same date everywhere. Go to + :ref:`section-simultaneous`. + +2. **My campaign rolled out in waves** (some markets in March, more in June, etc.) + + Different markets started at different times. Go to + :ref:`section-staggered`. + +3. **I varied spending levels across markets** (e.g., $50K, $100K, $200K) + + You want to know how the effect changes with the amount spent. Go to + :ref:`section-dose`. + +4. **I only have 3-5 test markets** + + Too few treated units for standard methods. Go to + :ref:`section-few-markets`. + +5. **I have survey data** (brand tracking, customer satisfaction, etc.) + + Your outcome comes from a survey with complex sampling. Go to + :ref:`section-survey`. + +.. tip:: + + In academic literature, "rolling out in waves" is called *staggered adoption*, + and markets are called *units*. You will see these terms in the detailed + documentation, but this guide uses business language throughout. + + +.. _section-simultaneous: + +Campaign Launched Simultaneously +-------------------------------- + +**Your situation:** You launched the campaign on the same date in all test markets. +You have outcome data (sales, signups, etc.) for both test and control markets, before +and after the launch. + +**Recommended method:** :class:`~diff_diff.DifferenceInDifferences` + +This is the simplest and most interpretable approach. It compares the before/after +change in your test markets to the before/after change in your control markets. + +.. code-block:: python + + from diff_diff import DifferenceInDifferences, generate_did_data + + # 20 markets, 12 months, campaign launches at month 7 in 8 markets + data = generate_did_data( + n_units=20, n_periods=12, treatment_effect=5.0, + treatment_fraction=0.4, treatment_period=7, seed=42, + ) + + did = DifferenceInDifferences() + results = did.fit(data, outcome="outcome", treatment="treated", time="post") + print(f"Campaign lift: {results.att:.1f} (p = {results.p_value:.4f})") + +.. note:: + + **Academic term:** This is the classic *2x2 DiD* design. The estimate is called the + *ATT* (Average Treatment Effect on the Treated) - it tells you the average lift + among the markets that received the campaign. + +**When to upgrade:** + +- If you have many time periods and want unit-level controls: + :class:`~diff_diff.TwoWayFixedEffects` +- If you want to see how the effect evolves over time (week by week): + :class:`~diff_diff.MultiPeriodDiD` + + +.. _section-staggered: + +Staggered Rollout +----------------- + +**Your situation:** Your campaign launched in some markets in one month, more markets +a few months later, and so on. Different markets were treated at different times. + +**Recommended method:** :class:`~diff_diff.CallawaySantAnna` + +.. warning:: + + Do **not** use basic DiD or TWFE for staggered rollouts. When markets are treated + at different times, these methods can compare already-active markets to newly-launched + ones - giving biased (potentially wrong-sign) results. Callaway-Sant'Anna avoids this + by comparing each wave only to true control markets. + +.. code-block:: python + + from diff_diff import CallawaySantAnna, generate_staggered_data + + # Campaign launches in wave 1 markets at month 4, wave 2 at month 7 + data = generate_staggered_data( + n_units=20, n_periods=12, cohort_periods=[4, 7], + never_treated_frac=0.4, treatment_effect=5.0, seed=42, + ) + + cs = CallawaySantAnna() + results = cs.fit( + data, outcome="outcome", unit="unit", + time="period", first_treat="first_treat", + ) + print(f"Overall campaign lift: {results.overall_att:.1f}") + +.. note:: + + **Academic term:** This is a *staggered adoption* design with *heterogeneous treatment + timing*. Callaway & Sant'Anna (2021) is the standard method. The *ATT(g,t)* gives you + the lift for each rollout wave at each time period, and the *overall ATT* aggregates + them into a single number. + + +.. _section-dose: + +Varying Spending Levels +----------------------- + +**Your situation:** You spent different amounts across markets - $50K in some, $100K in +others, $200K in others. You want to know how the effect changes with spending level. + +**Recommended method:** :class:`~diff_diff.ContinuousDiD` + +Instead of a single "did it work?" answer, this gives you a dose-response curve showing +how the lift changes with the amount spent. + +.. code-block:: python + + from diff_diff import ContinuousDiD, generate_continuous_did_data + + # Markets with varying spending levels (dose) + data = generate_continuous_did_data(n_units=100, n_periods=4, seed=42) + + cdid = ContinuousDiD() + results = cdid.fit( + data, outcome="outcome", unit="unit", + time="period", first_treat="first_treat", dose="dose", + ) + print(f"Average lift across dose levels: {results.overall_att:.1f}") + +.. note:: + + **Academic term:** This is a *continuous treatment* DiD (Callaway, Goodman-Bacon & + Sant'Anna 2024). The *dose* is the spending level. The estimate *ATT(d)* gives + you the lift at each spending level, not just an average. + + +.. _section-few-markets: + +Few Test Markets +---------------- + +**Your situation:** You have only 3-5 test markets and 15-50 controls. Standard methods +struggle because there are too few treated units to estimate reliably. + +**Recommended method:** :class:`~diff_diff.SyntheticDiD` + +This method constructs a weighted blend of control markets that closely tracks your test +markets before the campaign. The "synthetic control" provides a better counterfactual than +a simple average of all controls. + +.. code-block:: python + + from diff_diff import SyntheticDiD, generate_did_data + + # Only 3 test markets out of 20 (treatment_fraction=0.15) + data = generate_did_data( + n_units=20, n_periods=12, treatment_effect=5.0, + treatment_fraction=0.15, treatment_period=7, seed=42, + ) + + sdid = SyntheticDiD() + results = sdid.fit( + data, outcome="outcome", unit="unit", + time="period", treatment="treated", + ) + print(f"Campaign lift: {results.att:.1f} (SE = {results.se:.2f})") + +.. note:: + + **Academic term:** *Synthetic Difference-in-Differences* (Arkhangelsky et al. 2021) + combines the synthetic control method with DiD. It finds unit weights and time weights + that minimize pre-treatment differences, then estimates the treatment effect using those + weights. + + +.. _section-survey: + +Survey Data +----------- + +**Your situation:** Your outcome comes from a survey - brand tracking, customer +satisfaction, NPS, or similar. The survey uses stratified sampling, clustering (e.g., +by geography), or probability weights. + +**Answer:** Use any of the methods above, combined with +:class:`~diff_diff.SurveyDesign`. + +Ignoring survey weights and clustering makes your confidence intervals too narrow - +you will be overconfident about the result. Passing a ``SurveyDesign`` to ``fit()`` +corrects for this automatically. + +.. code-block:: python + + from diff_diff import DifferenceInDifferences, SurveyDesign + + survey = SurveyDesign( + data=data, + strata="stratum", # sampling strata + psu="cluster_id", # primary sampling unit (e.g., geography) + weight="sample_weight", + ) + + did = DifferenceInDifferences() + results = did.fit( + data, outcome="outcome", treatment="treated", + time="post", survey_design=survey, + ) + +.. tip:: + + For a full walkthrough with brand funnel metrics and staggered rollouts, see + `Tutorial 17: Brand Awareness Survey + `_. + + +At a Glance +----------- + +.. list-table:: + :header-rows: 1 + :widths: 35 30 35 + + * - Your Situation + - Recommended Method + - Key Benefit + * - Campaign in some markets, all at once + - ``DifferenceInDifferences`` + - Simple and interpretable + * - Staggered rollout (waves) + - ``CallawaySantAnna`` + - Handles different launch dates correctly + * - Varied spending levels + - ``ContinuousDiD`` + - Dose-response curve + * - Only a few test markets + - ``SyntheticDiD`` + - Optimal with few treated units + * - Survey data (any design above) + - Any + ``SurveyDesign`` + - Correct confidence intervals + + +What About the Other Estimators? +-------------------------------- + +diff-diff has 16 estimators covering advanced scenarios: Sun-Abraham for +interaction-weighted estimation, Imputation DiD and Two-Stage DiD for alternative +staggered approaches, Stacked DiD, Efficient DiD, Triple Difference, TROP, and more. +The five scenarios above cover the most common business use cases. + +For the full academic decision tree with all estimators, see :doc:`choosing_estimator`. + + +Next Steps +---------- + +- :doc:`practitioner_getting_started` - Walk through a complete analysis end-to-end +- `Tutorial 17: Brand Awareness Survey `_ - + Full example with survey design, brand funnel metrics, and staggered rollouts +- :doc:`quickstart` - Academic quickstart (if you already know DiD methodology) +- :doc:`api/index` - Full API reference diff --git a/docs/practitioner_getting_started.rst b/docs/practitioner_getting_started.rst new file mode 100644 index 00000000..0cdb4cb9 --- /dev/null +++ b/docs/practitioner_getting_started.rst @@ -0,0 +1,308 @@ +.. meta:: + :description: Measure marketing campaign impact with Python. Step-by-step guide to Difference-in-Differences for data scientists - from data to stakeholder-ready results. + :keywords: python measure campaign impact, marketing DiD tutorial, python campaign lift, causal inference for data scientists, python geo experiment + +Getting Started: Measuring Campaign Impact +========================================== + +Your company ran a marketing campaign in 8 of 20 metro markets. Sales data is +available for all markets before and after the campaign. Leadership wants to know: +**did the campaign work, and by how much?** + +This guide walks through the entire analysis - from data to a stakeholder-ready result. + + +What You'll Need +---------------- + +- Python 3.9+ +- diff-diff installed (``pip install diff-diff``) +- About 15 minutes + +.. code-block:: bash + + pip install diff-diff + + +Step 1: Set Up the Data +----------------------- + +We will use diff-diff's data generator to create a realistic scenario: 20 markets +tracked over 12 months, with a campaign that launches in month 7 in 8 of the 20 +markets. The true sales lift is 5 units per market. + +.. code-block:: python + + from diff_diff import DifferenceInDifferences, generate_did_data + + data = generate_did_data( + n_units=20, # 20 metro markets + n_periods=12, # 12 months of data + treatment_effect=5.0, # true lift: 5 units per market + treatment_fraction=0.4, # 8 of 20 markets got the campaign + treatment_period=7, # campaign launches in month 7 + seed=42, + ) + + print(data.head(10)) + print(f"\nMarkets: {data['unit'].nunique()}") + print(f"Campaign markets: {data.loc[data['treated'] == 1, 'unit'].nunique()}") + print(f"Control markets: {data.loc[data['treated'] == 0, 'unit'].nunique()}") + +.. tip:: + + **What the columns mean in business terms:** + + - ``unit`` = market ID (e.g., metro area) + - ``period`` = month number (1-12) + - ``treated`` = 1 if this market received the campaign, 0 for control + - ``post`` = 1 for months after the campaign launched (month 7+) + - ``outcome`` = the metric you are measuring (sales, signups, revenue, etc.) + + In your own data, these columns can have any name - you tell diff-diff which is + which when you call ``fit()``. + + +Step 2: Look at the Data +------------------------- + +Before running any analysis, plot the trends for campaign and control markets. This +visual check is the most important step. + +.. code-block:: python + + # Requires matplotlib: pip install matplotlib + import matplotlib.pyplot as plt + + trends = data.groupby(["period", "treated"])["outcome"].mean().unstack() + trends.columns = ["Control Markets", "Campaign Markets"] + + fig, ax = plt.subplots(figsize=(10, 5)) + trends.plot(ax=ax, marker="o", linewidth=2) + ax.axvline(x=6.5, color="gray", linestyle="--", label="Campaign Launch") + ax.set_xlabel("Month") + ax.set_ylabel("Sales") + ax.set_title("Sales by Market Group Over Time") + ax.legend() + plt.tight_layout() + plt.show() + +What you are looking for: **before the campaign, the two lines should track roughly in +parallel.** If they diverge before the launch, something else is driving the difference +and DiD may give misleading results. + +.. note:: + + **Why this matters:** DiD assumes that without the campaign, both groups would have + continued on the same trajectory. This is called the *parallel trends assumption*. + It is the single most important condition for the analysis to be valid. You cannot + prove it holds, but you can check whether it looks plausible. + +.. tip:: + + The plot requires ``matplotlib``, which is not a dependency of diff-diff. The + analysis itself works without it - the plot just helps you sanity-check the data. + + +Step 3: Measure the Campaign Lift +--------------------------------- + +.. code-block:: python + + did = DifferenceInDifferences() + results = did.fit( + data, + outcome="outcome", + treatment="treated", + time="post", + ) + + print(results) + +This prints a summary table with the estimate, standard error, confidence interval, +and p-value. + +.. tip:: + + **Reading the results in business terms:** + + - **ATT** = the estimated campaign lift (average increase in sales per market + due to the campaign) + - **Std. Err.** = how precisely we measured the lift (smaller is better) + - **95% Confidence Interval** = the range we are 95% confident the true lift falls + within. If it does not include zero, the effect is statistically significant. + - **p-value** = the probability of seeing this result by chance if the campaign + actually had no effect. Below 0.05 is conventionally considered significant. + + +Step 4: Check Whether the Result Is Trustworthy +------------------------------------------------ + +A statistically significant result is only meaningful if the underlying assumptions +hold. Two quick checks give you confidence (or flag problems). + +Pre-campaign trend check +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This tests whether campaign and control markets were trending at the same rate before +the launch. + +.. code-block:: python + + from diff_diff import check_parallel_trends + + pt = check_parallel_trends( + data, + outcome="outcome", + time="period", + treatment_group="treated", + ) + + print(f"Pre-campaign trend difference: {pt['trend_difference']:.3f}") + print(f"p-value: {pt['p_value']:.3f}") + + if pt["parallel_trends_plausible"]: + print("Pre-campaign trends are consistent - the analysis is on solid ground.") + else: + print("Warning: trends diverge before the campaign. Investigate further.") + +.. note:: + + **What this checks:** Were the two groups trending at the same rate before the + campaign? If yes, it supports (but does not prove) the assumption that they would + have continued on the same trajectory. A non-significant p-value here is good news. + + **Academic term:** This is a *pre-trends test*. Note that passing this test does not + guarantee the assumption holds - it is a necessary but not sufficient check. + +Placebo test +~~~~~~~~~~~~ + +Run the same analysis on pre-campaign data only, using a fake launch date. If you +find a "significant" effect where none should exist, something is wrong with the method +or data. + +.. code-block:: python + + # Use only pre-campaign data (months 1-6) + pre_data = data[data["period"] < 7].copy() + # Create a fake "launch" at month 4 + pre_data["placebo_post"] = (pre_data["period"] >= 4).astype(int) + + placebo = DifferenceInDifferences() + placebo_results = placebo.fit( + pre_data, + outcome="outcome", + treatment="treated", + time="placebo_post", + ) + + print(f"Placebo lift: {placebo_results.att:.2f} (p = {placebo_results.p_value:.3f})") + + if placebo_results.p_value > 0.05: + print("No spurious effect detected - the method is not picking up noise.") + else: + print("Warning: spurious effect found. Investigate the data for confounders.") + +.. note:: + + **What this checks:** If the method finds a "campaign effect" during a period when + no campaign was running, it means something else is systematically different between + the groups. This is called a *placebo test* or *falsification test*. + + +Step 5: Communicate the Result +------------------------------ + +Translate the statistical output into a stakeholder-ready statement. + +.. code-block:: python + + r = results + print(f""" + Campaign Impact Summary + ======================= + The campaign increased sales by {r.att:.1f} units per market + (95% CI: {r.conf_int[0]:.1f} to {r.conf_int[1]:.1f}). + + This result is statistically significant (p = {r.p_value:.4f}). + + Validity checks: + - Pre-campaign trends were consistent across groups (p = {pt['p_value']:.2f}) + - Placebo test detected no spurious effects + """) + +.. tip:: + + **For your stakeholder report:** + + - Lead with the point estimate and confidence interval, not the p-value + - Say "increased sales by X" not "the ATT is X" + - Include "we verified that markets were trending similarly before the campaign" + - Acknowledge uncertainty: "we are 95% confident the true lift is between A and B" + - Separate statistical significance from practical significance: a statistically + significant 0.1% lift may not justify the campaign spend + + +What If Your Campaign Rolled Out in Waves? +------------------------------------------ + +Many campaigns do not launch everywhere at once. If your campaign started in some +markets first and expanded later, you need a method designed for this - otherwise +the estimates can be biased. + +.. code-block:: python + + from diff_diff import CallawaySantAnna, generate_staggered_data + + # Campaign launches: wave 1 at month 4, wave 2 at month 7 + data = generate_staggered_data( + n_units=20, n_periods=12, cohort_periods=[4, 7], + never_treated_frac=0.4, treatment_effect=5.0, seed=42, + ) + + cs = CallawaySantAnna() + results = cs.fit( + data, outcome="outcome", unit="unit", + time="period", first_treat="first_treat", + ) + + print(f"Overall campaign lift: {results.overall_att:.1f}") + print(results.summary()) + +.. note:: + + **Why a different method?** When a campaign launches in waves, basic DiD can use + already-active markets as "controls" for newly-launched ones - producing biased + results. Callaway-Sant'Anna (2021) avoids this by comparing each wave only to + markets that have not yet received the campaign. + + **Academic term:** This is a *staggered adoption* design. The method provides + *ATT(g,t)* estimates for each wave at each time period, then aggregates them. + +For the full staggered analysis workflow, see :doc:`practitioner_decision_tree`. + + +What If You Have Survey Data? +----------------------------- + +If your outcome comes from a survey (brand awareness, NPS, customer satisfaction), +your data likely has a complex sampling design with strata, clusters, and weights. +Ignoring these makes your confidence intervals too narrow. + +diff-diff handles this via :class:`~diff_diff.SurveyDesign` - pass it to any estimator's +``fit()`` method. + +For a complete walkthrough with brand funnel metrics and survey design corrections, +see `Tutorial 17: Brand Awareness Survey +`_. + + +Next Steps +---------- + +- :doc:`practitioner_decision_tree` - Not sure which method fits? Match your scenario +- `Tutorial 17: Brand Awareness Survey `_ - + Full survey analysis with complex sampling design +- :doc:`choosing_estimator` - The complete academic estimator guide +- :doc:`api/index` - Full API reference