Visualization#

Plotting functions for DiD results visualization.

plot_event_study#

Create publication-ready event study coefficient plots.

diff_diff.plot_event_study(results=None, *, effects=None, se=None, periods=None, reference_period=None, pre_periods=None, post_periods=None, alpha=0.05, figsize=(10, 6), title='Event Study', xlabel='Period Relative to Treatment', ylabel='Treatment Effect', color='#2563eb', marker='o', markersize=8, linewidth=1.5, capsize=4, show_zero_line=True, show_reference_line=True, shade_pre=True, shade_color='#f0f0f0', ax=None, show=True, use_cband=True, backend='matplotlib')[source]#

Create an event study plot showing treatment effects over time.

This function creates a coefficient plot with point estimates and confidence intervals for each time period, commonly used to visualize dynamic treatment effects and assess pre-trends.

Parameters:
  • results (MultiPeriodDiDResults, CallawaySantAnnaResults, or DataFrame, optional) – Results object from MultiPeriodDiD, CallawaySantAnna, or a DataFrame with columns ‘period’, ‘effect’, ‘se’ (and optionally ‘conf_int_lower’, ‘conf_int_upper’). If None, must provide effects and se directly.

  • effects (dict, optional) – Dictionary mapping periods to effect estimates. Used if results is None.

  • se (dict, optional) – Dictionary mapping periods to standard errors. Used if results is None.

  • periods (list, optional) – List of periods to plot. If None, uses all periods from results.

  • reference_period (any, optional) – The reference period to highlight. When explicitly provided, effects are normalized (ref effect subtracted) and ref SE is set to NaN. When None and auto-inferred from results, only hollow marker styling is applied (no normalization). If None, tries to infer from results.

  • pre_periods (list, optional) – List of pre-treatment periods. Used for shading.

  • post_periods (list, optional) – List of post-treatment periods. Used for shading.

  • alpha (float, default=0.05) – Significance level for confidence intervals.

  • figsize (tuple, default=(10, 6)) – Figure size (width, height) in inches.

  • title (str, default="Event Study") – Plot title.

  • xlabel (str, default="Period Relative to Treatment") – X-axis label.

  • ylabel (str, default="Treatment Effect") – Y-axis label.

  • color (str, default="#2563eb") – Color for points and error bars.

  • marker (str, default="o") – Marker style for point estimates.

  • markersize (int, default=8) – Size of markers.

  • linewidth (float, default=1.5) – Width of error bar lines.

  • capsize (int, default=4) – Size of error bar caps.

  • show_zero_line (bool, default=True) – Whether to show a horizontal line at y=0.

  • show_reference_line (bool, default=True) – Whether to show a vertical line at the reference period.

  • shade_pre (bool, default=True) – Whether to shade the pre-treatment region.

  • shade_color (str, default="#f0f0f0") – Color for pre-treatment shading.

  • ax (matplotlib.axes.Axes, optional) – Axes to plot on. If None, creates new figure.

  • show (bool, default=True) – Whether to call plt.show() at the end.

  • use_cband (bool, default=True) – Whether to use simultaneous confidence band CIs when available from CallawaySantAnna results. When False, pointwise CIs from alpha are used regardless.

  • backend (str, default="matplotlib") – Plotting backend: "matplotlib" for static plots or "plotly" for interactive plots.

Returns:

The axes object (matplotlib) or figure (plotly) containing the plot.

Return type:

matplotlib.axes.Axes or plotly.graph_objects.Figure

Examples

Using with MultiPeriodDiD results:

>>> from diff_diff import MultiPeriodDiD, plot_event_study
>>> did = MultiPeriodDiD()
>>> results = did.fit(data, outcome='y', treatment='treated',
...                   time='period', post_periods=[3, 4, 5])
>>> plot_event_study(results)

Using with a DataFrame:

>>> df = pd.DataFrame({
...     'period': [-2, -1, 0, 1, 2],
...     'effect': [0.1, 0.05, 0.0, 0.5, 0.6],
...     'se': [0.1, 0.1, 0.0, 0.15, 0.15]
... })
>>> plot_event_study(df, reference_period=0)

