diffindiff 2.3.5__tar.gz → 2.3.7__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.3.5 → diffindiff-2.3.7}/PKG-INFO +12 -7
- {diffindiff-2.3.5 → diffindiff-2.3.7}/README.md +11 -6
- {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/config.py +21 -4
- {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/didanalysis.py +98 -26
- {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/didanalysis_helper.py +2 -2
- {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/diddata.py +135 -13
- {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/didtools.py +132 -26
- {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff.egg-info/PKG-INFO +12 -7
- {diffindiff-2.3.5 → diffindiff-2.3.7}/setup.py +1 -1
- {diffindiff-2.3.5 → diffindiff-2.3.7}/MANIFEST.in +0 -0
- {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/__init__.py +0 -0
- {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/tests/__init__.py +0 -0
- {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/tests/data/Corona_Hesse.xlsx +0 -0
- {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/tests/data/counties_DE.csv +0 -0
- {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/tests/data/curfew_DE.csv +0 -0
- {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/tests/tests_diffindiff.py +0 -0
- {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff.egg-info/SOURCES.txt +0 -0
- {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff.egg-info/dependency_links.txt +0 -0
- {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff.egg-info/requires.txt +0 -0
- {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff.egg-info/top_level.txt +0 -0
- {diffindiff-2.3.5 → diffindiff-2.3.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: diffindiff
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.7
|
|
4
4
|
Summary: diffindiff: Python library for convenient Difference-in-Differences analyses
|
|
5
5
|
Author: Thomas Wieland
|
|
6
6
|
Author-email: geowieland@googlemail.com
|
|
@@ -27,7 +27,7 @@ Thomas Wieland [ORCID](https://orcid.org/0000-0001-5168-9846) [EMail](mailto:geo
|
|
|
27
27
|
|
|
28
28
|
If you use this software, please cite:
|
|
29
29
|
|
|
30
|
-
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.3.
|
|
30
|
+
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.3.7) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
## Installation
|
|
@@ -173,10 +173,15 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
173
173
|
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).
|
|
174
174
|
|
|
175
175
|
|
|
176
|
-
## What's new (v2.3.
|
|
176
|
+
## What's new (v2.3.7)
|
|
177
177
|
|
|
178
|
+
- Extensions:
|
|
179
|
+
- DiffModel.counterfactual() and DiffModel.plot_counterfactual(): Re-transform log-transformed outcome via parameter retransform_log_outcome
|
|
180
|
+
- Difference between predicted and counterfactual-predicted in result df of DiffModel.counterfactual()
|
|
181
|
+
- Extended parameters in didtools.model_wrapper() to be passed to sklearn.model_selection.train_test_split()
|
|
182
|
+
- Extension of documentations
|
|
178
183
|
- Bugfixes:
|
|
179
|
-
-
|
|
180
|
-
-
|
|
181
|
-
-
|
|
182
|
-
-
|
|
184
|
+
- diddata.create_counterfactual(): Fixed calculation error, additional check of input parameters
|
|
185
|
+
- didtools.is_prepost() now also checks number of observational units
|
|
186
|
+
- didtools.is_notreatment() now checks whether data is singular with respect to 0 and 1
|
|
187
|
+
- didtools.fit_metrics() tries to convert observed and expected if not numeric before raising TypeError
|
|
@@ -19,7 +19,7 @@ Thomas Wieland [ORCID](https://orcid.org/0000-0001-5168-9846) [EMail](mailto:geo
|
|
|
19
19
|
|
|
20
20
|
If you use this software, please cite:
|
|
21
21
|
|
|
22
|
-
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.3.
|
|
22
|
+
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.3.7) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
## Installation
|
|
@@ -165,10 +165,15 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
165
165
|
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).
|
|
166
166
|
|
|
167
167
|
|
|
168
|
-
## What's new (v2.3.
|
|
168
|
+
## What's new (v2.3.7)
|
|
169
169
|
|
|
170
|
+
- Extensions:
|
|
171
|
+
- DiffModel.counterfactual() and DiffModel.plot_counterfactual(): Re-transform log-transformed outcome via parameter retransform_log_outcome
|
|
172
|
+
- Difference between predicted and counterfactual-predicted in result df of DiffModel.counterfactual()
|
|
173
|
+
- Extended parameters in didtools.model_wrapper() to be passed to sklearn.model_selection.train_test_split()
|
|
174
|
+
- Extension of documentations
|
|
170
175
|
- Bugfixes:
|
|
171
|
-
-
|
|
172
|
-
-
|
|
173
|
-
-
|
|
174
|
-
-
|
|
176
|
+
- diddata.create_counterfactual(): Fixed calculation error, additional check of input parameters
|
|
177
|
+
- didtools.is_prepost() now also checks number of observational units
|
|
178
|
+
- didtools.is_notreatment() now checks whether data is singular with respect to 0 and 1
|
|
179
|
+
- didtools.fit_metrics() tries to convert observed and expected if not numeric before raising TypeError
|
|
@@ -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-03-
|
|
7
|
+
# Version: 1.0.15
|
|
8
|
+
# Last update: 2026-03-24 18:04
|
|
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.3.
|
|
15
|
+
PACKAGE_VERSION = "2.3.7"
|
|
16
16
|
|
|
17
17
|
VERBOSE = False
|
|
18
18
|
|
|
@@ -29,11 +29,12 @@ DID_DESCRIPTION = "Difference-in-Differences Analysis"
|
|
|
29
29
|
DDD_DESCRIPTION = "Triple-Difference Analysis"
|
|
30
30
|
|
|
31
31
|
TREATMENT_DESCRIPTION = "Treatment"
|
|
32
|
+
CONTROL_DESCRIPTION = "Control"
|
|
32
33
|
|
|
33
34
|
GROUP_DESCRIPTION = "Group"
|
|
34
35
|
|
|
35
36
|
TREATMENT_GROUP_DESCRIPTION = f"{TREATMENT_DESCRIPTION} {GROUP_DESCRIPTION}"
|
|
36
|
-
CONTROL_GROUP_DESCRIPTION = f"
|
|
37
|
+
CONTROL_GROUP_DESCRIPTION = f"{CONTROL_DESCRIPTION} {GROUP_DESCRIPTION}"
|
|
37
38
|
GROUPS_DESCRIPTION = f"{TREATMENT_DESCRIPTION} and {CONTROL_GROUP_DESCRIPTION}"
|
|
38
39
|
|
|
39
40
|
TIME_PERIODS_DESCRIPTION = "Time periods"
|
|
@@ -96,6 +97,7 @@ PI_LOWER_SUFFIX = "PI_lower"
|
|
|
96
97
|
PI_UPPER_SUFFIX = "PI_upper"
|
|
97
98
|
SPILLOVER_PREFIX = "Spillover"
|
|
98
99
|
SPILLOVER_UNIT_PREFIX = f"{SPILLOVER_PREFIX}{DELIMITER}{UNIT_COL}"
|
|
100
|
+
DIFFERENCE_SUFFIX = "diff"
|
|
99
101
|
|
|
100
102
|
# Modeling config:
|
|
101
103
|
|
|
@@ -362,6 +364,21 @@ MODEL_FIT_METRICS = {
|
|
|
362
364
|
}
|
|
363
365
|
}
|
|
364
366
|
|
|
367
|
+
# Machine learning models:
|
|
368
|
+
MODEL_WRAPPER_AVAILABLE = {
|
|
369
|
+
"ols": "Ordinary Least Squares",
|
|
370
|
+
"olsbg": "Ordinary Least Squares with Bagging",
|
|
371
|
+
"dtbg": "Decision Trees with Bagging",
|
|
372
|
+
"rf": "Random Forest",
|
|
373
|
+
"gb": "Gradient Boosted Trees",
|
|
374
|
+
"knn": "K-nearest neighbor",
|
|
375
|
+
"xgb": "Extreme Gradient Boosting",
|
|
376
|
+
"lgbm": "LightGBM",
|
|
377
|
+
"svr": "Support-vector regression",
|
|
378
|
+
}
|
|
379
|
+
MODEL_WRAPPER_AVAILABLE_LIST = list(MODEL_WRAPPER_AVAILABLE.keys())
|
|
380
|
+
|
|
381
|
+
|
|
365
382
|
# Treatment diagnostics:
|
|
366
383
|
|
|
367
384
|
TREATMENT_DIAGNOSTICS_DESCRIPTION = f"{TREATMENT_DESCRIPTION} diagnostics"
|
|
@@ -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-03-
|
|
7
|
+
# Version: 2.3.7
|
|
8
|
+
# Last update: 2026-03-24 18:37
|
|
9
9
|
# Copyright (c) 2024-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -812,13 +812,13 @@ class DiffModel:
|
|
|
812
812
|
self,
|
|
813
813
|
colors = ["blue", "grey"],
|
|
814
814
|
colors_by_signficance = ["red", "coral", "dimgray", "silver", "green", "palegreen"],
|
|
815
|
-
point_type = "s",
|
|
816
|
-
point_size = 8,
|
|
817
|
-
line_width = 6,
|
|
818
|
-
line_cap_size = 5,
|
|
819
|
-
x_label = "Estimates with confidence intervals",
|
|
820
|
-
y_label = "Coefficient",
|
|
821
|
-
plot_title = "DiD effects",
|
|
815
|
+
point_type: str = "s",
|
|
816
|
+
point_size: int = 8,
|
|
817
|
+
line_width: int = 6,
|
|
818
|
+
line_cap_size: int = 5,
|
|
819
|
+
x_label: str = "Estimates with confidence intervals",
|
|
820
|
+
y_label: str = "Coefficient",
|
|
821
|
+
plot_title: str = "DiD effects",
|
|
822
822
|
plot_grid: bool = True,
|
|
823
823
|
sort_by_coef: bool = False,
|
|
824
824
|
sort_ascending: bool = True,
|
|
@@ -1042,11 +1042,13 @@ class DiffModel:
|
|
|
1042
1042
|
def counterfactual(
|
|
1043
1043
|
self,
|
|
1044
1044
|
treatment = None,
|
|
1045
|
-
after_treatment_col: str = None
|
|
1045
|
+
after_treatment_col: str = None,
|
|
1046
|
+
retransform_log_outcome: bool = False
|
|
1046
1047
|
):
|
|
1047
1048
|
|
|
1048
1049
|
"""
|
|
1049
|
-
Compute counterfactual predictions for the treatment group by
|
|
1050
|
+
Compute counterfactual predictions for the treatment group by
|
|
1051
|
+
setting treatment to zero.
|
|
1050
1052
|
|
|
1051
1053
|
Parameters
|
|
1052
1054
|
----------
|
|
@@ -1054,11 +1056,14 @@ class DiffModel:
|
|
|
1054
1056
|
Treatment name to analyse. If None, the first treatment is chosen.
|
|
1055
1057
|
after_treatment_col : str, optional
|
|
1056
1058
|
After-treatment indicator column name.
|
|
1059
|
+
retransform_log_outcome : bool, optional
|
|
1060
|
+
If outcome was log-transformed, retransform to original scale for plotting.
|
|
1061
|
+
Default is False.
|
|
1057
1062
|
|
|
1058
1063
|
Returns
|
|
1059
1064
|
-------
|
|
1060
1065
|
list
|
|
1061
|
-
[modified_model_data (DataFrame), outcome_pred_col (str), outcome_pred_cf_col (str)]
|
|
1066
|
+
[modified_model_data (DataFrame), outcome_pred_col (str), outcome_pred_cf_col (str), outcome_diff_col (str)]
|
|
1062
1067
|
|
|
1063
1068
|
Raises
|
|
1064
1069
|
------
|
|
@@ -1106,7 +1111,7 @@ class DiffModel:
|
|
|
1106
1111
|
didmodel = self.didmodel()
|
|
1107
1112
|
|
|
1108
1113
|
predictions = self.predictions()
|
|
1109
|
-
|
|
1114
|
+
|
|
1110
1115
|
model_data = self.data[2]
|
|
1111
1116
|
|
|
1112
1117
|
model_config = self.data[1]
|
|
@@ -1118,20 +1123,58 @@ class DiffModel:
|
|
|
1118
1123
|
|
|
1119
1124
|
predictions_counterfac = didmodel.get_prediction(model_data_mod).summary_frame()
|
|
1120
1125
|
|
|
1121
|
-
outcome_pred_col = f"{outcome_col}{config.PREDICTED_SUFFIX}"
|
|
1126
|
+
outcome_pred_col = f"{outcome_col}{config.DELIMITER}{config.PREDICTED_SUFFIX}"
|
|
1122
1127
|
if outcome_pred_col in model_data_mod.columns:
|
|
1123
1128
|
outcome_pred_col = f"{config.DELIMITER}{outcome_pred_col}"
|
|
1124
|
-
|
|
1129
|
+
|
|
1130
|
+
outcome_pred_cf_col = f"{outcome_col}{config.DELIMITER}{config.COUNTERFAC_SUFFIX_PRED_CF}"
|
|
1125
1131
|
if outcome_pred_cf_col in model_data_mod.columns:
|
|
1126
|
-
outcome_pred_cf_col = f"{config.DELIMITER}{outcome_pred_cf_col}"
|
|
1132
|
+
outcome_pred_cf_col = f"{config.DELIMITER}{outcome_pred_cf_col}"
|
|
1127
1133
|
|
|
1128
1134
|
model_data_mod[outcome_pred_col] = predictions[config.PREDICTIONS_SUMMARY_FRAME_COLS_LIST[0]]
|
|
1129
1135
|
model_data_mod[outcome_pred_cf_col] = predictions_counterfac[config.PREDICTIONS_SUMMARY_FRAME_COLS_LIST[0]]
|
|
1136
|
+
|
|
1137
|
+
if retransform_log_outcome:
|
|
1138
|
+
|
|
1139
|
+
if model_config["log_outcome"]:
|
|
1140
|
+
|
|
1141
|
+
if outcome_col.startswith(f"{config.LOG_PREFIX}{config.DELIMITER}"):
|
|
1142
|
+
outcome_col = outcome_col[len(f"{config.LOG_PREFIX}{config.DELIMITER}"):]
|
|
1143
|
+
|
|
1144
|
+
if outcome_pred_col.startswith(f"{config.LOG_PREFIX}{config.DELIMITER}"):
|
|
1145
|
+
|
|
1146
|
+
model_data_mod = model_data_mod.rename(
|
|
1147
|
+
columns = {
|
|
1148
|
+
outcome_pred_col: outcome_pred_col[len(f"{config.LOG_PREFIX}{config.DELIMITER}"):]
|
|
1149
|
+
}
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
outcome_pred_col = outcome_pred_col[len(f"{config.LOG_PREFIX}{config.DELIMITER}"):]
|
|
1153
|
+
|
|
1154
|
+
if outcome_pred_cf_col.startswith(f"{config.LOG_PREFIX}{config.DELIMITER}"):
|
|
1155
|
+
|
|
1156
|
+
model_data_mod = model_data_mod.rename(
|
|
1157
|
+
columns = {
|
|
1158
|
+
outcome_pred_cf_col: outcome_pred_cf_col[len(f"{config.LOG_PREFIX}{config.DELIMITER}"):]
|
|
1159
|
+
}
|
|
1160
|
+
)
|
|
1161
|
+
|
|
1162
|
+
outcome_pred_cf_col = outcome_pred_cf_col[len(f"{config.LOG_PREFIX}{config.DELIMITER}"):]
|
|
1163
|
+
|
|
1164
|
+
model_data_mod[outcome_pred_col] = np.exp(model_data_mod[outcome_pred_col])
|
|
1165
|
+
model_data_mod[outcome_pred_cf_col] = np.exp(model_data_mod[outcome_pred_cf_col])
|
|
1166
|
+
|
|
1167
|
+
outcome_diff_col = f"{outcome_col}{config.DELIMITER}{config.DIFFERENCE_SUFFIX}"
|
|
1168
|
+
if outcome_diff_col in model_data_mod.columns:
|
|
1169
|
+
outcome_diff_col = f"{config.DELIMITER}{outcome_diff_col}"
|
|
1170
|
+
|
|
1171
|
+
model_data_mod[outcome_diff_col] = model_data_mod[outcome_pred_col]-model_data_mod[outcome_pred_cf_col]
|
|
1130
1172
|
|
|
1131
1173
|
return [
|
|
1132
1174
|
model_data_mod,
|
|
1133
1175
|
outcome_pred_col,
|
|
1134
|
-
outcome_pred_cf_col
|
|
1176
|
+
outcome_pred_cf_col,
|
|
1177
|
+
outcome_diff_col
|
|
1135
1178
|
]
|
|
1136
1179
|
|
|
1137
1180
|
def didmodel(self):
|
|
@@ -1292,11 +1335,13 @@ class DiffModel:
|
|
|
1292
1335
|
TT_col_ = f"{config.TT_COL}{config.DELIMITER}{treatment}"
|
|
1293
1336
|
TGxTT_ = f"Placebo{config.DELIMITER}{treatment}"
|
|
1294
1337
|
|
|
1338
|
+
treatment_col_errors = []
|
|
1295
1339
|
if TG_col is None and TG_col_ not in model_config["TG_col"]:
|
|
1296
|
-
|
|
1297
|
-
|
|
1340
|
+
treatment_col_errors.append(f"No treatment group identification variable for treatment '{treatment}'. State parameter 'TG_col' = <<your_treatment_group_dummy>> (e.g., 'TG_{treatment}').")
|
|
1298
1341
|
if TT_col is None and TT_col_ not in model_config["TT_col"]:
|
|
1299
|
-
|
|
1342
|
+
treatment_col_errors.append(f"No treatment time variable for treatment '{treatment}'. State parameter 'TT_col' = <<your_treatment_time_dummy>> (e.g., 'TT_{treatment}.)")
|
|
1343
|
+
if len(treatment_col_errors) > 0:
|
|
1344
|
+
raise ValueError(f"Missing arguments in placebo analysis: {' '.join(treatment_col_errors)}")
|
|
1300
1345
|
|
|
1301
1346
|
if TG_col is not None:
|
|
1302
1347
|
TG_col_ = TG_col
|
|
@@ -1327,6 +1372,8 @@ class DiffModel:
|
|
|
1327
1372
|
|
|
1328
1373
|
model_data_c[TG_col_] = 0
|
|
1329
1374
|
model_data_c.loc[(model_data_c[unit_col].isin(units_random_sample)), TG_col_] = 1
|
|
1375
|
+
model_data_c[TT_col_] = 0
|
|
1376
|
+
model_data_c.loc[((model_data_c[time_col] >= treatment_period_start) & (model_data_c[time_col] <= treatment_period_end)), TT_col_] = 1
|
|
1330
1377
|
model_data_c[TGxTT_] = model_data_c[TG_col_] * model_data_c[TT_col_]
|
|
1331
1378
|
|
|
1332
1379
|
model_data_c_analysis = did_analysis(
|
|
@@ -1364,7 +1411,7 @@ class DiffModel:
|
|
|
1364
1411
|
y_lim = None,
|
|
1365
1412
|
plot_title: str = "Treatment time",
|
|
1366
1413
|
plot_symbol: str = "o",
|
|
1367
|
-
treatment_group_only = True
|
|
1414
|
+
treatment_group_only: bool = True
|
|
1368
1415
|
):
|
|
1369
1416
|
|
|
1370
1417
|
"""
|
|
@@ -1468,7 +1515,7 @@ class DiffModel:
|
|
|
1468
1515
|
|
|
1469
1516
|
def plot(
|
|
1470
1517
|
self,
|
|
1471
|
-
treatment = None,
|
|
1518
|
+
treatment: str = None,
|
|
1472
1519
|
x_label: str = "Time",
|
|
1473
1520
|
y_label: str = "Outcome",
|
|
1474
1521
|
y_lim = None,
|
|
@@ -1563,7 +1610,7 @@ class DiffModel:
|
|
|
1563
1610
|
treatment_diagnostics = model_config["treatment_diagnostics"]
|
|
1564
1611
|
no_treatments = model_config["no_treatments"]
|
|
1565
1612
|
outcome_col = model_config["outcome_col"]
|
|
1566
|
-
outcome_col_predicted = f"{outcome_col}{config.PREDICTED_SUFFIX}"
|
|
1613
|
+
outcome_col_predicted = f"{outcome_col}{config.DELIMITER}{config.PREDICTED_SUFFIX}"
|
|
1567
1614
|
|
|
1568
1615
|
if TG_col is None and treatment is None:
|
|
1569
1616
|
if no_treatments == 1:
|
|
@@ -1620,6 +1667,26 @@ class DiffModel:
|
|
|
1620
1667
|
|
|
1621
1668
|
if model_config["log_outcome"]:
|
|
1622
1669
|
|
|
1670
|
+
if outcome_col.startswith(f"{config.LOG_PREFIX}{config.DELIMITER}"):
|
|
1671
|
+
|
|
1672
|
+
model_data = model_data.rename(
|
|
1673
|
+
columns = {
|
|
1674
|
+
outcome_col: outcome_col[len(f"{config.LOG_PREFIX}{config.DELIMITER}"):]
|
|
1675
|
+
}
|
|
1676
|
+
)
|
|
1677
|
+
|
|
1678
|
+
outcome_col = outcome_col[len(f"{config.LOG_PREFIX}{config.DELIMITER}"):]
|
|
1679
|
+
|
|
1680
|
+
if outcome_col_predicted.startswith(f"{config.LOG_PREFIX}{config.DELIMITER}"):
|
|
1681
|
+
|
|
1682
|
+
model_data = model_data.rename(
|
|
1683
|
+
columns = {
|
|
1684
|
+
outcome_col_predicted: outcome_col_predicted[len(f"{config.LOG_PREFIX}{config.DELIMITER}"):]
|
|
1685
|
+
}
|
|
1686
|
+
)
|
|
1687
|
+
|
|
1688
|
+
outcome_col_predicted = outcome_col_predicted[len(f"{config.LOG_PREFIX}{config.DELIMITER}"):]
|
|
1689
|
+
|
|
1623
1690
|
model_data[outcome_col] = np.exp(model_data[outcome_col])
|
|
1624
1691
|
model_data[outcome_col_predicted] = np.exp(model_data[outcome_col_predicted])
|
|
1625
1692
|
|
|
@@ -1897,7 +1964,8 @@ class DiffModel:
|
|
|
1897
1964
|
lines_labels: list = ["TG", "TG counterfactual"],
|
|
1898
1965
|
plot_legend: bool = True,
|
|
1899
1966
|
plot_grid: bool = True,
|
|
1900
|
-
plot_size: list = [12, 6]
|
|
1967
|
+
plot_size: list = [12, 6],
|
|
1968
|
+
retransform_log_outcome: bool = False
|
|
1901
1969
|
):
|
|
1902
1970
|
|
|
1903
1971
|
"""
|
|
@@ -1929,6 +1997,9 @@ class DiffModel:
|
|
|
1929
1997
|
Show grid. Default is True.
|
|
1930
1998
|
plot_size : list, optional
|
|
1931
1999
|
Figure size as [width, height]. Default is [12, 6].
|
|
2000
|
+
retransform_log_outcome : bool, optional
|
|
2001
|
+
If outcome was log-transformed, retransform to original scale for plotting.
|
|
2002
|
+
Default is False.
|
|
1932
2003
|
|
|
1933
2004
|
Returns
|
|
1934
2005
|
-------
|
|
@@ -1965,11 +2036,12 @@ class DiffModel:
|
|
|
1965
2036
|
if no_treatments == 1:
|
|
1966
2037
|
raise ValueError ("Model object has no column for treatment group with respect to one treatment. Set parameter treatment = [your_treatment].")
|
|
1967
2038
|
else:
|
|
1968
|
-
raise ValueError ("Model object has no column for treatment group with respect to
|
|
2039
|
+
raise ValueError (f"Model object has no column for treatment group with respect to {no_treatments} treatments. Choose one with parameter treatment.")
|
|
1969
2040
|
|
|
1970
2041
|
counterfac_results = self.counterfactual(
|
|
1971
2042
|
treatment = treatment,
|
|
1972
|
-
after_treatment_col = after_treatment_col
|
|
2043
|
+
after_treatment_col = after_treatment_col,
|
|
2044
|
+
retransform_log_outcome = retransform_log_outcome
|
|
1973
2045
|
)
|
|
1974
2046
|
|
|
1975
2047
|
model_data_mod = counterfac_results[0]
|
|
@@ -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.1.
|
|
8
|
-
# Last update: 2025-03-
|
|
7
|
+
# Version: 1.1.3
|
|
8
|
+
# Last update: 2025-03-24 18:01
|
|
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.2.
|
|
8
|
-
# Last update: 2026-03-
|
|
7
|
+
# Version: 2.2.5
|
|
8
|
+
# Last update: 2026-03-24 20:23
|
|
9
9
|
# Copyright (c) 2024-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -2295,6 +2295,7 @@ def create_counterfactual(
|
|
|
2295
2295
|
treatment_col: str,
|
|
2296
2296
|
time_col: str,
|
|
2297
2297
|
cf_for_unit: str,
|
|
2298
|
+
use_treatment_col: bool = False,
|
|
2298
2299
|
use_data: str = "both",
|
|
2299
2300
|
model_type: str = "ols",
|
|
2300
2301
|
test_size: float = 0.2,
|
|
@@ -2310,7 +2311,8 @@ def create_counterfactual(
|
|
|
2310
2311
|
svr_kernel = "rbf",
|
|
2311
2312
|
xgb_learning_rate = 0.1,
|
|
2312
2313
|
lgbm_learning_rate = 0.1,
|
|
2313
|
-
random_state = 71
|
|
2314
|
+
random_state = 71,
|
|
2315
|
+
verbose: bool = False
|
|
2314
2316
|
):
|
|
2315
2317
|
|
|
2316
2318
|
"""
|
|
@@ -2332,12 +2334,76 @@ def create_counterfactual(
|
|
|
2332
2334
|
Time column name.
|
|
2333
2335
|
cf_for_unit : str
|
|
2334
2336
|
Unit id for which a counterfactual should be generated (excluded from training).
|
|
2337
|
+
use_treatment_col : bool, optional
|
|
2338
|
+
If True, treatment variable is included into counterfactual prediction.
|
|
2335
2339
|
use_data : {'both','treatment','control'}, optional
|
|
2336
2340
|
Subset used for training the predictive model (default 'both').
|
|
2337
2341
|
model_type : str, optional
|
|
2338
2342
|
Model type passed to ``tools.model_wrapper`` (e.g. 'ols','rf','xgb').
|
|
2339
|
-
|
|
2340
|
-
|
|
2343
|
+
test_size : float or int, optional
|
|
2344
|
+
Fraction or number of data to reserve for testing.
|
|
2345
|
+
Passed to `sklearn.model_selection.train_test_split`;
|
|
2346
|
+
see the corresponding documentation.
|
|
2347
|
+
train_size : float or int, optional
|
|
2348
|
+
Fraction or number of data to define for training.
|
|
2349
|
+
Passed to `sklearn.model_selection.train_test_split`;
|
|
2350
|
+
see the corresponding documentation.
|
|
2351
|
+
shuffle : bool, optional
|
|
2352
|
+
Whether or not to shuffle the data before splitting.
|
|
2353
|
+
Passed to `sklearn.model_selection.train_test_split`;
|
|
2354
|
+
see the corresponding documentation.
|
|
2355
|
+
stratify : array-like, default=None
|
|
2356
|
+
If not None, data is split in a stratified fashion,
|
|
2357
|
+
using this as the class labels.
|
|
2358
|
+
Passed to `sklearn.model_selection.train_test_split`;
|
|
2359
|
+
see the corresponding documentation.
|
|
2360
|
+
model_n_estimators : int, optional
|
|
2361
|
+
The number of estimators in the ensemble.
|
|
2362
|
+
Passed to `BaggingRegressor` and `RandomForestRegressor`;
|
|
2363
|
+
see the corresponding documentation.
|
|
2364
|
+
model_max_features : int or float, optional
|
|
2365
|
+
Passed to `BaggingRegressor`, `RandomForestRegressor`,
|
|
2366
|
+
and `GradientBoostingRegressor`; see the corresponding documentation.
|
|
2367
|
+
model_min_samples_split : int or float, optional
|
|
2368
|
+
Minimum number of samples required to split an internal node.
|
|
2369
|
+
Passed to `RandomForestRegressor`;
|
|
2370
|
+
see the corresponding documentation.
|
|
2371
|
+
rf_max_depth : int, optional
|
|
2372
|
+
Maximum depth of trees in Random Forest Regression.
|
|
2373
|
+
Passed to `RandomForestRegressor`; see the corresponding documentation.
|
|
2374
|
+
gb_iterations : int, optional
|
|
2375
|
+
Passed to `GradientBoostingRegressor`, `XGBRegressor`,
|
|
2376
|
+
and `LGBMRegressor`; see the corresponding documentation.
|
|
2377
|
+
gb_max_depth : int, optional
|
|
2378
|
+
Maximum depth trees in Gradient Boosting Regression.
|
|
2379
|
+
Passed to `GradientBoostingRegressor`;
|
|
2380
|
+
see the corresponding documentation.
|
|
2381
|
+
gb_learning_rate : float, optional
|
|
2382
|
+
Weighting with respect to the contribution of each tree
|
|
2383
|
+
in the Gradient Boosting algorithm.
|
|
2384
|
+
Passed to `GradientBoostingRegressor`;
|
|
2385
|
+
see the corresponding documentation.
|
|
2386
|
+
knn_n_neighbors : int, optional
|
|
2387
|
+
Number of neighbors to use in Nearest-neighbor algorithm.
|
|
2388
|
+
Passed to `KNeighborsRegressor`;
|
|
2389
|
+
see the corresponding documentation.
|
|
2390
|
+
svr_kernel : str or callable, optional
|
|
2391
|
+
Kernel type to be used in the Support Vector Regression algorithm.
|
|
2392
|
+
Passed to `SVR`; see the corresponding documentation.
|
|
2393
|
+
xgb_learning_rate : float, optional
|
|
2394
|
+
Weighting with respect to the contribution of each tree
|
|
2395
|
+
in the Extreme Gradient Boosting algorithm.
|
|
2396
|
+
Passed to `XGBRegressor`; see the corresponding documentation.
|
|
2397
|
+
lgbm_learning_rate : float, optional
|
|
2398
|
+
Weighting with respect to the contribution of each tree
|
|
2399
|
+
in the Light Gradient Boosting algorithm.
|
|
2400
|
+
Passed to `LGBMRegressor`; see the corresponding documentation.
|
|
2401
|
+
random_state : int, optional
|
|
2402
|
+
Random seed for reproducibility.
|
|
2403
|
+
Passed to `sklearn.model_selection.train_test_split`
|
|
2404
|
+
and regressors; see the corresponding documentations.
|
|
2405
|
+
verbose : bool, optional
|
|
2406
|
+
If True, print progress messages.
|
|
2341
2407
|
|
|
2342
2408
|
Returns
|
|
2343
2409
|
-------
|
|
@@ -2356,21 +2422,60 @@ def create_counterfactual(
|
|
|
2356
2422
|
... cf_for_unit='counterfac'
|
|
2357
2423
|
... )
|
|
2358
2424
|
"""
|
|
2425
|
+
|
|
2426
|
+
if not isinstance(y, str):
|
|
2427
|
+
if isinstance(y, list):
|
|
2428
|
+
if len(y) == 1:
|
|
2429
|
+
y = y[0]
|
|
2430
|
+
elif len(y) == 0:
|
|
2431
|
+
raise ValueError("Parameter y was stated as empty list")
|
|
2432
|
+
else:
|
|
2433
|
+
raise ValueError(f"Parameter y was stated as list with {len(y)} entries")
|
|
2434
|
+
else:
|
|
2435
|
+
raise TypeError(f"Parameter y must be stated as str, not: {y}")
|
|
2436
|
+
|
|
2437
|
+
if not isinstance(X, list):
|
|
2438
|
+
if isintance(X, str):
|
|
2439
|
+
X = [X]
|
|
2440
|
+
else:
|
|
2441
|
+
raise TypeError(f"Parameter X must be stated as list of strings, not: {X}")
|
|
2442
|
+
|
|
2443
|
+
cols = [
|
|
2444
|
+
unit_col,
|
|
2445
|
+
time_col,
|
|
2446
|
+
y
|
|
2447
|
+
]
|
|
2448
|
+
cols.extend(X)
|
|
2449
|
+
|
|
2450
|
+
if use_treatment_col:
|
|
2451
|
+
cols.append(treatment_col)
|
|
2359
2452
|
|
|
2360
|
-
data = data[
|
|
2361
|
-
|
|
2453
|
+
data = data[cols].copy()
|
|
2454
|
+
|
|
2362
2455
|
data_len = len(data)
|
|
2363
2456
|
data = data.dropna()
|
|
2364
2457
|
if len(data) < data_len:
|
|
2365
2458
|
print(f"NOTE: Because of NaN values, {data_len-len(data)} observations were skipped.")
|
|
2366
2459
|
|
|
2367
|
-
|
|
2368
|
-
|
|
2460
|
+
isnotreatment = tools.is_notreatment(
|
|
2461
|
+
data = data,
|
|
2462
|
+
unit_col = unit_col,
|
|
2463
|
+
treatment_col = treatment_col,
|
|
2464
|
+
verbose = verbose
|
|
2465
|
+
)
|
|
2466
|
+
if not isnotreatment[0]:
|
|
2467
|
+
print(f"NOTE: No {config.NO_TREATMENT_CG_DESCRIPTION}. Counterfactual will not cover full treatment time.")
|
|
2468
|
+
|
|
2469
|
+
data_unit = data.loc[data[unit_col].astype(str) == cf_for_unit]
|
|
2470
|
+
assert len(data_unit) > 0, f"Observational unit '{cf_for_unit}' does not exist in model data"
|
|
2471
|
+
|
|
2472
|
+
data = data.loc[data[unit_col].astype(str) != cf_for_unit]
|
|
2369
2473
|
|
|
2370
2474
|
isnotreatment = tools.is_notreatment(
|
|
2371
2475
|
data = data,
|
|
2372
2476
|
unit_col = unit_col,
|
|
2373
|
-
treatment_col = treatment_col
|
|
2477
|
+
treatment_col = treatment_col,
|
|
2478
|
+
verbose = verbose
|
|
2374
2479
|
)
|
|
2375
2480
|
control_group = isnotreatment[2]
|
|
2376
2481
|
|
|
@@ -2378,12 +2483,16 @@ def create_counterfactual(
|
|
|
2378
2483
|
data = data,
|
|
2379
2484
|
unit_col = unit_col,
|
|
2380
2485
|
time_col = time_col,
|
|
2381
|
-
treatment_col = treatment_col
|
|
2486
|
+
treatment_col = treatment_col,
|
|
2487
|
+
verbose = verbose
|
|
2382
2488
|
)[0]
|
|
2383
2489
|
units = tools.unique(units_tt[unit_col])
|
|
2384
2490
|
|
|
2385
2491
|
if not isnotreatment[0]:
|
|
2386
|
-
print(f"NOTE: No {config.
|
|
2492
|
+
print(f"NOTE: No {config.TREATMENT_GROUP_DESCRIPTION} units in model data left.")
|
|
2493
|
+
|
|
2494
|
+
if verbose:
|
|
2495
|
+
print(f"Creating data for counterfactual with {len(X)} independent variables", end = " ... ")
|
|
2387
2496
|
|
|
2388
2497
|
data_TG = pd.DataFrame(columns = data.columns)
|
|
2389
2498
|
|
|
@@ -2411,6 +2520,9 @@ def create_counterfactual(
|
|
|
2411
2520
|
|
|
2412
2521
|
data_cf[X] = data_cf[X].apply(pd.to_numeric, errors='coerce')
|
|
2413
2522
|
|
|
2523
|
+
if verbose:
|
|
2524
|
+
print("OK")
|
|
2525
|
+
|
|
2414
2526
|
counterfactual_pred = tools.model_wrapper(
|
|
2415
2527
|
y = data_cf[y],
|
|
2416
2528
|
X = data_cf[X],
|
|
@@ -2428,9 +2540,19 @@ def create_counterfactual(
|
|
|
2428
2540
|
svr_kernel = svr_kernel,
|
|
2429
2541
|
xgb_learning_rate = xgb_learning_rate,
|
|
2430
2542
|
lgbm_learning_rate = lgbm_learning_rate,
|
|
2431
|
-
random_state = random_state
|
|
2543
|
+
random_state = random_state,
|
|
2544
|
+
verbose = verbose
|
|
2432
2545
|
)
|
|
2433
2546
|
|
|
2547
|
+
if verbose:
|
|
2548
|
+
print(f"Counterfactual prediction for observational unit '{cf_for_unit}'", end = " ... ")
|
|
2549
|
+
|
|
2550
|
+
data_unit[treatment_col] = 0
|
|
2551
|
+
data_unit[y] = counterfactual_pred[1].predict(data_unit[X])
|
|
2552
|
+
|
|
2553
|
+
if verbose:
|
|
2554
|
+
print("OK")
|
|
2555
|
+
|
|
2434
2556
|
return [
|
|
2435
2557
|
counterfactual_pred,
|
|
2436
2558
|
data_cf,
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
# Author: Thomas Wieland
|
|
5
5
|
# ORCID: 0000-0001-5168-9846
|
|
6
6
|
# mail: geowieland@googlemail.com
|
|
7
|
-
# Version: 2.2.
|
|
8
|
-
# Last update: 2026-03-
|
|
7
|
+
# Version: 2.2.3
|
|
8
|
+
# Last update: 2026-03-24 20:21
|
|
9
9
|
# Copyright (c) 2025-2026 Thomas Wieland
|
|
10
10
|
#-----------------------------------------------------------------------
|
|
11
11
|
|
|
@@ -582,7 +582,7 @@ def is_notreatment(
|
|
|
582
582
|
treatment_timepoints = treatment_timepoints.reset_index()
|
|
583
583
|
|
|
584
584
|
no_treatment = (treatment_timepoints[treatment_col] == 0).any()
|
|
585
|
-
if (treatment_timepoints[treatment_col] == 0).all():
|
|
585
|
+
if (treatment_timepoints[treatment_col] == 0).all() or (treatment_timepoints[treatment_col] == 1).all():
|
|
586
586
|
no_treatment = False
|
|
587
587
|
|
|
588
588
|
treatment_group = treatment_timepoints.loc[treatment_timepoints[treatment_col] > 0, unit_col]
|
|
@@ -999,7 +999,7 @@ def is_prepost(
|
|
|
999
999
|
|
|
1000
1000
|
prepost = False
|
|
1001
1001
|
|
|
1002
|
-
if data[time_col].nunique() == 2:
|
|
1002
|
+
if data[time_col].nunique() == 2 and data[unit_col].nunique() > 1:
|
|
1003
1003
|
prepost = True
|
|
1004
1004
|
|
|
1005
1005
|
if verbose:
|
|
@@ -1305,8 +1305,14 @@ def model_wrapper(
|
|
|
1305
1305
|
y,
|
|
1306
1306
|
X,
|
|
1307
1307
|
model_type: str,
|
|
1308
|
-
test_size = 0.2,
|
|
1309
|
-
train_size = None,
|
|
1308
|
+
test_size: float = 0.2,
|
|
1309
|
+
train_size: float = None,
|
|
1310
|
+
shuffle: bool = True,
|
|
1311
|
+
stratify = None,
|
|
1312
|
+
X_train: list = None,
|
|
1313
|
+
X_test: list = None,
|
|
1314
|
+
y_train: list = None,
|
|
1315
|
+
y_test: list = None,
|
|
1310
1316
|
model_n_estimators = 1000,
|
|
1311
1317
|
model_max_features = 0.9,
|
|
1312
1318
|
model_min_samples_split = 2,
|
|
@@ -1333,12 +1339,68 @@ def model_wrapper(
|
|
|
1333
1339
|
Independent variables (features).
|
|
1334
1340
|
model_type : str
|
|
1335
1341
|
One of: 'ols', 'olsbg', 'dtbg', 'rf', 'gb', 'knn', 'svr', 'xgb', 'lgbm'.
|
|
1336
|
-
test_size : float, optional
|
|
1337
|
-
Fraction of data to reserve for testing.
|
|
1342
|
+
test_size : float or int, optional
|
|
1343
|
+
Fraction or number of data to reserve for testing.
|
|
1344
|
+
Passed to `sklearn.model_selection.train_test_split`;
|
|
1345
|
+
see the corresponding documentation.
|
|
1346
|
+
train_size : float or int, optional
|
|
1347
|
+
Fraction or number of data to define for training.
|
|
1348
|
+
Passed to `sklearn.model_selection.train_test_split`;
|
|
1349
|
+
see the corresponding documentation.
|
|
1350
|
+
shuffle : bool, optional
|
|
1351
|
+
Whether or not to shuffle the data before splitting.
|
|
1352
|
+
Passed to `sklearn.model_selection.train_test_split`;
|
|
1353
|
+
see the corresponding documentation.
|
|
1354
|
+
stratify : array-like, default=None
|
|
1355
|
+
If not None, data is split in a stratified fashion,
|
|
1356
|
+
using this as the class labels.
|
|
1357
|
+
Passed to `sklearn.model_selection.train_test_split`;
|
|
1358
|
+
see the corresponding documentation.
|
|
1359
|
+
model_n_estimators : int, optional
|
|
1360
|
+
The number of estimators in the ensemble.
|
|
1361
|
+
Passed to `BaggingRegressor` and `RandomForestRegressor`;
|
|
1362
|
+
see the corresponding documentation.
|
|
1363
|
+
model_max_features : int or float, optional
|
|
1364
|
+
Passed to `BaggingRegressor`, `RandomForestRegressor`,
|
|
1365
|
+
and `GradientBoostingRegressor`; see the corresponding documentation.
|
|
1366
|
+
model_min_samples_split : int or float, optional
|
|
1367
|
+
Minimum number of samples required to split an internal node.
|
|
1368
|
+
Passed to `RandomForestRegressor`;
|
|
1369
|
+
see the corresponding documentation.
|
|
1370
|
+
rf_max_depth : int, optional
|
|
1371
|
+
Maximum depth of trees in Random Forest Regression.
|
|
1372
|
+
Passed to `RandomForestRegressor`; see the corresponding documentation.
|
|
1373
|
+
gb_iterations : int, optional
|
|
1374
|
+
Passed to `GradientBoostingRegressor`, `XGBRegressor`,
|
|
1375
|
+
and `LGBMRegressor`; see the corresponding documentation.
|
|
1376
|
+
gb_max_depth : int, optional
|
|
1377
|
+
Maximum depth trees in Gradient Boosting Regression.
|
|
1378
|
+
Passed to `GradientBoostingRegressor`;
|
|
1379
|
+
see the corresponding documentation.
|
|
1380
|
+
gb_learning_rate : float, optional
|
|
1381
|
+
Weighting with respect to the contribution of each tree
|
|
1382
|
+
in the Gradient Boosting algorithm.
|
|
1383
|
+
Passed to `GradientBoostingRegressor`;
|
|
1384
|
+
see the corresponding documentation.
|
|
1385
|
+
knn_n_neighbors : int, optional
|
|
1386
|
+
Number of neighbors to use in Nearest-neighbor algorithm.
|
|
1387
|
+
Passed to `KNeighborsRegressor`;
|
|
1388
|
+
see the corresponding documentation.
|
|
1389
|
+
svr_kernel : str or callable, optional
|
|
1390
|
+
Kernel type to be used in the Support Vector Regression algorithm.
|
|
1391
|
+
Passed to `SVR`; see the corresponding documentation.
|
|
1392
|
+
xgb_learning_rate : float, optional
|
|
1393
|
+
Weighting with respect to the contribution of each tree
|
|
1394
|
+
in the Extreme Gradient Boosting algorithm.
|
|
1395
|
+
Passed to `XGBRegressor`; see the corresponding documentation.
|
|
1396
|
+
lgbm_learning_rate : float, optional
|
|
1397
|
+
Weighting with respect to the contribution of each tree
|
|
1398
|
+
in the Light Gradient Boosting algorithm.
|
|
1399
|
+
Passed to `LGBMRegressor`; see the corresponding documentation.
|
|
1338
1400
|
random_state : int, optional
|
|
1339
1401
|
Random seed for reproducibility.
|
|
1340
|
-
|
|
1341
|
-
|
|
1402
|
+
Passed to `sklearn.model_selection.train_test_split`
|
|
1403
|
+
and regressors; see the corresponding documentations.
|
|
1342
1404
|
verbose : bool, optional
|
|
1343
1405
|
If True, print progress messages.
|
|
1344
1406
|
|
|
@@ -1357,22 +1419,52 @@ def model_wrapper(
|
|
|
1357
1419
|
>>> model_wrapper(y, X, model_type='ols')
|
|
1358
1420
|
"""
|
|
1359
1421
|
|
|
1360
|
-
if model_type not in
|
|
1361
|
-
raise ValueError("Please enter a valid model type
|
|
1422
|
+
if model_type not in config.MODEL_WRAPPER_AVAILABLE_LIST:
|
|
1423
|
+
raise ValueError(f"Please enter a valid model type: {', '.join(config.MODEL_WRAPPER_AVAILABLE_LIST)}.")
|
|
1362
1424
|
|
|
1363
1425
|
if verbose:
|
|
1364
1426
|
print("Setting up training and testing data", end = " ... ")
|
|
1365
1427
|
|
|
1366
|
-
X_train
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1428
|
+
if X_train is None:
|
|
1429
|
+
X_train = []
|
|
1430
|
+
if X_test is None:
|
|
1431
|
+
X_test = []
|
|
1432
|
+
if y_train is None:
|
|
1433
|
+
y_train = []
|
|
1434
|
+
if y_test is None:
|
|
1435
|
+
y_test = []
|
|
1436
|
+
|
|
1437
|
+
self_defined_split = False
|
|
1438
|
+
|
|
1439
|
+
if len(X_train) > 0 and len(X_test) > 0 and len(y_train) > 0 and len(y_test) > 0:
|
|
1440
|
+
|
|
1441
|
+
if len(X_train) != len(y_train) or len(X_test) != len(y_test):
|
|
1442
|
+
raise ValueError(f"Train resp. tests subsets y and X must have the same length.")
|
|
1443
|
+
|
|
1444
|
+
else:
|
|
1445
|
+
self_defined_split = True
|
|
1446
|
+
|
|
1447
|
+
else:
|
|
1448
|
+
|
|
1449
|
+
X_train, X_test, y_train, y_test = train_test_split(
|
|
1450
|
+
X,
|
|
1451
|
+
y,
|
|
1452
|
+
test_size = test_size,
|
|
1453
|
+
train_size = train_size,
|
|
1454
|
+
shuffle = shuffle,
|
|
1455
|
+
stratify = stratify,
|
|
1456
|
+
random_state = random_state
|
|
1457
|
+
)
|
|
1373
1458
|
|
|
1374
1459
|
if verbose:
|
|
1460
|
+
|
|
1375
1461
|
print("OK")
|
|
1462
|
+
|
|
1463
|
+
if self_defined_split:
|
|
1464
|
+
print(f"NOTE: Train and test subsets are built by user-defined splitting with {len(X_train)} training and {len(X_test)} test observations.")
|
|
1465
|
+
else:
|
|
1466
|
+
print(f"NOTE: Random train and test subsets were built with test_size = {test_size} and train_size = {train_size}.")
|
|
1467
|
+
|
|
1376
1468
|
print(f"Training {model_type} model", end = " ... ")
|
|
1377
1469
|
|
|
1378
1470
|
model = None
|
|
@@ -1441,8 +1533,11 @@ def model_wrapper(
|
|
|
1441
1533
|
|
|
1442
1534
|
params = {
|
|
1443
1535
|
"model_type": model_type,
|
|
1536
|
+
"model_type_description": config.MODEL_WRAPPER_AVAILABLE[model_type],
|
|
1537
|
+
"self_defined_split": self_defined_split,
|
|
1444
1538
|
"test_size": test_size,
|
|
1445
1539
|
"train_size": train_size,
|
|
1540
|
+
"random_state": random_state,
|
|
1446
1541
|
"model_n_estimators": model_n_estimators,
|
|
1447
1542
|
"model_max_features": model_max_features,
|
|
1448
1543
|
"model_min_samples_split": model_min_samples_split,
|
|
@@ -1453,8 +1548,7 @@ def model_wrapper(
|
|
|
1453
1548
|
"knn_n_neighbors": knn_n_neighbors,
|
|
1454
1549
|
"svr_kernel": svr_kernel,
|
|
1455
1550
|
"xgb_learning_rate": xgb_learning_rate,
|
|
1456
|
-
"lgbm_learning_rate": lgbm_learning_rate
|
|
1457
|
-
"random_state": random_state
|
|
1551
|
+
"lgbm_learning_rate": lgbm_learning_rate
|
|
1458
1552
|
}
|
|
1459
1553
|
|
|
1460
1554
|
return [
|
|
@@ -1501,7 +1595,7 @@ def fit_metrics(
|
|
|
1501
1595
|
AssertionError
|
|
1502
1596
|
If observed and expected differ in length.
|
|
1503
1597
|
ValueError
|
|
1504
|
-
If observed or expected are not numeric, or contain NaNs when remove_nan is False.
|
|
1598
|
+
If observed and/or expected are not numeric, or contain NaNs when remove_nan is False.
|
|
1505
1599
|
|
|
1506
1600
|
Examples
|
|
1507
1601
|
--------
|
|
@@ -1513,10 +1607,19 @@ def fit_metrics(
|
|
|
1513
1607
|
|
|
1514
1608
|
assert observed_no == expected_no, "Error while calculating fit metrics: Observed and expected differ in length"
|
|
1515
1609
|
|
|
1610
|
+
type_errors = []
|
|
1516
1611
|
if not pd.api.types.is_numeric_dtype(observed) or not np.issubdtype(observed.dtype, np.number):
|
|
1517
|
-
|
|
1612
|
+
try:
|
|
1613
|
+
observed = pd.to_numeric(observed)
|
|
1614
|
+
except:
|
|
1615
|
+
type_errors.append("Observed column is not numeric.")
|
|
1518
1616
|
if not pd.api.types.is_numeric_dtype(expected) or not np.issubdtype(expected.dtype, np.number):
|
|
1519
|
-
|
|
1617
|
+
try:
|
|
1618
|
+
expected = pd.to_numeric(expected)
|
|
1619
|
+
except:
|
|
1620
|
+
type_errors.append("Expected column is not numeric.")
|
|
1621
|
+
if len(type_errors) > 0:
|
|
1622
|
+
raise TypeError(f"Error(s) while calculating fit metrics: {', '.join(type_errors)}")
|
|
1520
1623
|
|
|
1521
1624
|
if outcome_col is not None:
|
|
1522
1625
|
outcome_observed_col = f"{outcome_col}{config.DELIMITER}{config.OBSERVED_SUFFIX}"
|
|
@@ -1549,10 +1652,13 @@ def fit_metrics(
|
|
|
1549
1652
|
|
|
1550
1653
|
else:
|
|
1551
1654
|
|
|
1655
|
+
value_errors = []
|
|
1552
1656
|
if np.isnan(observed).any():
|
|
1553
|
-
|
|
1657
|
+
value_errors.append("Observed data contains NaNs.")
|
|
1554
1658
|
if np.isnan(expected).any():
|
|
1555
|
-
|
|
1659
|
+
value_errors.append("Expected data contains NaNs.")
|
|
1660
|
+
if len(value_errors) > 0:
|
|
1661
|
+
raise ValueError(f"Error(s) while calculating fit metrics: {' '.join(value_errors)} Parameter 'remove_nan' is False.")
|
|
1556
1662
|
|
|
1557
1663
|
if verbose:
|
|
1558
1664
|
print("Calculating model fit metrics", end = " ... ")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: diffindiff
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.7
|
|
4
4
|
Summary: diffindiff: Python library for convenient Difference-in-Differences analyses
|
|
5
5
|
Author: Thomas Wieland
|
|
6
6
|
Author-email: geowieland@googlemail.com
|
|
@@ -27,7 +27,7 @@ Thomas Wieland [ORCID](https://orcid.org/0000-0001-5168-9846) [EMail](mailto:geo
|
|
|
27
27
|
|
|
28
28
|
If you use this software, please cite:
|
|
29
29
|
|
|
30
|
-
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.3.
|
|
30
|
+
Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.3.7) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
## Installation
|
|
@@ -173,10 +173,15 @@ See the /tests directory for usage examples of most of the included functions.
|
|
|
173
173
|
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).
|
|
174
174
|
|
|
175
175
|
|
|
176
|
-
## What's new (v2.3.
|
|
176
|
+
## What's new (v2.3.7)
|
|
177
177
|
|
|
178
|
+
- Extensions:
|
|
179
|
+
- DiffModel.counterfactual() and DiffModel.plot_counterfactual(): Re-transform log-transformed outcome via parameter retransform_log_outcome
|
|
180
|
+
- Difference between predicted and counterfactual-predicted in result df of DiffModel.counterfactual()
|
|
181
|
+
- Extended parameters in didtools.model_wrapper() to be passed to sklearn.model_selection.train_test_split()
|
|
182
|
+
- Extension of documentations
|
|
178
183
|
- Bugfixes:
|
|
179
|
-
-
|
|
180
|
-
-
|
|
181
|
-
-
|
|
182
|
-
-
|
|
184
|
+
- diddata.create_counterfactual(): Fixed calculation error, additional check of input parameters
|
|
185
|
+
- didtools.is_prepost() now also checks number of observational units
|
|
186
|
+
- didtools.is_notreatment() now checks whether data is singular with respect to 0 and 1
|
|
187
|
+
- didtools.fit_metrics() tries to convert observed and expected if not numeric before raising TypeError
|
|
@@ -7,7 +7,7 @@ def read_README():
|
|
|
7
7
|
|
|
8
8
|
setup(
|
|
9
9
|
name='diffindiff',
|
|
10
|
-
version='2.3.
|
|
10
|
+
version='2.3.7',
|
|
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
|