diffindiff 2.5.0__tar.gz → 2.5.2__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.0 → diffindiff-2.5.2}/PKG-INFO +10 -13
- {diffindiff-2.5.0 → diffindiff-2.5.2}/README.md +9 -12
- {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/config.py +4 -3
- {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/didanalysis.py +97 -16
- {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/didanalysis_helper.py +2 -2
- {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/diddata.py +114 -16
- {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/tests/tests_diffindiff.py +59 -3
- {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff.egg-info/PKG-INFO +10 -13
- {diffindiff-2.5.0 → diffindiff-2.5.2}/setup.py +1 -1
- {diffindiff-2.5.0 → diffindiff-2.5.2}/MANIFEST.in +0 -0
- {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/__init__.py +0 -0
- {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/didtools.py +0 -0
- {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/tests/__init__.py +0 -0
- {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/tests/data/Corona_Hesse.xlsx +0 -0
- {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/tests/data/counties_DE.csv +0 -0
- {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/tests/data/curfew_DE.csv +0 -0
- {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff.egg-info/SOURCES.txt +0 -0
- {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff.egg-info/dependency_links.txt +0 -0
- {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff.egg-info/requires.txt +0 -0
- {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff.egg-info/top_level.txt +0 -0
- {diffindiff-2.5.0 → diffindiff-2.5.2}/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.2
|
|
4
4
|
Summary: diffindiff: Python library for convenient Difference-in-Differences analyses
|
|
5
5
|
Author: Thomas Wieland
|
|
6
6
|
Author-email: geowieland@googlemail.com
|
|
@@ -22,14 +22,14 @@ Thomas Wieland [ORCID](https://orcid.org/0000-0001-5168-9846) [EMail](mailto:geo
|
|
|
22
22
|
- 💻 GitHub Repository: [diffindiff_official](https://github.com/geowieland/diffindiff_official)
|
|
23
23
|
- 📄 DOI (Zenodo): [10.5281/zenodo.18656820](https://doi.org/10.5281/zenodo.18656820)
|
|
24
24
|
|
|
25
|
-
A
|
|
25
|
+
A case study that utilizes the diffindiff library is available on [arXiv](https://arxiv.org/abs/2605.21464).
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
## Citation
|
|
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.2) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
## Installation
|
|
@@ -53,6 +53,7 @@ pip install git+https://github.com/geowieland/diffindiff_official.git
|
|
|
53
53
|
- Define custom treatment and control groups as well as treatment periods
|
|
54
54
|
- Create ready-to-fit DiD data objects
|
|
55
55
|
- Create predictive counterfactuals
|
|
56
|
+
- Create a synthetic control unit
|
|
56
57
|
- **DiD analysis**:
|
|
57
58
|
- Perform standard DiD analysis with pre-post data
|
|
58
59
|
- Perform DiD analysis with two-way fixed effects models
|
|
@@ -66,6 +67,7 @@ pip install git+https://github.com/geowieland/diffindiff_official.git
|
|
|
66
67
|
- Including after-treatment period
|
|
67
68
|
- Perform Triple Difference (DDD) analysis
|
|
68
69
|
- Perform DiD analysis with demeaned data
|
|
70
|
+
- Perform Synthetic DiD analysis
|
|
69
71
|
- **Diagnosis tools and extensions of analyses**:
|
|
70
72
|
- Add own counterfactuals or create counterfactuals based on machine learning or OLS regression models
|
|
71
73
|
- Bonferroni correction for treatment effects
|
|
@@ -177,16 +179,11 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
177
179
|
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).
|
|
178
180
|
|
|
179
181
|
|
|
180
|
-
## What's new (v2.5.
|
|
182
|
+
## What's new (v2.5.2)
|
|
181
183
|
|
|
182
|
-
- General
|
|
183
|
-
- Update of dependencies to be compatible with Python 3.13 (still works with Python 3.11) while avoiding incompatibility conflict of statsmodels and scipy
|
|
184
|
-
- Internal changes in some functions to be compatible with Python 3.13 (still works with Python 3.11)
|
|
185
184
|
- Extensions
|
|
186
|
-
-
|
|
187
|
-
- didanalysis_helper.treatment_diagnostics() now additionally checks unique number of analysis units and time points
|
|
188
|
-
- didanalysis_helper.data_diagnostics() now additionally checks whether covariates are constant; didanalysis.did_analysis() and .ddd_analysis() automatically skip such variables from the model analysis
|
|
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
|
|
189
186
|
- Bugfixes
|
|
190
|
-
-
|
|
191
|
-
|
|
192
|
-
-
|
|
187
|
+
- DiffModel.placebo() now checks control group size and returns warning when no. of control units is equal to 1, not allowing any placebo test
|
|
188
|
+
- Other
|
|
189
|
+
- Some updates of documentation and README
|
|
@@ -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
|
|
17
|
+
A case study that utilizes the diffindiff library 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.2) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
## Installation
|
|
@@ -45,6 +45,7 @@ pip install git+https://github.com/geowieland/diffindiff_official.git
|
|
|
45
45
|
- Define custom treatment and control groups as well as treatment periods
|
|
46
46
|
- Create ready-to-fit DiD data objects
|
|
47
47
|
- Create predictive counterfactuals
|
|
48
|
+
- Create a synthetic control unit
|
|
48
49
|
- **DiD analysis**:
|
|
49
50
|
- Perform standard DiD analysis with pre-post data
|
|
50
51
|
- Perform DiD analysis with two-way fixed effects models
|
|
@@ -58,6 +59,7 @@ pip install git+https://github.com/geowieland/diffindiff_official.git
|
|
|
58
59
|
- Including after-treatment period
|
|
59
60
|
- Perform Triple Difference (DDD) analysis
|
|
60
61
|
- Perform DiD analysis with demeaned data
|
|
62
|
+
- Perform Synthetic DiD analysis
|
|
61
63
|
- **Diagnosis tools and extensions of analyses**:
|
|
62
64
|
- Add own counterfactuals or create counterfactuals based on machine learning or OLS regression models
|
|
63
65
|
- Bonferroni correction for treatment effects
|
|
@@ -169,16 +171,11 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
169
171
|
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).
|
|
170
172
|
|
|
171
173
|
|
|
172
|
-
## What's new (v2.5.
|
|
174
|
+
## What's new (v2.5.2)
|
|
173
175
|
|
|
174
|
-
- General
|
|
175
|
-
- Update of dependencies to be compatible with Python 3.13 (still works with Python 3.11) while avoiding incompatibility conflict of statsmodels and scipy
|
|
176
|
-
- Internal changes in some functions to be compatible with Python 3.13 (still works with Python 3.11)
|
|
177
176
|
- Extensions
|
|
178
|
-
-
|
|
179
|
-
- didanalysis_helper.treatment_diagnostics() now additionally checks unique number of analysis units and time points
|
|
180
|
-
- didanalysis_helper.data_diagnostics() now additionally checks whether covariates are constant; didanalysis.did_analysis() and .ddd_analysis() automatically skip such variables from the model analysis
|
|
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
|
|
181
178
|
- Bugfixes
|
|
182
|
-
-
|
|
183
|
-
|
|
184
|
-
-
|
|
179
|
+
- DiffModel.placebo() now checks control group size and returns warning when no. of control units is equal to 1, not allowing any placebo test
|
|
180
|
+
- Other
|
|
181
|
+
- Some updates of documentation and README
|
|
@@ -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-07-
|
|
7
|
+
# Version: 1.0.23
|
|
8
|
+
# Last update: 2026-07-14 19:48
|
|
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.2"
|
|
16
16
|
|
|
17
17
|
VERBOSE = False
|
|
18
18
|
|
|
@@ -52,6 +52,7 @@ TREATMENT_PERIOD_DESCRIPTION = f"{TREATMENT_DESCRIPTION} period"
|
|
|
52
52
|
STUDY_PERIOD_DESCRIPTION = "Study period"
|
|
53
53
|
PREPOST_DESCRIPTION = "Pre-post"
|
|
54
54
|
AFTER_TREATMENT_PERIOD_DESCRIPTION = "After-treatment period"
|
|
55
|
+
FOLLOW_UP_DESCRIPTION = "Follow-up"
|
|
55
56
|
|
|
56
57
|
UNITS_DESCRIPTION = "Units"
|
|
57
58
|
|
|
@@ -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-07-
|
|
7
|
+
# Version: 2.4.3
|
|
8
|
+
# Last update: 2026-07-14 19:49
|
|
9
9
|
# Copyright (c) 2024-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -898,6 +898,49 @@ class DiffModel:
|
|
|
898
898
|
|
|
899
899
|
treatment_effects = self.treatment_effects()
|
|
900
900
|
|
|
901
|
+
try:
|
|
902
|
+
if model_config.get("pre_post", False) and len(model_config.get("after_treatment_col", [])) > 0:
|
|
903
|
+
|
|
904
|
+
labels = treatment_effects.iloc[:, 0].astype(str).values
|
|
905
|
+
has_after = any((config.AFTER_TREATMENT_PERIOD_DESCRIPTION in lab) or (config.ATT_COL in lab) for lab in labels)
|
|
906
|
+
if not has_after:
|
|
907
|
+
model_results = self.data[0]
|
|
908
|
+
|
|
909
|
+
aate_key = config.EFFECTS_TYPES["AATE"]["model_results_key"] if "AATE" in config.EFFECTS_TYPES else "average_after_treatment_effects"
|
|
910
|
+
att_key = config.EFFECTS_TYPES["ATT"]["model_results_key"] if "ATT" in config.EFFECTS_TYPES else "after_treatment_time_effects"
|
|
911
|
+
appended = False
|
|
912
|
+
|
|
913
|
+
for key_try in [aate_key, att_key]:
|
|
914
|
+
if key_try in model_results and isinstance(model_results[key_try], dict):
|
|
915
|
+
for subkey, val in model_results[key_try].items():
|
|
916
|
+
|
|
917
|
+
if isinstance(val, dict):
|
|
918
|
+
|
|
919
|
+
coef_k = config.OLS_MODEL_RESULTS["coef"]["model_results_key"]
|
|
920
|
+
se_k = config.OLS_MODEL_RESULTS["coef_standard_errors"]["model_results_key"]
|
|
921
|
+
t_k = config.OLS_MODEL_RESULTS["coef_teststatistic"]["model_results_key"]
|
|
922
|
+
p_k = config.OLS_MODEL_RESULTS["coef_p"]["model_results_key"]
|
|
923
|
+
cil_k = config.OLS_MODEL_RESULTS["coef_confint_lower"]["model_results_key"]
|
|
924
|
+
ciu_k = config.OLS_MODEL_RESULTS["coef_confint_upper"]["model_results_key"]
|
|
925
|
+
|
|
926
|
+
new_row = {
|
|
927
|
+
"": f"{config.EFFECTS_TYPES.get('AATE', {'description': 'After-treatment'})['description']} {subkey}",
|
|
928
|
+
"Estimate": val.get(coef_k, np.nan),
|
|
929
|
+
"SE": val.get(se_k, np.nan),
|
|
930
|
+
"t": val.get(t_k, np.nan),
|
|
931
|
+
"p": val.get(p_k, np.nan),
|
|
932
|
+
"CI lower": val.get(cil_k, np.nan),
|
|
933
|
+
"CI upper": val.get(ciu_k, np.nan),
|
|
934
|
+
}
|
|
935
|
+
treatment_effects = pd.concat([treatment_effects, pd.DataFrame([new_row])], ignore_index=True)
|
|
936
|
+
appended = True
|
|
937
|
+
break
|
|
938
|
+
if appended:
|
|
939
|
+
break
|
|
940
|
+
except Exception:
|
|
941
|
+
|
|
942
|
+
pass
|
|
943
|
+
|
|
901
944
|
if sort_by_coef:
|
|
902
945
|
treatment_effects = treatment_effects.sort_values(
|
|
903
946
|
by = treatment_effects.columns[1],
|
|
@@ -1379,10 +1422,15 @@ class DiffModel:
|
|
|
1379
1422
|
model_data_c[time_col] = pd.to_datetime(model_data_c[time_col])
|
|
1380
1423
|
model_data_c[unit_col] = model_data_c[unit_col].astype(str)
|
|
1381
1424
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1425
|
+
if control_group_N < 2:
|
|
1426
|
+
print(f"WARNING: Model includes one control unit only: '{control_group[0]}'. Placebo analysis requires size of control group >1. Original model is returned.")
|
|
1427
|
+
return self
|
|
1428
|
+
|
|
1429
|
+
else:
|
|
1430
|
+
units_random_sample = model_data_c[unit_col].sample(
|
|
1431
|
+
n = int(round(divide*control_group_N*resample, 0)),
|
|
1432
|
+
random_state = random_state
|
|
1433
|
+
).astype(str).tolist()
|
|
1386
1434
|
|
|
1387
1435
|
model_data_c[TG_col_] = 0
|
|
1388
1436
|
model_data_c.loc[(model_data_c[unit_col].isin(units_random_sample)), TG_col_] = 1
|
|
@@ -1807,14 +1855,14 @@ class DiffModel:
|
|
|
1807
1855
|
model_data_TG_CG["t"] = pd.to_datetime(model_data_TG_CG["t"])
|
|
1808
1856
|
|
|
1809
1857
|
if not model_config["pre_post"]:
|
|
1810
|
-
pre_post_barplot = False
|
|
1858
|
+
pre_post_barplot = False
|
|
1811
1859
|
|
|
1812
1860
|
if pre_post_barplot:
|
|
1813
1861
|
|
|
1814
1862
|
x_pos_t1_TG = 0
|
|
1815
1863
|
x_pos_t1_CG = x_pos_t1_TG + pre_post_bar_width
|
|
1816
1864
|
x_pos_t2_TG = 1.5
|
|
1817
|
-
x_pos_t2_CG = x_pos_t2_TG + pre_post_bar_width
|
|
1865
|
+
x_pos_t2_CG = x_pos_t2_TG + pre_post_bar_width
|
|
1818
1866
|
|
|
1819
1867
|
plt.bar(
|
|
1820
1868
|
x = x_pos_t1_TG,
|
|
@@ -1842,6 +1890,24 @@ class DiffModel:
|
|
|
1842
1890
|
color=lines_col[3],
|
|
1843
1891
|
width = pre_post_bar_width
|
|
1844
1892
|
)
|
|
1893
|
+
|
|
1894
|
+
if len(model_data_TG_CG) == 3:
|
|
1895
|
+
|
|
1896
|
+
x_pos_t3_TG = 3
|
|
1897
|
+
x_pos_t3_CG = x_pos_t3_TG + pre_post_bar_width
|
|
1898
|
+
|
|
1899
|
+
plt.bar(
|
|
1900
|
+
x = x_pos_t3_TG,
|
|
1901
|
+
height = model_data_TG_CG[outcome_col_expected_TG][2],
|
|
1902
|
+
color = lines_col[2],
|
|
1903
|
+
width = pre_post_bar_width
|
|
1904
|
+
)
|
|
1905
|
+
plt.bar(
|
|
1906
|
+
x = x_pos_t3_CG,
|
|
1907
|
+
height = model_data_TG_CG[outcome_col_expected_CG][2],
|
|
1908
|
+
color=lines_col[3],
|
|
1909
|
+
width = pre_post_bar_width
|
|
1910
|
+
)
|
|
1845
1911
|
|
|
1846
1912
|
plt.xlabel(x_label)
|
|
1847
1913
|
plt.ylabel(y_label)
|
|
@@ -1935,16 +2001,29 @@ class DiffModel:
|
|
|
1935
2001
|
ax.xaxis.set_major_formatter(DateFormatter(model_config["date_format"]))
|
|
1936
2002
|
|
|
1937
2003
|
if model_config["pre_post"]:
|
|
2004
|
+
|
|
2005
|
+
ticks_line = model_data_TG_CG["t"].unique()
|
|
2006
|
+
ticks_bars = [0.25, 1.75]
|
|
2007
|
+
|
|
2008
|
+
if len(model_data_TG_CG) == 3:
|
|
2009
|
+
|
|
2010
|
+
if len(pre_post_ticks) < 3:
|
|
2011
|
+
pre_post_ticks.append("Follow-up")
|
|
2012
|
+
|
|
2013
|
+
ticks_bars.append(3.25)
|
|
2014
|
+
|
|
1938
2015
|
if not pre_post_barplot:
|
|
2016
|
+
|
|
1939
2017
|
plt.xticks(
|
|
1940
|
-
|
|
1941
|
-
labels =
|
|
2018
|
+
ticks_line,
|
|
2019
|
+
labels = pre_post_ticks
|
|
1942
2020
|
)
|
|
1943
2021
|
else:
|
|
1944
2022
|
plt.xticks(
|
|
1945
|
-
|
|
1946
|
-
labels =
|
|
1947
|
-
)
|
|
2023
|
+
ticks_bars,
|
|
2024
|
+
labels = pre_post_ticks
|
|
2025
|
+
)
|
|
2026
|
+
|
|
1948
2027
|
else:
|
|
1949
2028
|
plt.xticks(rotation=90)
|
|
1950
2029
|
|
|
@@ -2644,6 +2723,8 @@ def did_analysis(
|
|
|
2644
2723
|
FE_time_vars = []
|
|
2645
2724
|
dummy_time_original = []
|
|
2646
2725
|
|
|
2726
|
+
drop_first = intercept
|
|
2727
|
+
|
|
2647
2728
|
if not demean:
|
|
2648
2729
|
|
|
2649
2730
|
if FE_unit:
|
|
@@ -2652,7 +2733,7 @@ def did_analysis(
|
|
|
2652
2733
|
data = data,
|
|
2653
2734
|
col = unit_col,
|
|
2654
2735
|
type = "unit",
|
|
2655
|
-
drop_first =
|
|
2736
|
+
drop_first = drop_first,
|
|
2656
2737
|
verbose = verbose
|
|
2657
2738
|
)
|
|
2658
2739
|
|
|
@@ -2667,7 +2748,7 @@ def did_analysis(
|
|
|
2667
2748
|
data = data,
|
|
2668
2749
|
col = time_col,
|
|
2669
2750
|
type = "time",
|
|
2670
|
-
drop_first =
|
|
2751
|
+
drop_first = drop_first,
|
|
2671
2752
|
verbose = verbose
|
|
2672
2753
|
)
|
|
2673
2754
|
|
|
@@ -2691,7 +2772,7 @@ def did_analysis(
|
|
|
2691
2772
|
data = data,
|
|
2692
2773
|
col = group_by,
|
|
2693
2774
|
type = "group",
|
|
2694
|
-
drop_first =
|
|
2775
|
+
drop_first = drop_first,
|
|
2695
2776
|
verbose = verbose
|
|
2696
2777
|
)
|
|
2697
2778
|
|
|
@@ -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-07-
|
|
7
|
+
# Version: 1.2.3
|
|
8
|
+
# Last update: 2026-07-14 19:44
|
|
9
9
|
# Copyright (c) 2025-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -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-07-
|
|
7
|
+
# Version: 2.3.2
|
|
8
|
+
# Last update: 2026-07-16 18:52
|
|
9
9
|
# Copyright (c) 2024-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -622,15 +622,36 @@ def create_treatment(
|
|
|
622
622
|
|
|
623
623
|
if pre_post:
|
|
624
624
|
|
|
625
|
-
after_treatment_period
|
|
625
|
+
if after_treatment_period:
|
|
626
|
+
|
|
627
|
+
if study_period[1] <= treatment_period[1]:
|
|
628
|
+
|
|
629
|
+
print(f"WARNING: after_treatment_period is set True, but end of study period ({study_period[1]}) is <= end of treatment period ({treatment_period[1]}). After treatment period is ignored.")
|
|
630
|
+
|
|
631
|
+
after_treatment_period = False
|
|
626
632
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
633
|
+
else:
|
|
634
|
+
|
|
635
|
+
study_period_range = [study_period[0], study_period[1]]
|
|
636
|
+
study_period_N = 3
|
|
637
|
+
study_period_counter = [1,2,3]
|
|
630
638
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
639
|
+
treatment_period_N = 1
|
|
640
|
+
TT_dummies = [0,1,0]
|
|
641
|
+
ATT_dummies = [0,0,1]
|
|
642
|
+
|
|
643
|
+
treatment_period_range = [study_period[0], treatment_period[1], study_period[1]]
|
|
644
|
+
after_treatment_period_range = treatment_period_range
|
|
645
|
+
|
|
646
|
+
else:
|
|
647
|
+
|
|
648
|
+
study_period_range = [study_period[0], study_period[1]]
|
|
649
|
+
study_period_N = 2
|
|
650
|
+
study_period_counter = [1,2]
|
|
651
|
+
|
|
652
|
+
treatment_period_range = [treatment_period[0], treatment_period[1]]
|
|
653
|
+
treatment_period_N = 1
|
|
654
|
+
TT_dummies = [0,1]
|
|
634
655
|
|
|
635
656
|
study_period_range = pd.DataFrame (treatment_period_range, columns=[config.TIME_COL])
|
|
636
657
|
study_period_range[config.TIME_COUNTER_COL] = pd.DataFrame(study_period_counter)
|
|
@@ -640,7 +661,7 @@ def create_treatment(
|
|
|
640
661
|
TT_col: TT_dummies
|
|
641
662
|
}
|
|
642
663
|
|
|
643
|
-
TT_data = pd.DataFrame(TT_data)
|
|
664
|
+
TT_data = pd.DataFrame(TT_data)
|
|
644
665
|
|
|
645
666
|
treatment_period_range = pd.DataFrame(
|
|
646
667
|
study_period_range
|
|
@@ -650,6 +671,24 @@ def create_treatment(
|
|
|
650
671
|
TT_data,
|
|
651
672
|
how = "left"
|
|
652
673
|
)
|
|
674
|
+
|
|
675
|
+
if after_treatment_period:
|
|
676
|
+
|
|
677
|
+
ATT_data = {
|
|
678
|
+
config.TIME_COL: after_treatment_period_range,
|
|
679
|
+
ATT_col: ATT_dummies
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
ATT_data = pd.DataFrame(ATT_data)
|
|
683
|
+
|
|
684
|
+
after_treatment_period_range = pd.DataFrame (after_treatment_period_range, columns=[config.TIME_COL])
|
|
685
|
+
after_treatment_period_range[config.TIME_COUNTER_COL] = pd.DataFrame(study_period_counter)
|
|
686
|
+
|
|
687
|
+
after_treatment_data_df = after_treatment_period_range.merge(ATT_data, how = "left")
|
|
688
|
+
after_treatment_data_df[ATT_col] = after_treatment_data_df[ATT_col].fillna(0)
|
|
689
|
+
after_treatment_data_df = after_treatment_data_df.drop(columns=[config.TIME_COL, config.TIME_COUNTER_COL])
|
|
690
|
+
|
|
691
|
+
treatment_data_df = pd.concat([treatment_data_df, after_treatment_data_df], axis=1)
|
|
653
692
|
|
|
654
693
|
else:
|
|
655
694
|
|
|
@@ -689,7 +728,7 @@ def create_treatment(
|
|
|
689
728
|
|
|
690
729
|
treatment_data_df[TT_col] = treatment_data_df[TT_col].fillna(0)
|
|
691
730
|
|
|
692
|
-
if after_treatment_period:
|
|
731
|
+
if after_treatment_period and not pre_post:
|
|
693
732
|
|
|
694
733
|
treatment_period_last = datetime.strptime(
|
|
695
734
|
treatment_period[1],
|
|
@@ -720,6 +759,9 @@ def create_treatment(
|
|
|
720
759
|
|
|
721
760
|
else:
|
|
722
761
|
after_treatment_period_N = 0
|
|
762
|
+
|
|
763
|
+
if after_treatment_period and pre_post:
|
|
764
|
+
after_treatment_period_N = 1
|
|
723
765
|
|
|
724
766
|
no_treatments = 1
|
|
725
767
|
|
|
@@ -735,7 +777,7 @@ def create_treatment(
|
|
|
735
777
|
"date_format": date_format,
|
|
736
778
|
"pre_post": pre_post
|
|
737
779
|
}
|
|
738
|
-
|
|
780
|
+
|
|
739
781
|
treatment_config = {
|
|
740
782
|
0: {
|
|
741
783
|
"treatment_name": treatment_name,
|
|
@@ -773,7 +815,8 @@ class DiffData:
|
|
|
773
815
|
unit_time_col_original,
|
|
774
816
|
covariates,
|
|
775
817
|
treatment_cols,
|
|
776
|
-
timestamp
|
|
818
|
+
timestamp,
|
|
819
|
+
synthetic_control
|
|
777
820
|
):
|
|
778
821
|
|
|
779
822
|
"""
|
|
@@ -798,6 +841,8 @@ class DiffData:
|
|
|
798
841
|
Mapping of treatment columns metadata.
|
|
799
842
|
timestamp : dict
|
|
800
843
|
Timestamp metadata.
|
|
844
|
+
synthetic_control : dict
|
|
845
|
+
If included, data for a synthetic control unit, else None.
|
|
801
846
|
Returns
|
|
802
847
|
-------
|
|
803
848
|
None
|
|
@@ -813,7 +858,8 @@ class DiffData:
|
|
|
813
858
|
unit_time_col_original,
|
|
814
859
|
covariates,
|
|
815
860
|
treatment_cols,
|
|
816
|
-
timestamp
|
|
861
|
+
timestamp,
|
|
862
|
+
synthetic_control
|
|
817
863
|
]
|
|
818
864
|
|
|
819
865
|
def get_did_modeldata_df (self):
|
|
@@ -1018,6 +1064,46 @@ class DiffData:
|
|
|
1018
1064
|
"""
|
|
1019
1065
|
|
|
1020
1066
|
return self.data[7]
|
|
1067
|
+
|
|
1068
|
+
def get_synthetic_control_weightings(self):
|
|
1069
|
+
|
|
1070
|
+
"""
|
|
1071
|
+
Return weightings of the synthetic control unit (if included in the DiffData object).
|
|
1072
|
+
|
|
1073
|
+
Returns
|
|
1074
|
+
-------
|
|
1075
|
+
pandas.DataFrame
|
|
1076
|
+
Weighting by control unit.
|
|
1077
|
+
|
|
1078
|
+
Examples
|
|
1079
|
+
--------
|
|
1080
|
+
>>> my_diffdata.get_synthetic_control_weightings()
|
|
1081
|
+
"""
|
|
1082
|
+
|
|
1083
|
+
if len(self.data[8]) > 0:
|
|
1084
|
+
return self.data[8]["weights"]
|
|
1085
|
+
else:
|
|
1086
|
+
print("The DiffData object does not include a synthetic control unit.")
|
|
1087
|
+
|
|
1088
|
+
def get_synthetic_control_fitmetrics(self):
|
|
1089
|
+
|
|
1090
|
+
"""
|
|
1091
|
+
Return fit metrics of the synthetic control unit (if included in the DiffData object).
|
|
1092
|
+
|
|
1093
|
+
Returns
|
|
1094
|
+
-------
|
|
1095
|
+
list
|
|
1096
|
+
Fit metrics (dict) and data points with residuals (pandas.DataFrame).
|
|
1097
|
+
|
|
1098
|
+
Examples
|
|
1099
|
+
--------
|
|
1100
|
+
>>> my_diffdata.get_synthetic_control_fitmetrics()
|
|
1101
|
+
"""
|
|
1102
|
+
|
|
1103
|
+
if len(self.data[8]) > 0:
|
|
1104
|
+
return self.data[8]["fit_metrics"]
|
|
1105
|
+
else:
|
|
1106
|
+
print("The DiffData object does not include a synthetic control unit.")
|
|
1021
1107
|
|
|
1022
1108
|
def add_covariates(
|
|
1023
1109
|
self,
|
|
@@ -1991,6 +2077,14 @@ class DiffData:
|
|
|
1991
2077
|
verbose = False
|
|
1992
2078
|
)
|
|
1993
2079
|
|
|
2080
|
+
self.data[7] = helper.create_timestamp(function="add_synthetic")
|
|
2081
|
+
|
|
2082
|
+
self.data[8] = {
|
|
2083
|
+
"weights": weights_pd,
|
|
2084
|
+
"fit_metrics": synthetic_fit_metrics,
|
|
2085
|
+
"process_unit": process_unit
|
|
2086
|
+
}
|
|
2087
|
+
|
|
1994
2088
|
return self
|
|
1995
2089
|
|
|
1996
2090
|
def summary(self):
|
|
@@ -2050,7 +2144,10 @@ class DiffData:
|
|
|
2050
2144
|
else:
|
|
2051
2145
|
print(f" {config.STUDY_PERIOD_DESCRIPTION:<{config.DIDDATA_SUMMARY_MAX_WIDTH}} {treatment_meta['study_period_start']} - {treatment_meta['study_period_end']} ({treatment_meta['study_period']} {treatment_meta['frequency']})")
|
|
2052
2146
|
print(f" {config.TREATMENT_PERIOD_DESCRIPTION:<{config.DIDDATA_SUMMARY_MAX_WIDTH}} {treatment_config[key]['treatment_period_start']} - {treatment_config[key]['treatment_period_end']} ({treatment_config[key]['treatment_period']} {treatment_meta['frequency']})")
|
|
2053
|
-
|
|
2147
|
+
|
|
2148
|
+
if value["after_treatment_period"]:
|
|
2149
|
+
print(f" {config.AFTER_TREATMENT_PERIOD_DESCRIPTION:<{config.DIDDATA_SUMMARY_MAX_WIDTH}} {treatment_config[key]['treatment_period_end']} vs. {treatment_meta['study_period_end']} ({config.FOLLOW_UP_DESCRIPTION})")
|
|
2150
|
+
|
|
2054
2151
|
print("-" * total_width)
|
|
2055
2152
|
|
|
2056
2153
|
print(f"Outcome '{outcome_col_original}'")
|
|
@@ -2444,7 +2541,8 @@ def merge_data(
|
|
|
2444
2541
|
unit_time_col_original,
|
|
2445
2542
|
[],
|
|
2446
2543
|
treatment_cols,
|
|
2447
|
-
timestamp
|
|
2544
|
+
timestamp,
|
|
2545
|
+
{}
|
|
2448
2546
|
)
|
|
2449
2547
|
|
|
2450
2548
|
if verbose:
|
|
@@ -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.0
|
|
8
|
-
# Last update: 2026-
|
|
7
|
+
# Version: 2.1.0
|
|
8
|
+
# Last update: 2026-07-10 11:35
|
|
9
9
|
# Copyright (c) 2025-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -75,7 +75,7 @@ curfew_data_prepost=create_data(
|
|
|
75
75
|
treatment_period=["2020-03-21", "2020-05-05"],
|
|
76
76
|
freq="D",
|
|
77
77
|
pre_post=True,
|
|
78
|
-
after_treatment_period =
|
|
78
|
+
after_treatment_period = False
|
|
79
79
|
)
|
|
80
80
|
# Creating DiD treatement dataset by defining groups and
|
|
81
81
|
# treatment time at once
|
|
@@ -98,6 +98,9 @@ print(curfew_model_prepost.covariates())
|
|
|
98
98
|
curfew_model_prepost.summary()
|
|
99
99
|
# Model summary
|
|
100
100
|
|
|
101
|
+
print(curfew_model_prepost.didmodel().summary())
|
|
102
|
+
# Access statsmodels OLS object
|
|
103
|
+
|
|
101
104
|
print(curfew_model_prepost.fit_metrics())
|
|
102
105
|
# Show model fit metrics
|
|
103
106
|
|
|
@@ -137,6 +140,59 @@ curfew_model_prepost.plot_treatment_effects(
|
|
|
137
140
|
)
|
|
138
141
|
# plot effects
|
|
139
142
|
|
|
143
|
+
|
|
144
|
+
# Same analysis with after-treatment period (follow-up):
|
|
145
|
+
|
|
146
|
+
curfew_data_prepost_AT=create_data(
|
|
147
|
+
outcome_data=curfew_DE,
|
|
148
|
+
unit_id_col="county",
|
|
149
|
+
time_col="infection_date",
|
|
150
|
+
outcome_col="infections_cum_per100000",
|
|
151
|
+
treatment_group=
|
|
152
|
+
curfew_DE.loc[curfew_DE["Bundesland"].isin([9,10,14])]["county"],
|
|
153
|
+
control_group=
|
|
154
|
+
curfew_DE.loc[~curfew_DE["Bundesland"].isin([9,10,14])]["county"],
|
|
155
|
+
study_period=["2020-03-01", "2020-05-15"],
|
|
156
|
+
treatment_period=["2020-03-21", "2020-05-05"],
|
|
157
|
+
freq="D",
|
|
158
|
+
pre_post=True,
|
|
159
|
+
after_treatment_period = True
|
|
160
|
+
)
|
|
161
|
+
# Creating DiD treatement dataset by defining groups and
|
|
162
|
+
# treatment time at once
|
|
163
|
+
|
|
164
|
+
curfew_data_prepost_AT.summary()
|
|
165
|
+
# Summary of created data
|
|
166
|
+
|
|
167
|
+
curfew_model_prepost_AT=curfew_data_prepost_AT.analysis(verbose=True)
|
|
168
|
+
# Model analysis of created data
|
|
169
|
+
|
|
170
|
+
curfew_model_prepost_AT.summary()
|
|
171
|
+
# Model summary
|
|
172
|
+
|
|
173
|
+
curfew_model_prepost_AT.plot(
|
|
174
|
+
x_label="Timepoint",
|
|
175
|
+
y_label="Cumulative infections per 100,000",
|
|
176
|
+
plot_title="Curfew effectiveness pre-post - Groups over time",
|
|
177
|
+
plot_observed=False,
|
|
178
|
+
lines_col=[None,None,"blue","orange"],
|
|
179
|
+
lines_labels=[None,None,"Treatment group","Control group","Treatment group CI","Control group CI"],
|
|
180
|
+
lines_style=[None,None,"solid","solid"]
|
|
181
|
+
)
|
|
182
|
+
# Plot DiD pre vs. post results
|
|
183
|
+
# with user-determined style
|
|
184
|
+
|
|
185
|
+
curfew_model_prepost_AT.plot(
|
|
186
|
+
x_label="Timepoint",
|
|
187
|
+
y_label="Cumulative infections per 100,000",
|
|
188
|
+
plot_title="Curfew effectiveness pre-post - Groups over time",
|
|
189
|
+
lines_col=[None,None,"blue","orange"],
|
|
190
|
+
lines_labels=[None,None,"Treatment group","Control group","Treatment group CI", "Control group CI"],
|
|
191
|
+
pre_post_barplot=True
|
|
192
|
+
)
|
|
193
|
+
# Plot DiD pre vs. post results
|
|
194
|
+
# with user-determined style
|
|
195
|
+
|
|
140
196
|
counties_DE=pd.read_csv("data/counties_DE.csv", sep=";", decimal=",", encoding='latin1')
|
|
141
197
|
# Dataset with German county data
|
|
142
198
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: diffindiff
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.2
|
|
4
4
|
Summary: diffindiff: Python library for convenient Difference-in-Differences analyses
|
|
5
5
|
Author: Thomas Wieland
|
|
6
6
|
Author-email: geowieland@googlemail.com
|
|
@@ -22,14 +22,14 @@ Thomas Wieland [ORCID](https://orcid.org/0000-0001-5168-9846) [EMail](mailto:geo
|
|
|
22
22
|
- 💻 GitHub Repository: [diffindiff_official](https://github.com/geowieland/diffindiff_official)
|
|
23
23
|
- 📄 DOI (Zenodo): [10.5281/zenodo.18656820](https://doi.org/10.5281/zenodo.18656820)
|
|
24
24
|
|
|
25
|
-
A
|
|
25
|
+
A case study that utilizes the diffindiff library is available on [arXiv](https://arxiv.org/abs/2605.21464).
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
## Citation
|
|
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.2) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
## Installation
|
|
@@ -53,6 +53,7 @@ pip install git+https://github.com/geowieland/diffindiff_official.git
|
|
|
53
53
|
- Define custom treatment and control groups as well as treatment periods
|
|
54
54
|
- Create ready-to-fit DiD data objects
|
|
55
55
|
- Create predictive counterfactuals
|
|
56
|
+
- Create a synthetic control unit
|
|
56
57
|
- **DiD analysis**:
|
|
57
58
|
- Perform standard DiD analysis with pre-post data
|
|
58
59
|
- Perform DiD analysis with two-way fixed effects models
|
|
@@ -66,6 +67,7 @@ pip install git+https://github.com/geowieland/diffindiff_official.git
|
|
|
66
67
|
- Including after-treatment period
|
|
67
68
|
- Perform Triple Difference (DDD) analysis
|
|
68
69
|
- Perform DiD analysis with demeaned data
|
|
70
|
+
- Perform Synthetic DiD analysis
|
|
69
71
|
- **Diagnosis tools and extensions of analyses**:
|
|
70
72
|
- Add own counterfactuals or create counterfactuals based on machine learning or OLS regression models
|
|
71
73
|
- Bonferroni correction for treatment effects
|
|
@@ -177,16 +179,11 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
177
179
|
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).
|
|
178
180
|
|
|
179
181
|
|
|
180
|
-
## What's new (v2.5.
|
|
182
|
+
## What's new (v2.5.2)
|
|
181
183
|
|
|
182
|
-
- General
|
|
183
|
-
- Update of dependencies to be compatible with Python 3.13 (still works with Python 3.11) while avoiding incompatibility conflict of statsmodels and scipy
|
|
184
|
-
- Internal changes in some functions to be compatible with Python 3.13 (still works with Python 3.11)
|
|
185
184
|
- Extensions
|
|
186
|
-
-
|
|
187
|
-
- didanalysis_helper.treatment_diagnostics() now additionally checks unique number of analysis units and time points
|
|
188
|
-
- didanalysis_helper.data_diagnostics() now additionally checks whether covariates are constant; didanalysis.did_analysis() and .ddd_analysis() automatically skip such variables from the model analysis
|
|
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
|
|
189
186
|
- Bugfixes
|
|
190
|
-
-
|
|
191
|
-
|
|
192
|
-
-
|
|
187
|
+
- DiffModel.placebo() now checks control group size and returns warning when no. of control units is equal to 1, not allowing any placebo test
|
|
188
|
+
- Other
|
|
189
|
+
- Some updates of documentation and README
|
|
@@ -7,7 +7,7 @@ def read_README():
|
|
|
7
7
|
|
|
8
8
|
setup(
|
|
9
9
|
name='diffindiff',
|
|
10
|
-
version='2.5.
|
|
10
|
+
version='2.5.2',
|
|
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,
|
|
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
|
|
File without changes
|
|
File without changes
|