diffindiff 2.5.7__tar.gz → 2.5.8__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.7 → diffindiff-2.5.8}/PKG-INFO +10 -7
- {diffindiff-2.5.7 → diffindiff-2.5.8}/README.md +9 -6
- {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/config.py +3 -3
- {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/didanalysis.py +100 -46
- {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/didanalysis_helper.py +6 -6
- {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/diddata.py +32 -21
- {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/didtools.py +46 -29
- {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/tests/tests_diffindiff.py +2 -3
- {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff.egg-info/PKG-INFO +10 -7
- {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff.egg-info/SOURCES.txt +2 -1
- {diffindiff-2.5.7 → diffindiff-2.5.8}/setup.py +1 -1
- diffindiff-2.5.8/tests/test_diffindiff.py +257 -0
- {diffindiff-2.5.7 → diffindiff-2.5.8}/MANIFEST.in +0 -0
- {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/__init__.py +0 -0
- {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/tests/__init__.py +0 -0
- {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/tests/data/Corona_Hesse.xlsx +0 -0
- {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/tests/data/counties_DE.csv +0 -0
- {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/tests/data/curfew_DE.csv +0 -0
- {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff.egg-info/dependency_links.txt +0 -0
- {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff.egg-info/requires.txt +0 -0
- {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff.egg-info/top_level.txt +0 -0
- {diffindiff-2.5.7 → diffindiff-2.5.8}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffindiff
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.8
|
|
4
4
|
Summary: diffindiff: Python library for convenient Difference-in-Differences analyses
|
|
5
5
|
Author: Thomas Wieland
|
|
6
6
|
Author-email: geowieland@googlemail.com
|
|
@@ -40,14 +40,14 @@ Thomas Wieland [ORCID](https://orcid.org/0000-0001-5168-9846) [EMail](mailto:geo
|
|
|
40
40
|
- 💻 GitHub Repository: [diffindiff_official](https://github.com/geowieland/diffindiff_official)
|
|
41
41
|
- 📄 DOI (Zenodo): [10.5281/zenodo.18656820](https://doi.org/10.5281/zenodo.18656820)
|
|
42
42
|
|
|
43
|
-
A case study that utilizes the diffindiff library is available on [arXiv](https://arxiv.org/abs/2605.21464).
|
|
43
|
+
A case study that utilizes the diffindiff library (preprint) is available on [arXiv](https://arxiv.org/abs/2605.21464).
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
## Citation
|
|
47
47
|
|
|
48
48
|
If you use this software, please cite:
|
|
49
49
|
|
|
50
|
-
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.
|
|
50
|
+
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.8) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
## Installation
|
|
@@ -198,9 +198,12 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
198
198
|
This software was developed without the use of AI-generated code. The GitHub Copilot Chat 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).
|
|
199
199
|
|
|
200
200
|
|
|
201
|
-
## What's new (v2.5.
|
|
201
|
+
## What's new (v2.5.8)
|
|
202
202
|
|
|
203
203
|
- Bugfixes
|
|
204
|
-
-
|
|
205
|
-
-
|
|
206
|
-
-
|
|
204
|
+
- didtools.is_balanced() now conducts a truly comprehensive check whether the input panel data is balanced
|
|
205
|
+
- Fixed bug in didtools.is_numeric() with respect to unintentional modification of the dataset
|
|
206
|
+
- diddata.create_counterfactual() now catches error if no model was trained by didtools.model_wrapper()
|
|
207
|
+
- didanalysis.DiffModel.plot_treatment_effects() now treats expections correctly
|
|
208
|
+
- Other:
|
|
209
|
+
- verbose option in any function which prints NOTEs, with all NOTEs being disabled when verbose = False
|
|
@@ -14,14 +14,14 @@ Thomas Wieland [ORCID](https://orcid.org/0000-0001-5168-9846) [EMail](mailto:geo
|
|
|
14
14
|
- 💻 GitHub Repository: [diffindiff_official](https://github.com/geowieland/diffindiff_official)
|
|
15
15
|
- 📄 DOI (Zenodo): [10.5281/zenodo.18656820](https://doi.org/10.5281/zenodo.18656820)
|
|
16
16
|
|
|
17
|
-
A case study that utilizes the diffindiff library is available on [arXiv](https://arxiv.org/abs/2605.21464).
|
|
17
|
+
A case study that utilizes the diffindiff library (preprint) is available on [arXiv](https://arxiv.org/abs/2605.21464).
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
## Citation
|
|
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.8) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
## Installation
|
|
@@ -172,9 +172,12 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
172
172
|
This software was developed without the use of AI-generated code. The GitHub Copilot Chat 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).
|
|
173
173
|
|
|
174
174
|
|
|
175
|
-
## What's new (v2.5.
|
|
175
|
+
## What's new (v2.5.8)
|
|
176
176
|
|
|
177
177
|
- Bugfixes
|
|
178
|
-
-
|
|
179
|
-
-
|
|
180
|
-
-
|
|
178
|
+
- didtools.is_balanced() now conducts a truly comprehensive check whether the input panel data is balanced
|
|
179
|
+
- Fixed bug in didtools.is_numeric() with respect to unintentional modification of the dataset
|
|
180
|
+
- diddata.create_counterfactual() now catches error if no model was trained by didtools.model_wrapper()
|
|
181
|
+
- didanalysis.DiffModel.plot_treatment_effects() now treats expections correctly
|
|
182
|
+
- Other:
|
|
183
|
+
- verbose option in any function which prints NOTEs, with all NOTEs being disabled when verbose = False
|
|
@@ -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-10-
|
|
7
|
+
# Version: 1.0.29
|
|
8
|
+
# Last update: 2026-10-17 21: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.8"
|
|
16
16
|
|
|
17
17
|
VERBOSE = False
|
|
18
18
|
|
|
@@ -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.8
|
|
8
|
+
# Last update: 2026-10-17 21:15
|
|
9
9
|
# Copyright (c) 2024-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -111,7 +111,8 @@ class DiffModel:
|
|
|
111
111
|
def treatment_statistics(
|
|
112
112
|
self,
|
|
113
113
|
treatment: str = None,
|
|
114
|
-
after_treatment_col: str = None
|
|
114
|
+
after_treatment_col: str = None,
|
|
115
|
+
verbose: bool = False
|
|
115
116
|
):
|
|
116
117
|
|
|
117
118
|
"""
|
|
@@ -163,7 +164,9 @@ class DiffModel:
|
|
|
163
164
|
else:
|
|
164
165
|
|
|
165
166
|
treatment = model_config["treatment_col"][0]
|
|
166
|
-
|
|
167
|
+
|
|
168
|
+
if verbose:
|
|
169
|
+
print(f"NOTE: No treatment was stated. Choosing treatment '{treatment}' for calculating treatment statistics.")
|
|
167
170
|
|
|
168
171
|
if after_treatment_col is not None:
|
|
169
172
|
|
|
@@ -782,9 +785,9 @@ class DiffModel:
|
|
|
782
785
|
|
|
783
786
|
if len(no_control_conditions) > 0:
|
|
784
787
|
if len(no_control_conditions) == 1:
|
|
785
|
-
print(f"
|
|
788
|
+
print(f"WARNING: Treatment '{no_control_conditions[0]}' has no control conditions.")
|
|
786
789
|
else:
|
|
787
|
-
print(f"
|
|
790
|
+
print(f"WARNING: Treatments {', '.join(no_control_conditions)} have no control conditions.")
|
|
788
791
|
|
|
789
792
|
print("-" * int(total_width))
|
|
790
793
|
|
|
@@ -905,6 +908,7 @@ class DiffModel:
|
|
|
905
908
|
treatment_effects = self.treatment_effects()
|
|
906
909
|
|
|
907
910
|
try:
|
|
911
|
+
|
|
908
912
|
if model_config.get("pre_post", False) and len(model_config.get("after_treatment_col", [])) > 0:
|
|
909
913
|
|
|
910
914
|
labels = treatment_effects.iloc[:, 0].astype(str).values
|
|
@@ -943,9 +947,11 @@ class DiffModel:
|
|
|
943
947
|
break
|
|
944
948
|
if appended:
|
|
945
949
|
break
|
|
946
|
-
except Exception:
|
|
950
|
+
except Exception as e:
|
|
947
951
|
|
|
948
|
-
|
|
952
|
+
print(f"Plot of treatment effects not possible: {str(e)}")
|
|
953
|
+
|
|
954
|
+
return self
|
|
949
955
|
|
|
950
956
|
if sort_by_coef:
|
|
951
957
|
treatment_effects = treatment_effects.sort_values(
|
|
@@ -1099,7 +1105,8 @@ class DiffModel:
|
|
|
1099
1105
|
self,
|
|
1100
1106
|
treatment = None,
|
|
1101
1107
|
after_treatment_col: str = None,
|
|
1102
|
-
retransform_log_outcome: bool = False
|
|
1108
|
+
retransform_log_outcome: bool = False,
|
|
1109
|
+
verbose: bool = False
|
|
1103
1110
|
):
|
|
1104
1111
|
|
|
1105
1112
|
"""
|
|
@@ -1153,8 +1160,11 @@ class DiffModel:
|
|
|
1153
1160
|
if treatment not in model_config["treatment_col"]:
|
|
1154
1161
|
raise ValueError (f"Treatment '{treatment}' not in model object.")
|
|
1155
1162
|
else:
|
|
1163
|
+
|
|
1156
1164
|
treatment = model_config["treatment_col"][0]
|
|
1157
|
-
|
|
1165
|
+
|
|
1166
|
+
if verbose:
|
|
1167
|
+
print(f"NOTE: No treatment was stated. Choosing treatment '{treatment}' for calculating counterfactual.")
|
|
1158
1168
|
|
|
1159
1169
|
if after_treatment_col is not None:
|
|
1160
1170
|
if len(model_config["after_treatment_col"]) == 0:
|
|
@@ -1313,7 +1323,8 @@ class DiffModel:
|
|
|
1313
1323
|
TT_col: str = None,
|
|
1314
1324
|
divide: float = 0.5,
|
|
1315
1325
|
resample: float = 1.0,
|
|
1316
|
-
random_state = 71
|
|
1326
|
+
random_state = 71,
|
|
1327
|
+
verbose: bool = False
|
|
1317
1328
|
):
|
|
1318
1329
|
|
|
1319
1330
|
"""
|
|
@@ -1369,8 +1380,11 @@ class DiffModel:
|
|
|
1369
1380
|
if treatment not in model_config["treatment_col"]:
|
|
1370
1381
|
raise ValueError (f"Treatment '{treatment}' not in model object.")
|
|
1371
1382
|
else:
|
|
1383
|
+
|
|
1372
1384
|
treatment = model_config["treatment_col"][0]
|
|
1373
|
-
|
|
1385
|
+
|
|
1386
|
+
if verbose:
|
|
1387
|
+
print(f"NOTE: No treatment was stated. Choosing treatment '{treatment}' for placebo analysis.")
|
|
1374
1388
|
|
|
1375
1389
|
if after_treatment_col is not None:
|
|
1376
1390
|
if len(model_config["after_treatment_col"]) == 0:
|
|
@@ -1390,7 +1404,8 @@ class DiffModel:
|
|
|
1390
1404
|
treatment_demeaned = treatment
|
|
1391
1405
|
treatment = treatment.replace(f"{config.DELIMITER}{config.DEMEAN_SUFFIX}", "")
|
|
1392
1406
|
|
|
1393
|
-
|
|
1407
|
+
if verbose:
|
|
1408
|
+
print(f"NOTE: Data is demeaned. Treatment statistics are calculated based on original treatment '{treatment}'.")
|
|
1394
1409
|
|
|
1395
1410
|
treatment_statistics = self.treatment_statistics(treatment = treatment)
|
|
1396
1411
|
|
|
@@ -1484,7 +1499,8 @@ class DiffModel:
|
|
|
1484
1499
|
plot_symbol: str = "o",
|
|
1485
1500
|
treatment_group_only: bool = True,
|
|
1486
1501
|
save_fig: str = None,
|
|
1487
|
-
save_fig_kwargs: dict = {}
|
|
1502
|
+
save_fig_kwargs: dict = {},
|
|
1503
|
+
verbose: bool = False
|
|
1488
1504
|
):
|
|
1489
1505
|
|
|
1490
1506
|
"""
|
|
@@ -1542,8 +1558,11 @@ class DiffModel:
|
|
|
1542
1558
|
if treatment not in model_config["treatment_col"]:
|
|
1543
1559
|
raise ValueError (f"Treatment '{treatment}' not in model object.")
|
|
1544
1560
|
else:
|
|
1561
|
+
|
|
1545
1562
|
treatment = model_config["treatment_col"][0]
|
|
1546
|
-
|
|
1563
|
+
|
|
1564
|
+
if verbose:
|
|
1565
|
+
print(f"NOTE: No treatment was stated. Choosing treatment '{treatment}' for plotting timeline.")
|
|
1547
1566
|
|
|
1548
1567
|
if treatment_group_only:
|
|
1549
1568
|
if TG_col is None:
|
|
@@ -1615,7 +1634,8 @@ class DiffModel:
|
|
|
1615
1634
|
pre_post_bar_width = 0.5,
|
|
1616
1635
|
retransform_log_outcome: bool = False,
|
|
1617
1636
|
save_fig: str = None,
|
|
1618
|
-
save_fig_kwargs: dict = {}
|
|
1637
|
+
save_fig_kwargs: dict = {},
|
|
1638
|
+
verbose: bool = False
|
|
1619
1639
|
):
|
|
1620
1640
|
|
|
1621
1641
|
"""
|
|
@@ -1723,7 +1743,8 @@ class DiffModel:
|
|
|
1723
1743
|
break
|
|
1724
1744
|
else:
|
|
1725
1745
|
|
|
1726
|
-
|
|
1746
|
+
if verbose:
|
|
1747
|
+
print(f"NOTE: No treatment was stated. Choosing treatment '{treatment_diagnostics[0]['treatment']}' for plotting.")
|
|
1727
1748
|
|
|
1728
1749
|
treatment_group = treatment_diagnostics[0]["treatment_group"]
|
|
1729
1750
|
treatment = treatment_diagnostics[0]["treatment"]
|
|
@@ -1783,13 +1804,15 @@ class DiffModel:
|
|
|
1783
1804
|
model_data[config.PREDICTIONS_SUMMARY_FRAME_COLS_LIST[4]] = np.exp(model_data[config.PREDICTIONS_SUMMARY_FRAME_COLS_LIST[4]])
|
|
1784
1805
|
model_data[config.PREDICTIONS_SUMMARY_FRAME_COLS_LIST[5]] = np.exp(model_data[config.PREDICTIONS_SUMMARY_FRAME_COLS_LIST[5]])
|
|
1785
1806
|
|
|
1786
|
-
if
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1807
|
+
if verbose:
|
|
1808
|
+
if model_config["demean"]:
|
|
1809
|
+
print(f"NOTE: Outcome variable '{outcome_col.replace(f'{config.DELIMITER}{config.DEMEAN_SUFFIX}', '')}' was log-transformed and demeaned. Re-transformation was applied on '{outcome_col}'.")
|
|
1810
|
+
else:
|
|
1811
|
+
print(f"NOTE: Outcome variable '{outcome_col}' was log-transformed. Re-transformation was applied.")
|
|
1790
1812
|
|
|
1791
1813
|
else:
|
|
1792
|
-
|
|
1814
|
+
if verbose:
|
|
1815
|
+
print("NOTE: Parameter 'retransform_log_outcome' was set to True, but outcome variable was not log-transformed. No re-transformation applied.")
|
|
1793
1816
|
|
|
1794
1817
|
model_data_TG = model_data[model_data[TG_col] == 1]
|
|
1795
1818
|
model_data_CG = model_data[model_data[TG_col] == 0]
|
|
@@ -2096,7 +2119,8 @@ class DiffModel:
|
|
|
2096
2119
|
plot_size: list = [12, 6],
|
|
2097
2120
|
retransform_log_outcome: bool = False,
|
|
2098
2121
|
save_fig: str = None,
|
|
2099
|
-
save_fig_kwargs: dict = {}
|
|
2122
|
+
save_fig_kwargs: dict = {},
|
|
2123
|
+
verbose: bool = False
|
|
2100
2124
|
):
|
|
2101
2125
|
|
|
2102
2126
|
"""
|
|
@@ -2199,7 +2223,9 @@ class DiffModel:
|
|
|
2199
2223
|
break
|
|
2200
2224
|
|
|
2201
2225
|
else:
|
|
2202
|
-
|
|
2226
|
+
|
|
2227
|
+
if verbose:
|
|
2228
|
+
print(f"NOTE: No treatment was stated. Choosing treatment '{treatment_diagnostics[0]['treatment']}' for plotting counterfactual.")
|
|
2203
2229
|
|
|
2204
2230
|
treatment_group = treatment_diagnostics[0]["treatment_group"]
|
|
2205
2231
|
|
|
@@ -2527,8 +2553,11 @@ def did_analysis(
|
|
|
2527
2553
|
TG_col = []
|
|
2528
2554
|
|
|
2529
2555
|
if not FE_unit and config.AUTO_SWITCH_TO_FIXED_EFFECTS:
|
|
2556
|
+
|
|
2530
2557
|
FE_unit = True
|
|
2531
|
-
|
|
2558
|
+
|
|
2559
|
+
if verbose:
|
|
2560
|
+
print(f"NOTE: Quasi-experiment includes more than one treatment ({no_treatments}). Unit fixed effects are used instead of control group baseline and treatment group deviation.")
|
|
2532
2561
|
|
|
2533
2562
|
if unique_units < config.FIXED_EFFECTS_THRESHOLD:
|
|
2534
2563
|
|
|
@@ -2538,7 +2567,8 @@ def did_analysis(
|
|
|
2538
2567
|
ITT = False
|
|
2539
2568
|
intercept = True
|
|
2540
2569
|
|
|
2541
|
-
|
|
2570
|
+
if verbose:
|
|
2571
|
+
print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} units. Unit or group fixed effects are skipped.")
|
|
2542
2572
|
|
|
2543
2573
|
if unique_time_points < config.FIXED_EFFECTS_THRESHOLD:
|
|
2544
2574
|
|
|
@@ -2547,7 +2577,8 @@ def did_analysis(
|
|
|
2547
2577
|
GTT = False
|
|
2548
2578
|
intercept = True
|
|
2549
2579
|
|
|
2550
|
-
|
|
2580
|
+
if verbose:
|
|
2581
|
+
print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} time points. Time fixed effects are skipped.")
|
|
2551
2582
|
|
|
2552
2583
|
if demean and FE_unit:
|
|
2553
2584
|
|
|
@@ -2556,38 +2587,56 @@ def did_analysis(
|
|
|
2556
2587
|
ITE = False
|
|
2557
2588
|
ITT = False
|
|
2558
2589
|
|
|
2559
|
-
|
|
2590
|
+
if verbose:
|
|
2591
|
+
print(f"NOTE: Unit fixed effects are specified to be replaced by demeaning. {config.EFFECTS_TYPES['ITE']['description']} or {config.EFFECTS_TYPES['ITT']['description']} are not supported.")
|
|
2560
2592
|
|
|
2561
2593
|
if ITE:
|
|
2562
2594
|
|
|
2563
2595
|
if not FE_unit:
|
|
2596
|
+
|
|
2564
2597
|
FE_unit = True
|
|
2565
|
-
|
|
2598
|
+
|
|
2599
|
+
if verbose:
|
|
2600
|
+
print("NOTE: Model includes individual treatment effects. Unit fixed effects are included.")
|
|
2566
2601
|
|
|
2567
2602
|
if GTE:
|
|
2603
|
+
|
|
2568
2604
|
GTE = False
|
|
2569
|
-
|
|
2605
|
+
|
|
2606
|
+
if verbose:
|
|
2607
|
+
print("NOTE: Both group and individual treatment effects were stated. Switching to individual treatment effects only.")
|
|
2570
2608
|
|
|
2571
2609
|
if ITT:
|
|
2572
2610
|
|
|
2573
2611
|
TT_col = []
|
|
2574
2612
|
|
|
2575
2613
|
if not FE_unit:
|
|
2614
|
+
|
|
2576
2615
|
FE_unit = True
|
|
2577
|
-
|
|
2616
|
+
|
|
2617
|
+
if verbose:
|
|
2618
|
+
print("NOTE: Model includes individual time trends. Unit fixed effects are included. Treatment time variable is dropped.")
|
|
2578
2619
|
|
|
2579
2620
|
if FE_time:
|
|
2621
|
+
|
|
2580
2622
|
FE_time = False
|
|
2581
|
-
|
|
2623
|
+
|
|
2624
|
+
if verbose:
|
|
2625
|
+
print("NOTE: Model includes individual time trends. Time fixed effects are dropped.")
|
|
2582
2626
|
|
|
2583
2627
|
if GTT:
|
|
2628
|
+
|
|
2584
2629
|
GTT = False
|
|
2585
|
-
|
|
2630
|
+
|
|
2631
|
+
if verbose:
|
|
2632
|
+
print("NOTE: Both group and individual time trends were stated. Switching to individual time trends only.")
|
|
2586
2633
|
|
|
2587
2634
|
if staggered_adoption and config.AUTO_SWITCH_TO_FIXED_EFFECTS:
|
|
2588
2635
|
|
|
2589
2636
|
if not FE_unit or not FE_time:
|
|
2590
|
-
|
|
2637
|
+
|
|
2638
|
+
if verbose:
|
|
2639
|
+
print("NOTE: Quasi-experiment includes one or more staggered treatments. Two-way fixed effects model is used.")
|
|
2591
2640
|
|
|
2592
2641
|
FE_unit = True
|
|
2593
2642
|
FE_time = True
|
|
@@ -2604,7 +2653,9 @@ def did_analysis(
|
|
|
2604
2653
|
if FE_group:
|
|
2605
2654
|
|
|
2606
2655
|
if intercept or len(TG_col) > 0:
|
|
2607
|
-
|
|
2656
|
+
|
|
2657
|
+
if verbose:
|
|
2658
|
+
print("NOTE: Quasi-experiment includes group fixed effects. Control group baseline and treatment group deviation are dropped.")
|
|
2608
2659
|
|
|
2609
2660
|
TG_col = []
|
|
2610
2661
|
intercept = False
|
|
@@ -2716,13 +2767,14 @@ def did_analysis(
|
|
|
2716
2767
|
|
|
2717
2768
|
if config.AUTO_SKIP_CONSTANT_COLUMNS:
|
|
2718
2769
|
|
|
2719
|
-
print(f"
|
|
2770
|
+
print(f"WARNING: The following columns are constant and dropped from the analysis: {', '.join(data_diagnostics['cols_constants'])}.")
|
|
2720
2771
|
|
|
2721
2772
|
covariates = [col for col in covariates if col not in data_diagnostics["cols_constants"]]
|
|
2722
2773
|
|
|
2723
2774
|
if data_diagnostics["is_prepost"] and config.AUTO_SWITCH_TO_PREPOST:
|
|
2724
2775
|
|
|
2725
|
-
|
|
2776
|
+
if verbose:
|
|
2777
|
+
print(f"NOTE: Input is {config.PREPOST_PANELDATA_DESCRIPTION}. Data processing and model estimation will treat data as pre-post.")
|
|
2726
2778
|
|
|
2727
2779
|
pre_post = True
|
|
2728
2780
|
|
|
@@ -2730,8 +2782,8 @@ def did_analysis(
|
|
|
2730
2782
|
FE_time = False
|
|
2731
2783
|
FE_group = False
|
|
2732
2784
|
|
|
2733
|
-
if not data_diagnostics["is_prepost"] and not FE_unit and not FE_time:
|
|
2734
|
-
print(f"
|
|
2785
|
+
if not data_diagnostics["is_prepost"] and not FE_unit and not FE_time:
|
|
2786
|
+
print(f"WARNING: Input is {config.MULTIPERIOD_PANELDATA_DESCRIPTION}. Consider including {config.EFFECTS_TYPES['FE']['description']}.")
|
|
2735
2787
|
|
|
2736
2788
|
if log_outcome:
|
|
2737
2789
|
|
|
@@ -2999,9 +3051,8 @@ def did_analysis(
|
|
|
2999
3051
|
indep_vars_no = indep_vars_no-(len(FE_unit_vars)+len(FE_time_vars))
|
|
3000
3052
|
|
|
3001
3053
|
if verbose:
|
|
3002
|
-
print("OK")
|
|
3003
|
-
|
|
3004
|
-
print(f"NOTE: Outcome variable '{outcome_col}' is replaced by demeaned variable '{outcome_col_demeaned}', and {len(treatment_col)+len(covariates)+len(spillover_vars)+len(interactions)} other variables were remeaned.")
|
|
3054
|
+
print("OK")
|
|
3055
|
+
print(f"NOTE: Outcome variable '{outcome_col}' is replaced by demeaned variable '{outcome_col_demeaned}', and {len(treatment_col)+len(covariates)+len(spillover_vars)+len(interactions)} other variables were remeaned.")
|
|
3005
3056
|
|
|
3006
3057
|
outcome_col = outcome_col_demeaned
|
|
3007
3058
|
|
|
@@ -3283,13 +3334,15 @@ def ddd_analysis(
|
|
|
3283
3334
|
|
|
3284
3335
|
FE_unit = False
|
|
3285
3336
|
|
|
3286
|
-
|
|
3337
|
+
if verbose:
|
|
3338
|
+
print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} units. Unit fixed effects are skipped.")
|
|
3287
3339
|
|
|
3288
3340
|
if unique_time_points < config.FIXED_EFFECTS_THRESHOLD:
|
|
3289
3341
|
|
|
3290
3342
|
FE_time = False
|
|
3291
3343
|
|
|
3292
|
-
|
|
3344
|
+
if verbose:
|
|
3345
|
+
print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} time points. Time fixed effects are skipped.")
|
|
3293
3346
|
|
|
3294
3347
|
if FE_unit:
|
|
3295
3348
|
TG_col = None
|
|
@@ -3320,13 +3373,14 @@ def ddd_analysis(
|
|
|
3320
3373
|
|
|
3321
3374
|
if config.AUTO_SKIP_CONSTANT_COLUMNS:
|
|
3322
3375
|
|
|
3323
|
-
print(f"
|
|
3376
|
+
print(f"WARNING: The following columns are constant and dropped from the analysis: {', '.join(data_diagnostics['cols_constants'])}.")
|
|
3324
3377
|
|
|
3325
3378
|
covariates = [col for col in covariates if col not in data_diagnostics["cols_constants"]]
|
|
3326
3379
|
|
|
3327
3380
|
if data_diagnostics["is_prepost"] and config.AUTO_SWITCH_TO_PREPOST:
|
|
3328
3381
|
|
|
3329
|
-
|
|
3382
|
+
if verbose:
|
|
3383
|
+
print("NOTE: Panel data is pre-post. Data processing and model estimation will treat data as pre-post")
|
|
3330
3384
|
|
|
3331
3385
|
pre_post = True
|
|
3332
3386
|
|
|
@@ -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-10-
|
|
7
|
+
# Version: 1.2.6
|
|
8
|
+
# Last update: 2026-10-17 18:55
|
|
9
9
|
# Copyright (c) 2025-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -231,8 +231,8 @@ def demean_variables(
|
|
|
231
231
|
suffix = f"{config.DELIMITER}{config.DEMEAN_SUFFIX}"
|
|
232
232
|
|
|
233
233
|
if not FE_unit and not FE_time:
|
|
234
|
-
|
|
235
|
-
print("
|
|
234
|
+
|
|
235
|
+
print("WARNING: Neither 'FE_unit' nor 'FE_time' is set to True. No demeaning will be performed.")
|
|
236
236
|
|
|
237
237
|
for col in cols_to_demean:
|
|
238
238
|
|
|
@@ -258,13 +258,13 @@ def demean_variables(
|
|
|
258
258
|
else:
|
|
259
259
|
|
|
260
260
|
tools.check_columns(
|
|
261
|
-
df =
|
|
261
|
+
df = data_demean,
|
|
262
262
|
columns = cols_to_demean + [unit_col] + [time_col],
|
|
263
263
|
verbose = verbose
|
|
264
264
|
)
|
|
265
265
|
|
|
266
266
|
tools.is_numeric(
|
|
267
|
-
df =
|
|
267
|
+
df = data_demean,
|
|
268
268
|
columns = cols_to_demean,
|
|
269
269
|
verbose = verbose
|
|
270
270
|
)
|
|
@@ -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.4
|
|
8
|
+
# Last update: 2026-10-17 21:34
|
|
9
9
|
# Copyright (c) 2024-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -1763,7 +1763,7 @@ class DiffData:
|
|
|
1763
1763
|
TT_col = treatment_cols[0]["TT_col"]
|
|
1764
1764
|
treatment_name = treatment_cols[0]["treatment_name"]
|
|
1765
1765
|
|
|
1766
|
-
if len(treatment_cols) > 1:
|
|
1766
|
+
if len(treatment_cols) > 1 and verbose:
|
|
1767
1767
|
print(f"NOTE: Treatment data contains {len(treatment_cols)} treatments. Taking first treatment '{treatment_name}' for identifying treatment group and time.")
|
|
1768
1768
|
|
|
1769
1769
|
if verbose:
|
|
@@ -1853,9 +1853,8 @@ class DiffData:
|
|
|
1853
1853
|
self.data[1] = groups
|
|
1854
1854
|
|
|
1855
1855
|
if verbose:
|
|
1856
|
-
print("OK")
|
|
1857
|
-
|
|
1858
|
-
print("NOTE: DiffData object was changed with counterfactual.")
|
|
1856
|
+
print("OK")
|
|
1857
|
+
print("NOTE: DiffData object was changed with counterfactual.")
|
|
1859
1858
|
|
|
1860
1859
|
return self
|
|
1861
1860
|
|
|
@@ -1926,7 +1925,7 @@ class DiffData:
|
|
|
1926
1925
|
treatment_name = treatment_cols[0]["treatment_name"]
|
|
1927
1926
|
TG_col = treatment_cols[0]["TG_col"]
|
|
1928
1927
|
|
|
1929
|
-
if len(treatment_cols) > 1:
|
|
1928
|
+
if len(treatment_cols) > 1 and verbose:
|
|
1930
1929
|
print(f"NOTE: Treatment data contains {len(treatment_cols)} treatments. Taking first treatment '{treatment_name}' for identifying treatment group and time.")
|
|
1931
1930
|
|
|
1932
1931
|
if verbose:
|
|
@@ -2014,7 +2013,7 @@ class DiffData:
|
|
|
2014
2013
|
units = valid_units
|
|
2015
2014
|
|
|
2016
2015
|
if len(units_not_included) > 0:
|
|
2017
|
-
print(f"WARNING: {len(units_not_included)} analysis units were not included due to NaN
|
|
2016
|
+
print(f"WARNING: {len(units_not_included)} analysis units were not included due to NaN values.")
|
|
2018
2017
|
|
|
2019
2018
|
if verbose:
|
|
2020
2019
|
print("OK")
|
|
@@ -2863,7 +2862,7 @@ def create_counterfactual(
|
|
|
2863
2862
|
data_len = len(data)
|
|
2864
2863
|
data = data.dropna()
|
|
2865
2864
|
if len(data) < data_len:
|
|
2866
|
-
print(f"
|
|
2865
|
+
print(f"WARNING: Because of NaN values, {data_len-len(data)} observations were skipped.")
|
|
2867
2866
|
|
|
2868
2867
|
isnotreatment = tools.is_notreatment(
|
|
2869
2868
|
data = data,
|
|
@@ -2871,7 +2870,7 @@ def create_counterfactual(
|
|
|
2871
2870
|
treatment_col = treatment_col,
|
|
2872
2871
|
verbose = verbose
|
|
2873
2872
|
)
|
|
2874
|
-
if not isnotreatment[0]:
|
|
2873
|
+
if not isnotreatment[0] and verbose:
|
|
2875
2874
|
print(f"NOTE: No {config.NO_TREATMENT_CG_DESCRIPTION}. Counterfactual will not cover full treatment time.")
|
|
2876
2875
|
|
|
2877
2876
|
data_unit = data.loc[data[unit_col].astype(str) == cf_for_unit]
|
|
@@ -2897,7 +2896,7 @@ def create_counterfactual(
|
|
|
2897
2896
|
units = tools.unique(units_tt[unit_col])
|
|
2898
2897
|
|
|
2899
2898
|
if not isnotreatment[0]:
|
|
2900
|
-
print(f"
|
|
2899
|
+
print(f"WARNING: No {config.TREATMENT_GROUP_DESCRIPTION} units in model data left.")
|
|
2901
2900
|
|
|
2902
2901
|
if verbose:
|
|
2903
2902
|
print(f"Creating data for counterfactual with {len(X)} independent variables", end = " ... ")
|
|
@@ -2958,18 +2957,30 @@ def create_counterfactual(
|
|
|
2958
2957
|
random_state = random_state,
|
|
2959
2958
|
verbose = verbose
|
|
2960
2959
|
)
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2960
|
+
|
|
2961
|
+
model_estimation_error = counterfactual_pred[6]
|
|
2962
|
+
model_estimation_error_text = ""
|
|
2963
|
+
|
|
2964
|
+
if model_estimation_error:
|
|
2965
|
+
|
|
2966
|
+
model_estimation_error_text = counterfactual_pred[7]
|
|
2967
|
+
print(f"WARNING: The desired {config.MODEL_WRAPPER_AVAILABLE[model_type]} model was not estimated. The following error occurred: '{model_estimation_error_text}'.")
|
|
2968
|
+
|
|
2969
|
+
else:
|
|
2970
|
+
|
|
2971
|
+
if verbose:
|
|
2972
|
+
print(f"Counterfactual prediction for observational unit '{cf_for_unit}'", end = " ... ")
|
|
2973
|
+
|
|
2974
|
+
data_unit[treatment_col] = 0
|
|
2975
|
+
data_unit[y] = counterfactual_pred[1].predict(data_unit[X])
|
|
2976
|
+
|
|
2977
|
+
if verbose:
|
|
2978
|
+
print("OK")
|
|
2970
2979
|
|
|
2971
2980
|
return [
|
|
2972
2981
|
counterfactual_pred,
|
|
2973
2982
|
data_cf,
|
|
2974
|
-
data_unit
|
|
2983
|
+
data_unit,
|
|
2984
|
+
model_estimation_error,
|
|
2985
|
+
model_estimation_error_text
|
|
2975
2986
|
]
|
|
@@ -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-10-
|
|
7
|
+
# Version: 2.2.8
|
|
8
|
+
# Last update: 2026-10-17 18:53
|
|
9
9
|
# Copyright (c) 2025-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -77,6 +77,8 @@ def check_columns(
|
|
|
77
77
|
"""
|
|
78
78
|
|
|
79
79
|
if len(columns) > 0:
|
|
80
|
+
|
|
81
|
+
key_errors = []
|
|
80
82
|
|
|
81
83
|
if verbose:
|
|
82
84
|
print(f"Checking if column(s) {', '.join(columns)} exist(s) in data frame", end=" ... ")
|
|
@@ -87,7 +89,7 @@ def check_columns(
|
|
|
87
89
|
print("OK")
|
|
88
90
|
|
|
89
91
|
if missing_columns:
|
|
90
|
-
|
|
92
|
+
key_errors.append(f"Data do not contain column(s): {', '.join(missing_columns)}")
|
|
91
93
|
|
|
92
94
|
if verbose:
|
|
93
95
|
print("Checking whether columns are duplicated in data frame", end = " ... ")
|
|
@@ -98,7 +100,10 @@ def check_columns(
|
|
|
98
100
|
print("OK")
|
|
99
101
|
|
|
100
102
|
if len(cols_duplicated) > 0:
|
|
101
|
-
|
|
103
|
+
key_errors.append(f"Data contain duplicated relevant columns: {', '.join(cols_duplicated)}")
|
|
104
|
+
|
|
105
|
+
if len(key_errors) > 0:
|
|
106
|
+
raise KeyError(f"{' '.join(key_errors)}")
|
|
102
107
|
|
|
103
108
|
def is_numeric(
|
|
104
109
|
df: pd.DataFrame,
|
|
@@ -134,6 +139,8 @@ def is_numeric(
|
|
|
134
139
|
>>> is_numeric(df, ['x'])
|
|
135
140
|
"""
|
|
136
141
|
|
|
142
|
+
df = df.copy()
|
|
143
|
+
|
|
137
144
|
if len(columns) > 0:
|
|
138
145
|
|
|
139
146
|
if verbose:
|
|
@@ -266,32 +273,41 @@ def is_balanced(
|
|
|
266
273
|
if other_cols is None:
|
|
267
274
|
other_cols = []
|
|
268
275
|
|
|
276
|
+
cols_relevant = [unit_col, time_col, outcome_col] + other_cols
|
|
277
|
+
|
|
269
278
|
if verbose:
|
|
270
|
-
print(f"Checking whether panel data is balanced with respect to {
|
|
279
|
+
print(f"Checking whether panel data is balanced with respect to {len(cols_relevant)} columns", end = " ... ")
|
|
271
280
|
|
|
272
|
-
|
|
273
|
-
time_freq = data[time_col].nunique()
|
|
274
|
-
unitxtime = unit_freq*time_freq
|
|
281
|
+
panel = data[cols_relevant].copy()
|
|
275
282
|
|
|
276
|
-
|
|
277
|
-
cols_relevant = [unit_col, time_col, outcome_col]
|
|
278
|
-
else:
|
|
279
|
-
cols_relevant = [unit_col, time_col, outcome_col] + other_cols
|
|
283
|
+
warnings = []
|
|
280
284
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
285
|
+
if panel.isnull().any().any():
|
|
286
|
+
warnings.append("Relevant columns contain missing values.")
|
|
287
|
+
|
|
288
|
+
pair_counts = panel[[unit_col, time_col]].dropna().groupby([unit_col, time_col]).size()
|
|
289
|
+
if (pair_counts != 1).any():
|
|
290
|
+
warnings.append("Unit-time pairs are duplicated or incomplete.")
|
|
291
|
+
|
|
292
|
+
units = panel[unit_col].dropna().unique()
|
|
293
|
+
times = panel[time_col].dropna().unique()
|
|
294
|
+
expected_pairs = set((u, t) for u in units for t in times)
|
|
295
|
+
observed_pairs = set(pair_counts.index.tolist())
|
|
296
|
+
if observed_pairs != expected_pairs:
|
|
297
|
+
warnings.append("Some unit-time combinations are missing.")
|
|
298
|
+
|
|
288
299
|
if verbose:
|
|
289
300
|
print("OK")
|
|
290
301
|
|
|
291
|
-
if
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
302
|
+
if len(warnings) > 0:
|
|
303
|
+
|
|
304
|
+
if verbose:
|
|
305
|
+
print(f"WARNING: {' '.join(warnings)}")
|
|
306
|
+
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
else:
|
|
310
|
+
return True
|
|
295
311
|
|
|
296
312
|
def is_dummy(
|
|
297
313
|
iterable,
|
|
@@ -562,7 +578,7 @@ def is_simultaneous(
|
|
|
562
578
|
if verbose:
|
|
563
579
|
print("OK")
|
|
564
580
|
|
|
565
|
-
if not simultaneous and data_isnotreatment[0]:
|
|
581
|
+
if not simultaneous and data_isnotreatment[0] and verbose:
|
|
566
582
|
print(f"NOTE: Treatment '{treatment_col}' is not simultaneous.")
|
|
567
583
|
|
|
568
584
|
if simultaneous and not data_isnotreatment[0]:
|
|
@@ -621,8 +637,8 @@ def is_notreatment(
|
|
|
621
637
|
if verbose:
|
|
622
638
|
print("OK")
|
|
623
639
|
|
|
624
|
-
|
|
625
|
-
|
|
640
|
+
if not no_treatment:
|
|
641
|
+
print(f"WARNING: treatment '{treatment_col}' does not include a {config.NO_TREATMENT_CG_DESCRIPTION}.")
|
|
626
642
|
|
|
627
643
|
return [
|
|
628
644
|
no_treatment,
|
|
@@ -1708,7 +1724,9 @@ def model_wrapper(
|
|
|
1708
1724
|
X_test,
|
|
1709
1725
|
y_train,
|
|
1710
1726
|
y_test
|
|
1711
|
-
)
|
|
1727
|
+
),
|
|
1728
|
+
model_estimation_error,
|
|
1729
|
+
model_estimation_error_text
|
|
1712
1730
|
]
|
|
1713
1731
|
|
|
1714
1732
|
def fit_metrics(
|
|
@@ -1824,8 +1842,7 @@ def fit_metrics(
|
|
|
1824
1842
|
|
|
1825
1843
|
if any(observed == 0):
|
|
1826
1844
|
|
|
1827
|
-
|
|
1828
|
-
print ("NOTE: Vector 'observed' contains values equal to zero. No APE/MAPE calculated.")
|
|
1845
|
+
print ("WARNING: Vector 'observed' contains values equal to zero. No APE/MAPE calculated.")
|
|
1829
1846
|
|
|
1830
1847
|
APE = np.full_like(observed, np.nan)
|
|
1831
1848
|
MAPE = np.nan
|
|
@@ -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.1
|
|
8
|
+
# Last update: 2026-10-17 14:50
|
|
9
9
|
# Copyright (c) 2025-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -305,7 +305,6 @@ curfew_model_FE.plot(
|
|
|
305
305
|
|
|
306
306
|
fixed_effects = curfew_model_FE.fixed_effects()
|
|
307
307
|
# Fixed effects of CHBW_data_model_FE
|
|
308
|
-
#print(fixed_effects)
|
|
309
308
|
|
|
310
309
|
|
|
311
310
|
# Model with demeaned variables:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffindiff
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.8
|
|
4
4
|
Summary: diffindiff: Python library for convenient Difference-in-Differences analyses
|
|
5
5
|
Author: Thomas Wieland
|
|
6
6
|
Author-email: geowieland@googlemail.com
|
|
@@ -40,14 +40,14 @@ Thomas Wieland [ORCID](https://orcid.org/0000-0001-5168-9846) [EMail](mailto:geo
|
|
|
40
40
|
- 💻 GitHub Repository: [diffindiff_official](https://github.com/geowieland/diffindiff_official)
|
|
41
41
|
- 📄 DOI (Zenodo): [10.5281/zenodo.18656820](https://doi.org/10.5281/zenodo.18656820)
|
|
42
42
|
|
|
43
|
-
A case study that utilizes the diffindiff library is available on [arXiv](https://arxiv.org/abs/2605.21464).
|
|
43
|
+
A case study that utilizes the diffindiff library (preprint) is available on [arXiv](https://arxiv.org/abs/2605.21464).
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
## Citation
|
|
47
47
|
|
|
48
48
|
If you use this software, please cite:
|
|
49
49
|
|
|
50
|
-
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.
|
|
50
|
+
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.8) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
## Installation
|
|
@@ -198,9 +198,12 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
198
198
|
This software was developed without the use of AI-generated code. The GitHub Copilot Chat 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).
|
|
199
199
|
|
|
200
200
|
|
|
201
|
-
## What's new (v2.5.
|
|
201
|
+
## What's new (v2.5.8)
|
|
202
202
|
|
|
203
203
|
- Bugfixes
|
|
204
|
-
-
|
|
205
|
-
-
|
|
206
|
-
-
|
|
204
|
+
- didtools.is_balanced() now conducts a truly comprehensive check whether the input panel data is balanced
|
|
205
|
+
- Fixed bug in didtools.is_numeric() with respect to unintentional modification of the dataset
|
|
206
|
+
- diddata.create_counterfactual() now catches error if no model was trained by didtools.model_wrapper()
|
|
207
|
+
- didanalysis.DiffModel.plot_treatment_effects() now treats expections correctly
|
|
208
|
+
- Other:
|
|
209
|
+
- verbose option in any function which prints NOTEs, with all NOTEs being disabled when verbose = False
|
|
@@ -7,7 +7,7 @@ def read_README():
|
|
|
7
7
|
|
|
8
8
|
setup(
|
|
9
9
|
name='diffindiff',
|
|
10
|
-
version='2.5.
|
|
10
|
+
version='2.5.8',
|
|
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,
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#-----------------------------------------------------------------------
|
|
2
|
+
# Name: tests (diffindiff package)
|
|
3
|
+
# Purpose: Tests for diffindiff package functions
|
|
4
|
+
# Author: Thomas Wieland
|
|
5
|
+
# ORCID: 0000-0001-5168-9846
|
|
6
|
+
# mail: geowieland@googlemail.com
|
|
7
|
+
# Version: 1.0.0
|
|
8
|
+
# Last update: 2026-10-17 15:01
|
|
9
|
+
# Copyright (c) 2026 Thomas Wieland
|
|
10
|
+
#-----------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_import_diffindiff():
|
|
18
|
+
"""Test if diffindiff package can be imported."""
|
|
19
|
+
|
|
20
|
+
import diffindiff
|
|
21
|
+
from diffindiff.config import PACKAGE_NAME, PACKAGE_VERSION
|
|
22
|
+
|
|
23
|
+
print(f"This is the {PACKAGE_NAME} package v{PACKAGE_VERSION}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_prepost_analysis():
|
|
27
|
+
|
|
28
|
+
from diffindiff.diddata import create_groups, create_treatment, merge_data, create_data
|
|
29
|
+
|
|
30
|
+
curfew_DE = pd.read_csv(
|
|
31
|
+
Path(__file__).parent / "data" / "curfew_DE.csv",
|
|
32
|
+
sep=";",
|
|
33
|
+
decimal=","
|
|
34
|
+
)
|
|
35
|
+
# Dataset with daily and cumulative SARS-CoV-2 infections of German counties
|
|
36
|
+
# Data source: Wieland (2020) https://doi.org/10.18335/region.v7i2.324
|
|
37
|
+
|
|
38
|
+
curfew_groups=create_groups(
|
|
39
|
+
treatment_group=
|
|
40
|
+
curfew_DE.loc[curfew_DE["Bundesland"].isin([9,10,14])]["county"],
|
|
41
|
+
control_group=
|
|
42
|
+
curfew_DE.loc[~curfew_DE["Bundesland"].isin([9,10,14])]["county"]
|
|
43
|
+
)
|
|
44
|
+
# Creating treatment and control group
|
|
45
|
+
# "Bundesland" (=federal state): 9 = Bavaria, 10 = Saarland, 14 = Saxony
|
|
46
|
+
|
|
47
|
+
assert curfew_groups is not None
|
|
48
|
+
|
|
49
|
+
curfew_groups.summary()
|
|
50
|
+
# Groups summary
|
|
51
|
+
|
|
52
|
+
curfew_treatment_prepost=create_treatment(
|
|
53
|
+
study_period=["2020-03-01", "2020-05-15"],
|
|
54
|
+
treatment_period=["2020-03-21", "2020-05-05"],
|
|
55
|
+
freq = "D",
|
|
56
|
+
pre_post = True
|
|
57
|
+
)
|
|
58
|
+
# Creating treatment
|
|
59
|
+
# Curfew from March 21, 2020, to May, 5, 2020
|
|
60
|
+
|
|
61
|
+
assert curfew_treatment_prepost is not None
|
|
62
|
+
|
|
63
|
+
curfew_treatment_prepost.summary()
|
|
64
|
+
# Treatment summary
|
|
65
|
+
|
|
66
|
+
curfew_data_prepost_merge=merge_data(
|
|
67
|
+
outcome_data=curfew_DE,
|
|
68
|
+
unit_id_col="county",
|
|
69
|
+
time_col="infection_date",
|
|
70
|
+
outcome_col="infections_cum_per100000",
|
|
71
|
+
diff_groups=curfew_groups,
|
|
72
|
+
diff_treatment=curfew_treatment_prepost
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
assert curfew_data_prepost_merge is not None
|
|
76
|
+
|
|
77
|
+
curfew_data_prepost_merge.summary()
|
|
78
|
+
# Summary of created data
|
|
79
|
+
|
|
80
|
+
curfew_data_prepost=create_data(
|
|
81
|
+
outcome_data=curfew_DE,
|
|
82
|
+
unit_id_col="county",
|
|
83
|
+
time_col="infection_date",
|
|
84
|
+
outcome_col="infections_cum_per100000",
|
|
85
|
+
treatment_group=
|
|
86
|
+
curfew_DE.loc[curfew_DE["Bundesland"].isin([9,10,14])]["county"],
|
|
87
|
+
control_group=
|
|
88
|
+
curfew_DE.loc[~curfew_DE["Bundesland"].isin([9,10,14])]["county"],
|
|
89
|
+
study_period=["2020-03-01", "2020-05-15"],
|
|
90
|
+
treatment_period=["2020-03-21", "2020-05-05"],
|
|
91
|
+
freq="D",
|
|
92
|
+
pre_post=True,
|
|
93
|
+
after_treatment_period = False
|
|
94
|
+
)
|
|
95
|
+
# Creating DiD treatement dataset by defining groups and
|
|
96
|
+
# treatment time at once
|
|
97
|
+
|
|
98
|
+
assert curfew_data_prepost is not None
|
|
99
|
+
|
|
100
|
+
curfew_data_prepost.summary()
|
|
101
|
+
# Summary of created data
|
|
102
|
+
|
|
103
|
+
curfew_model_prepost=curfew_data_prepost.analysis(verbose=True)
|
|
104
|
+
# Model analysis of created data
|
|
105
|
+
|
|
106
|
+
assert curfew_model_prepost is not None
|
|
107
|
+
|
|
108
|
+
print(curfew_model_prepost.treatment_effects())
|
|
109
|
+
# Show treatment effects
|
|
110
|
+
|
|
111
|
+
print(curfew_model_prepost.fixed_effects())
|
|
112
|
+
# Show fixed effects (not included in model)
|
|
113
|
+
|
|
114
|
+
print(curfew_model_prepost.covariates())
|
|
115
|
+
# Show covariates (not included in model)
|
|
116
|
+
|
|
117
|
+
curfew_model_prepost.summary()
|
|
118
|
+
# Model summary
|
|
119
|
+
|
|
120
|
+
print(curfew_model_prepost.didmodel().summary())
|
|
121
|
+
# Access statsmodels OLS object
|
|
122
|
+
|
|
123
|
+
print(curfew_model_prepost.fit_metrics())
|
|
124
|
+
# Show model fit metrics
|
|
125
|
+
|
|
126
|
+
curfew_model_prepost.plot(
|
|
127
|
+
x_label="Timepoint",
|
|
128
|
+
y_label="Cumulative infections per 100,000",
|
|
129
|
+
plot_title="Curfew effectiveness pre-post - Groups over time",
|
|
130
|
+
plot_observed=False,
|
|
131
|
+
lines_col=[None,None,"blue","orange"],
|
|
132
|
+
lines_labels=[None,None,"Treatment group","Control group","Treatment group CI","Control group CI"],
|
|
133
|
+
lines_style=[None,None,"solid","solid"]
|
|
134
|
+
)
|
|
135
|
+
# Plot DiD pre vs. post results
|
|
136
|
+
# with user-determined style
|
|
137
|
+
|
|
138
|
+
curfew_model_prepost.plot(
|
|
139
|
+
x_label="Timepoint",
|
|
140
|
+
y_label="Cumulative infections per 100,000",
|
|
141
|
+
plot_title="Curfew effectiveness pre-post - Groups over time",
|
|
142
|
+
lines_col=[None,None,"blue","orange"],
|
|
143
|
+
lines_labels=[None,None,"Treatment group","Control group","Treatment group CI", "Control group CI"],
|
|
144
|
+
pre_post_barplot=True
|
|
145
|
+
)
|
|
146
|
+
# Plot DiD pre vs. post results
|
|
147
|
+
# with user-determined style
|
|
148
|
+
|
|
149
|
+
curfew_model_prepost.plot_treatment_effects(
|
|
150
|
+
x_label="Coefficients with 95% CI",
|
|
151
|
+
plot_title="Curfew effectiveness pre-post - DiD effects"
|
|
152
|
+
)
|
|
153
|
+
# plot effects
|
|
154
|
+
|
|
155
|
+
curfew_model_prepost.plot_treatment_effects(
|
|
156
|
+
x_label="Coefficients with 95% CI",
|
|
157
|
+
plot_title="Curfew effectiveness pre-post - DiD effects",
|
|
158
|
+
scale_plot=False
|
|
159
|
+
)
|
|
160
|
+
# plot effects
|
|
161
|
+
|
|
162
|
+
def test_multiperiod_paneldata_analysis():
|
|
163
|
+
|
|
164
|
+
from diffindiff.diddata import create_data
|
|
165
|
+
|
|
166
|
+
curfew_DE = pd.read_csv(
|
|
167
|
+
Path(__file__).parent / "data" / "curfew_DE.csv",
|
|
168
|
+
sep=";",
|
|
169
|
+
decimal=","
|
|
170
|
+
)
|
|
171
|
+
# Dataset with daily and cumulative SARS-CoV-2 infections of German counties
|
|
172
|
+
# Data source: Wieland (2020) https://doi.org/10.18335/region.v7i2.324
|
|
173
|
+
|
|
174
|
+
curfew_data=create_data(
|
|
175
|
+
outcome_data=curfew_DE,
|
|
176
|
+
unit_id_col="county",
|
|
177
|
+
time_col="infection_date",
|
|
178
|
+
outcome_col="infections_cum_per100000",
|
|
179
|
+
treatment_group=
|
|
180
|
+
curfew_DE.loc[curfew_DE["Bundesland"].isin([9,10,14])]["county"],
|
|
181
|
+
control_group=
|
|
182
|
+
curfew_DE.loc[~curfew_DE["Bundesland"].isin([9,10,14])]["county"],
|
|
183
|
+
treatment_name="Curfew",
|
|
184
|
+
study_period=["2020-03-01", "2020-05-15"],
|
|
185
|
+
treatment_period=["2020-03-21", "2020-05-05"],
|
|
186
|
+
freq="D"
|
|
187
|
+
)
|
|
188
|
+
# Creating DiD dataset by defining groups and treatment time at once
|
|
189
|
+
# Treatment curfew in three federal states: Bavaria (9), Saarland (10) and Saxony (14)
|
|
190
|
+
|
|
191
|
+
assert curfew_data is not None
|
|
192
|
+
|
|
193
|
+
curfew_data.summary()
|
|
194
|
+
# Summary of created treatment data
|
|
195
|
+
|
|
196
|
+
curfew_model=curfew_data.analysis(verbose=True)
|
|
197
|
+
# Model analysis of created data
|
|
198
|
+
|
|
199
|
+
assert curfew_model is not None
|
|
200
|
+
|
|
201
|
+
curfew_model.summary()
|
|
202
|
+
# Model summary
|
|
203
|
+
|
|
204
|
+
curfew_model.plot(
|
|
205
|
+
y_label="Cumulative infections per 100,000",
|
|
206
|
+
plot_title="Curfew effectiveness - Groups over time",
|
|
207
|
+
plot_observed=True
|
|
208
|
+
)
|
|
209
|
+
# Plot observed vs. predicted (means) separated by group (treatment and control)
|
|
210
|
+
|
|
211
|
+
curfew_model.plot_treatment_effects(
|
|
212
|
+
x_label="Coefficients with 95% CI",
|
|
213
|
+
plot_title="Curfew effectiveness - DiD effects"
|
|
214
|
+
)
|
|
215
|
+
# plot effects
|
|
216
|
+
|
|
217
|
+
curfew_placebo = curfew_model.placebo(
|
|
218
|
+
treatment="Curfew"
|
|
219
|
+
)
|
|
220
|
+
# Placebo test with default parameters
|
|
221
|
+
|
|
222
|
+
assert curfew_placebo is not None
|
|
223
|
+
|
|
224
|
+
curfew_placebo.summary()
|
|
225
|
+
# Summary of placebo test
|
|
226
|
+
|
|
227
|
+
# Two-way-fixed-effects model:
|
|
228
|
+
|
|
229
|
+
curfew_model_FE=curfew_data.analysis(
|
|
230
|
+
FE_unit=True,
|
|
231
|
+
FE_time=True,
|
|
232
|
+
demean = True,
|
|
233
|
+
verbose = True
|
|
234
|
+
)
|
|
235
|
+
# Model analysis of created data with fixed effects for
|
|
236
|
+
# units (FE_unit=True) and time (FE_time=True)
|
|
237
|
+
# data is demeaned
|
|
238
|
+
|
|
239
|
+
assert curfew_model_FE is not None
|
|
240
|
+
|
|
241
|
+
curfew_model_FE_summary = curfew_model_FE.summary()
|
|
242
|
+
# Model summary
|
|
243
|
+
|
|
244
|
+
assert curfew_model_FE_summary is not None
|
|
245
|
+
|
|
246
|
+
print(curfew_model_FE.treatment_statistics())
|
|
247
|
+
# Print treatment statistics
|
|
248
|
+
|
|
249
|
+
curfew_model_FE.plot(
|
|
250
|
+
y_label="Cumulative infections per 100,000",
|
|
251
|
+
plot_title="Curfew effectiveness (Two-way FE model) - Groups over time",
|
|
252
|
+
plot_observed=True
|
|
253
|
+
)
|
|
254
|
+
# Plot of treatment and control group
|
|
255
|
+
|
|
256
|
+
fixed_effects = curfew_model_FE.fixed_effects()
|
|
257
|
+
# Fixed effects of CHBW_data_model_FE
|
|
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
|