Skip to content

Plotting helpers

The DIDMultiplegtStat.plot() method is the common entry point. The standalone functions exposed here are used internally and can be called directly with a results dict if you prefer the functional API.

from did_multiplegt_stat import plot_event_study, plot_by_groups, plot_comparison

plot_event_study

plot_event_study

plot_event_study(results: dict[str, Any], estimator: str | None = None, show_ci: bool = True, ci_alpha: float = 0.2, figsize: tuple[float, float] = (10, 6), colors: dict[str, str] | None = None, title: str | None = None, xlabel: str = 'Relative Time', ylabel: str = 'Effect Estimate', show_zero_line: bool = True, separate_panels: bool = False, save_path: str | None = None, dpi: int = 150) -> plt.Figure | dict[str, plt.Figure]

Generate event-study style plots for DiD results.

Parameters:

Name Type Description Default
results dict

Results dictionary from DIDMultiplegtStat.fit().

required
estimator str

Which estimator to plot ('as', 'was', or 'iv-was'). If None, plots all available estimators.

None
show_ci bool

Display confidence interval bands.

True
ci_alpha float

Transparency for CI bands.

0.2
figsize tuple

Figure size in inches.

(10, 6)
colors dict

Custom colors for estimators.

None
title str

Custom title.

None
xlabel str

X-axis label.

"Relative Time"
ylabel str

Y-axis label.

"Effect Estimate"
show_zero_line bool

Show horizontal line at y=0.

True
separate_panels bool

Create separate subplots for each estimator.

False
save_path str

Path to save figure.

None
dpi int

Resolution for saved figure.

150

Returns:

Name Type Description
fig Figure or dict of Figures

Matplotlib figure(s).

Source code in src/did_multiplegt_stat/plotting.py
def plot_event_study(
    results: dict[str, Any],
    estimator: str | None = None,
    show_ci: bool = True,
    ci_alpha: float = 0.2,
    figsize: tuple[float, float] = (10, 6),
    colors: dict[str, str] | None = None,
    title: str | None = None,
    xlabel: str = "Relative Time",
    ylabel: str = "Effect Estimate",
    show_zero_line: bool = True,
    separate_panels: bool = False,
    save_path: str | None = None,
    dpi: int = 150,
) -> plt.Figure | dict[str, plt.Figure]:
    """
    Generate event-study style plots for DiD results.

    Parameters
    ----------
    results : dict
        Results dictionary from DIDMultiplegtStat.fit().
    estimator : str, optional
        Which estimator to plot ('as', 'was', or 'iv-was').
        If None, plots all available estimators.
    show_ci : bool, default=True
        Display confidence interval bands.
    ci_alpha : float, default=0.2
        Transparency for CI bands.
    figsize : tuple, default=(10, 6)
        Figure size in inches.
    colors : dict, optional
        Custom colors for estimators.
    title : str, optional
        Custom title.
    xlabel : str, default="Relative Time"
        X-axis label.
    ylabel : str, default="Effect Estimate"
        Y-axis label.
    show_zero_line : bool, default=True
        Show horizontal line at y=0.
    separate_panels : bool, default=False
        Create separate subplots for each estimator.
    save_path : str, optional
        Path to save figure.
    dpi : int, default=150
        Resolution for saved figure.

    Returns
    -------
    fig : Figure or dict of Figures
        Matplotlib figure(s).
    """
    # Get estimator list from results
    args = results.get("args", {})
    _, estimator_list = normalize_estimators(
        args.get("estimator", ["as", "was"]),
        has_instrument=args.get("Z") is not None,
        warn_legacy=False,
    )

    # Filter to requested estimator(s)
    if estimator is not None:
        requested = to_internal_estimator(estimator)
        estimator_list = [e for e in estimator_list if e == requested]

    # Use custom colors if provided, otherwise defaults
    color_map = {**ESTIMATOR_COLORS, **(normalize_color_mapping(colors) or {})}

    # Get table data
    # Try to get by-level results first, then fall back to main results
    print_obj = results.get("results", results)
    table = print_obj.get("table")
    placebo_n = args.get("placebo", 0)
    disaggregate = args.get("disaggregate", False)
    pairs = int(print_obj.get("pairs", 1))

    if table is None or not isinstance(table, pd.DataFrame):
        fig, ax = plt.subplots(figsize=figsize)
        ax.text(0.5, 0.5, "No data to plot", ha='center', va='center', fontsize=14)
        return fig

    estims_map = ESTIMATOR_POSITIONS

    if separate_panels and len(estimator_list) > 1:
        # Create separate panels for each estimator
        n_estimators = len(estimator_list)
        fig, axes = _setup_figure(figsize=(figsize[0], figsize[1] * n_estimators),
                                   nrows=n_estimators, ncols=1)

        for i, est in enumerate(estimator_list):
            ax = axes[i, 0]
            _plot_single_estimator(
                ax=ax,
                table=table,
                estimator=est,
                estims_map=estims_map,
                pairs=pairs,
                disaggregate=disaggregate,
                placebo_n=placebo_n,
                placebo_tables=_get_placebo_tables(print_obj, placebo_n),
                color=color_map.get(est, "blue"),
                show_ci=show_ci,
                ci_alpha=ci_alpha,
                show_zero_line=show_zero_line,
            )
            _format_axis(ax, xlabel=xlabel, ylabel=ylabel, title=estimator_label(est))

        plt.tight_layout()

    else:
        # Single panel with all estimators overlaid
        fig, ax = plt.subplots(figsize=figsize)

        for est in estimator_list:
            _plot_single_estimator(
                ax=ax,
                table=table,
                estimator=est,
                estims_map=estims_map,
                pairs=pairs,
                disaggregate=disaggregate,
                placebo_n=placebo_n,
                placebo_tables=_get_placebo_tables(print_obj, placebo_n),
                color=color_map.get(est, "blue"),
                show_ci=show_ci,
                ci_alpha=ci_alpha,
                show_zero_line=show_zero_line,
                label=estimator_label(est),
            )

        _format_axis(ax, xlabel=xlabel, ylabel=ylabel, title=title)
        if len(estimator_list) > 1:
            _create_legend(ax)
        if show_zero_line:
            _add_zero_line(ax)

        plt.tight_layout()

    if save_path:
        fig.savefig(save_path, dpi=dpi, bbox_inches='tight')

    return fig