Using with manual effects:

>>> effects = {-2: 0.1, -1: 0.05, 0: 0.0, 1: 0.5, 2: 0.6}
>>> se = {-2: 0.1, -1: 0.1, 0: 0.0, 1: 0.15, 2: 0.15}
>>> plot_event_study(effects=effects, se=se, reference_period=0)

Notes

Event study plots are a standard visualization in difference-in-differences analysis. They show:

  1. Pre-treatment periods: Effects should be close to zero if parallel trends holds. Large pre-treatment effects suggest the assumption may be violated.

  2. Reference period: Usually the last pre-treatment period (t=-1). When explicitly specified via reference_period, effects are normalized to zero at this period. When auto-inferred, shown with hollow marker only.

  3. Post-treatment periods: The treatment effects of interest. These show how the outcome evolved after treatment.

The confidence intervals help assess statistical significance. Effects whose CIs don’t include zero are typically considered significant.

Example#

from diff_diff import MultiPeriodDiD, plot_event_study

# Fit event study model
model = MultiPeriodDiD()
results = model.fit(data, outcome='y', treatment='treated',
                    time='period', unit='unit_id', reference_period=2)

# Create plot
ax = plot_event_study(results)
ax.figure.savefig('event_study.png', dpi=300, bbox_inches='tight')

plot_group_effects#

Visualize treatment effects by cohort.

diff_diff.plot_group_effects(results, *, groups=None, figsize=(10, 6), title='Treatment Effects by Cohort', xlabel='Time Period', ylabel='Treatment Effect', alpha=0.05, show=True, ax=None, backend='matplotlib')[source]#

Plot treatment effects by treatment cohort (group).

Parameters:
  • results (CallawaySantAnnaResults) – Results from CallawaySantAnna estimator.

  • groups (list, optional) – List of groups (cohorts) to plot. If None, plots all groups.

  • figsize (tuple, default=(10, 6)) – Figure size.

  • title (str) – Plot title.

  • xlabel (str) – X-axis label.

  • ylabel (str) – Y-axis label.

  • alpha (float, default=0.05) – Significance level for confidence intervals.

  • show (bool, default=True) – Whether to call plt.show().

  • ax (matplotlib.axes.Axes, optional) – Axes to plot on.

  • backend (str, default="matplotlib") – Plotting backend: "matplotlib" or "plotly".

Returns:

The axes object (matplotlib) or figure (plotly).

Return type:

matplotlib.axes.Axes or plotly.graph_objects.Figure

Example#

from diff_diff import CallawaySantAnna, plot_group_effects

cs = CallawaySantAnna()
results = cs.fit(data, outcome='y', unit='unit_id',
                 time='period', first_treat='first_treat')

# Plot effects by treatment cohort
ax = plot_group_effects(results)

plot_sensitivity#

Plot Honest DiD sensitivity analysis results.

diff_diff.plot_sensitivity(sensitivity_results, *, show_bounds=True, show_ci=True, breakdown_line=True, figsize=(10, 6), title='Honest DiD Sensitivity Analysis', xlabel='M (restriction parameter)', ylabel='Treatment Effect', bounds_color='#2563eb', bounds_alpha=0.3, ci_color='#2563eb', ci_linewidth=1.5, breakdown_color='#dc2626', original_color='#1f2937', ax=None, show=True, backend='matplotlib')[source]#

Plot sensitivity analysis results from Honest DiD.

Shows how treatment effect bounds and confidence intervals change as the restriction parameter M varies.

