diffindiff 2.5.2__tar.gz → 2.5.4__tar.gz
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.
- {diffindiff-2.5.2 → diffindiff-2.5.4}/PKG-INFO +9 -7
- {diffindiff-2.5.2 → diffindiff-2.5.4}/README.md +8 -6
- {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/config.py +5 -3
- {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/didanalysis.py +45 -9
- {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/didanalysis_helper.py +104 -19
- {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/diddata.py +16 -5
- {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/didtools.py +4 -4
- {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/tests/tests_diffindiff.py +90 -6
- {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff.egg-info/PKG-INFO +9 -7
- {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff.egg-info/requires.txt +0 -4
- {diffindiff-2.5.2 → diffindiff-2.5.4}/setup.py +1 -5
- {diffindiff-2.5.2 → diffindiff-2.5.4}/MANIFEST.in +0 -0
- {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/__init__.py +0 -0
- {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/tests/__init__.py +0 -0
- {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/tests/data/Corona_Hesse.xlsx +0 -0
- {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/tests/data/counties_DE.csv +0 -0
- {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/tests/data/curfew_DE.csv +0 -0
- {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff.egg-info/SOURCES.txt +0 -0
- {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff.egg-info/dependency_links.txt +0 -0
- {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff.egg-info/top_level.txt +0 -0
- {diffindiff-2.5.2 → diffindiff-2.5.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: diffindiff
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.4
|
|
4
4
|
Summary: diffindiff: Python library for convenient Difference-in-Differences analyses
|
|
5
5
|
Author: Thomas Wieland
|
|
6
6
|
Author-email: geowieland@googlemail.com
|
|
@@ -29,7 +29,7 @@ A case study that utilizes the diffindiff library is available on [arXiv](https:
|
|
|
29
29
|
|
|
30
30
|
If you use this software, please cite:
|
|
31
31
|
|
|
32
|
-
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.
|
|
32
|
+
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.4) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
## Installation
|
|
@@ -164,6 +164,7 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
164
164
|
- Goldfarb A, Tucker C, Wang Y (2022) Conducting Research in Marketing with Quasi-Experiments. *Journal of Marketing* 86(3): 1-19. [10.1177/00222429221082977](https://doi.org/10.1177/00222429221082977)
|
|
165
165
|
- Isporhing IE, Lipfert M, Pestel N (2021) Does re-opening schools contribute to the spread of SARS-CoV-2? Evidence from staggered summer breaks in Germany. *Journal of Public Economics* 198: 104426. [10.1016/j.jpubeco.2021.104426](https://doi.org/10.1016/j.jpubeco.2021.104426)
|
|
166
166
|
- Li KT, Luo L, Pattabhiramaiah A (2024) Causal Inference with Quasi-Experimental Data. *IMPACT at JMR* November 13, 2024. [AMA](https://www.ama.org/marketing-news/causal-inference-with-quasi-experimental-data/)
|
|
167
|
+
- Mitze T, Kosfeld R, Rode J, Wälde K (2020) Face masks considerably reduce COVID-19 cases in Germany. *Proceedings of the National Academy of Sciences of the United States of America* 117(51): 32293-32301. [10.1073/pnas.2015954117](https://doi.org/10.1073/pnas.2015954117)
|
|
167
168
|
- Olden A (2018) What do you buy when no one's watching? The effect of self-service checkouts on the composition of sales in retail. Discussion paper FOR 3/18, Norwegian School of Economics, Norway. [http://hdl.handle.net/11250/2490886](http://hdl.handle.net/11250/2490886)
|
|
168
169
|
- Olden A, Moen J (2022) The triple difference estimator. *The Econometrics Journal* 25(3): 531-553. [10.1093/ectj/utac010](https://doi.org/10.1093/ectj/utac010)
|
|
169
170
|
- Strassmann A, Çolak Y, Serra-Burriel M, Nordestgaard BG, Turk A, Afzal S, Puhan MA (2023) Nationwide indoor smoking ban and impact on smoking behaviour and lung function: a two-population natural experiment. *Thorax* 78(2): 144-150. [10.1136/thoraxjnl-2021-218436](https://doi.org/10.1136/thoraxjnl-2021-218436)
|
|
@@ -179,11 +180,12 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
179
180
|
This software was developed without the use of AI-generated code. The Continue Agent in Microsoft Visual Studio Code using the GPT-5 mini model (by OpenAI) was used solely to assist in drafting and refining docstrings for documentation. The corresponding guidelines and constraints defined by the author are documented in `AGENTS-docstrings.md` in the [public GitHub repository](https://github.com/geowieland/diffindiff_official).
|
|
180
181
|
|
|
181
182
|
|
|
182
|
-
## What's new (v2.5.
|
|
183
|
+
## What's new (v2.5.4)
|
|
183
184
|
|
|
184
|
-
- Extensions
|
|
185
|
-
- New functions DiffData.get_synthetic_control_weightings() and DiffData.get_synthetic_control_fitmetrics() for accessing control unit weightings and fit metrics of the synthetic control unit
|
|
186
185
|
- Bugfixes
|
|
187
|
-
-
|
|
186
|
+
- Correction of the nonsensical skipping of the treatment group dummy in the case of two observation units in didanalysis.did_analysis()
|
|
187
|
+
- Automatical drop of duplicates in results dictionaries in didanalysis_helper.extract_model_results()
|
|
188
|
+
- Inefficient successive data insert in diddata.DiffData.add_synthetic() replaced by efficient concatenating
|
|
188
189
|
- Other
|
|
189
|
-
-
|
|
190
|
+
- Test script extended by another example
|
|
191
|
+
- Cleaned dependencies
|
|
@@ -21,7 +21,7 @@ A case study that utilizes the diffindiff library is available on [arXiv](https:
|
|
|
21
21
|
|
|
22
22
|
If you use this software, please cite:
|
|
23
23
|
|
|
24
|
-
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.
|
|
24
|
+
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.4) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
## Installation
|
|
@@ -156,6 +156,7 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
156
156
|
- Goldfarb A, Tucker C, Wang Y (2022) Conducting Research in Marketing with Quasi-Experiments. *Journal of Marketing* 86(3): 1-19. [10.1177/00222429221082977](https://doi.org/10.1177/00222429221082977)
|
|
157
157
|
- Isporhing IE, Lipfert M, Pestel N (2021) Does re-opening schools contribute to the spread of SARS-CoV-2? Evidence from staggered summer breaks in Germany. *Journal of Public Economics* 198: 104426. [10.1016/j.jpubeco.2021.104426](https://doi.org/10.1016/j.jpubeco.2021.104426)
|
|
158
158
|
- Li KT, Luo L, Pattabhiramaiah A (2024) Causal Inference with Quasi-Experimental Data. *IMPACT at JMR* November 13, 2024. [AMA](https://www.ama.org/marketing-news/causal-inference-with-quasi-experimental-data/)
|
|
159
|
+
- Mitze T, Kosfeld R, Rode J, Wälde K (2020) Face masks considerably reduce COVID-19 cases in Germany. *Proceedings of the National Academy of Sciences of the United States of America* 117(51): 32293-32301. [10.1073/pnas.2015954117](https://doi.org/10.1073/pnas.2015954117)
|
|
159
160
|
- Olden A (2018) What do you buy when no one's watching? The effect of self-service checkouts on the composition of sales in retail. Discussion paper FOR 3/18, Norwegian School of Economics, Norway. [http://hdl.handle.net/11250/2490886](http://hdl.handle.net/11250/2490886)
|
|
160
161
|
- Olden A, Moen J (2022) The triple difference estimator. *The Econometrics Journal* 25(3): 531-553. [10.1093/ectj/utac010](https://doi.org/10.1093/ectj/utac010)
|
|
161
162
|
- Strassmann A, Çolak Y, Serra-Burriel M, Nordestgaard BG, Turk A, Afzal S, Puhan MA (2023) Nationwide indoor smoking ban and impact on smoking behaviour and lung function: a two-population natural experiment. *Thorax* 78(2): 144-150. [10.1136/thoraxjnl-2021-218436](https://doi.org/10.1136/thoraxjnl-2021-218436)
|
|
@@ -171,11 +172,12 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
171
172
|
This software was developed without the use of AI-generated code. The Continue Agent in Microsoft Visual Studio Code using the GPT-5 mini model (by OpenAI) was used solely to assist in drafting and refining docstrings for documentation. The corresponding guidelines and constraints defined by the author are documented in `AGENTS-docstrings.md` in the [public GitHub repository](https://github.com/geowieland/diffindiff_official).
|
|
172
173
|
|
|
173
174
|
|
|
174
|
-
## What's new (v2.5.
|
|
175
|
+
## What's new (v2.5.4)
|
|
175
176
|
|
|
176
|
-
- Extensions
|
|
177
|
-
- New functions DiffData.get_synthetic_control_weightings() and DiffData.get_synthetic_control_fitmetrics() for accessing control unit weightings and fit metrics of the synthetic control unit
|
|
178
177
|
- Bugfixes
|
|
179
|
-
-
|
|
178
|
+
- Correction of the nonsensical skipping of the treatment group dummy in the case of two observation units in didanalysis.did_analysis()
|
|
179
|
+
- Automatical drop of duplicates in results dictionaries in didanalysis_helper.extract_model_results()
|
|
180
|
+
- Inefficient successive data insert in diddata.DiffData.add_synthetic() replaced by efficient concatenating
|
|
180
181
|
- Other
|
|
181
|
-
-
|
|
182
|
+
- Test script extended by another example
|
|
183
|
+
- Cleaned dependencies
|
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
# Author: Thomas Wieland
|
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
|
6
6
|
# mail: geowieland@googlemail.com
|
|
7
|
-
# Version: 1.0.
|
|
8
|
-
# Last update: 2026-
|
|
7
|
+
# Version: 1.0.25
|
|
8
|
+
# Last update: 2026-08-04 18:17
|
|
9
9
|
# Copyright (c) 2025-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
12
12
|
# Basic config:
|
|
13
13
|
|
|
14
14
|
PACKAGE_NAME = "diffindiff"
|
|
15
|
-
PACKAGE_VERSION = "2.5.
|
|
15
|
+
PACKAGE_VERSION = "2.5.4"
|
|
16
16
|
|
|
17
17
|
VERBOSE = False
|
|
18
18
|
|
|
@@ -26,6 +26,8 @@ FIXED_EFFECTS_THRESHOLD = 3
|
|
|
26
26
|
|
|
27
27
|
AUTO_SKIP_CONSTANT_COLUMNS = True
|
|
28
28
|
|
|
29
|
+
REMOVE_DUPLICATES_FROM_RESULTS_DICT = True
|
|
30
|
+
|
|
29
31
|
ACCEPT_CONTINUOUS_TREATMENTS = True
|
|
30
32
|
|
|
31
33
|
# Description texts:
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
# Author: Thomas Wieland
|
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
|
6
6
|
# mail: geowieland@googlemail.com
|
|
7
|
-
# Version: 2.4.
|
|
8
|
-
# Last update: 2026-
|
|
7
|
+
# Version: 2.4.5
|
|
8
|
+
# Last update: 2026-08-03 20:25
|
|
9
9
|
# Copyright (c) 2024-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -782,7 +782,7 @@ class DiffModel:
|
|
|
782
782
|
|
|
783
783
|
if len(no_control_conditions) > 0:
|
|
784
784
|
if len(no_control_conditions) == 1:
|
|
785
|
-
print(f"NOTE: Treatment {no_control_conditions[0]} has no control conditions.")
|
|
785
|
+
print(f"NOTE: Treatment '{no_control_conditions[0]}' has no control conditions.")
|
|
786
786
|
else:
|
|
787
787
|
print(f"NOTE: Treatments {', '.join(no_control_conditions)} have no control conditions.")
|
|
788
788
|
|
|
@@ -829,7 +829,9 @@ class DiffModel:
|
|
|
829
829
|
plot_size: list = [7, 6],
|
|
830
830
|
scale_plot: bool = True,
|
|
831
831
|
show_central_tendency: bool = False,
|
|
832
|
-
central_tendency: str = "mean"
|
|
832
|
+
central_tendency: str = "mean",
|
|
833
|
+
save_fig: str = None,
|
|
834
|
+
save_fig_kwargs: dict = {}
|
|
833
835
|
):
|
|
834
836
|
|
|
835
837
|
"""
|
|
@@ -869,6 +871,10 @@ class DiffModel:
|
|
|
869
871
|
Show mean/median line for estimates. Default is False.
|
|
870
872
|
central_tendency : str, optional
|
|
871
873
|
'mean' or 'median' for central tendency if shown. Default is 'mean'.
|
|
874
|
+
save_fig : str, optional
|
|
875
|
+
If not none, filename of plot to be saved.
|
|
876
|
+
save_fig_kwargs : dict, optional
|
|
877
|
+
Optional arguments for plt.savefig() if save_fig is not None.
|
|
872
878
|
|
|
873
879
|
Returns
|
|
874
880
|
-------
|
|
@@ -1006,6 +1012,9 @@ class DiffModel:
|
|
|
1006
1012
|
if plot_grid:
|
|
1007
1013
|
plt.grid(True)
|
|
1008
1014
|
|
|
1015
|
+
if save_fig is not None and isinstance(save_fig, str):
|
|
1016
|
+
plt.savefig(save_fig, **save_fig_kwargs)
|
|
1017
|
+
|
|
1009
1018
|
plt.show()
|
|
1010
1019
|
|
|
1011
1020
|
return self
|
|
@@ -1473,7 +1482,9 @@ class DiffModel:
|
|
|
1473
1482
|
y_lim = None,
|
|
1474
1483
|
plot_title: str = "Treatment time",
|
|
1475
1484
|
plot_symbol: str = "o",
|
|
1476
|
-
treatment_group_only: bool = True
|
|
1485
|
+
treatment_group_only: bool = True,
|
|
1486
|
+
save_fig: str = None,
|
|
1487
|
+
save_fig_kwargs: dict = {}
|
|
1477
1488
|
):
|
|
1478
1489
|
|
|
1479
1490
|
"""
|
|
@@ -1497,6 +1508,10 @@ class DiffModel:
|
|
|
1497
1508
|
Symbol used for treatment timing points. Default is 'o'.
|
|
1498
1509
|
treatment_group_only : bool, optional
|
|
1499
1510
|
If True, only plot treated units.
|
|
1511
|
+
save_fig : str, optional
|
|
1512
|
+
If not none, filename of plot to be saved.
|
|
1513
|
+
save_fig_kwargs : dict, optional
|
|
1514
|
+
Optional arguments for plt.savefig() if save_fig is not None.
|
|
1500
1515
|
|
|
1501
1516
|
Returns
|
|
1502
1517
|
-------
|
|
@@ -1570,6 +1585,9 @@ class DiffModel:
|
|
|
1570
1585
|
|
|
1571
1586
|
if y_lim is not None:
|
|
1572
1587
|
ax.set_ylim(y_lim)
|
|
1588
|
+
|
|
1589
|
+
if save_fig is not None and isinstance(save_fig, str):
|
|
1590
|
+
plt.savefig(save_fig, **save_fig_kwargs)
|
|
1573
1591
|
|
|
1574
1592
|
plt.show()
|
|
1575
1593
|
|
|
@@ -1595,7 +1613,9 @@ class DiffModel:
|
|
|
1595
1613
|
pre_post_ticks: list = ["Pre", "Post"],
|
|
1596
1614
|
pre_post_barplot = False,
|
|
1597
1615
|
pre_post_bar_width = 0.5,
|
|
1598
|
-
retransform_log_outcome: bool = False
|
|
1616
|
+
retransform_log_outcome: bool = False,
|
|
1617
|
+
save_fig: str = None,
|
|
1618
|
+
save_fig_kwargs: dict = {}
|
|
1599
1619
|
):
|
|
1600
1620
|
|
|
1601
1621
|
"""
|
|
@@ -1642,6 +1662,10 @@ class DiffModel:
|
|
|
1642
1662
|
retransform_log_outcome : bool, optional
|
|
1643
1663
|
If outcome was log-transformed, retransform to original scale for plotting.
|
|
1644
1664
|
Default is False.
|
|
1665
|
+
save_fig : str, optional
|
|
1666
|
+
If not none, filename of plot to be saved.
|
|
1667
|
+
save_fig_kwargs : dict, optional
|
|
1668
|
+
Optional arguments for plt.savefig() if save_fig is not None.
|
|
1645
1669
|
|
|
1646
1670
|
Returns
|
|
1647
1671
|
-------
|
|
@@ -2044,7 +2068,10 @@ class DiffModel:
|
|
|
2044
2068
|
|
|
2045
2069
|
if y_lim is not None:
|
|
2046
2070
|
ax.set_ylim(y_lim)
|
|
2047
|
-
|
|
2071
|
+
|
|
2072
|
+
if save_fig is not None and isinstance(save_fig, str):
|
|
2073
|
+
plt.savefig(save_fig, **save_fig_kwargs)
|
|
2074
|
+
|
|
2048
2075
|
plt.show()
|
|
2049
2076
|
|
|
2050
2077
|
return model_data_TG_CG
|
|
@@ -2063,7 +2090,9 @@ class DiffModel:
|
|
|
2063
2090
|
plot_legend: bool = True,
|
|
2064
2091
|
plot_grid: bool = True,
|
|
2065
2092
|
plot_size: list = [12, 6],
|
|
2066
|
-
retransform_log_outcome: bool = False
|
|
2093
|
+
retransform_log_outcome: bool = False,
|
|
2094
|
+
save_fig: str = None,
|
|
2095
|
+
save_fig_kwargs: dict = {}
|
|
2067
2096
|
):
|
|
2068
2097
|
|
|
2069
2098
|
"""
|
|
@@ -2098,6 +2127,10 @@ class DiffModel:
|
|
|
2098
2127
|
retransform_log_outcome : bool, optional
|
|
2099
2128
|
If outcome was log-transformed, retransform to original scale for plotting.
|
|
2100
2129
|
Default is False.
|
|
2130
|
+
save_fig : str, optional
|
|
2131
|
+
If not none, filename of plot to be saved.
|
|
2132
|
+
save_fig_kwargs : dict, optional
|
|
2133
|
+
Optional arguments for plt.savefig() if save_fig is not None.
|
|
2101
2134
|
|
|
2102
2135
|
Returns
|
|
2103
2136
|
-------
|
|
@@ -2227,6 +2260,9 @@ class DiffModel:
|
|
|
2227
2260
|
|
|
2228
2261
|
if y_lim is not None:
|
|
2229
2262
|
ax.set_ylim(y_lim)
|
|
2263
|
+
|
|
2264
|
+
if save_fig is not None and isinstance(save_fig, str):
|
|
2265
|
+
plt.savefig(save_fig, **save_fig_kwargs)
|
|
2230
2266
|
|
|
2231
2267
|
plt.show()
|
|
2232
2268
|
|
|
@@ -2476,7 +2512,7 @@ def did_analysis(
|
|
|
2476
2512
|
unique_units = treatment_diagnostics_results[3]
|
|
2477
2513
|
unique_time_points = treatment_diagnostics_results[4]
|
|
2478
2514
|
|
|
2479
|
-
if no_treatments > 1:
|
|
2515
|
+
if no_treatments > 1 and unique_units >= config.FIXED_EFFECTS_THRESHOLD:
|
|
2480
2516
|
|
|
2481
2517
|
intercept = False
|
|
2482
2518
|
TG_col = []
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
# Author: Thomas Wieland
|
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
|
6
6
|
# mail: geowieland@googlemail.com
|
|
7
|
-
# Version: 1.2.
|
|
8
|
-
# Last update: 2026-
|
|
7
|
+
# Version: 1.2.4
|
|
8
|
+
# Last update: 2026-08-04 17:35
|
|
9
9
|
# Copyright (c) 2025-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -25,7 +25,7 @@ def create_fixed_effects(
|
|
|
25
25
|
type: str = "unit",
|
|
26
26
|
drop_first: bool = False,
|
|
27
27
|
verbose: bool = config.VERBOSE
|
|
28
|
-
):
|
|
28
|
+
) -> list:
|
|
29
29
|
|
|
30
30
|
"""
|
|
31
31
|
Create dummy variables for fixed effects and attach them to the data.
|
|
@@ -118,7 +118,7 @@ def demean_variables(
|
|
|
118
118
|
max_iter=10,
|
|
119
119
|
tol=1e-8,
|
|
120
120
|
verbose: bool = config.VERBOSE
|
|
121
|
-
):
|
|
121
|
+
) -> tuple:
|
|
122
122
|
|
|
123
123
|
"""
|
|
124
124
|
Demean numeric variables by removing unit and/or time means.
|
|
@@ -399,7 +399,7 @@ def create_specific_time_trends(
|
|
|
399
399
|
FE_vars: list,
|
|
400
400
|
type: str = "ITT",
|
|
401
401
|
verbose: bool = config.VERBOSE
|
|
402
|
-
):
|
|
402
|
+
) -> list:
|
|
403
403
|
|
|
404
404
|
"""
|
|
405
405
|
Create unit-specific time trend variables (interactions with a time counter).
|
|
@@ -482,7 +482,7 @@ def create_specific_treatment_effects(
|
|
|
482
482
|
FE_vars: list,
|
|
483
483
|
type: str = "ITE",
|
|
484
484
|
verbose: bool = config.VERBOSE
|
|
485
|
-
):
|
|
485
|
+
) -> list:
|
|
486
486
|
|
|
487
487
|
"""
|
|
488
488
|
Create unit-specific treatment effect interaction variables.
|
|
@@ -566,7 +566,7 @@ def create_spillover(
|
|
|
566
566
|
spillover_treatment: list = None,
|
|
567
567
|
spillover_units: list = None,
|
|
568
568
|
verbose: bool = config.VERBOSE
|
|
569
|
-
):
|
|
569
|
+
) -> list:
|
|
570
570
|
|
|
571
571
|
"""
|
|
572
572
|
Create spillover indicator variables for given treatments and units.
|
|
@@ -667,7 +667,7 @@ def create_interactions(
|
|
|
667
667
|
data: pd.DataFrame,
|
|
668
668
|
interactions: dict = None,
|
|
669
669
|
verbose: bool = config.VERBOSE
|
|
670
|
-
):
|
|
670
|
+
) -> list:
|
|
671
671
|
|
|
672
672
|
"""
|
|
673
673
|
Create interaction variables by multiplying specified treatment columns.
|
|
@@ -787,7 +787,7 @@ def data_diagnostics(
|
|
|
787
787
|
drop_missing: bool = True,
|
|
788
788
|
missing_replace_by_zero: bool = False,
|
|
789
789
|
verbose: bool = config.VERBOSE
|
|
790
|
-
):
|
|
790
|
+
) -> dict:
|
|
791
791
|
|
|
792
792
|
"""
|
|
793
793
|
Run diagnostics on model data: missingness, balance and basic stats.
|
|
@@ -906,7 +906,7 @@ def treatment_diagnostics(
|
|
|
906
906
|
pre_post: bool = False,
|
|
907
907
|
confint_alpha = 0.05,
|
|
908
908
|
verbose: bool = config.VERBOSE
|
|
909
|
-
):
|
|
909
|
+
) -> list:
|
|
910
910
|
|
|
911
911
|
"""
|
|
912
912
|
Compute diagnostics for each treatment: simultaneity, parallel trends,
|
|
@@ -1057,7 +1057,7 @@ def ols_fit(
|
|
|
1057
1057
|
confint_alpha = 0.05,
|
|
1058
1058
|
cluster_SE_by: str = None,
|
|
1059
1059
|
verbose: bool = config.VERBOSE
|
|
1060
|
-
):
|
|
1060
|
+
) -> list:
|
|
1061
1061
|
|
|
1062
1062
|
"""
|
|
1063
1063
|
Estimate an OLS model using a formula and return coefficients and predictions.
|
|
@@ -1130,7 +1130,7 @@ def ml_fit(
|
|
|
1130
1130
|
family=sm.families.Gaussian(),
|
|
1131
1131
|
link=sm.families.links.Identity(),
|
|
1132
1132
|
verbose: bool = config.VERBOSE
|
|
1133
|
-
):
|
|
1133
|
+
) -> list:
|
|
1134
1134
|
|
|
1135
1135
|
"""
|
|
1136
1136
|
Estimate a model by maximum likelihood (GLM) and return coefficients and predictions.
|
|
@@ -1217,7 +1217,7 @@ def extract_model_results(
|
|
|
1217
1217
|
BG_x_TT_col: list = None,
|
|
1218
1218
|
covariates: list = None,
|
|
1219
1219
|
verbose: bool = config.VERBOSE
|
|
1220
|
-
):
|
|
1220
|
+
) -> dict:
|
|
1221
1221
|
|
|
1222
1222
|
"""
|
|
1223
1223
|
Compile and format model coefficient results into a structured dict.
|
|
@@ -1311,6 +1311,9 @@ def extract_model_results(
|
|
|
1311
1311
|
"CI_lower": float(coef_conf_intervals.loc[treatment, 0]),
|
|
1312
1312
|
"CI_upper": float(coef_conf_intervals.loc[treatment, 1]),
|
|
1313
1313
|
}
|
|
1314
|
+
|
|
1315
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1316
|
+
ATE = remove_duplicates_from_dict(ATE)
|
|
1314
1317
|
|
|
1315
1318
|
model_results = {config.EFFECTS_TYPES["ATE"]["model_results_key"]: ATE}
|
|
1316
1319
|
|
|
@@ -1329,6 +1332,9 @@ def extract_model_results(
|
|
|
1329
1332
|
"CI_upper": float(coef_conf_intervals.loc[TG_, 1]),
|
|
1330
1333
|
}
|
|
1331
1334
|
|
|
1335
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1336
|
+
beta_1 = remove_duplicates_from_dict(beta_1)
|
|
1337
|
+
|
|
1332
1338
|
model_results[config.EFFECTS_TYPES["beta_1"]["model_results_key"]] = beta_1
|
|
1333
1339
|
|
|
1334
1340
|
if (len(TT_col) > 0) and (any(col in coefficients for col in TT_col)):
|
|
@@ -1346,6 +1352,9 @@ def extract_model_results(
|
|
|
1346
1352
|
"CI_upper": float(coef_conf_intervals.loc[TT_, 1]),
|
|
1347
1353
|
}
|
|
1348
1354
|
|
|
1355
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1356
|
+
delta_0 = remove_duplicates_from_dict(delta_0)
|
|
1357
|
+
|
|
1349
1358
|
model_results[config.EFFECTS_TYPES["delta_0"]["model_results_key"]] = delta_0
|
|
1350
1359
|
|
|
1351
1360
|
if "Intercept" in coefficients:
|
|
@@ -1377,6 +1386,9 @@ def extract_model_results(
|
|
|
1377
1386
|
"CI_upper": float(coef_conf_intervals.loc[AATE_, 1]),
|
|
1378
1387
|
}
|
|
1379
1388
|
|
|
1389
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1390
|
+
AATE = remove_duplicates_from_dict(AATE)
|
|
1391
|
+
|
|
1380
1392
|
model_results[config.EFFECTS_TYPES["AATE"]["model_results_key"]] = AATE
|
|
1381
1393
|
|
|
1382
1394
|
if (len(ATT_col) > 0) and (any(col in coefficients for col in ATT_col)):
|
|
@@ -1394,6 +1406,9 @@ def extract_model_results(
|
|
|
1394
1406
|
"CI_upper": float(coef_conf_intervals.loc[ATT_, 1]),
|
|
1395
1407
|
}
|
|
1396
1408
|
|
|
1409
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1410
|
+
ATT = remove_duplicates_from_dict(ATT)
|
|
1411
|
+
|
|
1397
1412
|
model_results[config.EFFECTS_TYPES["ATT"]["model_results_key"]] = ATT
|
|
1398
1413
|
|
|
1399
1414
|
if (len(spillover_vars) > 0) and (any(col in coefficients for col in spillover_vars)):
|
|
@@ -1411,6 +1426,9 @@ def extract_model_results(
|
|
|
1411
1426
|
"CI_upper": float(coef_conf_intervals.loc[spillover_var, 1]),
|
|
1412
1427
|
}
|
|
1413
1428
|
|
|
1429
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1430
|
+
spillover_coef = remove_duplicates_from_dict(spillover_coef)
|
|
1431
|
+
|
|
1414
1432
|
model_results[config.EFFECTS_TYPES["spillover"]["model_results_key"]] = spillover_coef
|
|
1415
1433
|
|
|
1416
1434
|
fixed_effects = [
|
|
@@ -1435,6 +1453,9 @@ def extract_model_results(
|
|
|
1435
1453
|
"Coefficient_type": config.EFFECTS_TYPES["FE"]["types"][0]["description"]
|
|
1436
1454
|
}
|
|
1437
1455
|
|
|
1456
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1457
|
+
FE_unit_coef = remove_duplicates_from_dict(FE_unit_coef)
|
|
1458
|
+
|
|
1438
1459
|
fixed_effects[0] = {config.EFFECTS_TYPES["FE"]["types"][0]["model_results_key"]: FE_unit_coef}
|
|
1439
1460
|
|
|
1440
1461
|
if (len(FE_time_vars) > 0) and (any(col in coefficients for col in FE_time_vars)):
|
|
@@ -1452,7 +1473,10 @@ def extract_model_results(
|
|
|
1452
1473
|
"CI_upper": float(coef_conf_intervals.loc[time_dummy, 1]),
|
|
1453
1474
|
"Coefficient_type": config.EFFECTS_TYPES["FE"]["types"][1]["description"]
|
|
1454
1475
|
}
|
|
1455
|
-
|
|
1476
|
+
|
|
1477
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1478
|
+
FE_time_coef = remove_duplicates_from_dict(FE_time_coef)
|
|
1479
|
+
|
|
1456
1480
|
fixed_effects[1] = {config.EFFECTS_TYPES["FE"]["types"][1]["model_results_key"]: FE_time_coef}
|
|
1457
1481
|
|
|
1458
1482
|
if (len(FE_group_vars) > 0) and (any(col in coefficients for col in FE_group_vars)):
|
|
@@ -1470,7 +1494,10 @@ def extract_model_results(
|
|
|
1470
1494
|
"CI_upper": float(coef_conf_intervals.loc[group_dummy, 1]),
|
|
1471
1495
|
"Coefficient_type": config.EFFECTS_TYPES["FE"]["types"][2]["description"]
|
|
1472
1496
|
}
|
|
1473
|
-
|
|
1497
|
+
|
|
1498
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1499
|
+
FE_group_coef = remove_duplicates_from_dict(FE_group_coef)
|
|
1500
|
+
|
|
1474
1501
|
fixed_effects[2] = {config.EFFECTS_TYPES["FE"]["types"][2]["model_results_key"]: FE_group_coef}
|
|
1475
1502
|
|
|
1476
1503
|
model_results[config.EFFECTS_TYPES["FE"]["model_results_key"]] = fixed_effects
|
|
@@ -1491,6 +1518,9 @@ def extract_model_results(
|
|
|
1491
1518
|
"CI_upper": float(coef_conf_intervals.loc[ITT_var, 1]),
|
|
1492
1519
|
}
|
|
1493
1520
|
|
|
1521
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1522
|
+
ITT_coef = remove_duplicates_from_dict(ITT_coef)
|
|
1523
|
+
|
|
1494
1524
|
model_results["individual_time_trends"] = ITT_coef
|
|
1495
1525
|
|
|
1496
1526
|
if (len(ITE_vars) > 0) and (any(col in coefficients for col in ITE_vars)):
|
|
@@ -1516,6 +1546,9 @@ def extract_model_results(
|
|
|
1516
1546
|
"CI_upper": float(coef_conf_intervals.loc[ITE_var, 1]),
|
|
1517
1547
|
}
|
|
1518
1548
|
|
|
1549
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1550
|
+
ITE_coef = remove_duplicates_from_dict(ITE_coef)
|
|
1551
|
+
|
|
1519
1552
|
model_results["individual_treatment_effects"] = ITE_coef
|
|
1520
1553
|
|
|
1521
1554
|
if (len(GTT_vars) > 0) and (any(col in coefficients for col in GTT_vars)):
|
|
@@ -1534,6 +1567,9 @@ def extract_model_results(
|
|
|
1534
1567
|
"CI_upper": float(coef_conf_intervals.loc[GTT_var, 1]),
|
|
1535
1568
|
}
|
|
1536
1569
|
|
|
1570
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1571
|
+
GTT_coef = remove_duplicates_from_dict(GTT_coef)
|
|
1572
|
+
|
|
1537
1573
|
model_results["group_time_trends"] = GTT_coef
|
|
1538
1574
|
|
|
1539
1575
|
if (len(GTE_vars) > 0) and (any(col in coefficients for col in GTE_vars)):
|
|
@@ -1558,6 +1594,9 @@ def extract_model_results(
|
|
|
1558
1594
|
"CI_upper": float(coef_conf_intervals.loc[GTE_var, 1]),
|
|
1559
1595
|
}
|
|
1560
1596
|
|
|
1597
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1598
|
+
GTE_coef = remove_duplicates_from_dict(GTE_coef)
|
|
1599
|
+
|
|
1561
1600
|
model_results["group_treatment_effects"] = GTE_coef
|
|
1562
1601
|
|
|
1563
1602
|
if (len(covariates) > 0) and (any(col in coefficients for col in covariates)):
|
|
@@ -1577,6 +1616,9 @@ def extract_model_results(
|
|
|
1577
1616
|
"CI_lower": float(coef_conf_intervals.loc[covariate, 0]),
|
|
1578
1617
|
"CI_upper": float(coef_conf_intervals.loc[covariate, 1]),
|
|
1579
1618
|
}
|
|
1619
|
+
|
|
1620
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1621
|
+
covariates_effects = remove_duplicates_from_dict(covariates_effects)
|
|
1580
1622
|
|
|
1581
1623
|
model_results["covariates_effects"] = covariates_effects
|
|
1582
1624
|
|
|
@@ -1595,6 +1637,9 @@ def extract_model_results(
|
|
|
1595
1637
|
"CI_upper": float(coef_conf_intervals.loc[TG_x_BG_x_TT_, 1]),
|
|
1596
1638
|
}
|
|
1597
1639
|
|
|
1640
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1641
|
+
TDATE = remove_duplicates_from_dict(TDATE)
|
|
1642
|
+
|
|
1598
1643
|
model_results = {config.EFFECTS_TYPES_DDD["TDATE"]["model_results_key"]: TDATE}
|
|
1599
1644
|
|
|
1600
1645
|
if (len(BG_col) > 0) and (any(col in coefficients for col in BG_col)):
|
|
@@ -1612,6 +1657,9 @@ def extract_model_results(
|
|
|
1612
1657
|
"CI_upper": float(coef_conf_intervals.loc[BG_, 1]),
|
|
1613
1658
|
}
|
|
1614
1659
|
|
|
1660
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1661
|
+
BG = remove_duplicates_from_dict(BG)
|
|
1662
|
+
|
|
1615
1663
|
model_results[config.EFFECTS_TYPES_DDD["beta_2"]["model_results_key"]] = BG
|
|
1616
1664
|
|
|
1617
1665
|
if (len(TG_x_BG_col) > 0) and (any(col in coefficients for col in TG_x_BG_col)):
|
|
@@ -1627,7 +1675,10 @@ def extract_model_results(
|
|
|
1627
1675
|
"p": float(coef_p[TG_x_BG_]),
|
|
1628
1676
|
"CI_lower": float(coef_conf_intervals.loc[TG_x_BG_, 0]),
|
|
1629
1677
|
"CI_upper": float(coef_conf_intervals.loc[TG_x_BG_, 1]),
|
|
1630
|
-
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1681
|
+
TG_x_BG = remove_duplicates_from_dict(TG_x_BG)
|
|
1631
1682
|
|
|
1632
1683
|
model_results[config.EFFECTS_TYPES_DDD["beta_4"]["model_results_key"]] = TG_x_BG
|
|
1633
1684
|
|
|
@@ -1646,6 +1697,9 @@ def extract_model_results(
|
|
|
1646
1697
|
"CI_upper": float(coef_conf_intervals.loc[BG_x_TT_, 1]),
|
|
1647
1698
|
}
|
|
1648
1699
|
|
|
1700
|
+
if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
|
|
1701
|
+
BG_x_TT = remove_duplicates_from_dict(BG_x_TT)
|
|
1702
|
+
|
|
1649
1703
|
model_results[config.EFFECTS_TYPES_DDD["beta_6"]["model_results_key"]] = BG_x_TT
|
|
1650
1704
|
|
|
1651
1705
|
if verbose:
|
|
@@ -1658,7 +1712,7 @@ def fit_metrics(
|
|
|
1658
1712
|
outcome_col,
|
|
1659
1713
|
model_predictions,
|
|
1660
1714
|
indep_vars_no: int = None
|
|
1661
|
-
):
|
|
1715
|
+
) -> list:
|
|
1662
1716
|
|
|
1663
1717
|
"""
|
|
1664
1718
|
Compute fit metrics for a model given data and predictions.
|
|
@@ -1704,7 +1758,7 @@ def fit_metrics(
|
|
|
1704
1758
|
|
|
1705
1759
|
return fit_metrics_result
|
|
1706
1760
|
|
|
1707
|
-
def create_timestamp(function):
|
|
1761
|
+
def create_timestamp(function) -> dict:
|
|
1708
1762
|
|
|
1709
1763
|
"""
|
|
1710
1764
|
Create a standard timestamp dictionary for logging or metadata.
|
|
@@ -1743,4 +1797,35 @@ def create_timestamp(function):
|
|
|
1743
1797
|
"datetime": now.strftime("%Y-%m-%d %H-%M-%S")
|
|
1744
1798
|
}
|
|
1745
1799
|
|
|
1746
|
-
return timestamp_dict
|
|
1800
|
+
return timestamp_dict
|
|
1801
|
+
|
|
1802
|
+
def remove_duplicates_from_dict(
|
|
1803
|
+
any_dict: dict
|
|
1804
|
+
) -> dict:
|
|
1805
|
+
|
|
1806
|
+
"""
|
|
1807
|
+
Remove duplicate values from a dictionary.
|
|
1808
|
+
|
|
1809
|
+
Parameters
|
|
1810
|
+
----------
|
|
1811
|
+
any_dict : dict
|
|
1812
|
+
Dictionary from which to remove duplicates.
|
|
1813
|
+
|
|
1814
|
+
Returns
|
|
1815
|
+
-------
|
|
1816
|
+
dict
|
|
1817
|
+
Dictionary with unique values.
|
|
1818
|
+
"""
|
|
1819
|
+
|
|
1820
|
+
any_dict_unique = {}
|
|
1821
|
+
seen = set()
|
|
1822
|
+
|
|
1823
|
+
for key, value in any_dict.items():
|
|
1824
|
+
|
|
1825
|
+
identifier = tuple(sorted(value.items()))
|
|
1826
|
+
|
|
1827
|
+
if identifier not in seen:
|
|
1828
|
+
seen.add(identifier)
|
|
1829
|
+
any_dict_unique[key] = value
|
|
1830
|
+
|
|
1831
|
+
return any_dict_unique
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
# Author: Thomas Wieland
|
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
|
6
6
|
# mail: geowieland@googlemail.com
|
|
7
|
-
# Version: 2.3.
|
|
8
|
-
# Last update: 2026-
|
|
7
|
+
# Version: 2.3.3
|
|
8
|
+
# Last update: 2026-08-04 17:51
|
|
9
9
|
# Copyright (c) 2024-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -1986,10 +1986,11 @@ class DiffData:
|
|
|
1986
1986
|
}
|
|
1987
1987
|
)
|
|
1988
1988
|
|
|
1989
|
+
columns = {}
|
|
1989
1990
|
units_not_included = []
|
|
1991
|
+
valid_units = []
|
|
1990
1992
|
|
|
1991
1993
|
for unit in units:
|
|
1992
|
-
|
|
1993
1994
|
did_modeldata_unit = did_modeldata.loc[
|
|
1994
1995
|
(did_modeldata[config.UNIT_COL].astype(str) == str(unit))
|
|
1995
1996
|
& (did_modeldata[outcome_col].notna()),
|
|
@@ -1998,9 +1999,19 @@ class DiffData:
|
|
|
1998
1999
|
|
|
1999
2000
|
if len(did_modeldata_unit) != len(outcome_matrix):
|
|
2000
2001
|
units_not_included.append(unit)
|
|
2001
|
-
units.remove(unit)
|
|
2002
2002
|
else:
|
|
2003
|
-
|
|
2003
|
+
valid_units.append(unit)
|
|
2004
|
+
columns[tools.clean_column_name(unit)] = did_modeldata_unit.reset_index(drop=True)
|
|
2005
|
+
|
|
2006
|
+
outcome_matrix = pd.concat(
|
|
2007
|
+
[
|
|
2008
|
+
outcome_matrix,
|
|
2009
|
+
pd.DataFrame(columns)
|
|
2010
|
+
],
|
|
2011
|
+
axis=1
|
|
2012
|
+
)
|
|
2013
|
+
|
|
2014
|
+
units = valid_units
|
|
2004
2015
|
|
|
2005
2016
|
if len(units_not_included) > 0:
|
|
2006
2017
|
print(f"WARNING: {len(units_not_included)} analysis units were not included due to NaN data.")
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
# Author: Thomas Wieland
|
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
|
6
6
|
# mail: geowieland@googlemail.com
|
|
7
|
-
# Version: 2.2.
|
|
8
|
-
# Last update: 2026-
|
|
7
|
+
# Version: 2.2.5
|
|
8
|
+
# Last update: 2026-07-23 19:34
|
|
9
9
|
# Copyright (c) 2025-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -540,10 +540,10 @@ def is_simultaneous(
|
|
|
540
540
|
print("OK")
|
|
541
541
|
|
|
542
542
|
if not simultaneous and data_isnotreatment[0]:
|
|
543
|
-
print(f"NOTE:
|
|
543
|
+
print(f"NOTE: Treatment '{treatment_col}' is not simultaneous.")
|
|
544
544
|
|
|
545
545
|
if simultaneous and not data_isnotreatment[0]:
|
|
546
|
-
print(f"WARNING:
|
|
546
|
+
print(f"WARNING: Treatment '{treatment_col}' is simultaneous and does not include a {config.NO_TREATMENT_CG_DESCRIPTION}")
|
|
547
547
|
|
|
548
548
|
return simultaneous
|
|
549
549
|
|
|
@@ -4,16 +4,18 @@
|
|
|
4
4
|
# Author: Thomas Wieland
|
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
|
6
6
|
# mail: geowieland@googlemail.com
|
|
7
|
-
# Version: 2.
|
|
8
|
-
# Last update: 2026-
|
|
7
|
+
# Version: 2.2.0
|
|
8
|
+
# Last update: 2026-08-04 18:42
|
|
9
9
|
# Copyright (c) 2025-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
"""
|
|
13
14
|
Please note that this tests script is a collection of examples for the use of most functions of the diffindiff package.
|
|
14
15
|
Although the data is real (i.e., non-simulated), no results relevant to the real world can be derived from it.
|
|
15
16
|
"""
|
|
16
17
|
|
|
18
|
+
from pathlib import Path
|
|
17
19
|
import pandas as pd
|
|
18
20
|
from diffindiff.didanalysis import did_analysis
|
|
19
21
|
from diffindiff.diddata import create_groups, create_treatment, merge_data, create_data
|
|
@@ -22,7 +24,11 @@ from diffindiff.diddata import create_groups, create_treatment, merge_data, crea
|
|
|
22
24
|
# Example 1: Effect of a curfew in German counties in the first
|
|
23
25
|
# wave of the COVID-19 pandemic (DiD pre-post analysis)
|
|
24
26
|
|
|
25
|
-
curfew_DE=pd.read_csv(
|
|
27
|
+
curfew_DE = pd.read_csv(
|
|
28
|
+
Path(__file__).parent / "data" / "curfew_DE.csv",
|
|
29
|
+
sep=";",
|
|
30
|
+
decimal=","
|
|
31
|
+
)
|
|
26
32
|
# Dataset with daily and cumulative SARS-CoV-2 infections of German counties
|
|
27
33
|
# Data source: Wieland (2020) https://doi.org/10.18335/region.v7i2.324
|
|
28
34
|
|
|
@@ -193,7 +199,12 @@ curfew_model_prepost_AT.plot(
|
|
|
193
199
|
# Plot DiD pre vs. post results
|
|
194
200
|
# with user-determined style
|
|
195
201
|
|
|
196
|
-
counties_DE=pd.read_csv(
|
|
202
|
+
counties_DE = pd.read_csv(
|
|
203
|
+
Path(__file__).parent / "data" / "counties_DE.csv",
|
|
204
|
+
sep=";",
|
|
205
|
+
decimal=",",
|
|
206
|
+
encoding='latin1'
|
|
207
|
+
)
|
|
197
208
|
# Dataset with German county data
|
|
198
209
|
|
|
199
210
|
curfew_data_prepost_withcov = curfew_data_prepost.add_covariates(
|
|
@@ -405,7 +416,9 @@ curfew_model_extended.plot(
|
|
|
405
416
|
# Example 3: Nighttime curfew and other NPI in Hesse
|
|
406
417
|
# (Staggered adoption)
|
|
407
418
|
|
|
408
|
-
Corona_Hesse=pd.read_excel(
|
|
419
|
+
Corona_Hesse = pd.read_excel(
|
|
420
|
+
Path(__file__).parent / "data" / "Corona_Hesse.xlsx",
|
|
421
|
+
)
|
|
409
422
|
# Test data effective reproduction number and Corona NPI Hesse
|
|
410
423
|
# Data source: Wieland (2025) https://doi.org/10.1007/s10389-024-02218-x
|
|
411
424
|
|
|
@@ -530,4 +543,75 @@ Hesse_model6=did_analysis(
|
|
|
530
543
|
# Model with two interventions and one interaction of the two treatments
|
|
531
544
|
|
|
532
545
|
Hesse_model6.summary()
|
|
533
|
-
# Model summary
|
|
546
|
+
# Model summary
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# Example 4: Mandatory face masks in Jena (Germany) during the first Corona wave
|
|
550
|
+
# data: German counties during the first Corona wave
|
|
551
|
+
|
|
552
|
+
facemasks_data=create_data(
|
|
553
|
+
outcome_data=curfew_DE,
|
|
554
|
+
unit_id_col="county",
|
|
555
|
+
time_col="infection_date",
|
|
556
|
+
outcome_col="infections_cum_per100000",
|
|
557
|
+
treatment_group=
|
|
558
|
+
curfew_DE.loc[curfew_DE["REG_NAME"] == "Jena"]["county"],
|
|
559
|
+
control_group=
|
|
560
|
+
curfew_DE.loc[curfew_DE["REG_NAME"] != "Jena"]["county"],
|
|
561
|
+
treatment_name="Mandatory face masks",
|
|
562
|
+
study_period=["2020-03-01", "2020-04-26"],
|
|
563
|
+
treatment_period=["2020-04-06", "2020-04-26"],
|
|
564
|
+
freq="D"
|
|
565
|
+
)
|
|
566
|
+
# Creating DiD dataset by defining groups and treatment time at once
|
|
567
|
+
# Treatment mandatory face masks in Jena beginning April 6, 2020 (first German city with mandatory face masks)
|
|
568
|
+
|
|
569
|
+
facemasks_data.summary()
|
|
570
|
+
# Summary of created treatment data
|
|
571
|
+
|
|
572
|
+
facemasks_data_analysis = facemasks_data.analysis(
|
|
573
|
+
FE_unit=True,
|
|
574
|
+
FE_time=True,
|
|
575
|
+
intercept=False,
|
|
576
|
+
verbose=True
|
|
577
|
+
)
|
|
578
|
+
# DiD analysis as two-way fixed effects model
|
|
579
|
+
|
|
580
|
+
facemasks_data_analysis.summary()
|
|
581
|
+
# Summary of model results
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
facemasks_data_analysis_demean = facemasks_data.analysis(
|
|
585
|
+
FE_unit=True,
|
|
586
|
+
FE_time=True,
|
|
587
|
+
intercept=False,
|
|
588
|
+
demean=True,
|
|
589
|
+
verbose=True
|
|
590
|
+
)
|
|
591
|
+
# DiD analysis as model with demeaened variables instead of fixed effects
|
|
592
|
+
|
|
593
|
+
facemasks_data_analysis_demean.summary()
|
|
594
|
+
# Summary of model results
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
facemasks_data_synth = facemasks_data.add_synthetic(process_unit="Jena")
|
|
598
|
+
# Add synthetic control for Jena
|
|
599
|
+
|
|
600
|
+
facemasks_data_synth.summary()
|
|
601
|
+
# Summary of synthetic DiD data
|
|
602
|
+
|
|
603
|
+
print(facemasks_data_synth.get_synthetic_control_weightings())
|
|
604
|
+
# Weights
|
|
605
|
+
|
|
606
|
+
print(facemasks_data_synth.data[0].head)
|
|
607
|
+
# Treatment unit with counterfactual (synthetic) control unit
|
|
608
|
+
|
|
609
|
+
facemasks_data_synth_analysis = facemasks_data_synth.analysis(
|
|
610
|
+
FE_time=True,
|
|
611
|
+
log_outcome=True,
|
|
612
|
+
verbose=True
|
|
613
|
+
)
|
|
614
|
+
# Synthetic DiD analysis
|
|
615
|
+
|
|
616
|
+
facemasks_data_synth_analysis.summary()
|
|
617
|
+
# Summary of model results
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: diffindiff
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.4
|
|
4
4
|
Summary: diffindiff: Python library for convenient Difference-in-Differences analyses
|
|
5
5
|
Author: Thomas Wieland
|
|
6
6
|
Author-email: geowieland@googlemail.com
|
|
@@ -29,7 +29,7 @@ A case study that utilizes the diffindiff library is available on [arXiv](https:
|
|
|
29
29
|
|
|
30
30
|
If you use this software, please cite:
|
|
31
31
|
|
|
32
|
-
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.
|
|
32
|
+
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.4) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
## Installation
|
|
@@ -164,6 +164,7 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
164
164
|
- Goldfarb A, Tucker C, Wang Y (2022) Conducting Research in Marketing with Quasi-Experiments. *Journal of Marketing* 86(3): 1-19. [10.1177/00222429221082977](https://doi.org/10.1177/00222429221082977)
|
|
165
165
|
- Isporhing IE, Lipfert M, Pestel N (2021) Does re-opening schools contribute to the spread of SARS-CoV-2? Evidence from staggered summer breaks in Germany. *Journal of Public Economics* 198: 104426. [10.1016/j.jpubeco.2021.104426](https://doi.org/10.1016/j.jpubeco.2021.104426)
|
|
166
166
|
- Li KT, Luo L, Pattabhiramaiah A (2024) Causal Inference with Quasi-Experimental Data. *IMPACT at JMR* November 13, 2024. [AMA](https://www.ama.org/marketing-news/causal-inference-with-quasi-experimental-data/)
|
|
167
|
+
- Mitze T, Kosfeld R, Rode J, Wälde K (2020) Face masks considerably reduce COVID-19 cases in Germany. *Proceedings of the National Academy of Sciences of the United States of America* 117(51): 32293-32301. [10.1073/pnas.2015954117](https://doi.org/10.1073/pnas.2015954117)
|
|
167
168
|
- Olden A (2018) What do you buy when no one's watching? The effect of self-service checkouts on the composition of sales in retail. Discussion paper FOR 3/18, Norwegian School of Economics, Norway. [http://hdl.handle.net/11250/2490886](http://hdl.handle.net/11250/2490886)
|
|
168
169
|
- Olden A, Moen J (2022) The triple difference estimator. *The Econometrics Journal* 25(3): 531-553. [10.1093/ectj/utac010](https://doi.org/10.1093/ectj/utac010)
|
|
169
170
|
- Strassmann A, Çolak Y, Serra-Burriel M, Nordestgaard BG, Turk A, Afzal S, Puhan MA (2023) Nationwide indoor smoking ban and impact on smoking behaviour and lung function: a two-population natural experiment. *Thorax* 78(2): 144-150. [10.1136/thoraxjnl-2021-218436](https://doi.org/10.1136/thoraxjnl-2021-218436)
|
|
@@ -179,11 +180,12 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
179
180
|
This software was developed without the use of AI-generated code. The Continue Agent in Microsoft Visual Studio Code using the GPT-5 mini model (by OpenAI) was used solely to assist in drafting and refining docstrings for documentation. The corresponding guidelines and constraints defined by the author are documented in `AGENTS-docstrings.md` in the [public GitHub repository](https://github.com/geowieland/diffindiff_official).
|
|
180
181
|
|
|
181
182
|
|
|
182
|
-
## What's new (v2.5.
|
|
183
|
+
## What's new (v2.5.4)
|
|
183
184
|
|
|
184
|
-
- Extensions
|
|
185
|
-
- New functions DiffData.get_synthetic_control_weightings() and DiffData.get_synthetic_control_fitmetrics() for accessing control unit weightings and fit metrics of the synthetic control unit
|
|
186
185
|
- Bugfixes
|
|
187
|
-
-
|
|
186
|
+
- Correction of the nonsensical skipping of the treatment group dummy in the case of two observation units in didanalysis.did_analysis()
|
|
187
|
+
- Automatical drop of duplicates in results dictionaries in didanalysis_helper.extract_model_results()
|
|
188
|
+
- Inefficient successive data insert in diddata.DiffData.add_synthetic() replaced by efficient concatenating
|
|
188
189
|
- Other
|
|
189
|
-
-
|
|
190
|
+
- Test script extended by another example
|
|
191
|
+
- Cleaned dependencies
|
|
@@ -7,7 +7,7 @@ def read_README():
|
|
|
7
7
|
|
|
8
8
|
setup(
|
|
9
9
|
name='diffindiff',
|
|
10
|
-
version='2.5.
|
|
10
|
+
version='2.5.4',
|
|
11
11
|
description='diffindiff: Python library for convenient Difference-in-Differences analyses',
|
|
12
12
|
packages=find_packages(include=["diffindiff", "diffindiff.tests"]),
|
|
13
13
|
include_package_data=True,
|
|
@@ -20,7 +20,6 @@ setup(
|
|
|
20
20
|
'diffindiff': ['tests/data/*'],
|
|
21
21
|
},
|
|
22
22
|
install_requires=[
|
|
23
|
-
'geopandas',
|
|
24
23
|
'pandas',
|
|
25
24
|
'numpy',
|
|
26
25
|
'statsmodels>=0.14.5',
|
|
@@ -28,9 +27,6 @@ setup(
|
|
|
28
27
|
'scikit-learn',
|
|
29
28
|
'xgboost',
|
|
30
29
|
'lightgbm',
|
|
31
|
-
'shapely',
|
|
32
|
-
'requests<3.0',
|
|
33
|
-
'contextily',
|
|
34
30
|
'openpyxl',
|
|
35
31
|
'matplotlib',
|
|
36
32
|
'patsy',
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|