diffindiff 2.4.1__tar.gz → 2.5.0__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.4.1 → diffindiff-2.5.0}/PKG-INFO +14 -5
- {diffindiff-2.4.1 → diffindiff-2.5.0}/README.md +13 -4
- {diffindiff-2.4.1 → diffindiff-2.5.0}/diffindiff/config.py +14 -4
- {diffindiff-2.4.1 → diffindiff-2.5.0}/diffindiff/didanalysis.py +72 -17
- {diffindiff-2.4.1 → diffindiff-2.5.0}/diffindiff/didanalysis_helper.py +36 -15
- {diffindiff-2.4.1 → diffindiff-2.5.0}/diffindiff/diddata.py +347 -75
- {diffindiff-2.4.1 → diffindiff-2.5.0}/diffindiff/tests/tests_diffindiff.py +2 -1
- {diffindiff-2.4.1 → diffindiff-2.5.0}/diffindiff.egg-info/PKG-INFO +14 -5
- diffindiff-2.5.0/diffindiff.egg-info/requires.txt +14 -0
- {diffindiff-2.4.1 → diffindiff-2.5.0}/setup.py +14 -9
- diffindiff-2.4.1/diffindiff.egg-info/requires.txt +0 -9
- {diffindiff-2.4.1 → diffindiff-2.5.0}/MANIFEST.in +0 -0
- {diffindiff-2.4.1 → diffindiff-2.5.0}/diffindiff/__init__.py +0 -0
- {diffindiff-2.4.1 → diffindiff-2.5.0}/diffindiff/didtools.py +0 -0
- {diffindiff-2.4.1 → diffindiff-2.5.0}/diffindiff/tests/__init__.py +0 -0
- {diffindiff-2.4.1 → diffindiff-2.5.0}/diffindiff/tests/data/Corona_Hesse.xlsx +0 -0
- {diffindiff-2.4.1 → diffindiff-2.5.0}/diffindiff/tests/data/counties_DE.csv +0 -0
- {diffindiff-2.4.1 → diffindiff-2.5.0}/diffindiff/tests/data/curfew_DE.csv +0 -0
- {diffindiff-2.4.1 → diffindiff-2.5.0}/diffindiff.egg-info/SOURCES.txt +0 -0
- {diffindiff-2.4.1 → diffindiff-2.5.0}/diffindiff.egg-info/dependency_links.txt +0 -0
- {diffindiff-2.4.1 → diffindiff-2.5.0}/diffindiff.egg-info/top_level.txt +0 -0
- {diffindiff-2.4.1 → diffindiff-2.5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: diffindiff
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
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 research note featuring a case study that utilizes the diffindiff library is a
|
|
|
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.
|
|
32
|
+
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.0) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
## Installation
|
|
@@ -177,7 +177,16 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
177
177
|
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
178
|
|
|
179
179
|
|
|
180
|
-
## What's new (v2.
|
|
180
|
+
## What's new (v2.5.0)
|
|
181
181
|
|
|
182
|
-
-
|
|
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
|
+
- Extensions
|
|
186
|
+
- Creating synthetic control units with DiffData.add_synthetic() and conducting a Synthetic DiD analysis
|
|
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
|
|
189
|
+
- Bugfixes
|
|
190
|
+
- DiffData.add_own_counterfactual() now works correctly in any case (also when no. of treatments > 1)
|
|
191
|
+
- Correct internal processing of treatment data in DiffTreatment and DiffData objects when treatments were added
|
|
192
|
+
- Fixed missing treatment group definition in DiffData.add_treatment()
|
|
@@ -21,7 +21,7 @@ A research note featuring a case study that utilizes the diffindiff library is a
|
|
|
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.
|
|
24
|
+
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.0) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
## Installation
|
|
@@ -169,7 +169,16 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
169
169
|
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
170
|
|
|
171
171
|
|
|
172
|
-
## What's new (v2.
|
|
172
|
+
## What's new (v2.5.0)
|
|
173
173
|
|
|
174
|
-
-
|
|
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
|
+
- Extensions
|
|
178
|
+
- Creating synthetic control units with DiffData.add_synthetic() and conducting a Synthetic DiD analysis
|
|
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
|
|
181
|
+
- Bugfixes
|
|
182
|
+
- DiffData.add_own_counterfactual() now works correctly in any case (also when no. of treatments > 1)
|
|
183
|
+
- Correct internal processing of treatment data in DiffTreatment and DiffData objects when treatments were added
|
|
184
|
+
- Fixed missing treatment group definition in DiffData.add_treatment()
|
|
@@ -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.21
|
|
8
|
+
# Last update: 2026-07-03 17:50
|
|
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.
|
|
15
|
+
PACKAGE_VERSION = "2.5.0"
|
|
16
16
|
|
|
17
17
|
VERBOSE = False
|
|
18
18
|
|
|
@@ -21,6 +21,11 @@ ROUND_PERCENT = 2
|
|
|
21
21
|
|
|
22
22
|
AUTO_SWITCH_TO_PREPOST = True
|
|
23
23
|
|
|
24
|
+
AUTO_SWITCH_TO_FIXED_EFFECTS = True
|
|
25
|
+
FIXED_EFFECTS_THRESHOLD = 3
|
|
26
|
+
|
|
27
|
+
AUTO_SKIP_CONSTANT_COLUMNS = True
|
|
28
|
+
|
|
24
29
|
ACCEPT_CONTINUOUS_TREATMENTS = True
|
|
25
30
|
|
|
26
31
|
# Description texts:
|
|
@@ -96,6 +101,7 @@ LOG_PREFIX = "log"
|
|
|
96
101
|
OBSERVED_SUFFIX = "observed"
|
|
97
102
|
EXPECTED_SUFFIX = "expected"
|
|
98
103
|
DEMEAN_SUFFIX = "demean"
|
|
104
|
+
WEIGHTED_SUFFIX = "_weighted"
|
|
99
105
|
PREDICTED_SUFFIX = "pred"
|
|
100
106
|
CI_LOWER_SUFFIX = "CI_lower"
|
|
101
107
|
CI_UPPER_SUFFIX = "CI_upper"
|
|
@@ -500,7 +506,11 @@ DATA_DIAGNOSTICS = {
|
|
|
500
506
|
"description": N_DESCRIPTION,
|
|
501
507
|
"show_in_summary": True
|
|
502
508
|
},
|
|
503
|
-
|
|
509
|
+
"cols_constants": {
|
|
510
|
+
"description": "Columns with constant values",
|
|
511
|
+
"show_in_summary": False
|
|
512
|
+
},
|
|
513
|
+
}
|
|
504
514
|
|
|
505
515
|
DIAGNOSTICS_COLUMN = "Result"
|
|
506
516
|
|
|
@@ -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.1
|
|
8
|
+
# Last update: 2026-07-02 20:17
|
|
9
9
|
# Copyright (c) 2024-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -735,16 +735,20 @@ class DiffModel:
|
|
|
735
735
|
treatment_effects_df = self.treatment_effects()
|
|
736
736
|
|
|
737
737
|
width = treatment_effects_df[""].str.len().max()
|
|
738
|
-
treatment_effects_df[""] = treatment_effects_df[""].str.ljust(width)
|
|
739
|
-
total_width = (sum(treatment_effects_df.astype(str).map(len).max()) + len(treatment_effects_df.columns) * 2)
|
|
738
|
+
treatment_effects_df[""] = treatment_effects_df[""].str.ljust(width)
|
|
740
739
|
|
|
741
|
-
|
|
740
|
+
total_width = (
|
|
741
|
+
treatment_effects_df.apply(lambda col: col.astype(str).str.len().max()).sum()
|
|
742
|
+
+ len(treatment_effects_df.columns) * 2
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
print("=" * int(total_width))
|
|
742
746
|
print(model_config["analysis_description"])
|
|
743
|
-
print("-" * total_width)
|
|
747
|
+
print("-" * int(total_width))
|
|
744
748
|
|
|
745
749
|
print(config.TREATMENT_EFFECTS_DESCRIPTION)
|
|
746
750
|
print(treatment_effects_df.to_string(index=False))
|
|
747
|
-
print("-" * total_width)
|
|
751
|
+
print("-" * int(total_width))
|
|
748
752
|
|
|
749
753
|
covariates_effects_df = pd.DataFrame(
|
|
750
754
|
[
|
|
@@ -761,7 +765,7 @@ class DiffModel:
|
|
|
761
765
|
covariates_effects_df[config.COVARIATES_DESCRIPTION] = covariates_effects_df[config.COVARIATES_DESCRIPTION].str.ljust(width)
|
|
762
766
|
|
|
763
767
|
print(covariates_effects_df.to_string(index=False, header=False))
|
|
764
|
-
print("-" * total_width)
|
|
768
|
+
print("-" * int(total_width))
|
|
765
769
|
|
|
766
770
|
treatment_diagnostics = self.treatment_diagnostics()
|
|
767
771
|
treatment_diagnostics_df = treatment_diagnostics[0]
|
|
@@ -782,7 +786,7 @@ class DiffModel:
|
|
|
782
786
|
else:
|
|
783
787
|
print(f"NOTE: Treatments {', '.join(no_control_conditions)} have no control conditions.")
|
|
784
788
|
|
|
785
|
-
print("-" * total_width)
|
|
789
|
+
print("-" * int(total_width))
|
|
786
790
|
|
|
787
791
|
data_diagnostics_df = self.data_diagnostics()
|
|
788
792
|
|
|
@@ -792,7 +796,7 @@ class DiffModel:
|
|
|
792
796
|
print(config.DATA_DIAGNOSTICS_DESCRIPTION)
|
|
793
797
|
print(data_diagnostics_df.to_string(index=False, header=False))
|
|
794
798
|
|
|
795
|
-
print("-" * total_width)
|
|
799
|
+
print("-" * int(total_width))
|
|
796
800
|
|
|
797
801
|
model_fit_metrics = self.fit_metrics()
|
|
798
802
|
|
|
@@ -804,7 +808,7 @@ class DiffModel:
|
|
|
804
808
|
print(f"{config.MODEL_FIT_METRICS_DESCRIPTION}s")
|
|
805
809
|
print(model_fit_metrics.to_string(index=False, header=False))
|
|
806
810
|
|
|
807
|
-
print("=" * total_width)
|
|
811
|
+
print("=" * int(total_width))
|
|
808
812
|
|
|
809
813
|
return self
|
|
810
814
|
|
|
@@ -2389,16 +2393,37 @@ def did_analysis(
|
|
|
2389
2393
|
verbose=verbose
|
|
2390
2394
|
)
|
|
2391
2395
|
treatment_diagnostics = treatment_diagnostics_results[0]
|
|
2392
|
-
staggered_adoption = treatment_diagnostics_results[1]
|
|
2396
|
+
staggered_adoption = treatment_diagnostics_results[1]
|
|
2397
|
+
unique_units = treatment_diagnostics_results[3]
|
|
2398
|
+
unique_time_points = treatment_diagnostics_results[4]
|
|
2393
2399
|
|
|
2394
2400
|
if no_treatments > 1:
|
|
2395
2401
|
|
|
2396
2402
|
intercept = False
|
|
2397
2403
|
TG_col = []
|
|
2398
2404
|
|
|
2399
|
-
if not FE_unit:
|
|
2405
|
+
if not FE_unit and config.AUTO_SWITCH_TO_FIXED_EFFECTS:
|
|
2400
2406
|
FE_unit = True
|
|
2401
|
-
print("NOTE: Quasi-experiment includes more than one treatment. Unit fixed effects are used instead of control group baseline and treatment group deviation.")
|
|
2407
|
+
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.")
|
|
2408
|
+
|
|
2409
|
+
if unique_units < config.FIXED_EFFECTS_THRESHOLD:
|
|
2410
|
+
|
|
2411
|
+
FE_unit = False
|
|
2412
|
+
FE_group = False
|
|
2413
|
+
ITE = False
|
|
2414
|
+
ITT = False
|
|
2415
|
+
intercept = True
|
|
2416
|
+
|
|
2417
|
+
print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} units. Unit or group fixed effects are skipped.")
|
|
2418
|
+
|
|
2419
|
+
if unique_time_points < config.FIXED_EFFECTS_THRESHOLD:
|
|
2420
|
+
|
|
2421
|
+
FE_time = False
|
|
2422
|
+
ITT = False
|
|
2423
|
+
GTT = False
|
|
2424
|
+
intercept = True
|
|
2425
|
+
|
|
2426
|
+
print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} time points. Time fixed effects are skipped.")
|
|
2402
2427
|
|
|
2403
2428
|
if demean and FE_unit:
|
|
2404
2429
|
|
|
@@ -2435,7 +2460,7 @@ def did_analysis(
|
|
|
2435
2460
|
GTT = False
|
|
2436
2461
|
print("NOTE: Both group and individual time trends were stated. Switching to individual time trends only.")
|
|
2437
2462
|
|
|
2438
|
-
if staggered_adoption:
|
|
2463
|
+
if staggered_adoption and config.AUTO_SWITCH_TO_FIXED_EFFECTS:
|
|
2439
2464
|
|
|
2440
2465
|
if not FE_unit or not FE_time:
|
|
2441
2466
|
print("NOTE: Quasi-experiment includes one or more staggered treatments. Two-way fixed effects model is used.")
|
|
@@ -2562,7 +2587,15 @@ def did_analysis(
|
|
|
2562
2587
|
missing_replace_by_zero = missing_replace_by_zero,
|
|
2563
2588
|
verbose = verbose
|
|
2564
2589
|
)
|
|
2590
|
+
|
|
2591
|
+
if len(data_diagnostics["cols_constants"]) > 0:
|
|
2592
|
+
|
|
2593
|
+
if config.AUTO_SKIP_CONSTANT_COLUMNS:
|
|
2565
2594
|
|
|
2595
|
+
print(f"NOTE: The following columns are constant and dropped from the analysis: {', '.join(data_diagnostics['cols_constants'])}.")
|
|
2596
|
+
|
|
2597
|
+
covariates = [col for col in covariates if col not in data_diagnostics["cols_constants"]]
|
|
2598
|
+
|
|
2566
2599
|
if data_diagnostics["is_prepost"] and config.AUTO_SWITCH_TO_PREPOST:
|
|
2567
2600
|
|
|
2568
2601
|
print(f"NOTE: Input is {config.PREPOST_PANELDATA_DESCRIPTION}. Data processing and model estimation will treat data as pre-post.")
|
|
@@ -3115,7 +3148,21 @@ def ddd_analysis(
|
|
|
3115
3148
|
verbose=verbose
|
|
3116
3149
|
)
|
|
3117
3150
|
treatment_diagnostics = treatment_diagnostics_results[0]
|
|
3118
|
-
|
|
3151
|
+
unique_units = treatment_diagnostics_results[3]
|
|
3152
|
+
unique_time_points = treatment_diagnostics_results[4]
|
|
3153
|
+
|
|
3154
|
+
if unique_units < config.FIXED_EFFECTS_THRESHOLD:
|
|
3155
|
+
|
|
3156
|
+
FE_unit = False
|
|
3157
|
+
|
|
3158
|
+
print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} units. Unit fixed effects are skipped.")
|
|
3159
|
+
|
|
3160
|
+
if unique_time_points < config.FIXED_EFFECTS_THRESHOLD:
|
|
3161
|
+
|
|
3162
|
+
FE_time = False
|
|
3163
|
+
|
|
3164
|
+
print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} time points. Time fixed effects are skipped.")
|
|
3165
|
+
|
|
3119
3166
|
if FE_unit:
|
|
3120
3167
|
TG_col = None
|
|
3121
3168
|
if FE_time:
|
|
@@ -3140,7 +3187,15 @@ def ddd_analysis(
|
|
|
3140
3187
|
missing_replace_by_zero = missing_replace_by_zero,
|
|
3141
3188
|
verbose = verbose
|
|
3142
3189
|
)
|
|
3143
|
-
|
|
3190
|
+
|
|
3191
|
+
if len(data_diagnostics["cols_constants"]) > 0:
|
|
3192
|
+
|
|
3193
|
+
if config.AUTO_SKIP_CONSTANT_COLUMNS:
|
|
3194
|
+
|
|
3195
|
+
print(f"NOTE: The following columns are constant and dropped from the analysis: {', '.join(data_diagnostics['cols_constants'])}.")
|
|
3196
|
+
|
|
3197
|
+
covariates = [col for col in covariates if col not in data_diagnostics["cols_constants"]]
|
|
3198
|
+
|
|
3144
3199
|
if data_diagnostics["is_prepost"] and config.AUTO_SWITCH_TO_PREPOST:
|
|
3145
3200
|
|
|
3146
3201
|
print("NOTE: Panel data is pre-post. Data processing and model estimation will treat data as pre-post")
|
|
@@ -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:
|
|
7
|
+
# Version: 1.2.2
|
|
8
|
+
# Last update: 2026-07-02 20:18
|
|
9
9
|
# Copyright (c) 2025-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -876,6 +876,12 @@ def data_diagnostics(
|
|
|
876
876
|
outcome_mean = np.mean(data[outcome_col])
|
|
877
877
|
outcome_sd = np.std(data[outcome_col])
|
|
878
878
|
|
|
879
|
+
cols_constants = []
|
|
880
|
+
if len(other_cols_relevant) > 0:
|
|
881
|
+
for col in other_cols_relevant:
|
|
882
|
+
if data[col].nunique() == 1:
|
|
883
|
+
cols_constants.append(col)
|
|
884
|
+
|
|
879
885
|
data_diagnostics_results = {
|
|
880
886
|
list(config.DATA_DIAGNOSTICS)[0]: bool(modeldata_isbalanced),
|
|
881
887
|
list(config.DATA_DIAGNOSTICS)[1]: bool(modeldata_ismissing[0]),
|
|
@@ -885,7 +891,8 @@ def data_diagnostics(
|
|
|
885
891
|
list(config.DATA_DIAGNOSTICS)[5]: data_type,
|
|
886
892
|
list(config.DATA_DIAGNOSTICS)[6]: outcome_col,
|
|
887
893
|
list(config.DATA_DIAGNOSTICS)[7]: f"Mean={round(outcome_mean, config.ROUND_STATISTIC)} SD={round(outcome_sd, config.ROUND_STATISTIC)}",
|
|
888
|
-
list(config.DATA_DIAGNOSTICS)[8]: observations,
|
|
894
|
+
list(config.DATA_DIAGNOSTICS)[8]: observations,
|
|
895
|
+
list(config.DATA_DIAGNOSTICS)[9]: cols_constants
|
|
889
896
|
}
|
|
890
897
|
|
|
891
898
|
return data_diagnostics_results
|
|
@@ -927,7 +934,7 @@ def treatment_diagnostics(
|
|
|
927
934
|
Returns
|
|
928
935
|
-------
|
|
929
936
|
list
|
|
930
|
-
[treatment_diagnostics_results (dict), staggered_adoption (bool), untreated (list)]
|
|
937
|
+
[treatment_diagnostics_results (dict), staggered_adoption (bool), untreated (list), unique_units (int), unique_timepoints (int)]
|
|
931
938
|
|
|
932
939
|
Examples
|
|
933
940
|
--------
|
|
@@ -1020,6 +1027,15 @@ def treatment_diagnostics(
|
|
|
1020
1027
|
verbose = verbose
|
|
1021
1028
|
)
|
|
1022
1029
|
|
|
1030
|
+
groups_units_list = []
|
|
1031
|
+
for key, value in treatment_diagnostics_results.items():
|
|
1032
|
+
groups_units_list.extend(value["treatment_group"])
|
|
1033
|
+
groups_units_list.extend(value["control_group"])
|
|
1034
|
+
groups_units_list = list(set(groups_units_list))
|
|
1035
|
+
unique_units = len(groups_units_list)
|
|
1036
|
+
|
|
1037
|
+
unique_timepoints = data[time_col].nunique()
|
|
1038
|
+
|
|
1023
1039
|
if verbose:
|
|
1024
1040
|
|
|
1025
1041
|
if no_treatments > 1:
|
|
@@ -1030,7 +1046,9 @@ def treatment_diagnostics(
|
|
|
1030
1046
|
return [
|
|
1031
1047
|
treatment_diagnostics_results,
|
|
1032
1048
|
staggered_adoption,
|
|
1033
|
-
untreated
|
|
1049
|
+
untreated,
|
|
1050
|
+
unique_units,
|
|
1051
|
+
unique_timepoints
|
|
1034
1052
|
]
|
|
1035
1053
|
|
|
1036
1054
|
def ols_fit(
|
|
@@ -1547,16 +1565,19 @@ def extract_model_results(
|
|
|
1547
1565
|
covariates_effects = {}
|
|
1548
1566
|
|
|
1549
1567
|
for i, covariate in enumerate(covariates):
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1568
|
+
|
|
1569
|
+
if covariate in coefficients:
|
|
1570
|
+
|
|
1571
|
+
covariates_effects[i] = {
|
|
1572
|
+
config.OLS_MODEL_RESULTS["coef_name"]["model_results_key"]: covariate,
|
|
1573
|
+
config.OLS_MODEL_RESULTS["coef"]["model_results_key"]: coefficients[covariate],
|
|
1574
|
+
"SE": float(coef_standarderrors[covariate]),
|
|
1575
|
+
"t": float(coef_t[covariate]),
|
|
1576
|
+
"p": float(coef_p[covariate]),
|
|
1577
|
+
"CI_lower": float(coef_conf_intervals.loc[covariate, 0]),
|
|
1578
|
+
"CI_upper": float(coef_conf_intervals.loc[covariate, 1]),
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1560
1581
|
model_results["covariates_effects"] = covariates_effects
|
|
1561
1582
|
|
|
1562
1583
|
if (len(TG_x_BG_x_TT_col) > 0) and (any(col in coefficients for col in TG_x_BG_x_TT_col)):
|
|
@@ -4,14 +4,15 @@
|
|
|
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.3.0
|
|
8
|
+
# Last update: 2026-07-03 18:00
|
|
9
9
|
# Copyright (c) 2024-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
import pandas as pd
|
|
14
14
|
import numpy as np
|
|
15
|
+
from scipy.optimize import minimize
|
|
15
16
|
from datetime import datetime, timedelta
|
|
16
17
|
import diffindiff.didanalysis as didanalysis
|
|
17
18
|
import diffindiff.didtools as tools
|
|
@@ -362,8 +363,8 @@ class DiffTreatment:
|
|
|
362
363
|
|
|
363
364
|
Parameters
|
|
364
365
|
----------
|
|
365
|
-
treatment_data_df :
|
|
366
|
-
|
|
366
|
+
treatment_data_df : list
|
|
367
|
+
List with data frames describing treatment timing (TT/ATT).
|
|
367
368
|
treatment_config_dict : dict
|
|
368
369
|
Configuration dictionary for the treatment.
|
|
369
370
|
treatment_meta : dict
|
|
@@ -375,7 +376,6 @@ class DiffTreatment:
|
|
|
375
376
|
-------
|
|
376
377
|
None
|
|
377
378
|
Constructor does not return a value; instance is initialized in-place.
|
|
378
|
-
|
|
379
379
|
"""
|
|
380
380
|
|
|
381
381
|
self.data = [
|
|
@@ -542,7 +542,6 @@ def create_treatment(
|
|
|
542
542
|
):
|
|
543
543
|
|
|
544
544
|
"""
|
|
545
|
-
|
|
546
545
|
Create a DiffTreatment object describing treatment timepoints.
|
|
547
546
|
|
|
548
547
|
Parameters
|
|
@@ -647,7 +646,10 @@ def create_treatment(
|
|
|
647
646
|
study_period_range
|
|
648
647
|
)
|
|
649
648
|
|
|
650
|
-
|
|
649
|
+
treatment_data_df = treatment_period_range.merge(
|
|
650
|
+
TT_data,
|
|
651
|
+
how = "left"
|
|
652
|
+
)
|
|
651
653
|
|
|
652
654
|
else:
|
|
653
655
|
|
|
@@ -680,12 +682,12 @@ def create_treatment(
|
|
|
680
682
|
}
|
|
681
683
|
TT_data = pd.DataFrame(TT_data)
|
|
682
684
|
|
|
683
|
-
|
|
685
|
+
treatment_data_df = study_period_range.merge(
|
|
684
686
|
TT_data,
|
|
685
687
|
how = "left"
|
|
686
688
|
)
|
|
687
689
|
|
|
688
|
-
|
|
690
|
+
treatment_data_df[TT_col] = treatment_data_df[TT_col].fillna(0)
|
|
689
691
|
|
|
690
692
|
if after_treatment_period:
|
|
691
693
|
|
|
@@ -710,17 +712,20 @@ def create_treatment(
|
|
|
710
712
|
}
|
|
711
713
|
ATT_data = pd.DataFrame(ATT_data)
|
|
712
714
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
715
|
+
after_treatment_data_df = study_period_range.merge(ATT_data, how = "left")
|
|
716
|
+
after_treatment_data_df[ATT_col] = after_treatment_data_df[ATT_col].fillna(0)
|
|
717
|
+
after_treatment_data_df = after_treatment_data_df.drop(columns=[config.TIME_COL, config.TIME_COUNTER_COL])
|
|
716
718
|
|
|
717
|
-
|
|
719
|
+
treatment_data_df = pd.concat([treatment_data_df, after_treatment_data_df], axis=1)
|
|
718
720
|
|
|
719
721
|
else:
|
|
720
722
|
after_treatment_period_N = 0
|
|
721
723
|
|
|
722
724
|
no_treatments = 1
|
|
723
725
|
|
|
726
|
+
treatment_data = [0] * no_treatments
|
|
727
|
+
treatment_data[0] = treatment_data_df
|
|
728
|
+
|
|
724
729
|
treatment_meta = {
|
|
725
730
|
"no_treatments": 1,
|
|
726
731
|
"study_period_start": study_period[0],
|
|
@@ -732,19 +737,18 @@ def create_treatment(
|
|
|
732
737
|
}
|
|
733
738
|
|
|
734
739
|
treatment_config = {
|
|
735
|
-
0:
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
"ATT_col": ATT_col,
|
|
746
|
-
}
|
|
740
|
+
0: {
|
|
741
|
+
"treatment_name": treatment_name,
|
|
742
|
+
"treatment_period_start": treatment_period[0],
|
|
743
|
+
"treatment_period_end": treatment_period[1],
|
|
744
|
+
"treatment_period": treatment_period_N,
|
|
745
|
+
"after_treatment_period": after_treatment_period,
|
|
746
|
+
"after_treatment_period_N": after_treatment_period_N,
|
|
747
|
+
"no_treatments": no_treatments,
|
|
748
|
+
"TT_col": TT_col,
|
|
749
|
+
"ATT_col": ATT_col,
|
|
747
750
|
}
|
|
751
|
+
}
|
|
748
752
|
|
|
749
753
|
treatment = DiffTreatment(
|
|
750
754
|
treatment_data,
|
|
@@ -1249,6 +1253,7 @@ class DiffData:
|
|
|
1249
1253
|
did_treatment_old = self.get_did_treatment()
|
|
1250
1254
|
treatment_config_old = did_treatment_old.get_config()
|
|
1251
1255
|
treatment_meta_old = did_treatment_old.get_metadata()
|
|
1256
|
+
treatment_data_old = did_treatment_old.get_data()
|
|
1252
1257
|
if treatment_meta_old["pre_post"]:
|
|
1253
1258
|
raise ValueError ("Adding treatments in a pre-post design is currently not possible.")
|
|
1254
1259
|
no_treatments_old = treatment_meta_old["no_treatments"]
|
|
@@ -1288,7 +1293,8 @@ class DiffData:
|
|
|
1288
1293
|
after_treatment_period = after_treatment_period,
|
|
1289
1294
|
verbose=False
|
|
1290
1295
|
)
|
|
1291
|
-
|
|
1296
|
+
new_treatment_data = new_treatment.get_data()
|
|
1297
|
+
new_treatment_data_df = new_treatment_data[0]
|
|
1292
1298
|
|
|
1293
1299
|
new_treatment_config = new_treatment.get_config()
|
|
1294
1300
|
TT_col = new_treatment_config[0]["TT_col"]
|
|
@@ -1336,7 +1342,8 @@ class DiffData:
|
|
|
1336
1342
|
|
|
1337
1343
|
treatment_cols_new[key_counter] = {
|
|
1338
1344
|
"TT_col": TT_col,
|
|
1339
|
-
"ATT_col": ATT_col,
|
|
1345
|
+
"ATT_col": ATT_col,
|
|
1346
|
+
"TG_col": TG_col,
|
|
1340
1347
|
"treatment_name": treatment_name,
|
|
1341
1348
|
"after_treatment_name": after_treatment_name
|
|
1342
1349
|
}
|
|
@@ -1355,9 +1362,12 @@ class DiffData:
|
|
|
1355
1362
|
treatment_meta_new["no_treatments"] = no_treatments
|
|
1356
1363
|
treatment_config_new = treatment_config_old
|
|
1357
1364
|
treatment_config_new[key_counter] = new_treatment_config[0]
|
|
1365
|
+
|
|
1366
|
+
treatment_data_new = treatment_data_old.copy()
|
|
1367
|
+
treatment_data_new.append(new_treatment_data_df)
|
|
1358
1368
|
|
|
1359
1369
|
treatment_new = DiffTreatment(
|
|
1360
|
-
|
|
1370
|
+
treatment_data_new,
|
|
1361
1371
|
treatment_config_new,
|
|
1362
1372
|
treatment_meta_new,
|
|
1363
1373
|
timestamp = helper.create_timestamp(function="add_treatment")
|
|
@@ -1647,81 +1657,342 @@ class DiffData:
|
|
|
1647
1657
|
if time_col is None or counterfactual_outcome_col is None:
|
|
1648
1658
|
raise ValueError("Parameters 'time_col' and 'counterfactual_outcome_col' must be stated")
|
|
1649
1659
|
|
|
1660
|
+
necessary_cols = [time_col, counterfactual_outcome_col]
|
|
1661
|
+
covariates = self.data[5].copy()
|
|
1662
|
+
if len(covariates) > 0:
|
|
1663
|
+
necessary_cols.extend(covariates)
|
|
1664
|
+
|
|
1650
1665
|
tools.check_columns(
|
|
1651
1666
|
df = additional_df,
|
|
1652
|
-
columns =
|
|
1667
|
+
columns = necessary_cols,
|
|
1668
|
+
verbose = verbose
|
|
1653
1669
|
)
|
|
1670
|
+
|
|
1671
|
+
additional_df = additional_df[necessary_cols].copy()
|
|
1654
1672
|
|
|
1655
|
-
did_modeldata = self.data[0]
|
|
1656
|
-
groups_data = self.data[1].get_data()
|
|
1657
|
-
groups_config = self.data[1].get_config()
|
|
1658
|
-
treatment_group = groups_data.loc[groups_data[config.TG_COL] == 1, config.UNIT_COL].values
|
|
1659
|
-
treatment_config = self.data[2].get_config()
|
|
1660
|
-
outcome_col_original = self.data[3]
|
|
1661
|
-
treatment_data = self.data[2].get_data()
|
|
1673
|
+
did_modeldata = self.data[0].copy()
|
|
1662
1674
|
|
|
1663
|
-
|
|
1675
|
+
treatment_cols = self.get_treatment_cols().copy()
|
|
1676
|
+
TG_col = treatment_cols[0]["TG_col"]
|
|
1677
|
+
TT_col = treatment_cols[0]["TT_col"]
|
|
1678
|
+
treatment_name = treatment_cols[0]["treatment_name"]
|
|
1679
|
+
|
|
1680
|
+
if len(treatment_cols) > 1:
|
|
1681
|
+
print(f"NOTE: Treatment data contains {len(treatment_cols)} treatments. Taking first treatment '{treatment_name}' for identifying treatment group and time.")
|
|
1664
1682
|
|
|
1665
|
-
|
|
1683
|
+
if verbose:
|
|
1684
|
+
print("Collecting treatment information", end = " ... ")
|
|
1685
|
+
|
|
1686
|
+
groups_data = self.data[1].get_data().copy()
|
|
1687
|
+
groups_config = self.data[1].get_config().copy()
|
|
1688
|
+
|
|
1689
|
+
treatment_data = self.data[2].get_data().copy()
|
|
1690
|
+
treatment_data_df = treatment_data[0]
|
|
1691
|
+
treatment_config = self.data[2].get_config().copy()
|
|
1692
|
+
|
|
1693
|
+
treatment_group = (did_modeldata.loc[did_modeldata[TG_col] == 1, config.UNIT_COL].astype(str).unique()).tolist()
|
|
1694
|
+
did_modeldata_TG = did_modeldata[did_modeldata[config.UNIT_COL].astype(str).isin(treatment_group)].copy()
|
|
1695
|
+
|
|
1696
|
+
outcome_col_original = self.data[3]
|
|
1666
1697
|
|
|
1667
|
-
|
|
1698
|
+
if verbose:
|
|
1699
|
+
print("OK")
|
|
1700
|
+
print("Compiling and merging counterfactual data", end = " ... ")
|
|
1701
|
+
|
|
1702
|
+
did_modeldata_counterfac = pd.DataFrame(columns=did_modeldata_TG.columns, index=range(len(treatment_data_df)))
|
|
1703
|
+
|
|
1668
1704
|
did_modeldata_counterfac[config.UNIT_COL] = counterfactual_UID
|
|
1669
|
-
did_modeldata_counterfac[config.
|
|
1670
|
-
|
|
1671
|
-
did_modeldata_counterfac[
|
|
1672
|
-
|
|
1673
|
-
did_modeldata_counterfac[config.
|
|
1674
|
-
|
|
1675
|
-
did_modeldata_counterfac = tools.panel_index(
|
|
1676
|
-
data=did_modeldata_counterfac,
|
|
1677
|
-
unit_col=config.UNIT_COL,
|
|
1678
|
-
time_col=config.TIME_COL,
|
|
1679
|
-
verbose=verbose
|
|
1680
|
-
)
|
|
1705
|
+
did_modeldata_counterfac[config.UNIT_COL] = did_modeldata_counterfac[config.UNIT_COL].astype(str)
|
|
1706
|
+
|
|
1707
|
+
did_modeldata_counterfac[TG_col] = 0
|
|
1708
|
+
|
|
1709
|
+
did_modeldata_counterfac[config.TIME_COL] = treatment_data_df[config.TIME_COL].values
|
|
1710
|
+
did_modeldata_counterfac[config.TIME_COL] = did_modeldata_counterfac[config.TIME_COL].astype(str)
|
|
1681
1711
|
|
|
1682
|
-
|
|
1683
|
-
|
|
1712
|
+
did_modeldata_counterfac[config.TIME_COUNTER_COL] = treatment_data_df[config.TIME_COUNTER_COL].values
|
|
1713
|
+
|
|
1714
|
+
for treatment_col_key, treatment_col_data in treatment_cols.items():
|
|
1715
|
+
did_modeldata_counterfac[treatment_cols[treatment_col_key]["treatment_name"]] = 0
|
|
1716
|
+
if treatment_col_data["TG_col"] in did_modeldata_counterfac.columns:
|
|
1717
|
+
did_modeldata_counterfac[treatment_col_data["TG_col"]] = 0
|
|
1718
|
+
if treatment_col_data["TT_col"] in did_modeldata_counterfac.columns:
|
|
1719
|
+
did_modeldata_counterfac[treatment_col_data["TT_col"]] = treatment_data_df[TT_col].values
|
|
1720
|
+
|
|
1721
|
+
did_modeldata_counterfac[config.UNIT_TIME_COL] = did_modeldata_counterfac[config.UNIT_COL]+config.DELIMITER+did_modeldata_counterfac[config.TIME_COL]
|
|
1722
|
+
|
|
1723
|
+
if treatment_config[0]["after_treatment_period"]:
|
|
1724
|
+
did_modeldata_counterfac["ATT"] = treatment_data_df["ATT"].values
|
|
1684
1725
|
|
|
1685
1726
|
if counterfactual_outcome_col == outcome_col_original:
|
|
1686
1727
|
additional_df = additional_df.rename(columns={counterfactual_outcome_col: counterfactual_outcome_col+"_cf"})
|
|
1687
1728
|
counterfactual_outcome_col = counterfactual_outcome_col+"_cf"
|
|
1688
1729
|
|
|
1689
|
-
did_modeldata_counterfac =
|
|
1690
|
-
|
|
1691
|
-
additional_df
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
)
|
|
1696
|
-
|
|
1697
|
-
did_modeldata_counterfac[outcome_col_original] = did_modeldata_counterfac[counterfactual_outcome_col]
|
|
1698
|
-
did_modeldata_counterfac = did_modeldata_counterfac.drop(counterfactual_outcome_col, axis = 1)
|
|
1730
|
+
did_modeldata_counterfac[outcome_col_original] = additional_df[counterfactual_outcome_col]
|
|
1731
|
+
for covariate in covariates:
|
|
1732
|
+
did_modeldata_counterfac[covariate] = additional_df[covariate]
|
|
1733
|
+
|
|
1734
|
+
if time_col in did_modeldata_counterfac.columns and time_col != config.TIME_COL:
|
|
1735
|
+
did_modeldata_counterfac = did_modeldata_counterfac.drop(time_col, axis = 1)
|
|
1699
1736
|
|
|
1700
1737
|
did_modeldata_TG_with_counterfac = pd.concat(
|
|
1701
|
-
[
|
|
1738
|
+
[
|
|
1739
|
+
did_modeldata_TG,
|
|
1740
|
+
did_modeldata_counterfac
|
|
1741
|
+
],
|
|
1702
1742
|
ignore_index=True
|
|
1703
1743
|
)
|
|
1744
|
+
|
|
1745
|
+
groups_config_new = groups_config.copy()
|
|
1746
|
+
for groups_key in groups_config.keys():
|
|
1704
1747
|
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
}
|
|
1712
|
-
groups_data = pd.concat([groups_data, pd.DataFrame([groups_data_cf])], ignore_index=True)
|
|
1748
|
+
groups_config_new[groups_key]["own_counterfactual"] = True
|
|
1749
|
+
groups_config_new[groups_key]["treatment_group"] = len(treatment_group)
|
|
1750
|
+
groups_config_new[groups_key]["control_group"] = 1
|
|
1751
|
+
groups_config_new[groups_key]["full_sample"] = len(treatment_group)+1
|
|
1752
|
+
|
|
1753
|
+
groups_data_new = groups_data.copy()
|
|
1713
1754
|
|
|
1755
|
+
for groups_key, entry in enumerate(groups_data_new):
|
|
1756
|
+
|
|
1757
|
+
groups_data_new[groups_key] = groups_data[groups_key][groups_data[groups_key].iloc[:, 1] == 1].copy()
|
|
1758
|
+
groups_data_new[groups_key].loc[len(groups_data_new[groups_key])] = [counterfactual_UID, 0]
|
|
1759
|
+
|
|
1714
1760
|
groups = DiffGroups(
|
|
1715
|
-
groups_data,
|
|
1716
|
-
|
|
1761
|
+
groups_data = groups_data_new,
|
|
1762
|
+
groups_config_dict = groups_config_new,
|
|
1717
1763
|
timestamp = helper.create_timestamp(function="add_own_counterfactual")
|
|
1718
1764
|
)
|
|
1719
1765
|
|
|
1720
1766
|
self.data[0] = did_modeldata_TG_with_counterfac
|
|
1721
1767
|
self.data[1] = groups
|
|
1768
|
+
|
|
1769
|
+
if verbose:
|
|
1770
|
+
print("OK")
|
|
1722
1771
|
|
|
1772
|
+
print("NOTE: DiffData object was changed with counterfactual.")
|
|
1773
|
+
|
|
1723
1774
|
return self
|
|
1724
1775
|
|
|
1776
|
+
def add_synthetic(
|
|
1777
|
+
self,
|
|
1778
|
+
process_unit: str = None,
|
|
1779
|
+
SCU_UID: str = "synthetic_control",
|
|
1780
|
+
log_outcome: bool = False,
|
|
1781
|
+
log_outcome_add = 0.01,
|
|
1782
|
+
verbose: bool = False
|
|
1783
|
+
):
|
|
1784
|
+
|
|
1785
|
+
"""
|
|
1786
|
+
Create a synthetic control unit (SCU) and add it as a counterfactual.
|
|
1787
|
+
|
|
1788
|
+
Parameters
|
|
1789
|
+
----------
|
|
1790
|
+
process_unit : str, optional
|
|
1791
|
+
Identifier of the treatment unit for which the synthetic control unit must be created.
|
|
1792
|
+
SCU_UID : str, optional
|
|
1793
|
+
Identifier assigned to the synthetic control unit in the counterfactual data.
|
|
1794
|
+
log_outcome : bool, optional
|
|
1795
|
+
If True, transform the outcome by natural logarithm before fitting the synthetic control weights.
|
|
1796
|
+
log_outcome_add : float, optional
|
|
1797
|
+
Constant added before applying the natural logarithm when log_outcome is True.
|
|
1798
|
+
verbose : bool, optional
|
|
1799
|
+
If True, print progress messages.
|
|
1800
|
+
|
|
1801
|
+
Returns
|
|
1802
|
+
-------
|
|
1803
|
+
DiffData
|
|
1804
|
+
The updated DiffData object including the synthetic control unit as counterfactual.
|
|
1805
|
+
|
|
1806
|
+
Raises
|
|
1807
|
+
------
|
|
1808
|
+
ValueError
|
|
1809
|
+
If the treatment group contains multiple analysis units and no process unit is specified, if the specified process unit is not part of the model data, or if the control group contains fewer than two units.
|
|
1810
|
+
|
|
1811
|
+
Examples
|
|
1812
|
+
--------
|
|
1813
|
+
>>> curfew_data_prepost = create_data(
|
|
1814
|
+
... outcome_data=curfew_DE,
|
|
1815
|
+
... unit_id_col="county",
|
|
1816
|
+
... time_col="infection_date",
|
|
1817
|
+
... outcome_col="infections_cum_per100000",
|
|
1818
|
+
... treatment_group=curfew_DE.loc[curfew_DE["Bundesland"].isin([9,10,14])]["county"],
|
|
1819
|
+
... control_group=curfew_DE.loc[~curfew_DE["Bundesland"].isin([9,10,14])]["county"],
|
|
1820
|
+
... study_period=["2020-03-01", "2020-05-15"],
|
|
1821
|
+
... treatment_period=["2020-03-21", "2020-05-05"],
|
|
1822
|
+
... freq="D",
|
|
1823
|
+
... pre_post=True,
|
|
1824
|
+
... after_treatment_period=True
|
|
1825
|
+
... )
|
|
1826
|
+
>>> curfew_data_prepost.add_synthetic(process_unit="Aachen")
|
|
1827
|
+
"""
|
|
1828
|
+
|
|
1829
|
+
did_modeldata = self.data[0].copy()
|
|
1830
|
+
|
|
1831
|
+
covariates = self.data[5].copy()
|
|
1832
|
+
if len(covariates) > 0:
|
|
1833
|
+
tools.check_columns(
|
|
1834
|
+
df = did_modeldata,
|
|
1835
|
+
columns = covariates,
|
|
1836
|
+
verbose = verbose
|
|
1837
|
+
)
|
|
1838
|
+
|
|
1839
|
+
treatment_cols = self.get_treatment_cols().copy()
|
|
1840
|
+
treatment_name = treatment_cols[0]["treatment_name"]
|
|
1841
|
+
TG_col = treatment_cols[0]["TG_col"]
|
|
1842
|
+
|
|
1843
|
+
if len(treatment_cols) > 1:
|
|
1844
|
+
print(f"NOTE: Treatment data contains {len(treatment_cols)} treatments. Taking first treatment '{treatment_name}' for identifying treatment group and time.")
|
|
1845
|
+
|
|
1846
|
+
if verbose:
|
|
1847
|
+
print("Collecting treatment information", end = " ... ")
|
|
1848
|
+
|
|
1849
|
+
groups_config = self.data[1].get_config().copy()
|
|
1850
|
+
treatment_group_size = groups_config[0]["treatment_group"]
|
|
1851
|
+
control_group_size = groups_config[0]["control_group"]
|
|
1852
|
+
|
|
1853
|
+
if verbose:
|
|
1854
|
+
print("OK")
|
|
1855
|
+
|
|
1856
|
+
if treatment_group_size > 1 and process_unit is None:
|
|
1857
|
+
raise ValueError(f"Treatment group contains {treatment_group_size} analysis units and process_unit is set to None. Specify for which unit the SCU must be created.")
|
|
1858
|
+
if control_group_size < 2:
|
|
1859
|
+
raise ValueError(f"Control group contains only {control_group_size} units, which does not allow for creating a SCU.")
|
|
1860
|
+
|
|
1861
|
+
if verbose:
|
|
1862
|
+
print("Extracting control group data", end = " ... ")
|
|
1863
|
+
|
|
1864
|
+
units = list(did_modeldata[config.UNIT_COL].unique())
|
|
1865
|
+
|
|
1866
|
+
process_unit = did_modeldata.loc[did_modeldata[TG_col] == 1, config.UNIT_COL][0]
|
|
1867
|
+
|
|
1868
|
+
if treatment_group_size > 1 and process_unit is not None:
|
|
1869
|
+
|
|
1870
|
+
if process_unit not in units:
|
|
1871
|
+
raise ValueError(f"Diff-in-diff model data does not contain specified treatment unit '{process_unit}'.")
|
|
1872
|
+
|
|
1873
|
+
units.remove(process_unit)
|
|
1874
|
+
units_size = len(units)
|
|
1875
|
+
|
|
1876
|
+
if verbose:
|
|
1877
|
+
print("OK")
|
|
1878
|
+
|
|
1879
|
+
outcome_col_original = self.data[3]
|
|
1880
|
+
outcome_col = outcome_col_original
|
|
1881
|
+
|
|
1882
|
+
if log_outcome:
|
|
1883
|
+
|
|
1884
|
+
print(f"Compiling outcome data for natural logarithm of outcome '{outcome_col}'", end = " ... ")
|
|
1885
|
+
|
|
1886
|
+
outcome_col = f"{config.LOG_PREFIX}{config.DELIMITER}{outcome_col_original}"
|
|
1887
|
+
did_modeldata[outcome_col] = np.log(did_modeldata[outcome_col_original]+log_outcome_add)
|
|
1888
|
+
|
|
1889
|
+
else:
|
|
1890
|
+
if verbose:
|
|
1891
|
+
print(f"Compiling outcome data for outcome '{outcome_col}'", end = " ... ")
|
|
1892
|
+
|
|
1893
|
+
process_unit_time = pd.Series(did_modeldata.loc[did_modeldata[config.UNIT_COL] == process_unit, config.TIME_COL])
|
|
1894
|
+
process_unit_outcome = pd.Series(did_modeldata.loc[did_modeldata[config.UNIT_COL] == process_unit, outcome_col])
|
|
1895
|
+
|
|
1896
|
+
outcome_matrix = pd.DataFrame(
|
|
1897
|
+
{
|
|
1898
|
+
config.TIME_COL: process_unit_time.reset_index(drop=True),
|
|
1899
|
+
tools.clean_column_name(process_unit): process_unit_outcome.reset_index(drop=True)
|
|
1900
|
+
}
|
|
1901
|
+
)
|
|
1902
|
+
|
|
1903
|
+
units_not_included = []
|
|
1904
|
+
|
|
1905
|
+
for unit in units:
|
|
1906
|
+
|
|
1907
|
+
did_modeldata_unit = did_modeldata.loc[
|
|
1908
|
+
(did_modeldata[config.UNIT_COL].astype(str) == str(unit))
|
|
1909
|
+
& (did_modeldata[outcome_col].notna()),
|
|
1910
|
+
outcome_col
|
|
1911
|
+
]
|
|
1912
|
+
|
|
1913
|
+
if len(did_modeldata_unit) != len(outcome_matrix):
|
|
1914
|
+
units_not_included.append(unit)
|
|
1915
|
+
units.remove(unit)
|
|
1916
|
+
else:
|
|
1917
|
+
outcome_matrix[tools.clean_column_name(unit)] = did_modeldata_unit.reset_index(drop=True)
|
|
1918
|
+
|
|
1919
|
+
if len(units_not_included) > 0:
|
|
1920
|
+
print(f"WARNING: {len(units_not_included)} analysis units were not included due to NaN data.")
|
|
1921
|
+
|
|
1922
|
+
if verbose:
|
|
1923
|
+
print("OK")
|
|
1924
|
+
print(f"Calculating weights based on {len(units)} analysis units", end = " ... ")
|
|
1925
|
+
|
|
1926
|
+
outcome_matrix_Y = outcome_matrix.iloc[:, :2]
|
|
1927
|
+
outcome_matrix_X = outcome_matrix.iloc[:, 2:]
|
|
1928
|
+
|
|
1929
|
+
y = outcome_matrix_Y[tools.clean_column_name(process_unit)]
|
|
1930
|
+
X = outcome_matrix_X.values
|
|
1931
|
+
|
|
1932
|
+
def objective(w):
|
|
1933
|
+
return np.sum((y - X @ w)**2)
|
|
1934
|
+
|
|
1935
|
+
n = X.shape[1]
|
|
1936
|
+
w0 = np.ones(n) / n
|
|
1937
|
+
constraints = {"type": "eq", "fun": lambda w: np.sum(w) - 1}
|
|
1938
|
+
bounds = [(0, 1)] * n
|
|
1939
|
+
|
|
1940
|
+
result = minimize(
|
|
1941
|
+
objective,
|
|
1942
|
+
w0,
|
|
1943
|
+
bounds=bounds,
|
|
1944
|
+
constraints=constraints
|
|
1945
|
+
)
|
|
1946
|
+
|
|
1947
|
+
weights = result.x
|
|
1948
|
+
weights_pd = pd.Series(weights, index=units)
|
|
1949
|
+
|
|
1950
|
+
synthetic_pre = X @ weights
|
|
1951
|
+
|
|
1952
|
+
synthetic_fit_metrics = tools.fit_metrics(
|
|
1953
|
+
observed = y,
|
|
1954
|
+
expected = synthetic_pre
|
|
1955
|
+
)
|
|
1956
|
+
|
|
1957
|
+
if verbose:
|
|
1958
|
+
print("OK")
|
|
1959
|
+
print(f"Creating synthetic control unit", end = " ... ")
|
|
1960
|
+
|
|
1961
|
+
control_units_data = did_modeldata[did_modeldata[config.UNIT_COL].isin(weights_pd.index)].copy()
|
|
1962
|
+
control_units_data["weights"] = control_units_data[config.UNIT_COL].map(weights_pd)
|
|
1963
|
+
control_units_data[outcome_col] = control_units_data[outcome_col].astype(float)
|
|
1964
|
+
control_units_data.loc[:, outcome_col] = (control_units_data[outcome_col] * control_units_data["weights"])
|
|
1965
|
+
|
|
1966
|
+
variables_dict = {
|
|
1967
|
+
outcome_col: "sum"
|
|
1968
|
+
}
|
|
1969
|
+
for covariate in covariates:
|
|
1970
|
+
control_units_data[covariate] = control_units_data[covariate].astype(float)
|
|
1971
|
+
control_units_data.loc[:, covariate] = control_units_data[covariate]*control_units_data["weights"]
|
|
1972
|
+
variables_dict[covariate] = "sum"
|
|
1973
|
+
|
|
1974
|
+
synthetic = (
|
|
1975
|
+
control_units_data
|
|
1976
|
+
.groupby(config.TIME_COL, as_index=False)
|
|
1977
|
+
.agg(variables_dict)
|
|
1978
|
+
)
|
|
1979
|
+
|
|
1980
|
+
if log_outcome:
|
|
1981
|
+
synthetic[outcome_col_original] = np.exp(synthetic[outcome_col])
|
|
1982
|
+
|
|
1983
|
+
if verbose:
|
|
1984
|
+
print("OK")
|
|
1985
|
+
|
|
1986
|
+
self.add_own_counterfactual(
|
|
1987
|
+
additional_df=synthetic,
|
|
1988
|
+
counterfactual_outcome_col=outcome_col_original,
|
|
1989
|
+
counterfactual_UID=SCU_UID,
|
|
1990
|
+
time_col=config.TIME_COL,
|
|
1991
|
+
verbose = False
|
|
1992
|
+
)
|
|
1993
|
+
|
|
1994
|
+
return self
|
|
1995
|
+
|
|
1725
1996
|
def summary(self):
|
|
1726
1997
|
|
|
1727
1998
|
"""
|
|
@@ -2076,7 +2347,8 @@ def merge_data(
|
|
|
2076
2347
|
groups_config = diff_groups.get_config()
|
|
2077
2348
|
groups_config = groups_config[0]
|
|
2078
2349
|
|
|
2079
|
-
|
|
2350
|
+
treatment_data = diff_treatment.get_data()
|
|
2351
|
+
treatment_data_df = treatment_data[0]
|
|
2080
2352
|
treatment_config = diff_treatment.get_config()
|
|
2081
2353
|
treatment_config = treatment_config[0]
|
|
2082
2354
|
|
|
@@ -74,7 +74,8 @@ curfew_data_prepost=create_data(
|
|
|
74
74
|
study_period=["2020-03-01", "2020-05-15"],
|
|
75
75
|
treatment_period=["2020-03-21", "2020-05-05"],
|
|
76
76
|
freq="D",
|
|
77
|
-
pre_post=True
|
|
77
|
+
pre_post=True,
|
|
78
|
+
after_treatment_period = True
|
|
78
79
|
)
|
|
79
80
|
# Creating DiD treatement dataset by defining groups and
|
|
80
81
|
# treatment time at once
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: diffindiff
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
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 research note featuring a case study that utilizes the diffindiff library is a
|
|
|
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.
|
|
32
|
+
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.0) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
## Installation
|
|
@@ -177,7 +177,16 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
177
177
|
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
178
|
|
|
179
179
|
|
|
180
|
-
## What's new (v2.
|
|
180
|
+
## What's new (v2.5.0)
|
|
181
181
|
|
|
182
|
-
-
|
|
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
|
+
- Extensions
|
|
186
|
+
- Creating synthetic control units with DiffData.add_synthetic() and conducting a Synthetic DiD analysis
|
|
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
|
|
189
|
+
- Bugfixes
|
|
190
|
+
- DiffData.add_own_counterfactual() now works correctly in any case (also when no. of treatments > 1)
|
|
191
|
+
- Correct internal processing of treatment data in DiffTreatment and DiffData objects when treatments were added
|
|
192
|
+
- Fixed missing treatment group definition in DiffData.add_treatment()
|
|
@@ -7,7 +7,7 @@ def read_README():
|
|
|
7
7
|
|
|
8
8
|
setup(
|
|
9
9
|
name='diffindiff',
|
|
10
|
-
version='2.
|
|
10
|
+
version='2.5.0',
|
|
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,15 +20,20 @@ setup(
|
|
|
20
20
|
'diffindiff': ['tests/data/*'],
|
|
21
21
|
},
|
|
22
22
|
install_requires=[
|
|
23
|
-
'
|
|
24
|
-
'pandas
|
|
25
|
-
'
|
|
26
|
-
'
|
|
23
|
+
'geopandas',
|
|
24
|
+
'pandas',
|
|
25
|
+
'numpy',
|
|
26
|
+
'statsmodels>=0.14.5',
|
|
27
|
+
'scipy>=1.17',
|
|
28
|
+
'scikit-learn',
|
|
29
|
+
'xgboost',
|
|
30
|
+
'lightgbm',
|
|
31
|
+
'shapely',
|
|
32
|
+
'requests<3.0',
|
|
33
|
+
'contextily',
|
|
34
|
+
'openpyxl',
|
|
27
35
|
'matplotlib',
|
|
28
|
-
'
|
|
29
|
-
'xgboost>=1.7,<2.1',
|
|
30
|
-
'lightgbm>=4.0,<4.5',
|
|
31
|
-
'patsy>=0.5.6',
|
|
36
|
+
'patsy',
|
|
32
37
|
],
|
|
33
38
|
test_suite='tests',
|
|
34
39
|
)
|
|
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
|