Parameters:
  • sensitivity_results (SensitivityResults) – Results from HonestDiD.sensitivity_analysis().

  • show_bounds (bool, default=True) – Whether to show the identified set bounds as shaded region.

  • show_ci (bool, default=True) – Whether to show robust confidence interval lines.

  • breakdown_line (bool, default=True) – Whether to show vertical line at breakdown value.

  • figsize (tuple, default=(10, 6)) – Figure size (width, height) in inches.

  • title (str) – Plot title.

  • xlabel (str) – X-axis label.

  • ylabel (str) – Y-axis label.

  • bounds_color (str) – Color for identified set shading.

  • bounds_alpha (float) – Transparency for identified set shading.

  • ci_color (str) – Color for confidence interval lines.

  • ci_linewidth (float) – Line width for CI lines.

  • breakdown_color (str) – Color for breakdown value line.

  • original_color (str) – Color for original estimate line.

  • ax (matplotlib.axes.Axes, optional) – Axes to plot on. If None, creates new figure.

  • show (bool, default=True) – Whether to call plt.show().

  • backend (str, default="matplotlib") – Plotting backend: "matplotlib" or "plotly".

Returns:

The axes object (matplotlib) or figure (plotly).

Return type:

matplotlib.axes.Axes or plotly.graph_objects.Figure

Examples

>>> from diff_diff import MultiPeriodDiD
>>> from diff_diff.honest_did import HonestDiD
>>> from diff_diff.visualization import plot_sensitivity
>>>
>>> # Fit event study and run sensitivity analysis
>>> results = MultiPeriodDiD().fit(data, ...)
>>> honest = HonestDiD(method='relative_magnitude')
>>> sensitivity = honest.sensitivity_analysis(results)
>>>
>>> # Create sensitivity plot
>>> plot_sensitivity(sensitivity)

Example#

from diff_diff import HonestDiD, plot_sensitivity

honest = HonestDiD(method='relative_magnitude', M=1.0)
sensitivity = honest.sensitivity_analysis(
    results,
    M_grid=[0, 0.5, 1.0, 1.5, 2.0]
)

ax = plot_sensitivity(sensitivity)

plot_honest_event_study#

Event study plot with honest confidence intervals.

diff_diff.plot_honest_event_study(honest_results, *, periods=None, reference_period=None, figsize=(10, 6), title='Event Study with Honest Confidence Intervals', xlabel='Period Relative to Treatment', ylabel='Treatment Effect', original_color='#6b7280', honest_color='#2563eb', marker='o', markersize=8, capsize=4, ax=None, show=True, backend='matplotlib')[source]#

Create event study plot with Honest DiD confidence intervals.

Shows both the original confidence intervals (assuming parallel trends) and the robust confidence intervals that allow for bounded violations.

Parameters:
  • honest_results (HonestDiDResults) – Results from HonestDiD.fit() that include event_study_bounds.

  • periods (list, optional) – Periods to plot. If None, uses all available periods.

  • reference_period (any, optional) – Reference period to show as hollow marker.

  • figsize (tuple, default=(10, 6)) – Figure size.

  • title (str) – Plot title.

  • xlabel (str) – X-axis label.

  • ylabel (str) – Y-axis label.

  • original_color (str) – Color for original (standard) confidence intervals.

  • honest_color (str) – Color for honest (robust) confidence intervals.

  • marker (str) – Marker style.

  • markersize (int) – Marker size.

  • capsize (int) – Error bar cap size.

  • ax (matplotlib.axes.Axes, optional) – Axes to plot on.

  • show (bool, default=True) – Whether to call plt.show().

  • backend (str, default="matplotlib") – Plotting backend: "matplotlib" or "plotly".

Returns:

The axes object (matplotlib) or figure (plotly).

Return type:

matplotlib.axes.Axes or plotly.graph_objects.Figure

Notes

This function requires the HonestDiDResults to have been computed with event_study_bounds. If only a scalar bound was computed, use plot_sensitivity() instead.

Example#

from diff_diff import HonestDiD, plot_honest_event_study

honest = HonestDiD(method='relative_magnitude', M=1.0)
bounds = honest.fit(event_study_results)

ax = plot_honest_event_study(bounds)

plot_synth_weights#

Visualize synthetic control unit or time weights.

diff_diff.plot_synth_weights(results=None, *, weights=None, weight_type='unit', top_n=None, min_weight=0.001, figsize=(10, 6), title=None, color='#2563eb', ax=None, show=True, backend='matplotlib')[source]#

Plot synthetic control weights as a bar chart.

