diffindiff 2.4.2__tar.gz → 2.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {diffindiff-2.4.2 → diffindiff-2.5.1}/PKG-INFO +7 -5
  2. {diffindiff-2.4.2 → diffindiff-2.5.1}/README.md +6 -4
  3. {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/config.py +15 -4
  4. {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/didanalysis.py +158 -27
  5. {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/didanalysis_helper.py +23 -5
  6. {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/diddata.py +369 -88
  7. {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/tests/tests_diffindiff.py +60 -3
  8. {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff.egg-info/PKG-INFO +7 -5
  9. diffindiff-2.5.1/diffindiff.egg-info/requires.txt +14 -0
  10. {diffindiff-2.4.2 → diffindiff-2.5.1}/setup.py +14 -9
  11. diffindiff-2.4.2/diffindiff.egg-info/requires.txt +0 -9
  12. {diffindiff-2.4.2 → diffindiff-2.5.1}/MANIFEST.in +0 -0
  13. {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/__init__.py +0 -0
  14. {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/didtools.py +0 -0
  15. {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/tests/__init__.py +0 -0
  16. {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/tests/data/Corona_Hesse.xlsx +0 -0
  17. {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/tests/data/counties_DE.csv +0 -0
  18. {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff/tests/data/curfew_DE.csv +0 -0
  19. {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff.egg-info/SOURCES.txt +0 -0
  20. {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff.egg-info/dependency_links.txt +0 -0
  21. {diffindiff-2.4.2 → diffindiff-2.5.1}/diffindiff.egg-info/top_level.txt +0 -0
  22. {diffindiff-2.4.2 → diffindiff-2.5.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: diffindiff
3
- Version: 2.4.2
3
+ Version: 2.5.1
4
4
  Summary: diffindiff: Python library for convenient Difference-in-Differences analyses
5
5
  Author: Thomas Wieland
6
6
  Author-email: geowieland@googlemail.com
@@ -29,7 +29,7 @@ A research note featuring a case study that utilizes the diffindiff library is a
29
29
 
30
30
  If you use this software, please cite:
31
31
 
32
- Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.4.2) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
32
+ Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.5.1) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
33
33
 
34
34
 
35
35
  ## Installation
@@ -177,7 +177,9 @@ See the /tests directory for usage examples of most of the included functions.
177
177
  This software was developed without the use of AI-generated code. The Continue Agent in Microsoft Visual Studio Code using the GPT-5 mini model (by OpenAI) was used solely to assist in drafting and refining docstrings for documentation. The corresponding guidelines and constraints defined by the author are documented in `AGENTS-docstrings.md` in the [public GitHub repository](https://github.com/geowieland/diffindiff_official).
178
178
 
179
179
 
180
- ## What's new (v2.4.2)
180
+ ## What's new (v2.5.1)
181
181
 
182
- - Bugfixes:
183
- - DiffData.add_own_counterfactual() now works correctly in any case
182
+ - Bugfixes
183
+ - In DiffData.create_treatment() and DiffData.create_data(), pre-post designs with after-treatment period (follow-up) now work correctly
184
+ - DiffModel.plot() nows supports pre-post designs with after-treatment period
185
+ - Some small stabilizations
@@ -21,7 +21,7 @@ A research note featuring a case study that utilizes the diffindiff library is a
21
21
 
22
22
  If you use this software, please cite:
23
23
 
24
- Wieland, T. (2026). diffindiff: A Python library for convenient difference-in-differences analyses (Version 2.4.2) [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.1) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
25
25
 
26
26
 
27
27
  ## Installation
@@ -169,7 +169,9 @@ See the /tests directory for usage examples of most of the included functions.
169
169
  This software was developed without the use of AI-generated code. The Continue Agent in Microsoft Visual Studio Code using the GPT-5 mini model (by OpenAI) was used solely to assist in drafting and refining docstrings for documentation. The corresponding guidelines and constraints defined by the author are documented in `AGENTS-docstrings.md` in the [public GitHub repository](https://github.com/geowieland/diffindiff_official).
170
170
 
171
171
 
172
- ## What's new (v2.4.2)
172
+ ## What's new (v2.5.1)
173
173
 
174
- - Bugfixes:
175
- - DiffData.add_own_counterfactual() now works correctly in any case
174
+ - Bugfixes
175
+ - In DiffData.create_treatment() and DiffData.create_data(), pre-post designs with after-treatment period (follow-up) now work correctly
176
+ - DiffModel.plot() nows supports pre-post designs with after-treatment period
177
+ - Some small stabilizations
@@ -4,15 +4,15 @@
4
4
  # Author: Thomas Wieland
5
5
  # ORCID: 0000-0001-5168-9846
6
6
  # mail: geowieland@googlemail.com
7
- # Version: 1.0.20
8
- # Last update: 2026-06-28 12:31
7
+ # Version: 1.0.22
8
+ # Last update: 2026-07-10 11:36
9
9
  # Copyright (c) 2025-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
12
12
  # Basic config:
13
13
 
14
14
  PACKAGE_NAME = "diffindiff"
15
- PACKAGE_VERSION = "2.4.2"
15
+ PACKAGE_VERSION = "2.5.1"
16
16
 
17
17
  VERBOSE = False
18
18
 
@@ -21,6 +21,11 @@ ROUND_PERCENT = 2
21
21
 
22
22
  AUTO_SWITCH_TO_PREPOST = True
23
23
 
24
+ AUTO_SWITCH_TO_FIXED_EFFECTS = True
25
+ FIXED_EFFECTS_THRESHOLD = 3
26
+
27
+ AUTO_SKIP_CONSTANT_COLUMNS = True
28
+
24
29
  ACCEPT_CONTINUOUS_TREATMENTS = True
25
30
 
26
31
  # Description texts:
@@ -47,6 +52,7 @@ TREATMENT_PERIOD_DESCRIPTION = f"{TREATMENT_DESCRIPTION} period"
47
52
  STUDY_PERIOD_DESCRIPTION = "Study period"
48
53
  PREPOST_DESCRIPTION = "Pre-post"
49
54
  AFTER_TREATMENT_PERIOD_DESCRIPTION = "After-treatment period"
55
+ FOLLOW_UP_DESCRIPTION = "Follow-up"
50
56
 
51
57
  UNITS_DESCRIPTION = "Units"
52
58
 
@@ -96,6 +102,7 @@ LOG_PREFIX = "log"
96
102
  OBSERVED_SUFFIX = "observed"
97
103
  EXPECTED_SUFFIX = "expected"
98
104
  DEMEAN_SUFFIX = "demean"
105
+ WEIGHTED_SUFFIX = "_weighted"
99
106
  PREDICTED_SUFFIX = "pred"
100
107
  CI_LOWER_SUFFIX = "CI_lower"
101
108
  CI_UPPER_SUFFIX = "CI_upper"
@@ -500,7 +507,11 @@ DATA_DIAGNOSTICS = {
500
507
  "description": N_DESCRIPTION,
501
508
  "show_in_summary": True
502
509
  },
503
- }
510
+ "cols_constants": {
511
+ "description": "Columns with constant values",
512
+ "show_in_summary": False
513
+ },
514
+ }
504
515
 
505
516
  DIAGNOSTICS_COLUMN = "Result"
506
517
 
@@ -4,8 +4,8 @@
4
4
  # Author: Thomas Wieland
5
5
  # ORCID: 0000-0001-5168-9846
6
6
  # mail: geowieland@googlemail.com
7
- # Version: 2.4.0
8
- # Last update: 2026-05-01 09:52
7
+ # Version: 2.4.2
8
+ # Last update: 2026-07-10 11:30
9
9
  # Copyright (c) 2024-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -735,16 +735,20 @@ class DiffModel:
735
735
  treatment_effects_df = self.treatment_effects()
736
736
 
737
737
  width = treatment_effects_df[""].str.len().max()
738
- treatment_effects_df[""] = treatment_effects_df[""].str.ljust(width)
739
- total_width = (sum(treatment_effects_df.astype(str).map(len).max()) + len(treatment_effects_df.columns) * 2)
738
+ treatment_effects_df[""] = treatment_effects_df[""].str.ljust(width)
740
739
 
741
- print("=" * total_width)
740
+ total_width = (
741
+ treatment_effects_df.apply(lambda col: col.astype(str).str.len().max()).sum()
742
+ + len(treatment_effects_df.columns) * 2
743
+ )
744
+
745
+ print("=" * int(total_width))
742
746
  print(model_config["analysis_description"])
743
- print("-" * total_width)
747
+ print("-" * int(total_width))
744
748
 
745
749
  print(config.TREATMENT_EFFECTS_DESCRIPTION)
746
750
  print(treatment_effects_df.to_string(index=False))
747
- print("-" * total_width)
751
+ print("-" * int(total_width))
748
752
 
749
753
  covariates_effects_df = pd.DataFrame(
750
754
  [
@@ -761,7 +765,7 @@ class DiffModel:
761
765
  covariates_effects_df[config.COVARIATES_DESCRIPTION] = covariates_effects_df[config.COVARIATES_DESCRIPTION].str.ljust(width)
762
766
 
763
767
  print(covariates_effects_df.to_string(index=False, header=False))
764
- print("-" * total_width)
768
+ print("-" * int(total_width))
765
769
 
766
770
  treatment_diagnostics = self.treatment_diagnostics()
767
771
  treatment_diagnostics_df = treatment_diagnostics[0]
@@ -782,7 +786,7 @@ class DiffModel:
782
786
  else:
783
787
  print(f"NOTE: Treatments {', '.join(no_control_conditions)} have no control conditions.")
784
788
 
785
- print("-" * total_width)
789
+ print("-" * int(total_width))
786
790
 
787
791
  data_diagnostics_df = self.data_diagnostics()
788
792
 
@@ -792,7 +796,7 @@ class DiffModel:
792
796
  print(config.DATA_DIAGNOSTICS_DESCRIPTION)
793
797
  print(data_diagnostics_df.to_string(index=False, header=False))
794
798
 
795
- print("-" * total_width)
799
+ print("-" * int(total_width))
796
800
 
797
801
  model_fit_metrics = self.fit_metrics()
798
802
 
@@ -804,7 +808,7 @@ class DiffModel:
804
808
  print(f"{config.MODEL_FIT_METRICS_DESCRIPTION}s")
805
809
  print(model_fit_metrics.to_string(index=False, header=False))
806
810
 
807
- print("=" * total_width)
811
+ print("=" * int(total_width))
808
812
 
809
813
  return self
810
814
 
@@ -894,6 +898,49 @@ class DiffModel:
894
898
 
895
899
  treatment_effects = self.treatment_effects()
896
900
 
901
+ try:
902
+ if model_config.get("pre_post", False) and len(model_config.get("after_treatment_col", [])) > 0:
903
+
904
+ labels = treatment_effects.iloc[:, 0].astype(str).values
905
+ has_after = any((config.AFTER_TREATMENT_PERIOD_DESCRIPTION in lab) or (config.ATT_COL in lab) for lab in labels)
906
+ if not has_after:
907
+ model_results = self.data[0]
908
+
909
+ aate_key = config.EFFECTS_TYPES["AATE"]["model_results_key"] if "AATE" in config.EFFECTS_TYPES else "average_after_treatment_effects"
910
+ att_key = config.EFFECTS_TYPES["ATT"]["model_results_key"] if "ATT" in config.EFFECTS_TYPES else "after_treatment_time_effects"
911
+ appended = False
912
+
913
+ for key_try in [aate_key, att_key]:
914
+ if key_try in model_results and isinstance(model_results[key_try], dict):
915
+ for subkey, val in model_results[key_try].items():
916
+
917
+ if isinstance(val, dict):
918
+
919
+ coef_k = config.OLS_MODEL_RESULTS["coef"]["model_results_key"]
920
+ se_k = config.OLS_MODEL_RESULTS["coef_standard_errors"]["model_results_key"]
921
+ t_k = config.OLS_MODEL_RESULTS["coef_teststatistic"]["model_results_key"]
922
+ p_k = config.OLS_MODEL_RESULTS["coef_p"]["model_results_key"]
923
+ cil_k = config.OLS_MODEL_RESULTS["coef_confint_lower"]["model_results_key"]
924
+ ciu_k = config.OLS_MODEL_RESULTS["coef_confint_upper"]["model_results_key"]
925
+
926
+ new_row = {
927
+ "": f"{config.EFFECTS_TYPES.get('AATE', {'description': 'After-treatment'})['description']} {subkey}",
928
+ "Estimate": val.get(coef_k, np.nan),
929
+ "SE": val.get(se_k, np.nan),
930
+ "t": val.get(t_k, np.nan),
931
+ "p": val.get(p_k, np.nan),
932
+ "CI lower": val.get(cil_k, np.nan),
933
+ "CI upper": val.get(ciu_k, np.nan),
934
+ }
935
+ treatment_effects = pd.concat([treatment_effects, pd.DataFrame([new_row])], ignore_index=True)
936
+ appended = True
937
+ break
938
+ if appended:
939
+ break
940
+ except Exception:
941
+
942
+ pass
943
+
897
944
  if sort_by_coef:
898
945
  treatment_effects = treatment_effects.sort_values(
899
946
  by = treatment_effects.columns[1],
@@ -1803,14 +1850,14 @@ class DiffModel:
1803
1850
  model_data_TG_CG["t"] = pd.to_datetime(model_data_TG_CG["t"])
1804
1851
 
1805
1852
  if not model_config["pre_post"]:
1806
- pre_post_barplot = False
1853
+ pre_post_barplot = False
1807
1854
 
1808
1855
  if pre_post_barplot:
1809
1856
 
1810
1857
  x_pos_t1_TG = 0
1811
1858
  x_pos_t1_CG = x_pos_t1_TG + pre_post_bar_width
1812
1859
  x_pos_t2_TG = 1.5
1813
- x_pos_t2_CG = x_pos_t2_TG + pre_post_bar_width
1860
+ x_pos_t2_CG = x_pos_t2_TG + pre_post_bar_width
1814
1861
 
1815
1862
  plt.bar(
1816
1863
  x = x_pos_t1_TG,
@@ -1838,6 +1885,24 @@ class DiffModel:
1838
1885
  color=lines_col[3],
1839
1886
  width = pre_post_bar_width
1840
1887
  )
1888
+
1889
+ if len(model_data_TG_CG) == 3:
1890
+
1891
+ x_pos_t3_TG = 3
1892
+ x_pos_t3_CG = x_pos_t3_TG + pre_post_bar_width
1893
+
1894
+ plt.bar(
1895
+ x = x_pos_t3_TG,
1896
+ height = model_data_TG_CG[outcome_col_expected_TG][2],
1897
+ color = lines_col[2],
1898
+ width = pre_post_bar_width
1899
+ )
1900
+ plt.bar(
1901
+ x = x_pos_t3_CG,
1902
+ height = model_data_TG_CG[outcome_col_expected_CG][2],
1903
+ color=lines_col[3],
1904
+ width = pre_post_bar_width
1905
+ )
1841
1906
 
1842
1907
  plt.xlabel(x_label)
1843
1908
  plt.ylabel(y_label)
@@ -1931,16 +1996,29 @@ class DiffModel:
1931
1996
  ax.xaxis.set_major_formatter(DateFormatter(model_config["date_format"]))
1932
1997
 
1933
1998
  if model_config["pre_post"]:
1999
+
2000
+ ticks_line = model_data_TG_CG["t"].unique()
2001
+ ticks_bars = [0.25, 1.75]
2002
+
2003
+ if len(model_data_TG_CG) == 3:
2004
+
2005
+ if len(pre_post_ticks) < 3:
2006
+ pre_post_ticks.append("Follow-up")
2007
+
2008
+ ticks_bars.append(3.25)
2009
+
1934
2010
  if not pre_post_barplot:
2011
+
1935
2012
  plt.xticks(
1936
- model_data_TG_CG["t"].unique(),
1937
- labels = [pre_post_ticks[0], pre_post_ticks[1]]
2013
+ ticks_line,
2014
+ labels = pre_post_ticks
1938
2015
  )
1939
2016
  else:
1940
2017
  plt.xticks(
1941
- [0.25, 1.75],
1942
- labels = [pre_post_ticks[0], pre_post_ticks[1]]
1943
- )
2018
+ ticks_bars,
2019
+ labels = pre_post_ticks
2020
+ )
2021
+
1944
2022
  else:
1945
2023
  plt.xticks(rotation=90)
1946
2024
 
@@ -2389,16 +2467,37 @@ def did_analysis(
2389
2467
  verbose=verbose
2390
2468
  )
2391
2469
  treatment_diagnostics = treatment_diagnostics_results[0]
2392
- staggered_adoption = treatment_diagnostics_results[1]
2470
+ staggered_adoption = treatment_diagnostics_results[1]
2471
+ unique_units = treatment_diagnostics_results[3]
2472
+ unique_time_points = treatment_diagnostics_results[4]
2393
2473
 
2394
2474
  if no_treatments > 1:
2395
2475
 
2396
2476
  intercept = False
2397
2477
  TG_col = []
2398
2478
 
2399
- if not FE_unit:
2479
+ if not FE_unit and config.AUTO_SWITCH_TO_FIXED_EFFECTS:
2400
2480
  FE_unit = True
2401
- print("NOTE: Quasi-experiment includes more than one treatment. Unit fixed effects are used instead of control group baseline and treatment group deviation.")
2481
+ print(f"NOTE: Quasi-experiment includes more than one treatment ({no_treatments}). Unit fixed effects are used instead of control group baseline and treatment group deviation.")
2482
+
2483
+ if unique_units < config.FIXED_EFFECTS_THRESHOLD:
2484
+
2485
+ FE_unit = False
2486
+ FE_group = False
2487
+ ITE = False
2488
+ ITT = False
2489
+ intercept = True
2490
+
2491
+ print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} units. Unit or group fixed effects are skipped.")
2492
+
2493
+ if unique_time_points < config.FIXED_EFFECTS_THRESHOLD:
2494
+
2495
+ FE_time = False
2496
+ ITT = False
2497
+ GTT = False
2498
+ intercept = True
2499
+
2500
+ print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} time points. Time fixed effects are skipped.")
2402
2501
 
2403
2502
  if demean and FE_unit:
2404
2503
 
@@ -2435,7 +2534,7 @@ def did_analysis(
2435
2534
  GTT = False
2436
2535
  print("NOTE: Both group and individual time trends were stated. Switching to individual time trends only.")
2437
2536
 
2438
- if staggered_adoption:
2537
+ if staggered_adoption and config.AUTO_SWITCH_TO_FIXED_EFFECTS:
2439
2538
 
2440
2539
  if not FE_unit or not FE_time:
2441
2540
  print("NOTE: Quasi-experiment includes one or more staggered treatments. Two-way fixed effects model is used.")
@@ -2562,7 +2661,15 @@ def did_analysis(
2562
2661
  missing_replace_by_zero = missing_replace_by_zero,
2563
2662
  verbose = verbose
2564
2663
  )
2664
+
2665
+ if len(data_diagnostics["cols_constants"]) > 0:
2565
2666
 
2667
+ if config.AUTO_SKIP_CONSTANT_COLUMNS:
2668
+
2669
+ print(f"NOTE: The following columns are constant and dropped from the analysis: {', '.join(data_diagnostics['cols_constants'])}.")
2670
+
2671
+ covariates = [col for col in covariates if col not in data_diagnostics["cols_constants"]]
2672
+
2566
2673
  if data_diagnostics["is_prepost"] and config.AUTO_SWITCH_TO_PREPOST:
2567
2674
 
2568
2675
  print(f"NOTE: Input is {config.PREPOST_PANELDATA_DESCRIPTION}. Data processing and model estimation will treat data as pre-post.")
@@ -2611,6 +2718,8 @@ def did_analysis(
2611
2718
  FE_time_vars = []
2612
2719
  dummy_time_original = []
2613
2720
 
2721
+ drop_first = intercept
2722
+
2614
2723
  if not demean:
2615
2724
 
2616
2725
  if FE_unit:
@@ -2619,7 +2728,7 @@ def did_analysis(
2619
2728
  data = data,
2620
2729
  col = unit_col,
2621
2730
  type = "unit",
2622
- drop_first = intercept,
2731
+ drop_first = drop_first,
2623
2732
  verbose = verbose
2624
2733
  )
2625
2734
 
@@ -2634,7 +2743,7 @@ def did_analysis(
2634
2743
  data = data,
2635
2744
  col = time_col,
2636
2745
  type = "time",
2637
- drop_first = intercept,
2746
+ drop_first = drop_first,
2638
2747
  verbose = verbose
2639
2748
  )
2640
2749
 
@@ -2658,7 +2767,7 @@ def did_analysis(
2658
2767
  data = data,
2659
2768
  col = group_by,
2660
2769
  type = "group",
2661
- drop_first = intercept,
2770
+ drop_first = drop_first,
2662
2771
  verbose = verbose
2663
2772
  )
2664
2773
 
@@ -3115,7 +3224,21 @@ def ddd_analysis(
3115
3224
  verbose=verbose
3116
3225
  )
3117
3226
  treatment_diagnostics = treatment_diagnostics_results[0]
3118
-
3227
+ unique_units = treatment_diagnostics_results[3]
3228
+ unique_time_points = treatment_diagnostics_results[4]
3229
+
3230
+ if unique_units < config.FIXED_EFFECTS_THRESHOLD:
3231
+
3232
+ FE_unit = False
3233
+
3234
+ print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} units. Unit fixed effects are skipped.")
3235
+
3236
+ if unique_time_points < config.FIXED_EFFECTS_THRESHOLD:
3237
+
3238
+ FE_time = False
3239
+
3240
+ print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} time points. Time fixed effects are skipped.")
3241
+
3119
3242
  if FE_unit:
3120
3243
  TG_col = None
3121
3244
  if FE_time:
@@ -3140,7 +3263,15 @@ def ddd_analysis(
3140
3263
  missing_replace_by_zero = missing_replace_by_zero,
3141
3264
  verbose = verbose
3142
3265
  )
3143
-
3266
+
3267
+ if len(data_diagnostics["cols_constants"]) > 0:
3268
+
3269
+ if config.AUTO_SKIP_CONSTANT_COLUMNS:
3270
+
3271
+ print(f"NOTE: The following columns are constant and dropped from the analysis: {', '.join(data_diagnostics['cols_constants'])}.")
3272
+
3273
+ covariates = [col for col in covariates if col not in data_diagnostics["cols_constants"]]
3274
+
3144
3275
  if data_diagnostics["is_prepost"] and config.AUTO_SWITCH_TO_PREPOST:
3145
3276
 
3146
3277
  print("NOTE: Panel data is pre-post. Data processing and model estimation will treat data as pre-post")
@@ -4,8 +4,8 @@
4
4
  # Author: Thomas Wieland
5
5
  # ORCID: 0000-0001-5168-9846
6
6
  # mail: geowieland@googlemail.com
7
- # Version: 1.2.1
8
- # Last update: 2025-06 28 13:03
7
+ # Version: 1.2.2
8
+ # Last update: 2026-07-02 20:18
9
9
  # Copyright (c) 2025-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -876,6 +876,12 @@ def data_diagnostics(
876
876
  outcome_mean = np.mean(data[outcome_col])
877
877
  outcome_sd = np.std(data[outcome_col])
878
878
 
879
+ cols_constants = []
880
+ if len(other_cols_relevant) > 0:
881
+ for col in other_cols_relevant:
882
+ if data[col].nunique() == 1:
883
+ cols_constants.append(col)
884
+
879
885
  data_diagnostics_results = {
880
886
  list(config.DATA_DIAGNOSTICS)[0]: bool(modeldata_isbalanced),
881
887
  list(config.DATA_DIAGNOSTICS)[1]: bool(modeldata_ismissing[0]),
@@ -885,7 +891,8 @@ def data_diagnostics(
885
891
  list(config.DATA_DIAGNOSTICS)[5]: data_type,
886
892
  list(config.DATA_DIAGNOSTICS)[6]: outcome_col,
887
893
  list(config.DATA_DIAGNOSTICS)[7]: f"Mean={round(outcome_mean, config.ROUND_STATISTIC)} SD={round(outcome_sd, config.ROUND_STATISTIC)}",
888
- list(config.DATA_DIAGNOSTICS)[8]: observations,
894
+ list(config.DATA_DIAGNOSTICS)[8]: observations,
895
+ list(config.DATA_DIAGNOSTICS)[9]: cols_constants
889
896
  }
890
897
 
891
898
  return data_diagnostics_results
@@ -927,7 +934,7 @@ def treatment_diagnostics(
927
934
  Returns
928
935
  -------
929
936
  list
930
- [treatment_diagnostics_results (dict), staggered_adoption (bool), untreated (list)]
937
+ [treatment_diagnostics_results (dict), staggered_adoption (bool), untreated (list), unique_units (int), unique_timepoints (int)]
931
938
 
932
939
  Examples
933
940
  --------
@@ -1020,6 +1027,15 @@ def treatment_diagnostics(
1020
1027
  verbose = verbose
1021
1028
  )
1022
1029
 
1030
+ groups_units_list = []
1031
+ for key, value in treatment_diagnostics_results.items():
1032
+ groups_units_list.extend(value["treatment_group"])
1033
+ groups_units_list.extend(value["control_group"])
1034
+ groups_units_list = list(set(groups_units_list))
1035
+ unique_units = len(groups_units_list)
1036
+
1037
+ unique_timepoints = data[time_col].nunique()
1038
+
1023
1039
  if verbose:
1024
1040
 
1025
1041
  if no_treatments > 1:
@@ -1030,7 +1046,9 @@ def treatment_diagnostics(
1030
1046
  return [
1031
1047
  treatment_diagnostics_results,
1032
1048
  staggered_adoption,
1033
- untreated
1049
+ untreated,
1050
+ unique_units,
1051
+ unique_timepoints
1034
1052
  ]
1035
1053
 
1036
1054
  def ols_fit(