diff-diff 3.0.1__cp314-cp314-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diff_diff/__init__.py +382 -0
- diff_diff/_backend.py +134 -0
- diff_diff/_rust_backend.cp314-win_amd64.pyd +0 -0
- diff_diff/bacon.py +1140 -0
- diff_diff/bootstrap_utils.py +730 -0
- diff_diff/continuous_did.py +1626 -0
- diff_diff/continuous_did_bspline.py +190 -0
- diff_diff/continuous_did_results.py +374 -0
- diff_diff/datasets.py +815 -0
- diff_diff/diagnostics.py +882 -0
- diff_diff/efficient_did.py +1770 -0
- diff_diff/efficient_did_bootstrap.py +359 -0
- diff_diff/efficient_did_covariates.py +899 -0
- diff_diff/efficient_did_results.py +368 -0
- diff_diff/efficient_did_weights.py +617 -0
- diff_diff/estimators.py +1501 -0
- diff_diff/honest_did.py +2585 -0
- diff_diff/imputation.py +2458 -0
- diff_diff/imputation_bootstrap.py +418 -0
- diff_diff/imputation_results.py +448 -0
- diff_diff/linalg.py +2538 -0
- diff_diff/power.py +2588 -0
- diff_diff/practitioner.py +869 -0
- diff_diff/prep.py +1738 -0
- diff_diff/prep_dgp.py +1718 -0
- diff_diff/pretrends.py +1105 -0
- diff_diff/results.py +918 -0
- diff_diff/stacked_did.py +1049 -0
- diff_diff/stacked_did_results.py +339 -0
- diff_diff/staggered.py +3895 -0
- diff_diff/staggered_aggregation.py +864 -0
- diff_diff/staggered_bootstrap.py +752 -0
- diff_diff/staggered_results.py +416 -0
- diff_diff/staggered_triple_diff.py +1545 -0
- diff_diff/staggered_triple_diff_results.py +416 -0
- diff_diff/sun_abraham.py +1685 -0
- diff_diff/survey.py +1981 -0
- diff_diff/synthetic_did.py +1136 -0
- diff_diff/triple_diff.py +2047 -0
- diff_diff/trop.py +952 -0
- diff_diff/trop_global.py +1270 -0
- diff_diff/trop_local.py +1307 -0
- diff_diff/trop_results.py +356 -0
- diff_diff/twfe.py +542 -0
- diff_diff/two_stage.py +1952 -0
- diff_diff/two_stage_bootstrap.py +520 -0
- diff_diff/two_stage_results.py +400 -0
- diff_diff/utils.py +1902 -0
- diff_diff/visualization/__init__.py +61 -0
- diff_diff/visualization/_common.py +328 -0
- diff_diff/visualization/_continuous.py +274 -0
- diff_diff/visualization/_diagnostic.py +817 -0
- diff_diff/visualization/_event_study.py +1086 -0
- diff_diff/visualization/_power.py +661 -0
- diff_diff/visualization/_staggered.py +833 -0
- diff_diff/visualization/_synthetic.py +197 -0
- diff_diff/wooldridge.py +1285 -0
- diff_diff/wooldridge_results.py +349 -0
- diff_diff-3.0.1.dist-info/METADATA +2997 -0
- diff_diff-3.0.1.dist-info/RECORD +62 -0
- diff_diff-3.0.1.dist-info/WHEEL +4 -0
- diff_diff-3.0.1.dist-info/sboms/diff_diff_rust.cyclonedx.json +5843 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Synthetic control visualization functions."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from diff_diff.results import SyntheticDiDResults
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def plot_synth_weights(
|
|
10
|
+
results: Optional["SyntheticDiDResults"] = None,
|
|
11
|
+
*,
|
|
12
|
+
weights: Optional[Dict[Any, float]] = None,
|
|
13
|
+
weight_type: str = "unit",
|
|
14
|
+
top_n: Optional[int] = None,
|
|
15
|
+
min_weight: float = 0.001,
|
|
16
|
+
figsize: Tuple[float, float] = (10, 6),
|
|
17
|
+
title: Optional[str] = None,
|
|
18
|
+
color: str = "#2563eb",
|
|
19
|
+
ax: Optional[Any] = None,
|
|
20
|
+
show: bool = True,
|
|
21
|
+
backend: str = "matplotlib",
|
|
22
|
+
) -> Any:
|
|
23
|
+
"""
|
|
24
|
+
Plot synthetic control weights as a bar chart.
|
|
25
|
+
|
|
26
|
+
Visualizes the unit weights or time weights from a Synthetic
|
|
27
|
+
Difference-in-Differences estimation.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
results : SyntheticDiDResults, optional
|
|
32
|
+
Results from SyntheticDiD estimator. Extracts weights based on
|
|
33
|
+
``weight_type``.
|
|
34
|
+
weights : dict, optional
|
|
35
|
+
Dictionary mapping unit/period IDs to weights. Used if results
|
|
36
|
+
is None.
|
|
37
|
+
weight_type : str, default="unit"
|
|
38
|
+
Which weights to plot: ``"unit"`` for control unit weights or
|
|
39
|
+
``"time"`` for pre-treatment time weights.
|
|
40
|
+
top_n : int, optional
|
|
41
|
+
Show only the top N weights by magnitude. Useful when there
|
|
42
|
+
are many control units.
|
|
43
|
+
min_weight : float, default=0.001
|
|
44
|
+
Minimum weight threshold for display.
|
|
45
|
+
figsize : tuple, default=(10, 6)
|
|
46
|
+
Figure size (width, height) in inches.
|
|
47
|
+
title : str, optional
|
|
48
|
+
Plot title. If None, auto-generated based on ``weight_type``.
|
|
49
|
+
color : str, default="#2563eb"
|
|
50
|
+
Bar color.
|
|
51
|
+
ax : matplotlib.axes.Axes, optional
|
|
52
|
+
Axes to plot on. If None, creates new figure.
|
|
53
|
+
show : bool, default=True
|
|
54
|
+
Whether to call plt.show() at the end.
|
|
55
|
+
backend : str, default="matplotlib"
|
|
56
|
+
Plotting backend: ``"matplotlib"`` or ``"plotly"``.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
matplotlib.axes.Axes or plotly.graph_objects.Figure
|
|
61
|
+
The axes object (matplotlib) or figure (plotly).
|
|
62
|
+
"""
|
|
63
|
+
# Extract weights
|
|
64
|
+
if results is not None and weights is not None:
|
|
65
|
+
raise ValueError("Provide either 'results' or 'weights', not both.")
|
|
66
|
+
|
|
67
|
+
if results is not None:
|
|
68
|
+
if weight_type == "unit":
|
|
69
|
+
weights = results.unit_weights
|
|
70
|
+
elif weight_type == "time":
|
|
71
|
+
weights = results.time_weights
|
|
72
|
+
else:
|
|
73
|
+
raise ValueError(f"weight_type must be 'unit' or 'time', got '{weight_type}'")
|
|
74
|
+
|
|
75
|
+
if weights is None:
|
|
76
|
+
raise ValueError("Must provide either 'results' or 'weights'.")
|
|
77
|
+
|
|
78
|
+
if not weights:
|
|
79
|
+
raise ValueError("No weights available to plot.")
|
|
80
|
+
|
|
81
|
+
# Filter by min_weight
|
|
82
|
+
filtered = {k: v for k, v in weights.items() if abs(v) >= min_weight}
|
|
83
|
+
if not filtered:
|
|
84
|
+
raise ValueError(f"No weights >= {min_weight} to plot.")
|
|
85
|
+
|
|
86
|
+
# Sort by weight descending
|
|
87
|
+
sorted_items = sorted(filtered.items(), key=lambda x: x[1], reverse=True)
|
|
88
|
+
|
|
89
|
+
# Apply top_n limit
|
|
90
|
+
if top_n is not None:
|
|
91
|
+
sorted_items = sorted_items[:top_n]
|
|
92
|
+
|
|
93
|
+
labels = [str(k) for k, _ in sorted_items]
|
|
94
|
+
values = [v for _, v in sorted_items]
|
|
95
|
+
|
|
96
|
+
# Auto-generate title
|
|
97
|
+
if title is None:
|
|
98
|
+
if weight_type == "unit":
|
|
99
|
+
title = "Synthetic Control Unit Weights"
|
|
100
|
+
else:
|
|
101
|
+
title = "Synthetic Control Time Weights"
|
|
102
|
+
|
|
103
|
+
if backend == "plotly":
|
|
104
|
+
return _render_synth_weights_plotly(
|
|
105
|
+
labels=labels,
|
|
106
|
+
values=values,
|
|
107
|
+
title=title,
|
|
108
|
+
color=color,
|
|
109
|
+
weight_type=weight_type,
|
|
110
|
+
show=show,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return _render_synth_weights_mpl(
|
|
114
|
+
labels=labels,
|
|
115
|
+
values=values,
|
|
116
|
+
figsize=figsize,
|
|
117
|
+
title=title,
|
|
118
|
+
color=color,
|
|
119
|
+
weight_type=weight_type,
|
|
120
|
+
ax=ax,
|
|
121
|
+
show=show,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _render_synth_weights_mpl(*, labels, values, figsize, title, color, weight_type, ax, show):
|
|
126
|
+
"""Render synthetic control weights with matplotlib."""
|
|
127
|
+
from diff_diff.visualization._common import _require_matplotlib
|
|
128
|
+
|
|
129
|
+
plt = _require_matplotlib()
|
|
130
|
+
|
|
131
|
+
if ax is None:
|
|
132
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
133
|
+
else:
|
|
134
|
+
fig = ax.get_figure()
|
|
135
|
+
|
|
136
|
+
# Horizontal bar chart
|
|
137
|
+
y_pos = range(len(labels))
|
|
138
|
+
ax.barh(y_pos, values, color=color, alpha=0.8, edgecolor="white")
|
|
139
|
+
|
|
140
|
+
ax.set_yticks(y_pos)
|
|
141
|
+
ax.set_yticklabels(labels)
|
|
142
|
+
ax.invert_yaxis() # Highest weight at top
|
|
143
|
+
|
|
144
|
+
xlabel = "Weight"
|
|
145
|
+
ylabel = "Control Unit" if weight_type == "unit" else "Time Period"
|
|
146
|
+
ax.set_xlabel(xlabel)
|
|
147
|
+
ax.set_ylabel(ylabel)
|
|
148
|
+
ax.set_title(title)
|
|
149
|
+
ax.grid(True, alpha=0.3, axis="x")
|
|
150
|
+
|
|
151
|
+
# Add value labels on bars
|
|
152
|
+
for i, v in enumerate(values):
|
|
153
|
+
ax.text(v + 0.005, i, f"{v:.4f}", va="center", fontsize=9)
|
|
154
|
+
|
|
155
|
+
fig.tight_layout()
|
|
156
|
+
|
|
157
|
+
if show:
|
|
158
|
+
plt.show()
|
|
159
|
+
|
|
160
|
+
return ax
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _render_synth_weights_plotly(*, labels, values, title, color, weight_type, show):
|
|
164
|
+
"""Render synthetic control weights with plotly."""
|
|
165
|
+
from diff_diff.visualization._common import _plotly_default_layout, _require_plotly
|
|
166
|
+
|
|
167
|
+
go = _require_plotly()
|
|
168
|
+
|
|
169
|
+
fig = go.Figure()
|
|
170
|
+
|
|
171
|
+
fig.add_trace(
|
|
172
|
+
go.Bar(
|
|
173
|
+
y=labels,
|
|
174
|
+
x=values,
|
|
175
|
+
orientation="h",
|
|
176
|
+
marker_color=color,
|
|
177
|
+
opacity=0.8,
|
|
178
|
+
text=[f"{v:.4f}" for v in values],
|
|
179
|
+
textposition="outside",
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
ylabel = "Control Unit" if weight_type == "unit" else "Time Period"
|
|
184
|
+
_plotly_default_layout(
|
|
185
|
+
fig,
|
|
186
|
+
title=title,
|
|
187
|
+
xlabel="Weight",
|
|
188
|
+
ylabel=ylabel,
|
|
189
|
+
show_legend=False,
|
|
190
|
+
)
|
|
191
|
+
# Reverse y-axis so highest weight is at top
|
|
192
|
+
fig.update_yaxes(autorange="reversed")
|
|
193
|
+
|
|
194
|
+
if show:
|
|
195
|
+
fig.show()
|
|
196
|
+
|
|
197
|
+
return fig
|