diffindiff 2.4.2__tar.gz → 2.5.1__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.2 → diffindiff-2.5.1}/PKG-INFO +7 -5
- {diffindiff-2.4.2 → diffindiff-2.5.1}/README.md +6 -4
- {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/config.py +15 -4
- {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/didanalysis.py +158 -27
- {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/didanalysis_helper.py +23 -5
- {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/diddata.py +369 -88
- {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/tests/tests_diffindiff.py +60 -3
- {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff.egg-info/PKG-INFO +7 -5
- diffindiff-2.5.1/diffindiff.egg-info/requires.txt +14 -0
- {diffindiff-2.4.2 → diffindiff-2.5.1}/setup.py +14 -9
- diffindiff-2.4.2/diffindiff.egg-info/requires.txt +0 -9
- {diffindiff-2.4.2 → diffindiff-2.5.1}/MANIFEST.in +0 -0
- {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/__init__.py +0 -0
- {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/didtools.py +0 -0
- {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/tests/__init__.py +0 -0
- {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/tests/data/Corona_Hesse.xlsx +0 -0
- {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/tests/data/counties_DE.csv +0 -0
- {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/tests/data/curfew_DE.csv +0 -0
- {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff.egg-info/SOURCES.txt +0 -0
- {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff.egg-info/dependency_links.txt +0 -0
- {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff.egg-info/top_level.txt +0 -0
- {diffindiff-2.4.2 → diffindiff-2.5.1}/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.1
|
|
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.1) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
## Installation
|
|
@@ -177,7 +177,9 @@ 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.1)
|
|
181
181
|
|
|
182
|
-
- Bugfixes
|
|
183
|
-
- DiffData.
|
|
182
|
+
- Bugfixes
|
|
183
|
+
- In DiffData.create_treatment() and DiffData.create_data(), pre-post designs with after-treatment period (follow-up) now work correctly
|
|
184
|
+
- DiffModel.plot() nows supports pre-post designs with after-treatment period
|
|
185
|
+
- Some small stabilizations
|
|
@@ -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.1) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
## Installation
|
|
@@ -169,7 +169,9 @@ 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.1)
|
|
173
173
|
|
|
174
|
-
- Bugfixes
|
|
175
|
-
- DiffData.
|
|
174
|
+
- Bugfixes
|
|
175
|
+
- In DiffData.create_treatment() and DiffData.create_data(), pre-post designs with after-treatment period (follow-up) now work correctly
|
|
176
|
+
- DiffModel.plot() nows supports pre-post designs with after-treatment period
|
|
177
|
+
- Some small stabilizations
|
|
@@ -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.22
|
|
8
|
+
# Last update: 2026-07-10 11:36
|
|
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.1"
|
|
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:
|
|
@@ -47,6 +52,7 @@ TREATMENT_PERIOD_DESCRIPTION = f"{TREATMENT_DESCRIPTION} period"
|
|
|
47
52
|
STUDY_PERIOD_DESCRIPTION = "Study period"
|
|
48
53
|
PREPOST_DESCRIPTION = "Pre-post"
|
|
49
54
|
AFTER_TREATMENT_PERIOD_DESCRIPTION = "After-treatment period"
|
|
55
|
+
FOLLOW_UP_DESCRIPTION = "Follow-up"
|
|
50
56
|
|
|
51
57
|
UNITS_DESCRIPTION = "Units"
|
|
52
58
|
|
|
@@ -96,6 +102,7 @@ LOG_PREFIX = "log"
|
|
|
96
102
|
OBSERVED_SUFFIX = "observed"
|
|
97
103
|
EXPECTED_SUFFIX = "expected"
|
|
98
104
|
DEMEAN_SUFFIX = "demean"
|
|
105
|
+
WEIGHTED_SUFFIX = "_weighted"
|
|
99
106
|
PREDICTED_SUFFIX = "pred"
|
|
100
107
|
CI_LOWER_SUFFIX = "CI_lower"
|
|
101
108
|
CI_UPPER_SUFFIX = "CI_upper"
|
|
@@ -500,7 +507,11 @@ DATA_DIAGNOSTICS = {
|
|
|
500
507
|
"description": N_DESCRIPTION,
|
|
501
508
|
"show_in_summary": True
|
|
502
509
|
},
|
|
503
|
-
|
|
510
|
+
"cols_constants": {
|
|
511
|
+
"description": "Columns with constant values",
|
|
512
|
+
"show_in_summary": False
|
|
513
|
+
},
|
|
514
|
+
}
|
|
504
515
|
|
|
505
516
|
DIAGNOSTICS_COLUMN = "Result"
|
|
506
517
|
|
|
@@ -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.2
|
|
8
|
+
# Last update: 2026-07-10 11:30
|
|
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
|
|
|
@@ -894,6 +898,49 @@ class DiffModel:
|
|
|
894
898
|
|
|
895
899
|
treatment_effects = self.treatment_effects()
|
|
896
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
|
+
|
|
897
944
|
if sort_by_coef:
|
|
898
945
|
treatment_effects = treatment_effects.sort_values(
|
|
899
946
|
by = treatment_effects.columns[1],
|
|
@@ -1803,14 +1850,14 @@ class DiffModel:
|
|
|
1803
1850
|
model_data_TG_CG["t"] = pd.to_datetime(model_data_TG_CG["t"])
|
|
1804
1851
|
|
|
1805
1852
|
if not model_config["pre_post"]:
|
|
1806
|
-
pre_post_barplot = False
|
|
1853
|
+
pre_post_barplot = False
|
|
1807
1854
|
|
|
1808
1855
|
if pre_post_barplot:
|
|
1809
1856
|
|
|
1810
1857
|
x_pos_t1_TG = 0
|
|
1811
1858
|
x_pos_t1_CG = x_pos_t1_TG + pre_post_bar_width
|
|
1812
1859
|
x_pos_t2_TG = 1.5
|
|
1813
|
-
x_pos_t2_CG = x_pos_t2_TG + pre_post_bar_width
|
|
1860
|
+
x_pos_t2_CG = x_pos_t2_TG + pre_post_bar_width
|
|
1814
1861
|
|
|
1815
1862
|
plt.bar(
|
|
1816
1863
|
x = x_pos_t1_TG,
|
|
@@ -1838,6 +1885,24 @@ class DiffModel:
|
|
|
1838
1885
|
color=lines_col[3],
|
|
1839
1886
|
width = pre_post_bar_width
|
|
1840
1887
|
)
|
|
1888
|
+
|
|
1889
|
+
if len(model_data_TG_CG) == 3:
|
|
1890
|
+
|
|
1891
|
+
x_pos_t3_TG = 3
|
|
1892
|
+
x_pos_t3_CG = x_pos_t3_TG + pre_post_bar_width
|
|
1893
|
+
|
|
1894
|
+
plt.bar(
|
|
1895
|
+
x = x_pos_t3_TG,
|
|
1896
|
+
height = model_data_TG_CG[outcome_col_expected_TG][2],
|
|
1897
|
+
color = lines_col[2],
|
|
1898
|
+
width = pre_post_bar_width
|
|
1899
|
+
)
|
|
1900
|
+
plt.bar(
|
|
1901
|
+
x = x_pos_t3_CG,
|
|
1902
|
+
height = model_data_TG_CG[outcome_col_expected_CG][2],
|
|
1903
|
+
color=lines_col[3],
|
|
1904
|
+
width = pre_post_bar_width
|
|
1905
|
+
)
|
|
1841
1906
|
|
|
1842
1907
|
plt.xlabel(x_label)
|
|
1843
1908
|
plt.ylabel(y_label)
|
|
@@ -1931,16 +1996,29 @@ class DiffModel:
|
|
|
1931
1996
|
ax.xaxis.set_major_formatter(DateFormatter(model_config["date_format"]))
|
|
1932
1997
|
|
|
1933
1998
|
if model_config["pre_post"]:
|
|
1999
|
+
|
|
2000
|
+
ticks_line = model_data_TG_CG["t"].unique()
|
|
2001
|
+
ticks_bars = [0.25, 1.75]
|
|
2002
|
+
|
|
2003
|
+
if len(model_data_TG_CG) == 3:
|
|
2004
|
+
|
|
2005
|
+
if len(pre_post_ticks) < 3:
|
|
2006
|
+
pre_post_ticks.append("Follow-up")
|
|
2007
|
+
|
|
2008
|
+
ticks_bars.append(3.25)
|
|
2009
|
+
|
|
1934
2010
|
if not pre_post_barplot:
|
|
2011
|
+
|
|
1935
2012
|
plt.xticks(
|
|
1936
|
-
|
|
1937
|
-
labels =
|
|
2013
|
+
ticks_line,
|
|
2014
|
+
labels = pre_post_ticks
|
|
1938
2015
|
)
|
|
1939
2016
|
else:
|
|
1940
2017
|
plt.xticks(
|
|
1941
|
-
|
|
1942
|
-
labels =
|
|
1943
|
-
)
|
|
2018
|
+
ticks_bars,
|
|
2019
|
+
labels = pre_post_ticks
|
|
2020
|
+
)
|
|
2021
|
+
|
|
1944
2022
|
else:
|
|
1945
2023
|
plt.xticks(rotation=90)
|
|
1946
2024
|
|
|
@@ -2389,16 +2467,37 @@ def did_analysis(
|
|
|
2389
2467
|
verbose=verbose
|
|
2390
2468
|
)
|
|
2391
2469
|
treatment_diagnostics = treatment_diagnostics_results[0]
|
|
2392
|
-
staggered_adoption = treatment_diagnostics_results[1]
|
|
2470
|
+
staggered_adoption = treatment_diagnostics_results[1]
|
|
2471
|
+
unique_units = treatment_diagnostics_results[3]
|
|
2472
|
+
unique_time_points = treatment_diagnostics_results[4]
|
|
2393
2473
|
|
|
2394
2474
|
if no_treatments > 1:
|
|
2395
2475
|
|
|
2396
2476
|
intercept = False
|
|
2397
2477
|
TG_col = []
|
|
2398
2478
|
|
|
2399
|
-
if not FE_unit:
|
|
2479
|
+
if not FE_unit and config.AUTO_SWITCH_TO_FIXED_EFFECTS:
|
|
2400
2480
|
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.")
|
|
2481
|
+
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.")
|
|
2482
|
+
|
|
2483
|
+
if unique_units < config.FIXED_EFFECTS_THRESHOLD:
|
|
2484
|
+
|
|
2485
|
+
FE_unit = False
|
|
2486
|
+
FE_group = False
|
|
2487
|
+
ITE = False
|
|
2488
|
+
ITT = False
|
|
2489
|
+
intercept = True
|
|
2490
|
+
|
|
2491
|
+
print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} units. Unit or group fixed effects are skipped.")
|
|
2492
|
+
|
|
2493
|
+
if unique_time_points < config.FIXED_EFFECTS_THRESHOLD:
|
|
2494
|
+
|
|
2495
|
+
FE_time = False
|
|
2496
|
+
ITT = False
|
|
2497
|
+
GTT = False
|
|
2498
|
+
intercept = True
|
|
2499
|
+
|
|
2500
|
+
print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} time points. Time fixed effects are skipped.")
|
|
2402
2501
|
|
|
2403
2502
|
if demean and FE_unit:
|
|
2404
2503
|
|
|
@@ -2435,7 +2534,7 @@ def did_analysis(
|
|
|
2435
2534
|
GTT = False
|
|
2436
2535
|
print("NOTE: Both group and individual time trends were stated. Switching to individual time trends only.")
|
|
2437
2536
|
|
|
2438
|
-
if staggered_adoption:
|
|
2537
|
+
if staggered_adoption and config.AUTO_SWITCH_TO_FIXED_EFFECTS:
|
|
2439
2538
|
|
|
2440
2539
|
if not FE_unit or not FE_time:
|
|
2441
2540
|
print("NOTE: Quasi-experiment includes one or more staggered treatments. Two-way fixed effects model is used.")
|
|
@@ -2562,7 +2661,15 @@ def did_analysis(
|
|
|
2562
2661
|
missing_replace_by_zero = missing_replace_by_zero,
|
|
2563
2662
|
verbose = verbose
|
|
2564
2663
|
)
|
|
2664
|
+
|
|
2665
|
+
if len(data_diagnostics["cols_constants"]) > 0:
|
|
2565
2666
|
|
|
2667
|
+
if config.AUTO_SKIP_CONSTANT_COLUMNS:
|
|
2668
|
+
|
|
2669
|
+
print(f"NOTE: The following columns are constant and dropped from the analysis: {', '.join(data_diagnostics['cols_constants'])}.")
|
|
2670
|
+
|
|
2671
|
+
covariates = [col for col in covariates if col not in data_diagnostics["cols_constants"]]
|
|
2672
|
+
|
|
2566
2673
|
if data_diagnostics["is_prepost"] and config.AUTO_SWITCH_TO_PREPOST:
|
|
2567
2674
|
|
|
2568
2675
|
print(f"NOTE: Input is {config.PREPOST_PANELDATA_DESCRIPTION}. Data processing and model estimation will treat data as pre-post.")
|
|
@@ -2611,6 +2718,8 @@ def did_analysis(
|
|
|
2611
2718
|
FE_time_vars = []
|
|
2612
2719
|
dummy_time_original = []
|
|
2613
2720
|
|
|
2721
|
+
drop_first = intercept
|
|
2722
|
+
|
|
2614
2723
|
if not demean:
|
|
2615
2724
|
|
|
2616
2725
|
if FE_unit:
|
|
@@ -2619,7 +2728,7 @@ def did_analysis(
|
|
|
2619
2728
|
data = data,
|
|
2620
2729
|
col = unit_col,
|
|
2621
2730
|
type = "unit",
|
|
2622
|
-
drop_first =
|
|
2731
|
+
drop_first = drop_first,
|
|
2623
2732
|
verbose = verbose
|
|
2624
2733
|
)
|
|
2625
2734
|
|
|
@@ -2634,7 +2743,7 @@ def did_analysis(
|
|
|
2634
2743
|
data = data,
|
|
2635
2744
|
col = time_col,
|
|
2636
2745
|
type = "time",
|
|
2637
|
-
drop_first =
|
|
2746
|
+
drop_first = drop_first,
|
|
2638
2747
|
verbose = verbose
|
|
2639
2748
|
)
|
|
2640
2749
|
|
|
@@ -2658,7 +2767,7 @@ def did_analysis(
|
|
|
2658
2767
|
data = data,
|
|
2659
2768
|
col = group_by,
|
|
2660
2769
|
type = "group",
|
|
2661
|
-
drop_first =
|
|
2770
|
+
drop_first = drop_first,
|
|
2662
2771
|
verbose = verbose
|
|
2663
2772
|
)
|
|
2664
2773
|
|
|
@@ -3115,7 +3224,21 @@ def ddd_analysis(
|
|
|
3115
3224
|
verbose=verbose
|
|
3116
3225
|
)
|
|
3117
3226
|
treatment_diagnostics = treatment_diagnostics_results[0]
|
|
3118
|
-
|
|
3227
|
+
unique_units = treatment_diagnostics_results[3]
|
|
3228
|
+
unique_time_points = treatment_diagnostics_results[4]
|
|
3229
|
+
|
|
3230
|
+
if unique_units < config.FIXED_EFFECTS_THRESHOLD:
|
|
3231
|
+
|
|
3232
|
+
FE_unit = False
|
|
3233
|
+
|
|
3234
|
+
print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} units. Unit fixed effects are skipped.")
|
|
3235
|
+
|
|
3236
|
+
if unique_time_points < config.FIXED_EFFECTS_THRESHOLD:
|
|
3237
|
+
|
|
3238
|
+
FE_time = False
|
|
3239
|
+
|
|
3240
|
+
print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} time points. Time fixed effects are skipped.")
|
|
3241
|
+
|
|
3119
3242
|
if FE_unit:
|
|
3120
3243
|
TG_col = None
|
|
3121
3244
|
if FE_time:
|
|
@@ -3140,7 +3263,15 @@ def ddd_analysis(
|
|
|
3140
3263
|
missing_replace_by_zero = missing_replace_by_zero,
|
|
3141
3264
|
verbose = verbose
|
|
3142
3265
|
)
|
|
3143
|
-
|
|
3266
|
+
|
|
3267
|
+
if len(data_diagnostics["cols_constants"]) > 0:
|
|
3268
|
+
|
|
3269
|
+
if config.AUTO_SKIP_CONSTANT_COLUMNS:
|
|
3270
|
+
|
|
3271
|
+
print(f"NOTE: The following columns are constant and dropped from the analysis: {', '.join(data_diagnostics['cols_constants'])}.")
|
|
3272
|
+
|
|
3273
|
+
covariates = [col for col in covariates if col not in data_diagnostics["cols_constants"]]
|
|
3274
|
+
|
|
3144
3275
|
if data_diagnostics["is_prepost"] and config.AUTO_SWITCH_TO_PREPOST:
|
|
3145
3276
|
|
|
3146
3277
|
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(
|