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.
Files changed (21) hide show
  1. {diffindiff-2.3.5 → diffindiff-2.3.7}/PKG-INFO +12 -7
  2. {diffindiff-2.3.5 → diffindiff-2.3.7}/README.md +11 -6
  3. {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/config.py +21 -4
  4. {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/didanalysis.py +98 -26
  5. {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/didanalysis_helper.py +2 -2
  6. {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/diddata.py +135 -13
  7. {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/didtools.py +132 -26
  8. {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff.egg-info/PKG-INFO +12 -7
  9. {diffindiff-2.3.5 → diffindiff-2.3.7}/setup.py +1 -1
  10. {diffindiff-2.3.5 → diffindiff-2.3.7}/MANIFEST.in +0 -0
  11. {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/__init__.py +0 -0
  12. {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/tests/__init__.py +0 -0
  13. {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/tests/data/Corona_Hesse.xlsx +0 -0
  14. {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/tests/data/counties_DE.csv +0 -0
  15. {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/tests/data/curfew_DE.csv +0 -0
  16. {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff/tests/tests_diffindiff.py +0 -0
  17. {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff.egg-info/SOURCES.txt +0 -0
  18. {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff.egg-info/dependency_links.txt +0 -0
  19. {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff.egg-info/requires.txt +0 -0
  20. {diffindiff-2.3.5 → diffindiff-2.3.7}/diffindiff.egg-info/top_level.txt +0 -0
  21. {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.5
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.5) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
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.5)
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
- - Test whether input data is panel data via didtools.is_panel() which is included in didanalysis_helper.data_diagnostics()
180
- - Fixed false test results given continuous treatments are accepted in didtools.is_simultaneous()
181
- - Fixed false test results given continuous treatments are accepted in didtools.is_prepost()
182
- - Argument 'pre_post' is passed to is_simultaneous() in didanalysis_helper.treatment_diagnostics()
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.5) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
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.5)
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
- - Test whether input data is panel data via didtools.is_panel() which is included in didanalysis_helper.data_diagnostics()
172
- - Fixed false test results given continuous treatments are accepted in didtools.is_simultaneous()
173
- - Fixed false test results given continuous treatments are accepted in didtools.is_prepost()
174
- - Argument 'pre_post' is passed to is_simultaneous() in didanalysis_helper.treatment_diagnostics()
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.13
8
- # Last update: 2026-03-16 17:54
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.5"
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"Control {GROUP_DESCRIPTION}"
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.4
8
- # Last update: 2026-03-16 17:39
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 setting treatment to zero.
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
- outcome_pred_cf_col = f"{outcome_col}{config.COUNTERFAC_SUFFIX_PRED_CF}"
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
- raise ValueError(f"No treatment group identification variable for treatment {treatment}. Please state TG_col = your_treatment_group_dummy.")
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
- raise ValueError(f"No treatment time variable for treatment {treatment}. Please state TG_col = your_treatment_time_dummy.")
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 ", str(no_treatments), " treatments. Choose one with parameter treatment.")
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.2
8
- # Last update: 2025-03-16 17:46
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.4
8
- # Last update: 2026-03-14 11:28
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
- Other parameters
2340
- Passed to the model wrapper (hyperparameters, test/train split, random_state).
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[[y] + X + [unit_col, treatment_col, time_col]].copy()
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
- data = data[data[unit_col].astype(str) != cf_for_unit]
2368
- data_unit = data[data[unit_col].astype(str) == cf_for_unit]
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.NO_TREATMENT_CG_DESCRIPTION}. Counterfactual will not cover full treatment time.")
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.2
8
- # Last update: 2026-03-16 18:04
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
- Other parameters
1341
- Model-specific hyperparameters.
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 ["ols", "olsbg", "dtbg", "rf", "gb", "knn", "svr", "xgb", "lgbm"]:
1361
- raise ValueError("Please enter a valid model type ('ols', 'olsbg', 'dtbg', 'rf', 'gb', 'knn', 'svr', 'xgb', 'lgbm')")
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, X_test, y_train, y_test = train_test_split(
1367
- X,
1368
- y,
1369
- test_size = test_size,
1370
- train_size = train_size,
1371
- random_state = random_state
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
- raise ValueError("Error while calculating fit metrics: Observed column is not numeric")
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
- raise ValueError("Error while calculating fit metrics: Expected column is not numeric")
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
- raise ValueError("Error while calculating fit metrics: Vector with observed data contains NaNs and 'remove_nan' is False")
1657
+ value_errors.append("Observed data contains NaNs.")
1554
1658
  if np.isnan(expected).any():
1555
- raise ValueError("Error while calculating fit metrics: Vector with expected data contains NaNs and 'remove_nan' is False")
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.5
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.5) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
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.5)
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
- - Test whether input data is panel data via didtools.is_panel() which is included in didanalysis_helper.data_diagnostics()
180
- - Fixed false test results given continuous treatments are accepted in didtools.is_simultaneous()
181
- - Fixed false test results given continuous treatments are accepted in didtools.is_prepost()
182
- - Argument 'pre_post' is passed to is_simultaneous() in didanalysis_helper.treatment_diagnostics()
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.5',
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