Visualizes the unit weights or time weights from a Synthetic Difference-in-Differences estimation.

Parameters:
  • results (SyntheticDiDResults, optional) – Results from SyntheticDiD estimator. Extracts weights based on weight_type.

  • weights (dict, optional) – Dictionary mapping unit/period IDs to weights. Used if results is None.

  • weight_type (str, default="unit") – Which weights to plot: "unit" for control unit weights or "time" for pre-treatment time weights.

  • top_n (int, optional) – Show only the top N weights by magnitude. Useful when there are many control units.

  • min_weight (float, default=0.001) – Minimum weight threshold for display.

  • figsize (tuple, default=(10, 6)) – Figure size (width, height) in inches.

  • title (str, optional) – Plot title. If None, auto-generated based on weight_type.

  • color (str, default="#2563eb") – Bar color.

  • ax (matplotlib.axes.Axes, optional) – Axes to plot on. If None, creates new figure.

  • show (bool, default=True) – Whether to call plt.show() at the end.

  • backend (str, default="matplotlib") – Plotting backend: "matplotlib" or "plotly".

Returns:

The axes object (matplotlib) or figure (plotly).

Return type:

matplotlib.axes.Axes or plotly.graph_objects.Figure

Example#

from diff_diff import plot_synth_weights

# Plot from a weights dictionary
weights = {"unit_A": 0.40, "unit_B": 0.30, "unit_C": 0.20, "unit_D": 0.10}
ax = plot_synth_weights(weights=weights)

# Or from SyntheticDiD results:
# ax = plot_synth_weights(results)
# ax = plot_synth_weights(results, weight_type='time')

plot_staircase#

Visualize treatment adoption timing in staggered designs.

diff_diff.plot_staircase(results=None, *, data=None, unit=None, time=None, first_treat=None, figsize=(10, 6), title='Treatment Adoption Over Time', color='#2563eb', show_counts=True, ax=None, show=True, backend='matplotlib')[source]#

Plot treatment adoption “staircase” for staggered designs.

Shows how many units enter treatment over time, creating a step-function pattern that illustrates the staggered adoption of treatment.

Parameters:
  • results (CallawaySantAnnaResults, optional) – Results from CallawaySantAnna estimator. Extracts groups and cohort sizes from group_time_effects.

  • data (pd.DataFrame, optional) – Raw panel data. Must provide unit, time, and first_treat column names.

  • unit (str, optional) – Column name for unit identifier (required with data).

  • time (str, optional) – Column name for time period (required with data).

  • first_treat (str, optional) – Column name for first treatment period (required with data).

  • figsize (tuple, default=(10, 6)) – Figure size (width, height) in inches.

  • title (str, default="Treatment Adoption Over Time") – Plot title.

  • color (str, default="#2563eb") – Base color for the staircase.

  • show_counts (bool, default=True) – Whether to annotate each step with the cohort size.

  • ax (matplotlib.axes.Axes, optional) – Axes to plot on. If None, creates new figure.

  • show (bool, default=True) – Whether to call plt.show() at the end.

  • backend (str, default="matplotlib") – Plotting backend: "matplotlib" or "plotly".

Returns:

The axes object (matplotlib) or figure (plotly).

Return type:

matplotlib.axes.Axes or plotly.graph_objects.Figure

Example#

from diff_diff import CallawaySantAnna, plot_staircase

cs = CallawaySantAnna()
results = cs.fit(data, outcome='y', unit='unit_id',
                 time='period', first_treat='first_treat')

# Staircase plot from results
ax = plot_staircase(results)

# Or from raw panel data
ax = plot_staircase(data=data, unit='unit_id', time='period',
                    first_treat='first_treat')

plot_dose_response#

Visualize dose-response curves from continuous DiD.

diff_diff.plot_dose_response(results=None, *, curve=None, data=None, target='att', alpha=0.05, figsize=(10, 6), title=None, xlabel='Dose', ylabel='Treatment Effect', color='#2563eb', ci_color=None, show_zero_line=True, ax=None, show=True, backend='matplotlib')[source]#

