Python & Data Science
Causal Inference Under review

Root Cause Analysis: Why Did My Data Suddenly Go Weird?

Last Time on the 90-Day Policy

Maya — the data scientist at Blue Harbor Hotels — just found that the 90-day policy’s average effect was hiding a split. The policy helped new guests but was quietly hurting loyal repeat guests. She’d moved from average effects to individual ones using CATE (Conditional Average Treatment Effect) and Double Machine Learning.

She was about to write up her findings when the dashboard turned red. Cancellations jumped 20% in a single Tuesday. The policy debate is on hold. Mr. Delgado, the VP of Revenue, is in her office asking one thing: why?

Every previous tutorial asked the same kind of question: “Does X cause Y, and by how much?” — the average question. This crisis is something else. Maya isn’t asking about average effects. She’s asking: “Why did this specific spike happen?”

That shift — from averages to individual events — is Root Cause Analysis (RCA), and it needs a different tool: the Graphical Causal Model (GCM) API.

What you’ll learn here:

  • The difference between “average effect” questions and “why is my data weird” questions
  • How the GCM API differs from the CausalModel you’ve been using
  • How to fit a model of “normal” so it can spot “weird”
  • How anomaly attribution assigns a numerical “blame” score to each variable
  • How distribution change tells you whether the world changed or just its inputs

Prerequisites: Tutorials 1–7, or comfort with DoWhy, DAGs, and the GCM concept mentioned in Part 1.


1. Beyond Average Effects: Why is My Data Acting Weird?

Traditional causal inference works well for policy decisions — like whether to keep the 90-day fee. In a crisis, though, it falls short. RCA moves from asking “Does smoking cause cancer?” to “Why did this specific patient get sick?”

Blue Harbor’s cancellation rate usually sits around 10%, then suddenly hits 30%. A standard regression might tell Maya that lead time still matters — but it won’t tell her what caused the spike:

  • a change in lead times (people booking much further ahead),
  • a change in guest loyalty (a different kind of customer showing up), or
  • a glitch in the booking system (something internal breaking).

That answer requires the whole system, not just one arrow between treatment and outcome.

Here’s a scenario where things go wrong. We’ll take the familiar hotel data and inject a sudden shift — guests start booking much further ahead of their arrival date:

import pandas as pd
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
from dowhy import gcm

# 1. Create our baseline 'normal' data
np.random.seed(42)
n = 2000

def generate_data(n, anomaly=False):
    # Confounder: Is the guest a 'Planner'?
    planner = np.random.binomial(1, 0.5, n)
    
    # Lead time: Normally planners book early (high lead time)
    # If anomaly is True, every guest suddenly books much further ahead
    if anomaly:
        lead_time = np.random.normal(100, 10, n) # The 'weird' shift
    else:
        lead_time = 50 * planner + np.random.normal(20, 10, n)
    
    # Outcome: Cancellations depend on lead time and planner status
    # Longer lead time = higher cancellation risk in this model
    cancel_prob = 0.03 + (0.0045 * lead_time) - (0.12 * planner)
    is_cancelled = np.random.binomial(1, np.clip(cancel_prob, 0, 1))
    
    return pd.DataFrame({"is_planner": planner, "lead_time": lead_time, "is_cancelled": is_cancelled})

normal_df = generate_data(n)
anomaly_df = generate_data(200, anomaly=True)

print(f"Normal Cancel Rate: {normal_df['is_cancelled'].mean():.2%}")
print(f"Anomaly Cancel Rate: {anomaly_df['is_cancelled'].mean():.2%}")

The normal cancellation rate sits around 17.65%. In the anomaly data, it jumps to nearly 40% (39.50%). Maya’s job is to prove why it happened, not just that it happened.

2. The GCM API: A Different Kind of Causal Model

In earlier tutorials, Maya used dowhy.CausalModel. That tool answers “how much does the treatment affect the outcome?” For root cause analysis, we use the Graphical Causal Model (GCM) API.

What’s the difference? A standard model focuses on the arrow between Treatment and Outcome. A GCM models the mechanism of every single node in the graph. Think of each node as a mini-machine. The GCM learns what each one does: what inputs it takes and how much random noise (unpredictable variation) it adds.

This is the hardest part to grasp: we aren’t just adjusting for variables — we’re building a digital twin of the entire data-generating process. A digital twin mirrors how a real system produces its outputs. You can poke it, break it, and ask “what if.”

Let’s define the structure and tell DoWhy this is a Structural Causal Model:

# Define the structure
causal_graph = nx.DiGraph([('is_planner', 'lead_time'), 
                           ('is_planner', 'is_cancelled'), 
                           ('lead_time', 'is_cancelled')])

# Create the GCM object — the *invertible* variant, so the attribution
# step in Section 4 can reconstruct each node's 'normal' noise
scm = gcm.InvertibleStructuralCausalModel(causal_graph)

