diffindiff 2.5.7__tar.gz → 2.5.8__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 (22) hide show
  1. {diffindiff-2.5.7 → diffindiff-2.5.8}/PKG-INFO +10 -7
  2. {diffindiff-2.5.7 → diffindiff-2.5.8}/README.md +9 -6
  3. {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/config.py +3 -3
  4. {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/didanalysis.py +100 -46
  5. {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/didanalysis_helper.py +6 -6
  6. {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/diddata.py +32 -21
  7. {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/didtools.py +46 -29
  8. {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/tests/tests_diffindiff.py +2 -3
  9. {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff.egg-info/PKG-INFO +10 -7
  10. {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff.egg-info/SOURCES.txt +2 -1
  11. {diffindiff-2.5.7 → diffindiff-2.5.8}/setup.py +1 -1
  12. diffindiff-2.5.8/tests/test_diffindiff.py +257 -0
  13. {diffindiff-2.5.7 → diffindiff-2.5.8}/MANIFEST.in +0 -0
  14. {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/__init__.py +0 -0
  15. {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/tests/__init__.py +0 -0
  16. {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/tests/data/Corona_Hesse.xlsx +0 -0
  17. {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/tests/data/counties_DE.csv +0 -0
  18. {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff/tests/data/curfew_DE.csv +0 -0
  19. {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff.egg-info/dependency_links.txt +0 -0
  20. {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff.egg-info/requires.txt +0 -0
  21. {diffindiff-2.5.7 → diffindiff-2.5.8}/diffindiff.egg-info/top_level.txt +0 -0
  22. {diffindiff-2.5.7 → diffindiff-2.5.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffindiff
3
- Version: 2.5.7
3
+ Version: 2.5.8
4
4
  Summary: diffindiff: Python library for convenient Difference-in-Differences analyses
5
5
  Author: Thomas Wieland
6
6
  Author-email: geowieland@googlemail.com
@@ -40,14 +40,14 @@ Thomas Wieland [ORCID](https://orcid.org/0000-0001-5168-9846) [EMail](mailto:geo
40
40
  - 💻 GitHub Repository: [diffindiff_official](https://github.com/geowieland/diffindiff_official)
41
41
  - 📄 DOI (Zenodo): [10.5281/zenodo.18656820](https://doi.org/10.5281/zenodo.18656820)
42
42
 
43
- A case study that utilizes the diffindiff library is available on [arXiv](https://arxiv.org/abs/2605.21464).
43
+ A case study that utilizes the diffindiff library (preprint) is available on [arXiv](https://arxiv.org/abs/2605.21464).
44
44
 
45
45
 
46
46
  ## Citation
47
47
 
48
48
  If you use this software, please cite:
49
49
 
50
- Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.7) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
50
+ Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.8) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
51
51
 
52
52
 
53
53
  ## Installation
@@ -198,9 +198,12 @@ See the /tests directory for usage examples of most of the included functions.
198
198
  This software was developed without the use of AI-generated code. The GitHub Copilot Chat 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).
199
199
 
200
200
 
201
- ## What's new (v2.5.7)
201
+ ## What's new (v2.5.8)
202
202
 
203
203
  - Bugfixes
204
- - Catching RecursionError when model formulas include too many variables
205
- - Checking input data frames for duplicated columns
206
- - Avoiding treatment group deviation in summary when treatment/control groups are identical
204
+ - didtools.is_balanced() now conducts a truly comprehensive check whether the input panel data is balanced
205
+ - Fixed bug in didtools.is_numeric() with respect to unintentional modification of the dataset
206
+ - diddata.create_counterfactual() now catches error if no model was trained by didtools.model_wrapper()
207
+ - didanalysis.DiffModel.plot_treatment_effects() now treats expections correctly
208
+ - Other:
209
+ - verbose option in any function which prints NOTEs, with all NOTEs being disabled when verbose = False
@@ -14,14 +14,14 @@ Thomas Wieland [ORCID](https://orcid.org/0000-0001-5168-9846) [EMail](mailto:geo
14
14
  - 💻 GitHub Repository: [diffindiff_official](https://github.com/geowieland/diffindiff_official)
15
15
  - 📄 DOI (Zenodo): [10.5281/zenodo.18656820](https://doi.org/10.5281/zenodo.18656820)
16
16
 
17
- A case study that utilizes the diffindiff library is available on [arXiv](https://arxiv.org/abs/2605.21464).
17
+ A case study that utilizes the diffindiff library (preprint) is available on [arXiv](https://arxiv.org/abs/2605.21464).
18
18
 
19
19
 
20
20
  ## Citation
21
21
 
22
22
  If you use this software, please cite:
23
23
 
24
- Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.7) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
24
+ Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.8) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
25
25
 
26
26
 
27
27
  ## Installation
@@ -172,9 +172,12 @@ See the /tests directory for usage examples of most of the included functions.
172
172
  This software was developed without the use of AI-generated code. The GitHub Copilot Chat 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).
173
173
 
174
174
 
175
- ## What's new (v2.5.7)
175
+ ## What's new (v2.5.8)
176
176
 
177
177
  - Bugfixes
178
- - Catching RecursionError when model formulas include too many variables
179
- - Checking input data frames for duplicated columns
180
- - Avoiding treatment group deviation in summary when treatment/control groups are identical
178
+ - didtools.is_balanced() now conducts a truly comprehensive check whether the input panel data is balanced
179
+ - Fixed bug in didtools.is_numeric() with respect to unintentional modification of the dataset
180
+ - diddata.create_counterfactual() now catches error if no model was trained by didtools.model_wrapper()
181
+ - didanalysis.DiffModel.plot_treatment_effects() now treats expections correctly
182
+ - Other:
183
+ - verbose option in any function which prints NOTEs, with all NOTEs being disabled when verbose = False
@@ -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.28
8
- # Last update: 2026-10-10 09:58
7
+ # Version: 1.0.29
8
+ # Last update: 2026-10-17 21:17
9
9
  # Copyright (c) 2025-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
12
12
  # Basic config:
13
13
 
14
14
  PACKAGE_NAME = "diffindiff"
15
- PACKAGE_VERSION = "2.5.7"
15
+ PACKAGE_VERSION = "2.5.8"
16
16
 
17
17
  VERBOSE = False
18
18
 
@@ -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.6
8
- # Last update: 2026-09-11 21:46
7
+ # Version: 2.4.8
8
+ # Last update: 2026-10-17 21:15
9
9
  # Copyright (c) 2024-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -111,7 +111,8 @@ class DiffModel:
111
111
  def treatment_statistics(
112
112
  self,
113
113
  treatment: str = None,
114
- after_treatment_col: str = None
114
+ after_treatment_col: str = None,
115
+ verbose: bool = False
115
116
  ):
116
117
 
117
118
  """
@@ -163,7 +164,9 @@ class DiffModel:
163
164
  else:
164
165
 
165
166
  treatment = model_config["treatment_col"][0]
166
- print(f"NOTE: No treatment was stated. Choosing treatment '{treatment}' for calculating treatment statistics.")
167
+
168
+ if verbose:
169
+ print(f"NOTE: No treatment was stated. Choosing treatment '{treatment}' for calculating treatment statistics.")
167
170
 
168
171
  if after_treatment_col is not None:
169
172
 
@@ -782,9 +785,9 @@ class DiffModel:
782
785
 
783
786
  if len(no_control_conditions) > 0:
784
787
  if len(no_control_conditions) == 1:
785
- print(f"NOTE: Treatment '{no_control_conditions[0]}' has no control conditions.")
788
+ print(f"WARNING: Treatment '{no_control_conditions[0]}' has no control conditions.")
786
789
  else:
787
- print(f"NOTE: Treatments {', '.join(no_control_conditions)} have no control conditions.")
790
+ print(f"WARNING: Treatments {', '.join(no_control_conditions)} have no control conditions.")
788
791
 
789
792
  print("-" * int(total_width))
790
793
 
@@ -905,6 +908,7 @@ class DiffModel:
905
908
  treatment_effects = self.treatment_effects()
906
909
 
907
910
  try:
911
+
908
912
  if model_config.get("pre_post", False) and len(model_config.get("after_treatment_col", [])) > 0:
909
913
 
910
914
  labels = treatment_effects.iloc[:, 0].astype(str).values
@@ -943,9 +947,11 @@ class DiffModel:
943
947
  break
944
948
  if appended:
945
949
  break
946
- except Exception:
950
+ except Exception as e:
947
951
 
948
- pass
952
+ print(f"Plot of treatment effects not possible: {str(e)}")
953
+
954
+ return self
949
955
 
950
956
  if sort_by_coef:
951
957
  treatment_effects = treatment_effects.sort_values(
@@ -1099,7 +1105,8 @@ class DiffModel:
1099
1105
  self,
1100
1106
  treatment = None,
1101
1107
  after_treatment_col: str = None,
1102
- retransform_log_outcome: bool = False
1108
+ retransform_log_outcome: bool = False,
1109
+ verbose: bool = False
1103
1110
  ):
1104
1111
 
1105
1112
  """
@@ -1153,8 +1160,11 @@ class DiffModel:
1153
1160
  if treatment not in model_config["treatment_col"]:
1154
1161
  raise ValueError (f"Treatment '{treatment}' not in model object.")
1155
1162
  else:
1163
+
1156
1164
  treatment = model_config["treatment_col"][0]
1157
- print(f"NOTE: No treatment was stated. Choosing treatment '{treatment}' for calculating counterfactual.")
1165
+
1166
+ if verbose:
1167
+ print(f"NOTE: No treatment was stated. Choosing treatment '{treatment}' for calculating counterfactual.")
1158
1168
 
1159
1169
  if after_treatment_col is not None:
1160
1170
  if len(model_config["after_treatment_col"]) == 0:
@@ -1313,7 +1323,8 @@ class DiffModel:
1313
1323
  TT_col: str = None,
1314
1324
  divide: float = 0.5,
1315
1325
  resample: float = 1.0,
1316
- random_state = 71
1326
+ random_state = 71,
1327
+ verbose: bool = False
1317
1328
  ):
1318
1329
 
1319
1330
  """
@@ -1369,8 +1380,11 @@ class DiffModel:
1369
1380
  if treatment not in model_config["treatment_col"]:
1370
1381
  raise ValueError (f"Treatment '{treatment}' not in model object.")
1371
1382
  else:
1383
+
1372
1384
  treatment = model_config["treatment_col"][0]
1373
- print(f"NOTE: No treatment was stated. Choosing treatment '{treatment}' for placebo analysis.")
1385
+
1386
+ if verbose:
1387
+ print(f"NOTE: No treatment was stated. Choosing treatment '{treatment}' for placebo analysis.")
1374
1388
 
1375
1389
  if after_treatment_col is not None:
1376
1390
  if len(model_config["after_treatment_col"]) == 0:
@@ -1390,7 +1404,8 @@ class DiffModel:
1390
1404
  treatment_demeaned = treatment
1391
1405
  treatment = treatment.replace(f"{config.DELIMITER}{config.DEMEAN_SUFFIX}", "")
1392
1406
 
1393
- print(f"NOTE: Data is demeaned. Treatment statistics are calculated based on original treatment '{treatment}'.")
1407
+ if verbose:
1408
+ print(f"NOTE: Data is demeaned. Treatment statistics are calculated based on original treatment '{treatment}'.")
1394
1409
 
1395
1410
  treatment_statistics = self.treatment_statistics(treatment = treatment)
1396
1411
 
@@ -1484,7 +1499,8 @@ class DiffModel:
1484
1499
  plot_symbol: str = "o",
1485
1500
  treatment_group_only: bool = True,
1486
1501
  save_fig: str = None,
1487
- save_fig_kwargs: dict = {}
1502
+ save_fig_kwargs: dict = {},
1503
+ verbose: bool = False
1488
1504
  ):
1489
1505
 
1490
1506
  """
@@ -1542,8 +1558,11 @@ class DiffModel:
1542
1558
  if treatment not in model_config["treatment_col"]:
1543
1559
  raise ValueError (f"Treatment '{treatment}' not in model object.")
1544
1560
  else:
1561
+
1545
1562
  treatment = model_config["treatment_col"][0]
1546
- print(f"NOTE: No treatment was stated. Choosing treatment '{treatment}' for plotting timeline.")
1563
+
1564
+ if verbose:
1565
+ print(f"NOTE: No treatment was stated. Choosing treatment '{treatment}' for plotting timeline.")
1547
1566
 
1548
1567
  if treatment_group_only:
1549
1568
  if TG_col is None:
@@ -1615,7 +1634,8 @@ class DiffModel:
1615
1634
  pre_post_bar_width = 0.5,
1616
1635
  retransform_log_outcome: bool = False,
1617
1636
  save_fig: str = None,
1618
- save_fig_kwargs: dict = {}
1637
+ save_fig_kwargs: dict = {},
1638
+ verbose: bool = False
1619
1639
  ):
1620
1640
 
1621
1641
  """
@@ -1723,7 +1743,8 @@ class DiffModel:
1723
1743
  break
1724
1744
  else:
1725
1745
 
1726
- print(f"NOTE: No treatment was stated. Choosing treatment '{treatment_diagnostics[0]['treatment']}' for plotting.")
1746
+ if verbose:
1747
+ print(f"NOTE: No treatment was stated. Choosing treatment '{treatment_diagnostics[0]['treatment']}' for plotting.")
1727
1748
 
1728
1749
  treatment_group = treatment_diagnostics[0]["treatment_group"]
1729
1750
  treatment = treatment_diagnostics[0]["treatment"]
@@ -1783,13 +1804,15 @@ class DiffModel:
1783
1804
  model_data[config.PREDICTIONS_SUMMARY_FRAME_COLS_LIST[4]] = np.exp(model_data[config.PREDICTIONS_SUMMARY_FRAME_COLS_LIST[4]])
1784
1805
  model_data[config.PREDICTIONS_SUMMARY_FRAME_COLS_LIST[5]] = np.exp(model_data[config.PREDICTIONS_SUMMARY_FRAME_COLS_LIST[5]])
1785
1806
 
1786
- if model_config["demean"]:
1787
- print(f"NOTE: Outcome variable '{outcome_col.replace(f'{config.DELIMITER}{config.DEMEAN_SUFFIX}', '')}' was log-transformed and demeaned. Re-transformation was applied on '{outcome_col}'.")
1788
- else:
1789
- print(f"NOTE: Outcome variable '{outcome_col}' was log-transformed. Re-transformation was applied.")
1807
+ if verbose:
1808
+ if model_config["demean"]:
1809
+ print(f"NOTE: Outcome variable '{outcome_col.replace(f'{config.DELIMITER}{config.DEMEAN_SUFFIX}', '')}' was log-transformed and demeaned. Re-transformation was applied on '{outcome_col}'.")
1810
+ else:
1811
+ print(f"NOTE: Outcome variable '{outcome_col}' was log-transformed. Re-transformation was applied.")
1790
1812
 
1791
1813
  else:
1792
- print("NOTE: Parameter 'retransform_log_outcome' was set to True, but outcome variable was not log-transformed. No re-transformation applied.")
1814
+ if verbose:
1815
+ print("NOTE: Parameter 'retransform_log_outcome' was set to True, but outcome variable was not log-transformed. No re-transformation applied.")
1793
1816
 
1794
1817
  model_data_TG = model_data[model_data[TG_col] == 1]
1795
1818
  model_data_CG = model_data[model_data[TG_col] == 0]
@@ -2096,7 +2119,8 @@ class DiffModel:
2096
2119
  plot_size: list = [12, 6],
2097
2120
  retransform_log_outcome: bool = False,
2098
2121
  save_fig: str = None,
2099
- save_fig_kwargs: dict = {}
2122
+ save_fig_kwargs: dict = {},
2123
+ verbose: bool = False
2100
2124
  ):
2101
2125
 
2102
2126
  """
@@ -2199,7 +2223,9 @@ class DiffModel:
2199
2223
  break
2200
2224
 
2201
2225
  else:
2202
- print(f"NOTE: No treatment was stated. Choosing treatment '{treatment_diagnostics[0]['treatment']}' for plotting counterfactual.")
2226
+
2227
+ if verbose:
2228
+ print(f"NOTE: No treatment was stated. Choosing treatment '{treatment_diagnostics[0]['treatment']}' for plotting counterfactual.")
2203
2229
 
2204
2230
  treatment_group = treatment_diagnostics[0]["treatment_group"]
2205
2231
 
@@ -2527,8 +2553,11 @@ def did_analysis(
2527
2553
  TG_col = []
2528
2554
 
2529
2555
  if not FE_unit and config.AUTO_SWITCH_TO_FIXED_EFFECTS:
2556
+
2530
2557
  FE_unit = True
2531
- 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.")
2558
+
2559
+ if verbose:
2560
+ 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.")
2532
2561
 
2533
2562
  if unique_units < config.FIXED_EFFECTS_THRESHOLD:
2534
2563
 
@@ -2538,7 +2567,8 @@ def did_analysis(
2538
2567
  ITT = False
2539
2568
  intercept = True
2540
2569
 
2541
- print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} units. Unit or group fixed effects are skipped.")
2570
+ if verbose:
2571
+ print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} units. Unit or group fixed effects are skipped.")
2542
2572
 
2543
2573
  if unique_time_points < config.FIXED_EFFECTS_THRESHOLD:
2544
2574
 
@@ -2547,7 +2577,8 @@ def did_analysis(
2547
2577
  GTT = False
2548
2578
  intercept = True
2549
2579
 
2550
- print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} time points. Time fixed effects are skipped.")
2580
+ if verbose:
2581
+ print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} time points. Time fixed effects are skipped.")
2551
2582
 
2552
2583
  if demean and FE_unit:
2553
2584
 
@@ -2556,38 +2587,56 @@ def did_analysis(
2556
2587
  ITE = False
2557
2588
  ITT = False
2558
2589
 
2559
- print(f"NOTE: Unit fixed effects are specified to be replaced by demeaning. {config.EFFECTS_TYPES['ITE']['description']} or {config.EFFECTS_TYPES['ITT']['description']} are not supported.")
2590
+ if verbose:
2591
+ print(f"NOTE: Unit fixed effects are specified to be replaced by demeaning. {config.EFFECTS_TYPES['ITE']['description']} or {config.EFFECTS_TYPES['ITT']['description']} are not supported.")
2560
2592
 
2561
2593
  if ITE:
2562
2594
 
2563
2595
  if not FE_unit:
2596
+
2564
2597
  FE_unit = True
2565
- print("NOTE: Model includes individual treatment effects. Unit fixed effects are included.")
2598
+
2599
+ if verbose:
2600
+ print("NOTE: Model includes individual treatment effects. Unit fixed effects are included.")
2566
2601
 
2567
2602
  if GTE:
2603
+
2568
2604
  GTE = False
2569
- print("NOTE: Both group and individual treatment effects were stated. Switching to individual treatment effects only.")
2605
+
2606
+ if verbose:
2607
+ print("NOTE: Both group and individual treatment effects were stated. Switching to individual treatment effects only.")
2570
2608
 
2571
2609
  if ITT:
2572
2610
 
2573
2611
  TT_col = []
2574
2612
 
2575
2613
  if not FE_unit:
2614
+
2576
2615
  FE_unit = True
2577
- print("NOTE: Model includes individual time trends. Unit fixed effects are included. Treatment time variable is dropped.")
2616
+
2617
+ if verbose:
2618
+ print("NOTE: Model includes individual time trends. Unit fixed effects are included. Treatment time variable is dropped.")
2578
2619
 
2579
2620
  if FE_time:
2621
+
2580
2622
  FE_time = False
2581
- print("NOTE: Model includes individual time trends. Time fixed effects are dropped.")
2623
+
2624
+ if verbose:
2625
+ print("NOTE: Model includes individual time trends. Time fixed effects are dropped.")
2582
2626
 
2583
2627
  if GTT:
2628
+
2584
2629
  GTT = False
2585
- print("NOTE: Both group and individual time trends were stated. Switching to individual time trends only.")
2630
+
2631
+ if verbose:
2632
+ print("NOTE: Both group and individual time trends were stated. Switching to individual time trends only.")
2586
2633
 
2587
2634
  if staggered_adoption and config.AUTO_SWITCH_TO_FIXED_EFFECTS:
2588
2635
 
2589
2636
  if not FE_unit or not FE_time:
2590
- print("NOTE: Quasi-experiment includes one or more staggered treatments. Two-way fixed effects model is used.")
2637
+
2638
+ if verbose:
2639
+ print("NOTE: Quasi-experiment includes one or more staggered treatments. Two-way fixed effects model is used.")
2591
2640
 
2592
2641
  FE_unit = True
2593
2642
  FE_time = True
@@ -2604,7 +2653,9 @@ def did_analysis(
2604
2653
  if FE_group:
2605
2654
 
2606
2655
  if intercept or len(TG_col) > 0:
2607
- print("NOTE: Quasi-experiment includes group fixed effects. Control group baseline and treatment group deviation are dropped.")
2656
+
2657
+ if verbose:
2658
+ print("NOTE: Quasi-experiment includes group fixed effects. Control group baseline and treatment group deviation are dropped.")
2608
2659
 
2609
2660
  TG_col = []
2610
2661
  intercept = False
@@ -2716,13 +2767,14 @@ def did_analysis(
2716
2767
 
2717
2768
  if config.AUTO_SKIP_CONSTANT_COLUMNS:
2718
2769
 
2719
- print(f"NOTE: The following columns are constant and dropped from the analysis: {', '.join(data_diagnostics['cols_constants'])}.")
2770
+ print(f"WARNING: The following columns are constant and dropped from the analysis: {', '.join(data_diagnostics['cols_constants'])}.")
2720
2771
 
2721
2772
  covariates = [col for col in covariates if col not in data_diagnostics["cols_constants"]]
2722
2773
 
2723
2774
  if data_diagnostics["is_prepost"] and config.AUTO_SWITCH_TO_PREPOST:
2724
2775
 
2725
- print(f"NOTE: Input is {config.PREPOST_PANELDATA_DESCRIPTION}. Data processing and model estimation will treat data as pre-post.")
2776
+ if verbose:
2777
+ print(f"NOTE: Input is {config.PREPOST_PANELDATA_DESCRIPTION}. Data processing and model estimation will treat data as pre-post.")
2726
2778
 
2727
2779
  pre_post = True
2728
2780
 
@@ -2730,8 +2782,8 @@ def did_analysis(
2730
2782
  FE_time = False
2731
2783
  FE_group = False
2732
2784
 
2733
- if not data_diagnostics["is_prepost"] and not FE_unit and not FE_time:
2734
- print(f"NOTE: Input is {config.MULTIPERIOD_PANELDATA_DESCRIPTION}. Consider including {config.EFFECTS_TYPES['FE']['description']}.")
2785
+ if not data_diagnostics["is_prepost"] and not FE_unit and not FE_time:
2786
+ print(f"WARNING: Input is {config.MULTIPERIOD_PANELDATA_DESCRIPTION}. Consider including {config.EFFECTS_TYPES['FE']['description']}.")
2735
2787
 
2736
2788
  if log_outcome:
2737
2789
 
@@ -2999,9 +3051,8 @@ def did_analysis(
2999
3051
  indep_vars_no = indep_vars_no-(len(FE_unit_vars)+len(FE_time_vars))
3000
3052
 
3001
3053
  if verbose:
3002
- print("OK")
3003
-
3004
- print(f"NOTE: Outcome variable '{outcome_col}' is replaced by demeaned variable '{outcome_col_demeaned}', and {len(treatment_col)+len(covariates)+len(spillover_vars)+len(interactions)} other variables were remeaned.")
3054
+ print("OK")
3055
+ print(f"NOTE: Outcome variable '{outcome_col}' is replaced by demeaned variable '{outcome_col_demeaned}', and {len(treatment_col)+len(covariates)+len(spillover_vars)+len(interactions)} other variables were remeaned.")
3005
3056
 
3006
3057
  outcome_col = outcome_col_demeaned
3007
3058
 
@@ -3283,13 +3334,15 @@ def ddd_analysis(
3283
3334
 
3284
3335
  FE_unit = False
3285
3336
 
3286
- print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} units. Unit fixed effects are skipped.")
3337
+ if verbose:
3338
+ print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} units. Unit fixed effects are skipped.")
3287
3339
 
3288
3340
  if unique_time_points < config.FIXED_EFFECTS_THRESHOLD:
3289
3341
 
3290
3342
  FE_time = False
3291
3343
 
3292
- print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} time points. Time fixed effects are skipped.")
3344
+ if verbose:
3345
+ print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} time points. Time fixed effects are skipped.")
3293
3346
 
3294
3347
  if FE_unit:
3295
3348
  TG_col = None
@@ -3320,13 +3373,14 @@ def ddd_analysis(
3320
3373
 
3321
3374
  if config.AUTO_SKIP_CONSTANT_COLUMNS:
3322
3375
 
3323
- print(f"NOTE: The following columns are constant and dropped from the analysis: {', '.join(data_diagnostics['cols_constants'])}.")
3376
+ print(f"WARNING: The following columns are constant and dropped from the analysis: {', '.join(data_diagnostics['cols_constants'])}.")
3324
3377
 
3325
3378
  covariates = [col for col in covariates if col not in data_diagnostics["cols_constants"]]
3326
3379
 
3327
3380
  if data_diagnostics["is_prepost"] and config.AUTO_SWITCH_TO_PREPOST:
3328
3381
 
3329
- print("NOTE: Panel data is pre-post. Data processing and model estimation will treat data as pre-post")
3382
+ if verbose:
3383
+ print("NOTE: Panel data is pre-post. Data processing and model estimation will treat data as pre-post")
3330
3384
 
3331
3385
  pre_post = True
3332
3386
 
@@ -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.5
8
- # Last update: 2026-10-09 23:04
7
+ # Version: 1.2.6
8
+ # Last update: 2026-10-17 18:55
9
9
  # Copyright (c) 2025-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -231,8 +231,8 @@ def demean_variables(
231
231
  suffix = f"{config.DELIMITER}{config.DEMEAN_SUFFIX}"
232
232
 
233
233
  if not FE_unit and not FE_time:
234
-
235
- print("NOTE: Neither 'FE_unit' nor 'FE_time' is set to True. No demeaning will be performed.")
234
+
235
+ print("WARNING: Neither 'FE_unit' nor 'FE_time' is set to True. No demeaning will be performed.")
236
236
 
237
237
  for col in cols_to_demean:
238
238
 
@@ -258,13 +258,13 @@ def demean_variables(
258
258
  else:
259
259
 
260
260
  tools.check_columns(
261
- df = data,
261
+ df = data_demean,
262
262
  columns = cols_to_demean + [unit_col] + [time_col],
263
263
  verbose = verbose
264
264
  )
265
265
 
266
266
  tools.is_numeric(
267
- df = data,
267
+ df = data_demean,
268
268
  columns = cols_to_demean,
269
269
  verbose = verbose
270
270
  )
@@ -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.3
8
- # Last update: 2026-08-04 17:51
7
+ # Version: 2.3.4
8
+ # Last update: 2026-10-17 21:34
9
9
  # Copyright (c) 2024-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -1763,7 +1763,7 @@ class DiffData:
1763
1763
  TT_col = treatment_cols[0]["TT_col"]
1764
1764
  treatment_name = treatment_cols[0]["treatment_name"]
1765
1765
 
1766
- if len(treatment_cols) > 1:
1766
+ if len(treatment_cols) > 1 and verbose:
1767
1767
  print(f"NOTE: Treatment data contains {len(treatment_cols)} treatments. Taking first treatment '{treatment_name}' for identifying treatment group and time.")
1768
1768
 
1769
1769
  if verbose:
@@ -1853,9 +1853,8 @@ class DiffData:
1853
1853
  self.data[1] = groups
1854
1854
 
1855
1855
  if verbose:
1856
- print("OK")
1857
-
1858
- print("NOTE: DiffData object was changed with counterfactual.")
1856
+ print("OK")
1857
+ print("NOTE: DiffData object was changed with counterfactual.")
1859
1858
 
1860
1859
  return self
1861
1860
 
@@ -1926,7 +1925,7 @@ class DiffData:
1926
1925
  treatment_name = treatment_cols[0]["treatment_name"]
1927
1926
  TG_col = treatment_cols[0]["TG_col"]
1928
1927
 
1929
- if len(treatment_cols) > 1:
1928
+ if len(treatment_cols) > 1 and verbose:
1930
1929
  print(f"NOTE: Treatment data contains {len(treatment_cols)} treatments. Taking first treatment '{treatment_name}' for identifying treatment group and time.")
1931
1930
 
1932
1931
  if verbose:
@@ -2014,7 +2013,7 @@ class DiffData:
2014
2013
  units = valid_units
2015
2014
 
2016
2015
  if len(units_not_included) > 0:
2017
- print(f"WARNING: {len(units_not_included)} analysis units were not included due to NaN data.")
2016
+ print(f"WARNING: {len(units_not_included)} analysis units were not included due to NaN values.")
2018
2017
 
2019
2018
  if verbose:
2020
2019
  print("OK")
@@ -2863,7 +2862,7 @@ def create_counterfactual(
2863
2862
  data_len = len(data)
2864
2863
  data = data.dropna()
2865
2864
  if len(data) < data_len:
2866
- print(f"NOTE: Because of NaN values, {data_len-len(data)} observations were skipped.")
2865
+ print(f"WARNING: Because of NaN values, {data_len-len(data)} observations were skipped.")
2867
2866
 
2868
2867
  isnotreatment = tools.is_notreatment(
2869
2868
  data = data,
@@ -2871,7 +2870,7 @@ def create_counterfactual(
2871
2870
  treatment_col = treatment_col,
2872
2871
  verbose = verbose
2873
2872
  )
2874
- if not isnotreatment[0]:
2873
+ if not isnotreatment[0] and verbose:
2875
2874
  print(f"NOTE: No {config.NO_TREATMENT_CG_DESCRIPTION}. Counterfactual will not cover full treatment time.")
2876
2875
 
2877
2876
  data_unit = data.loc[data[unit_col].astype(str) == cf_for_unit]
@@ -2897,7 +2896,7 @@ def create_counterfactual(
2897
2896
  units = tools.unique(units_tt[unit_col])
2898
2897
 
2899
2898
  if not isnotreatment[0]:
2900
- print(f"NOTE: No {config.TREATMENT_GROUP_DESCRIPTION} units in model data left.")
2899
+ print(f"WARNING: No {config.TREATMENT_GROUP_DESCRIPTION} units in model data left.")
2901
2900
 
2902
2901
  if verbose:
2903
2902
  print(f"Creating data for counterfactual with {len(X)} independent variables", end = " ... ")
@@ -2958,18 +2957,30 @@ def create_counterfactual(
2958
2957
  random_state = random_state,
2959
2958
  verbose = verbose
2960
2959
  )
2961
-
2962
- if verbose:
2963
- print(f"Counterfactual prediction for observational unit '{cf_for_unit}'", end = " ... ")
2964
-
2965
- data_unit[treatment_col] = 0
2966
- data_unit[y] = counterfactual_pred[1].predict(data_unit[X])
2967
-
2968
- if verbose:
2969
- print("OK")
2960
+
2961
+ model_estimation_error = counterfactual_pred[6]
2962
+ model_estimation_error_text = ""
2963
+
2964
+ if model_estimation_error:
2965
+
2966
+ model_estimation_error_text = counterfactual_pred[7]
2967
+ print(f"WARNING: The desired {config.MODEL_WRAPPER_AVAILABLE[model_type]} model was not estimated. The following error occurred: '{model_estimation_error_text}'.")
2968
+
2969
+ else:
2970
+
2971
+ if verbose:
2972
+ print(f"Counterfactual prediction for observational unit '{cf_for_unit}'", end = " ... ")
2973
+
2974
+ data_unit[treatment_col] = 0
2975
+ data_unit[y] = counterfactual_pred[1].predict(data_unit[X])
2976
+
2977
+ if verbose:
2978
+ print("OK")
2970
2979
 
2971
2980
  return [
2972
2981
  counterfactual_pred,
2973
2982
  data_cf,
2974
- data_unit
2983
+ data_unit,
2984
+ model_estimation_error,
2985
+ model_estimation_error_text
2975
2986
  ]
@@ -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.7
8
- # Last update: 2026-10-10 09:57
7
+ # Version: 2.2.8
8
+ # Last update: 2026-10-17 18:53
9
9
  # Copyright (c) 2025-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -77,6 +77,8 @@ def check_columns(
77
77
  """
78
78
 
79
79
  if len(columns) > 0:
80
+
81
+ key_errors = []
80
82
 
81
83
  if verbose:
82
84
  print(f"Checking if column(s) {', '.join(columns)} exist(s) in data frame", end=" ... ")
@@ -87,7 +89,7 @@ def check_columns(
87
89
  print("OK")
88
90
 
89
91
  if missing_columns:
90
- raise KeyError(f"Data do not contain column(s): {', '.join(missing_columns)}")
92
+ key_errors.append(f"Data do not contain column(s): {', '.join(missing_columns)}")
91
93
 
92
94
  if verbose:
93
95
  print("Checking whether columns are duplicated in data frame", end = " ... ")
@@ -98,7 +100,10 @@ def check_columns(
98
100
  print("OK")
99
101
 
100
102
  if len(cols_duplicated) > 0:
101
- raise KeyError(f"Data contain duplicated relevant columns: {', '.join(cols_duplicated)}")
103
+ key_errors.append(f"Data contain duplicated relevant columns: {', '.join(cols_duplicated)}")
104
+
105
+ if len(key_errors) > 0:
106
+ raise KeyError(f"{' '.join(key_errors)}")
102
107
 
103
108
  def is_numeric(
104
109
  df: pd.DataFrame,
@@ -134,6 +139,8 @@ def is_numeric(
134
139
  >>> is_numeric(df, ['x'])
135
140
  """
136
141
 
142
+ df = df.copy()
143
+
137
144
  if len(columns) > 0:
138
145
 
139
146
  if verbose:
@@ -266,32 +273,41 @@ def is_balanced(
266
273
  if other_cols is None:
267
274
  other_cols = []
268
275
 
276
+ cols_relevant = [unit_col, time_col, outcome_col] + other_cols
277
+
269
278
  if verbose:
270
- print(f"Checking whether panel data is balanced with respect to {3+len(other_cols)} columns", end = " ... ")
279
+ print(f"Checking whether panel data is balanced with respect to {len(cols_relevant)} columns", end = " ... ")
271
280
 
272
- unit_freq = data[unit_col].nunique()
273
- time_freq = data[time_col].nunique()
274
- unitxtime = unit_freq*time_freq
281
+ panel = data[cols_relevant].copy()
275
282
 
276
- if other_cols is None:
277
- cols_relevant = [unit_col, time_col, outcome_col]
278
- else:
279
- cols_relevant = [unit_col, time_col, outcome_col] + other_cols
283
+ warnings = []
280
284
 
281
- data_relevant = data[cols_relevant]
282
-
283
- if unitxtime != len(data_relevant.notna()):
284
- balanced = False
285
- else:
286
- balanced = True
287
-
285
+ if panel.isnull().any().any():
286
+ warnings.append("Relevant columns contain missing values.")
287
+
288
+ pair_counts = panel[[unit_col, time_col]].dropna().groupby([unit_col, time_col]).size()
289
+ if (pair_counts != 1).any():
290
+ warnings.append("Unit-time pairs are duplicated or incomplete.")
291
+
292
+ units = panel[unit_col].dropna().unique()
293
+ times = panel[time_col].dropna().unique()
294
+ expected_pairs = set((u, t) for u in units for t in times)
295
+ observed_pairs = set(pair_counts.index.tolist())
296
+ if observed_pairs != expected_pairs:
297
+ warnings.append("Some unit-time combinations are missing.")
298
+
288
299
  if verbose:
289
300
  print("OK")
290
301
 
291
- if not balanced:
292
- print("WARNING: Panel data is not balanced.")
293
-
294
- return balanced
302
+ if len(warnings) > 0:
303
+
304
+ if verbose:
305
+ print(f"WARNING: {' '.join(warnings)}")
306
+
307
+ return False
308
+
309
+ else:
310
+ return True
295
311
 
296
312
  def is_dummy(
297
313
  iterable,
@@ -562,7 +578,7 @@ def is_simultaneous(
562
578
  if verbose:
563
579
  print("OK")
564
580
 
565
- if not simultaneous and data_isnotreatment[0]:
581
+ if not simultaneous and data_isnotreatment[0] and verbose:
566
582
  print(f"NOTE: Treatment '{treatment_col}' is not simultaneous.")
567
583
 
568
584
  if simultaneous and not data_isnotreatment[0]:
@@ -621,8 +637,8 @@ def is_notreatment(
621
637
  if verbose:
622
638
  print("OK")
623
639
 
624
- if not no_treatment:
625
- print(f"NOTE: treatment '{treatment_col}' does not include a {config.NO_TREATMENT_CG_DESCRIPTION}.")
640
+ if not no_treatment:
641
+ print(f"WARNING: treatment '{treatment_col}' does not include a {config.NO_TREATMENT_CG_DESCRIPTION}.")
626
642
 
627
643
  return [
628
644
  no_treatment,
@@ -1708,7 +1724,9 @@ def model_wrapper(
1708
1724
  X_test,
1709
1725
  y_train,
1710
1726
  y_test
1711
- )
1727
+ ),
1728
+ model_estimation_error,
1729
+ model_estimation_error_text
1712
1730
  ]
1713
1731
 
1714
1732
  def fit_metrics(
@@ -1824,8 +1842,7 @@ def fit_metrics(
1824
1842
 
1825
1843
  if any(observed == 0):
1826
1844
 
1827
- if verbose:
1828
- print ("NOTE: Vector 'observed' contains values equal to zero. No APE/MAPE calculated.")
1845
+ print ("WARNING: Vector 'observed' contains values equal to zero. No APE/MAPE calculated.")
1829
1846
 
1830
1847
  APE = np.full_like(observed, np.nan)
1831
1848
  MAPE = np.nan
@@ -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.0
8
- # Last update: 2026-08-04 18:42
7
+ # Version: 2.2.1
8
+ # Last update: 2026-10-17 14:50
9
9
  # Copyright (c) 2025-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -305,7 +305,6 @@ curfew_model_FE.plot(
305
305
 
306
306
  fixed_effects = curfew_model_FE.fixed_effects()
307
307
  # Fixed effects of CHBW_data_model_FE
308
- #print(fixed_effects)
309
308
 
310
309
 
311
310
  # Model with demeaned variables:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffindiff
3
- Version: 2.5.7
3
+ Version: 2.5.8
4
4
  Summary: diffindiff: Python library for convenient Difference-in-Differences analyses
5
5
  Author: Thomas Wieland
6
6
  Author-email: geowieland@googlemail.com
@@ -40,14 +40,14 @@ Thomas Wieland [ORCID](https://orcid.org/0000-0001-5168-9846) [EMail](mailto:geo
40
40
  - 💻 GitHub Repository: [diffindiff_official](https://github.com/geowieland/diffindiff_official)
41
41
  - 📄 DOI (Zenodo): [10.5281/zenodo.18656820](https://doi.org/10.5281/zenodo.18656820)
42
42
 
43
- A case study that utilizes the diffindiff library is available on [arXiv](https://arxiv.org/abs/2605.21464).
43
+ A case study that utilizes the diffindiff library (preprint) is available on [arXiv](https://arxiv.org/abs/2605.21464).
44
44
 
45
45
 
46
46
  ## Citation
47
47
 
48
48
  If you use this software, please cite:
49
49
 
50
- Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.7) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
50
+ Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.8) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
51
51
 
52
52
 
53
53
  ## Installation
@@ -198,9 +198,12 @@ See the /tests directory for usage examples of most of the included functions.
198
198
  This software was developed without the use of AI-generated code. The GitHub Copilot Chat 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).
199
199
 
200
200
 
201
- ## What's new (v2.5.7)
201
+ ## What's new (v2.5.8)
202
202
 
203
203
  - Bugfixes
204
- - Catching RecursionError when model formulas include too many variables
205
- - Checking input data frames for duplicated columns
206
- - Avoiding treatment group deviation in summary when treatment/control groups are identical
204
+ - didtools.is_balanced() now conducts a truly comprehensive check whether the input panel data is balanced
205
+ - Fixed bug in didtools.is_numeric() with respect to unintentional modification of the dataset
206
+ - diddata.create_counterfactual() now catches error if no model was trained by didtools.model_wrapper()
207
+ - didanalysis.DiffModel.plot_treatment_effects() now treats expections correctly
208
+ - Other:
209
+ - verbose option in any function which prints NOTEs, with all NOTEs being disabled when verbose = False
@@ -16,4 +16,5 @@ diffindiff/tests/__init__.py
16
16
  diffindiff/tests/tests_diffindiff.py
17
17
  diffindiff/tests/data/Corona_Hesse.xlsx
18
18
  diffindiff/tests/data/counties_DE.csv
19
- diffindiff/tests/data/curfew_DE.csv
19
+ diffindiff/tests/data/curfew_DE.csv
20
+ tests/test_diffindiff.py
@@ -7,7 +7,7 @@ def read_README():
7
7
 
8
8
  setup(
9
9
  name='diffindiff',
10
- version='2.5.7',
10
+ version='2.5.8',
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,
@@ -0,0 +1,257 @@
1
+ #-----------------------------------------------------------------------
2
+ # Name: tests (diffindiff package)
3
+ # Purpose: Tests for diffindiff package functions
4
+ # Author: Thomas Wieland
5
+ # ORCID: 0000-0001-5168-9846
6
+ # mail: geowieland@googlemail.com
7
+ # Version: 1.0.0
8
+ # Last update: 2026-10-17 15:01
9
+ # Copyright (c) 2026 Thomas Wieland
10
+ #-----------------------------------------------------------------------
11
+
12
+
13
+ from pathlib import Path
14
+ import pandas as pd
15
+
16
+
17
+ def test_import_diffindiff():
18
+ """Test if diffindiff package can be imported."""
19
+
20
+ import diffindiff
21
+ from diffindiff.config import PACKAGE_NAME, PACKAGE_VERSION
22
+
23
+ print(f"This is the {PACKAGE_NAME} package v{PACKAGE_VERSION}")
24
+
25
+
26
+ def test_prepost_analysis():
27
+
28
+ from diffindiff.diddata import create_groups, create_treatment, merge_data, create_data
29
+
30
+ curfew_DE = pd.read_csv(
31
+ Path(__file__).parent / "data" / "curfew_DE.csv",
32
+ sep=";",
33
+ decimal=","
34
+ )
35
+ # Dataset with daily and cumulative SARS-CoV-2 infections of German counties
36
+ # Data source: Wieland (2020) https://doi.org/10.18335/region.v7i2.324
37
+
38
+ curfew_groups=create_groups(
39
+ treatment_group=
40
+ curfew_DE.loc[curfew_DE["Bundesland"].isin([9,10,14])]["county"],
41
+ control_group=
42
+ curfew_DE.loc[~curfew_DE["Bundesland"].isin([9,10,14])]["county"]
43
+ )
44
+ # Creating treatment and control group
45
+ # "Bundesland" (=federal state): 9 = Bavaria, 10 = Saarland, 14 = Saxony
46
+
47
+ assert curfew_groups is not None
48
+
49
+ curfew_groups.summary()
50
+ # Groups summary
51
+
52
+ curfew_treatment_prepost=create_treatment(
53
+ study_period=["2020-03-01", "2020-05-15"],
54
+ treatment_period=["2020-03-21", "2020-05-05"],
55
+ freq = "D",
56
+ pre_post = True
57
+ )
58
+ # Creating treatment
59
+ # Curfew from March 21, 2020, to May, 5, 2020
60
+
61
+ assert curfew_treatment_prepost is not None
62
+
63
+ curfew_treatment_prepost.summary()
64
+ # Treatment summary
65
+
66
+ curfew_data_prepost_merge=merge_data(
67
+ outcome_data=curfew_DE,
68
+ unit_id_col="county",
69
+ time_col="infection_date",
70
+ outcome_col="infections_cum_per100000",
71
+ diff_groups=curfew_groups,
72
+ diff_treatment=curfew_treatment_prepost
73
+ )
74
+
75
+ assert curfew_data_prepost_merge is not None
76
+
77
+ curfew_data_prepost_merge.summary()
78
+ # Summary of created data
79
+
80
+ curfew_data_prepost=create_data(
81
+ outcome_data=curfew_DE,
82
+ unit_id_col="county",
83
+ time_col="infection_date",
84
+ outcome_col="infections_cum_per100000",
85
+ treatment_group=
86
+ curfew_DE.loc[curfew_DE["Bundesland"].isin([9,10,14])]["county"],
87
+ control_group=
88
+ curfew_DE.loc[~curfew_DE["Bundesland"].isin([9,10,14])]["county"],
89
+ study_period=["2020-03-01", "2020-05-15"],
90
+ treatment_period=["2020-03-21", "2020-05-05"],
91
+ freq="D",
92
+ pre_post=True,
93
+ after_treatment_period = False
94
+ )
95
+ # Creating DiD treatement dataset by defining groups and
96
+ # treatment time at once
97
+
98
+ assert curfew_data_prepost is not None
99
+
100
+ curfew_data_prepost.summary()
101
+ # Summary of created data
102
+
103
+ curfew_model_prepost=curfew_data_prepost.analysis(verbose=True)
104
+ # Model analysis of created data
105
+
106
+ assert curfew_model_prepost is not None
107
+
108
+ print(curfew_model_prepost.treatment_effects())
109
+ # Show treatment effects
110
+
111
+ print(curfew_model_prepost.fixed_effects())
112
+ # Show fixed effects (not included in model)
113
+
114
+ print(curfew_model_prepost.covariates())
115
+ # Show covariates (not included in model)
116
+
117
+ curfew_model_prepost.summary()
118
+ # Model summary
119
+
120
+ print(curfew_model_prepost.didmodel().summary())
121
+ # Access statsmodels OLS object
122
+
123
+ print(curfew_model_prepost.fit_metrics())
124
+ # Show model fit metrics
125
+
126
+ curfew_model_prepost.plot(
127
+ x_label="Timepoint",
128
+ y_label="Cumulative infections per 100,000",
129
+ plot_title="Curfew effectiveness pre-post - Groups over time",
130
+ plot_observed=False,
131
+ lines_col=[None,None,"blue","orange"],
132
+ lines_labels=[None,None,"Treatment group","Control group","Treatment group CI","Control group CI"],
133
+ lines_style=[None,None,"solid","solid"]
134
+ )
135
+ # Plot DiD pre vs. post results
136
+ # with user-determined style
137
+
138
+ curfew_model_prepost.plot(
139
+ x_label="Timepoint",
140
+ y_label="Cumulative infections per 100,000",
141
+ plot_title="Curfew effectiveness pre-post - Groups over time",
142
+ lines_col=[None,None,"blue","orange"],
143
+ lines_labels=[None,None,"Treatment group","Control group","Treatment group CI", "Control group CI"],
144
+ pre_post_barplot=True
145
+ )
146
+ # Plot DiD pre vs. post results
147
+ # with user-determined style
148
+
149
+ curfew_model_prepost.plot_treatment_effects(
150
+ x_label="Coefficients with 95% CI",
151
+ plot_title="Curfew effectiveness pre-post - DiD effects"
152
+ )
153
+ # plot effects
154
+
155
+ curfew_model_prepost.plot_treatment_effects(
156
+ x_label="Coefficients with 95% CI",
157
+ plot_title="Curfew effectiveness pre-post - DiD effects",
158
+ scale_plot=False
159
+ )
160
+ # plot effects
161
+
162
+ def test_multiperiod_paneldata_analysis():
163
+
164
+ from diffindiff.diddata import create_data
165
+
166
+ curfew_DE = pd.read_csv(
167
+ Path(__file__).parent / "data" / "curfew_DE.csv",
168
+ sep=";",
169
+ decimal=","
170
+ )
171
+ # Dataset with daily and cumulative SARS-CoV-2 infections of German counties
172
+ # Data source: Wieland (2020) https://doi.org/10.18335/region.v7i2.324
173
+
174
+ curfew_data=create_data(
175
+ outcome_data=curfew_DE,
176
+ unit_id_col="county",
177
+ time_col="infection_date",
178
+ outcome_col="infections_cum_per100000",
179
+ treatment_group=
180
+ curfew_DE.loc[curfew_DE["Bundesland"].isin([9,10,14])]["county"],
181
+ control_group=
182
+ curfew_DE.loc[~curfew_DE["Bundesland"].isin([9,10,14])]["county"],
183
+ treatment_name="Curfew",
184
+ study_period=["2020-03-01", "2020-05-15"],
185
+ treatment_period=["2020-03-21", "2020-05-05"],
186
+ freq="D"
187
+ )
188
+ # Creating DiD dataset by defining groups and treatment time at once
189
+ # Treatment curfew in three federal states: Bavaria (9), Saarland (10) and Saxony (14)
190
+
191
+ assert curfew_data is not None
192
+
193
+ curfew_data.summary()
194
+ # Summary of created treatment data
195
+
196
+ curfew_model=curfew_data.analysis(verbose=True)
197
+ # Model analysis of created data
198
+
199
+ assert curfew_model is not None
200
+
201
+ curfew_model.summary()
202
+ # Model summary
203
+
204
+ curfew_model.plot(
205
+ y_label="Cumulative infections per 100,000",
206
+ plot_title="Curfew effectiveness - Groups over time",
207
+ plot_observed=True
208
+ )
209
+ # Plot observed vs. predicted (means) separated by group (treatment and control)
210
+
211
+ curfew_model.plot_treatment_effects(
212
+ x_label="Coefficients with 95% CI",
213
+ plot_title="Curfew effectiveness - DiD effects"
214
+ )
215
+ # plot effects
216
+
217
+ curfew_placebo = curfew_model.placebo(
218
+ treatment="Curfew"
219
+ )
220
+ # Placebo test with default parameters
221
+
222
+ assert curfew_placebo is not None
223
+
224
+ curfew_placebo.summary()
225
+ # Summary of placebo test
226
+
227
+ # Two-way-fixed-effects model:
228
+
229
+ curfew_model_FE=curfew_data.analysis(
230
+ FE_unit=True,
231
+ FE_time=True,
232
+ demean = True,
233
+ verbose = True
234
+ )
235
+ # Model analysis of created data with fixed effects for
236
+ # units (FE_unit=True) and time (FE_time=True)
237
+ # data is demeaned
238
+
239
+ assert curfew_model_FE is not None
240
+
241
+ curfew_model_FE_summary = curfew_model_FE.summary()
242
+ # Model summary
243
+
244
+ assert curfew_model_FE_summary is not None
245
+
246
+ print(curfew_model_FE.treatment_statistics())
247
+ # Print treatment statistics
248
+
249
+ curfew_model_FE.plot(
250
+ y_label="Cumulative infections per 100,000",
251
+ plot_title="Curfew effectiveness (Two-way FE model) - Groups over time",
252
+ plot_observed=True
253
+ )
254
+ # Plot of treatment and control group
255
+
256
+ fixed_effects = curfew_model_FE.fixed_effects()
257
+ # Fixed effects of CHBW_data_model_FE
File without changes
File without changes