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.
Files changed (62) hide show
  1. diff_diff/__init__.py +382 -0
  2. diff_diff/_backend.py +134 -0
  3. diff_diff/_rust_backend.cp314-win_amd64.pyd +0 -0
  4. diff_diff/bacon.py +1140 -0
  5. diff_diff/bootstrap_utils.py +730 -0
  6. diff_diff/continuous_did.py +1626 -0
  7. diff_diff/continuous_did_bspline.py +190 -0
  8. diff_diff/continuous_did_results.py +374 -0
  9. diff_diff/datasets.py +815 -0
  10. diff_diff/diagnostics.py +882 -0
  11. diff_diff/efficient_did.py +1770 -0
  12. diff_diff/efficient_did_bootstrap.py +359 -0
  13. diff_diff/efficient_did_covariates.py +899 -0
  14. diff_diff/efficient_did_results.py +368 -0
  15. diff_diff/efficient_did_weights.py +617 -0
  16. diff_diff/estimators.py +1501 -0
  17. diff_diff/honest_did.py +2585 -0
  18. diff_diff/imputation.py +2458 -0
  19. diff_diff/imputation_bootstrap.py +418 -0
  20. diff_diff/imputation_results.py +448 -0
  21. diff_diff/linalg.py +2538 -0
  22. diff_diff/power.py +2588 -0
  23. diff_diff/practitioner.py +869 -0
  24. diff_diff/prep.py +1738 -0
  25. diff_diff/prep_dgp.py +1718 -0
  26. diff_diff/pretrends.py +1105 -0
  27. diff_diff/results.py +918 -0
  28. diff_diff/stacked_did.py +1049 -0
  29. diff_diff/stacked_did_results.py +339 -0
  30. diff_diff/staggered.py +3895 -0
  31. diff_diff/staggered_aggregation.py +864 -0
  32. diff_diff/staggered_bootstrap.py +752 -0
  33. diff_diff/staggered_results.py +416 -0
  34. diff_diff/staggered_triple_diff.py +1545 -0
  35. diff_diff/staggered_triple_diff_results.py +416 -0
  36. diff_diff/sun_abraham.py +1685 -0
  37. diff_diff/survey.py +1981 -0
  38. diff_diff/synthetic_did.py +1136 -0
  39. diff_diff/triple_diff.py +2047 -0
  40. diff_diff/trop.py +952 -0
  41. diff_diff/trop_global.py +1270 -0
  42. diff_diff/trop_local.py +1307 -0
  43. diff_diff/trop_results.py +356 -0
  44. diff_diff/twfe.py +542 -0
  45. diff_diff/two_stage.py +1952 -0
  46. diff_diff/two_stage_bootstrap.py +520 -0
  47. diff_diff/two_stage_results.py +400 -0
  48. diff_diff/utils.py +1902 -0
  49. diff_diff/visualization/__init__.py +61 -0
  50. diff_diff/visualization/_common.py +328 -0
  51. diff_diff/visualization/_continuous.py +274 -0
  52. diff_diff/visualization/_diagnostic.py +817 -0
  53. diff_diff/visualization/_event_study.py +1086 -0
  54. diff_diff/visualization/_power.py +661 -0
  55. diff_diff/visualization/_staggered.py +833 -0
  56. diff_diff/visualization/_synthetic.py +197 -0
  57. diff_diff/wooldridge.py +1285 -0
  58. diff_diff/wooldridge_results.py +349 -0
  59. diff_diff-3.0.1.dist-info/METADATA +2997 -0
  60. diff_diff-3.0.1.dist-info/RECORD +62 -0
  61. diff_diff-3.0.1.dist-info/WHEEL +4 -0
  62. 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