# Assign 'mechanisms' to each node
# For continuous data like lead_time, we use Additive Noise Models
# is_cancelled is a yes/no outcome, but it still needs an *invertible*
# mechanism — a Discrete Additive Noise Model rounds its regression
# output to whole numbers, which keeps it invertible
scm.set_causal_mechanism('is_planner', gcm.EmpiricalDistribution())
scm.set_causal_mechanism('lead_time', gcm.AdditiveNoiseModel(gcm.ml.create_linear_regressor()))
scm.set_causal_mechanism('is_cancelled', gcm.DiscreteAdditiveNoiseModel(gcm.ml.create_linear_regressor()))

A quick vocabulary pass, because these names scare people:

  • Structural Causal Model (SCM): a DAG where every node has a mechanism — a recipe — for how it’s generated from its causes plus noise. This is the “digital twin.”
  • Additive Noise Model: a mechanism that says “output = some function of inputs + random noise added on top.” Great for continuous values like lead time.
  • DiscreteAdditiveNoiseModel: an Additive Noise Model whose output is rounded to whole numbers — the right choice for a yes/no outcome like is_cancelled when the mechanism also needs to be invertible (Section 4’s blame-attribution step needs that to reconstruct each node’s “normal” noise).
  • EmpiricalDistribution: for variables with no causes (like is_planner), just sample from the observed distribution.

3. Step 1: Fitting the World to Your Data

Before Maya can blame a variable for being weird, the model needs to learn what “normal” looks like. She calls the fit method to teach the GCM the hotel system’s baseline behavior.

The model reads through normal_df and picks up that planners usually have high lead times. It also records how much random noise it should expect. Once it knows “normal,” deviations stand out — same principle as a security camera learning a quiet hallway’s patterns before it flags someone running through it.

# Teach the model what a normal day looks like
gcm.fit(scm, normal_df)

4. Step 2: Attributing the Anomaly (The ‘Blame’ Game)

Now for the attribution. Maya has a spike in cancellations in her anomaly_df, and she wants to know: “If lead_time hadn’t changed its behavior, would we still see this spike?”

DoWhy uses a concept similar to Shapley values to assign “blame” scores to each node. A Shapley value comes from game theory — it measures, fairly, how much each player contributed to a team’s total result. Here, each variable is a “player” and the anomaly is the “result.”

A high attribution score means that node is the primary driver of the anomaly.

# We want to explain the 'is_cancelled' node in the anomaly data
# We compare it against the 'normal' data baseline
np.random.seed(0)
attributions = gcm.attribute_anomalies(scm, target_node='is_cancelled', anomaly_samples=anomaly_df)

# Let's look at the average attribution for each variable
for node, scores in attributions.items():
    print(f"Node {node} Attribution Score: {np.mean(scores):.4f}")

Running this prints:

Node is_planner Attribution Score: -0.0301
Node lead_time Attribution Score: 0.6080
Node is_cancelled Attribution Score: 0.1066

Interpret every number: lead_time shows a score of ≈0.61 — the change in how lead times are generated accounts for the vast majority of the cancellation spike. is_planner shows ≈-0.03 — essentially zero, and if anything it pulls slightly against the anomaly. The “planner” distribution didn’t change, so it gets almost no blame either way.

That’s the result Maya needed. She can now tell Delgado: “It wasn’t the customers. It wasn’t the loyalty program. The way lead times are being generated broke — and that’s what pushed cancellations up.”

5. Step 3: Root Cause Analysis of Distribution Shifts

Sometimes it’s not a handful of odd data points — it’s a permanent shift in the business itself. That’s where distribution_change helps. It separates two scenarios:

  • Intrinsic changes — the node’s internal logic changed (a new company policy, a bug in the booking system). The machine got re-wired.
  • Extrinsic changes — the node changed only because its inputs changed (different customers, same machine underneath).

Back to Maya: did cancellations rise because the customers changed (is_planner), or because the relationship between lead time and cancellation shifted? Those call for completely different responses — “get better customers” versus “fix your system.”

# distribution_change's default difference metric (KL divergence) isn't
# informative for a 0/1 outcome, so we score the shift in cancellation
# rate directly instead
def mean_diff(old_samples, new_samples):
    return float(np.mean(new_samples) - np.mean(old_samples))

# Compare the normal distribution to the anomaly distribution
np.random.seed(0)
root_causes = gcm.distribution_change(
    scm, normal_df, anomaly_df, 'is_cancelled', difference_estimation_func=mean_diff
)

# Visualize the contribution
# This tells us which node's 'mechanism' shifted
for node, contribution in root_causes.items():
    print(f"Root Cause Contribution from {node}: {contribution:.4f}")

Running this prints:

Root Cause Contribution from is_cancelled: 0.0044
Root Cause Contribution from is_planner: -0.0056
Root Cause Contribution from lead_time: 0.1697

What this means in practice: lead_time’s contribution (≈0.17) dwarfs the other two (both near zero) — the way lead times are generated has shifted, an intrinsic change, meaning something in the system broke. Maya can skip the loyalty-program rabbit hole (is_planner) and focus on the fact that people are suddenly booking much further ahead.

That distinction is the real value of RCA. It tells you where to look, not just that something went wrong. In a crisis, that’s the difference between fixing the system in an hour and chasing ghosts all week.

6. Summary and Next Steps

Maya has the answer. The GCM’s attribution pointed squarely at lead_time — and the distribution-change analysis confirmed it was an intrinsic break in how lead times were generated. A third-party booking vendor had shipped a bug that pushed every recorded arrival date months into the future, artificially inflating lead_time on every new booking. Customers weren’t actually booking that much further ahead; the system was recording them that way.

The spike wasn’t real cancellations — it was a data-entry glitch. Maya only found that because she looked at the whole system, not just the average.

Here’s what we covered:

  • GCMs vs. Standard Models: GCMs model the whole system’s flow, not just one effect.
  • Fitting: you must teach the model “normal” before it can understand “weird.”
  • Attribution: assigning a numerical “blame” score to variables to explain a spike.
  • RCA: identifying whether a change was internal to a variable or just a ripple from elsewhere.

Maya has now seen the entire system’s machinery. It gives her an idea. If she can model how every variable is generated — if she has a digital twin of the hotel — then she can ask the question that drives data science: “What if?”

She pulls up the record of a specific guest who cancelled under the 90-day policy. “What if,” she whispers, “we hadn’t charged them the fee? Would they have stayed?”

What you learned:

  • How root-cause questions differ from average-effect questions
  • How the GCM API models every node’s mechanism, not just one arrow
  • How to fit “normal” so the model can spot “weird”
  • How anomaly attribution assigns blame scores
  • How distribution change distinguishes intrinsic breaks from input changes

Coming next: Tutorial 9 — Counterfactuals: what would have happened to this specific guest?

Check Your Understanding

Questions below move from simple recall up to open-ended design, roughly following Bloom’s Taxonomy.

Remember What does gcm.fit() teach the model, and why does it need to happen before gcm.attribute_anomalies() can run?

Understand In your own words, explain the difference between an “intrinsic” change and an “extrinsic” change, using the tutorial’s lead_time vs. is_planner example.

Apply The tutorial’s attribution output shows lead_time at a score of 0.61 and is_planner at -0.03. Based on these two numbers alone, roughly what fraction of the remaining attribution (out of the total) would need to come from the third node (is_cancelled’s own mechanism) for the scores to sum to 1.0?

Analyze The tutorial says a GCM builds “a digital twin of the entire data-generating process,” unlike a standard CausalModel that only focuses on the treatment-outcome arrow. Walk through why you need mechanisms for every node (not just treatment and outcome) to answer “why did cancellations spike,” when a standard ATE model only ever needed the backdoor-adjustment variables.

Evaluate The tutorial frames distribution_change as telling you whether to investigate lead_time or is_planner. Critique the reliability of this: what has to be true about the GCM’s chosen mechanisms (e.g., AdditiveNoiseModel with a linear regressor for lead_time) for the attribution scores to be trustworthy, and what happens to the “blame” assignment if the real relationship is actually nonlinear?

Create Design a root-cause investigation for a different anomaly: a SaaS product’s weekly active users suddenly dropped 15%. Sketch a 3-node causal graph (treatment/mediator/outcome-style) for this scenario, and describe what an attribute_anomalies result pointing to each node would mean for what the team should investigate next.


Apply What You Learned is for Supporter and Insider subscribers.

Subscribe to unlock the exercises on this post.

See plans
  • Causal Inference Under review

    Correlation Isn't Causation

    Learn why correlation isn't causation, how hidden confounders distort your analysis, and preview how Python's DoWhy library recovers true causal effects from data.

  • Causal Inference Under review

    Sensitivity Analysis: How Robust Is Your Estimate to an Unmeasured Confounder?

    Learn how to stress-test your causal estimates against unmeasured confounders using sensitivity analysis, E-values, and tipping-point plots in DoWhy.

  • Causal Inference Under review

    Building Your First Causal Model

    Learn to specify a causal graph in DoWhy three ways, load a CausalModel with your own data, visualize assumptions, and test your DAG against the data to catch hidden bias.

  • Causal Inference Under review

    Simpson's Paradox: When Aggregated Data Tells the Opposite Story

    Understand how aggregated data misleads data scientists into making wrong decisions, and discover why stratifying your analysis is key to revealing the truth.

Looking for something else?

Search every article by title, summary or topic.