plot_by_groups

plot_by_groups

plot_by_groups(results: dict[str, Any], estimator: str = 'as', show_ci: bool = True, ci_alpha: float = 0.15, figsize: tuple[float, float] = (12, 6), colors: list[str] | None = None, title: str | None = None, xlabel: str = 'Treatment Change', ylabel: str = 'Effect Estimate', save_path: str | None = None, dpi: int = 150) -> plt.Figure

Generate plots for by-group analysis with multiple colored lines.

Parameters:

Name Type Description Default
results dict

Results dictionary with by-group results.

required
estimator str

Which estimator to plot.

"as"
show_ci bool

Display confidence interval bands.

True
ci_alpha float

Transparency for CI bands.

0.15
figsize tuple

Figure size.

(12, 6)
colors list

Custom colors for each group.

None
title str

Plot title.

None
xlabel str

X-axis label.

"Treatment Change"
ylabel str

Y-axis label.

"Effect Estimate"
save_path str

Path to save figure.

None
dpi int

Resolution for saved figure.

150

Returns:

Name Type Description
fig Figure

Matplotlib figure.

Source code in src/did_multiplegt_stat/plotting.py
def plot_by_groups(
    results: dict[str, Any],
    estimator: str = "as",
    show_ci: bool = True,
    ci_alpha: float = 0.15,
    figsize: tuple[float, float] = (12, 6),
    colors: list[str] | None = None,
    title: str | None = None,
    xlabel: str = "Treatment Change",
    ylabel: str = "Effect Estimate",
    save_path: str | None = None,
    dpi: int = 150,
) -> plt.Figure:
    """
    Generate plots for by-group analysis with multiple colored lines.

    Parameters
    ----------
    results : dict
        Results dictionary with by-group results.
    estimator : str, default="as"
        Which estimator to plot.
    show_ci : bool, default=True
        Display confidence interval bands.
    ci_alpha : float, default=0.15
        Transparency for CI bands.
    figsize : tuple, default=(12, 6)
        Figure size.
    colors : list, optional
        Custom colors for each group.
    title : str, optional
        Plot title.
    xlabel : str, default="Treatment Change"
        X-axis label.
    ylabel : str, default="Effect Estimate"
        Y-axis label.
    save_path : str, optional
        Path to save figure.
    dpi : int, default=150
        Resolution for saved figure.

    Returns
    -------
    fig : Figure
        Matplotlib figure.
    """
    by_levels = results.get("by_levels", [])
    color_list = colors if colors else BY_GROUP_COLORS
    estimator_internal = to_internal_estimator(estimator)

    fig, ax = plt.subplots(figsize=figsize)

    estim_idx = ESTIMATOR_POSITIONS[estimator_internal]

    for i, level in enumerate(by_levels):
        result_key = f"results_by_{i + 1}"
        print_obj = results.get(result_key)
        if print_obj is None:
            continue

        table = print_obj.get("table")
        if not isinstance(table, pd.DataFrame):
            continue

        pairs = int(print_obj.get("pairs", 1))
        l_bound = estim_idx * pairs
        l_bound + 1  # Just first row for each by-group

        if l_bound >= len(table):
            continue

        row = table.iloc[l_bound]
        color = color_list[i % len(color_list)]

        y = row["Estimate"]
        lb = row.get("LB CI", y - 1.96 * row.get("SE", 0))
        ub = row.get("UB CI", y + 1.96 * row.get("SE", 0))

        # Plot as bar with error bars
        ax.bar(i, y, color=color, alpha=0.7, label=str(level))
        ax.errorbar(i, y, yerr=[[y - lb], [ub - y]], fmt='none',
                    color='black', capsize=5, capthick=1.5)

    ax.axhline(y=0, color='gray', linestyle='--', linewidth=1, alpha=0.7)
    ax.set_xticks(range(len(by_levels)))
    ax.set_xticklabels([str(l) for l in by_levels], rotation=45, ha='right')

    _format_axis(ax, xlabel=xlabel, ylabel=ylabel,
                 title=title or f"{estimator_label(estimator_internal)} by Group")
    ax.legend(title="Group", loc='best')

    plt.tight_layout()

    if save_path:
        fig.savefig(save_path, dpi=dpi, bbox_inches='tight')

    return fig