Plot dose-response curve from Continuous DiD estimation.

Visualizes how the treatment effect varies with the treatment dose (intensity), with confidence bands.

Parameters:
  • results (ContinuousDiDResults, optional) – Results from ContinuousDiD estimator. Extracts the dose-response curve based on target.

  • curve (DoseResponseCurve, optional) – A DoseResponseCurve object directly.

  • data (pd.DataFrame, optional) – DataFrame with columns dose, effect, se (and optionally conf_int_lower, conf_int_upper).

  • target (str, default="att") – Which dose-response curve: "att" or "acrt".

  • alpha (float, default=0.05) – Significance level for confidence intervals (used with DataFrame input).

  • figsize (tuple, default=(10, 6)) – Figure size (width, height) in inches.

  • title (str, optional) – Plot title. Auto-generated if None.

  • xlabel (str, default="Dose") – X-axis label.

  • ylabel (str, default="Treatment Effect") – Y-axis label.

  • color (str, default="#2563eb") – Color for the line.

  • ci_color (str, optional) – Color for confidence band. Defaults to color with transparency.

  • show_zero_line (bool, default=True) – Whether to show a horizontal line at y=0.

  • ax (matplotlib.axes.Axes, optional) – Axes to plot on. If None, creates new figure.

  • show (bool, default=True) – Whether to call plt.show() at the end.

  • backend (str, default="matplotlib") – Plotting backend: "matplotlib" or "plotly".

Returns:

The axes object (matplotlib) or figure (plotly).

Return type:

matplotlib.axes.Axes or plotly.graph_objects.Figure

Example#

import pandas as pd
from diff_diff import plot_dose_response

# Plot from a DataFrame with dose-response data
dr_data = pd.DataFrame({
    'dose': [1, 2, 3, 4, 5],
    'effect': [0.1, 0.3, 0.5, 0.4, 0.3],
    'se': [0.05, 0.06, 0.07, 0.08, 0.09],
})
ax = plot_dose_response(data=dr_data)

# Or from ContinuousDiD results:
# ax = plot_dose_response(results, target='att')
# ax = plot_dose_response(results, target='acrt')

plot_group_time_heatmap#

Heatmap of group-time treatment effects.

diff_diff.plot_group_time_heatmap(results=None, *, data=None, figsize=(10, 8), title='Group-Time Treatment Effects', cmap='RdBu_r', center=0.0, annotate=True, fmt='.3f', mask_insignificant=False, alpha=0.05, ax=None, show=True, backend='matplotlib')[source]#

Plot heatmap of group-time treatment effects ATT(g,t).

Displays treatment effects as a colored matrix with treatment cohorts (groups) on the y-axis and calendar time periods on the x-axis.

Parameters:
  • results (CallawaySantAnnaResults, EfficientDiDResults, or ContinuousDiDResults, optional) – Results object with group_time_effects dict.

  • data (pd.DataFrame, optional) – DataFrame with columns group, time, effect (and optionally p_value).

  • figsize (tuple, default=(10, 8)) – Figure size (width, height) in inches.

  • title (str, default="Group-Time Treatment Effects") – Plot title.

  • cmap (str, default="RdBu_r") – Colormap name. Diverging colormaps centered at zero work best.

  • center (float, default=0.0) – Value to center the colormap at.

  • annotate (bool, default=True) – Whether to show effect values in each cell.

  • fmt (str, default=".3f") – Format string for cell annotations.

  • mask_insignificant (bool, default=False) – Whether to grey out cells with non-significant effects.

  • alpha (float, default=0.05) – Significance level for masking (when mask_insignificant=True).

  • ax (matplotlib.axes.Axes, optional) – Axes to plot on. If None, creates new figure.

  • show (bool, default=True) – Whether to call plt.show() at the end.

  • backend (str, default="matplotlib") – Plotting backend: "matplotlib" or "plotly".

Returns:

The axes object (matplotlib) or figure (plotly).

Return type:

matplotlib.axes.Axes or plotly.graph_objects.Figure

Example#

from diff_diff import CallawaySantAnna, plot_group_time_heatmap

cs = CallawaySantAnna()
results = cs.fit(data, outcome='y', unit='unit_id',
                 time='period', first_treat='first_treat')

# Heatmap of ATT(g,t)
ax = plot_group_time_heatmap(results)

# Grey out non-significant cells
ax = plot_group_time_heatmap(results, mask_insignificant=True)

plot_bacon#

Visualize Goodman-Bacon decomposition results.

diff_diff.plot_bacon(results, *, plot_type='scatter', figsize=(10, 6), title=None, xlabel='2x2 DiD Estimate', ylabel='Weight', colors=None, marker='o', markersize=80, alpha=0.7, show_weighted_avg=True, show_twfe_line=True, ax=None, show=True, backend='matplotlib')[source]#

Visualize Goodman-Bacon decomposition results.

Creates either a scatter plot showing the weight and estimate for each 2x2 comparison, or a stacked bar chart showing total weight by comparison type.

Parameters:
  • results (BaconDecompositionResults) – Results from BaconDecomposition.fit() or bacon_decompose().

  • plot_type (str, default="scatter") – Type of plot to create: - “scatter”: Scatter plot with estimates on x-axis, weights on y-axis - “bar”: Stacked bar chart of weights by comparison type

  • figsize (tuple, default=(10, 6)) – Figure size (width, height) in inches.

  • title (str, optional) – Plot title. If None, uses a default based on plot_type.

  • xlabel (str, default="2x2 DiD Estimate") – X-axis label (scatter plot only).

  • ylabel (str, default="Weight") – Y-axis label.

  • colors (dict, optional) – Dictionary mapping comparison types to colors. Keys are: “treated_vs_never”, “earlier_vs_later”, “later_vs_earlier”. If None, uses default colors.

  • marker (str, default="o") – Marker style for scatter plot.

  • markersize (int, default=80) – Marker size for scatter plot.

  • alpha (float, default=0.7) – Transparency for markers/bars.

  • show_weighted_avg (bool, default=True) – Whether to show weighted average lines for each comparison type (scatter plot only).

  • show_twfe_line (bool, default=True) – Whether to show a vertical line at the TWFE estimate (scatter plot only).

  • ax (matplotlib.axes.Axes, optional) – Axes to plot on. If None, creates new figure.

  • show (bool, default=True) – Whether to call plt.show() at the end.

  • backend (str, default="matplotlib") – Plotting backend: "matplotlib" or "plotly".

Returns:

The axes object (matplotlib) or figure (plotly).

Return type:

matplotlib.axes.Axes or plotly.graph_objects.Figure

Examples

Scatter plot (default):

>>> from diff_diff import bacon_decompose, plot_bacon
>>> results = bacon_decompose(data, outcome='y', unit='id',
...                           time='t', first_treat='first_treat')
>>> plot_bacon(results)

Bar chart of weights by type:

>>> plot_bacon(results, plot_type='bar')

Notes

The scatter plot is particularly useful for understanding:

  1. Distribution of estimates: Are 2x2 estimates clustered or spread? Wide spread suggests heterogeneous treatment effects.

  2. Weight concentration: Do a few comparisons dominate the TWFE? Points with high weights have more influence.

  3. Forbidden comparison problem: Red points (later_vs_earlier) show comparisons using already-treated units as controls. If these have different estimates than clean comparisons, TWFE may be biased.

See also

bacon_decompose

Perform the decomposition

BaconDecomposition

Class-based interface

Example#

from diff_diff import BaconDecomposition, plot_bacon

bd = BaconDecomposition()
results = bd.fit(data, outcome='y', unit='unit_id',
                 time='period', first_treat='first_treat')

# Scatter of 2x2 comparisons by weight, colored by comparison type
ax = plot_bacon(results)

Plotly Backend#

All visualization functions support an interactive plotly backend via the backend parameter:

# Interactive event study
fig = plot_event_study(results, backend='plotly')

# Interactive dose-response curve
fig = plot_dose_response(results, backend='plotly')

Install plotly with: pip install diff-diff[plotly]