plot_comparison

plot_comparison

plot_comparison(results: dict[str, Any], estimators: list[str] | None = None, figsize: tuple[float, float] = (10, 6), title: str = 'Estimator Comparison', save_path: str | None = None, dpi: int = 150) -> plt.Figure

Generate a side-by-side comparison plot of different estimators.

Parameters:

Name Type Description Default
results dict

Results dictionary.

required
estimators list

Which estimators to compare.

None
figsize tuple

Figure size.

(10, 6)
title str

Plot title.

"Estimator Comparison"
save_path str

Path to save figure.

None
dpi int

Resolution.

150

Returns:

Name Type Description
fig Figure

Matplotlib figure.

Source code in src/did_multiplegt_stat/plotting.py
def plot_comparison(
    results: dict[str, Any],
    estimators: list[str] | None = None,
    figsize: tuple[float, float] = (10, 6),
    title: str = "Estimator Comparison",
    save_path: str | None = None,
    dpi: int = 150,
) -> plt.Figure:
    """
    Generate a side-by-side comparison plot of different estimators.

    Parameters
    ----------
    results : dict
        Results dictionary.
    estimators : list, optional
        Which estimators to compare.
    figsize : tuple, default=(10, 6)
        Figure size.
    title : str, default="Estimator Comparison"
        Plot title.
    save_path : str, optional
        Path to save figure.
    dpi : int, default=150
        Resolution.

    Returns
    -------
    fig : Figure
        Matplotlib figure.
    """
    args = results.get("args", {})
    selected = estimators if estimators is not None else args.get("estimator", ["as", "was"])
    _, estimator_list = normalize_estimators(
        selected,
        has_instrument=args.get("Z") is not None,
        warn_legacy=estimators is not None,
    )

    print_obj = results.get("results", results)
    table = print_obj.get("table")

    if not isinstance(table, pd.DataFrame):
        fig, ax = plt.subplots(figsize=figsize)
        ax.text(0.5, 0.5, "No data to plot", ha='center', va='center')
        return fig

    estims_map = ESTIMATOR_POSITIONS
    pairs = int(print_obj.get("pairs", 1))

    fig, ax = plt.subplots(figsize=figsize)

    x_pos = []
    y_vals = []
    errors = []
    labels = []
    colors = []

    for i, est in enumerate(estimator_list):
        l_bound = estims_map.get(est, 0) * pairs
        if l_bound >= len(table):
            continue

        row = table.iloc[l_bound]
        y = row["Estimate"]
        se = row.get("SE", 0)

        x_pos.append(i)
        y_vals.append(y)
        errors.append(1.96 * se)
        labels.append(estimator_label(est))
        colors.append(ESTIMATOR_COLORS.get(est, "blue"))

    ax.bar(x_pos, y_vals, color=colors, alpha=0.7)
    ax.errorbar(x_pos, y_vals, yerr=errors, fmt='none',
                color='black', capsize=8, capthick=2)

    ax.axhline(y=0, color='gray', linestyle='--', linewidth=1, alpha=0.7)
    ax.set_xticks(x_pos)
    ax.set_xticklabels(labels)

    _format_axis(ax, xlabel="Estimator", ylabel="Effect Estimate", title=title)

    plt.tight_layout()

    if save_path:
        fig.savefig(save_path, dpi=dpi, bbox_inches='tight')

    return fig