diffindiff 2.5.0__tar.gz → 2.5.2__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.5.0 → diffindiff-2.5.2}/PKG-INFO +10 -13
  2. {diffindiff-2.5.0 → diffindiff-2.5.2}/README.md +9 -12
  3. {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/config.py +4 -3
  4. {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/didanalysis.py +97 -16
  5. {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/didanalysis_helper.py +2 -2
  6. {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/diddata.py +114 -16
  7. {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/tests/tests_diffindiff.py +59 -3
  8. {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff.egg-info/PKG-INFO +10 -13
  9. {diffindiff-2.5.0 → diffindiff-2.5.2}/setup.py +1 -1
  10. {diffindiff-2.5.0 → diffindiff-2.5.2}/MANIFEST.in +0 -0
  11. {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/__init__.py +0 -0
  12. {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/didtools.py +0 -0
  13. {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/tests/__init__.py +0 -0
  14. {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/tests/data/Corona_Hesse.xlsx +0 -0
  15. {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/tests/data/counties_DE.csv +0 -0
  16. {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff/tests/data/curfew_DE.csv +0 -0
  17. {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff.egg-info/SOURCES.txt +0 -0
  18. {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff.egg-info/dependency_links.txt +0 -0
  19. {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff.egg-info/requires.txt +0 -0
  20. {diffindiff-2.5.0 → diffindiff-2.5.2}/diffindiff.egg-info/top_level.txt +0 -0
  21. {diffindiff-2.5.0 → diffindiff-2.5.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: diffindiff
3
- Version: 2.5.0
3
+ Version: 2.5.2
4
4
  Summary: diffindiff: Python library for convenient Difference-in-Differences analyses
5
5
  Author: Thomas Wieland
6
6
  Author-email: geowieland@googlemail.com
@@ -22,14 +22,14 @@ Thomas Wieland [ORCID](https://orcid.org/0000-0001-5168-9846) [EMail](mailto:geo
22
22
  - 💻 GitHub Repository: [diffindiff_official](https://github.com/geowieland/diffindiff_official)
23
23
  - 📄 DOI (Zenodo): [10.5281/zenodo.18656820](https://doi.org/10.5281/zenodo.18656820)
24
24
 
25
- A research note featuring a case study that utilizes the diffindiff library is available on [arXiv](https://arxiv.org/abs/2605.21464).
25
+ A case study that utilizes the diffindiff library is available on [arXiv](https://arxiv.org/abs/2605.21464).
26
26
 
27
27
 
28
28
  ## Citation
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.5.0) [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.2) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
33
33
 
34
34
 
35
35
  ## Installation
@@ -53,6 +53,7 @@ pip install git+https://github.com/geowieland/diffindiff_official.git
53
53
  - Define custom treatment and control groups as well as treatment periods
54
54
  - Create ready-to-fit DiD data objects
55
55
  - Create predictive counterfactuals
56
+ - Create a synthetic control unit
56
57
  - **DiD analysis**:
57
58
  - Perform standard DiD analysis with pre-post data
58
59
  - Perform DiD analysis with two-way fixed effects models
@@ -66,6 +67,7 @@ pip install git+https://github.com/geowieland/diffindiff_official.git
66
67
  - Including after-treatment period
67
68
  - Perform Triple Difference (DDD) analysis
68
69
  - Perform DiD analysis with demeaned data
70
+ - Perform Synthetic DiD analysis
69
71
  - **Diagnosis tools and extensions of analyses**:
70
72
  - Add own counterfactuals or create counterfactuals based on machine learning or OLS regression models
71
73
  - Bonferroni correction for treatment effects
@@ -177,16 +179,11 @@ See the /tests directory for usage examples of most of the included functions.
177
179
  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
180
 
179
181
 
180
- ## What's new (v2.5.0)
182
+ ## What's new (v2.5.2)
181
183
 
182
- - General
183
- - Update of dependencies to be compatible with Python 3.13 (still works with Python 3.11) while avoiding incompatibility conflict of statsmodels and scipy
184
- - Internal changes in some functions to be compatible with Python 3.13 (still works with Python 3.11)
185
184
  - Extensions
186
- - Creating synthetic control units with DiffData.add_synthetic() and conducting a Synthetic DiD analysis
187
- - didanalysis_helper.treatment_diagnostics() now additionally checks unique number of analysis units and time points
188
- - didanalysis_helper.data_diagnostics() now additionally checks whether covariates are constant; didanalysis.did_analysis() and .ddd_analysis() automatically skip such variables from the model analysis
185
+ - New functions DiffData.get_synthetic_control_weightings() and DiffData.get_synthetic_control_fitmetrics() for accessing control unit weightings and fit metrics of the synthetic control unit
189
186
  - Bugfixes
190
- - DiffData.add_own_counterfactual() now works correctly in any case (also when no. of treatments > 1)
191
- - Correct internal processing of treatment data in DiffTreatment and DiffData objects when treatments were added
192
- - Fixed missing treatment group definition in DiffData.add_treatment()
187
+ - DiffModel.placebo() now checks control group size and returns warning when no. of control units is equal to 1, not allowing any placebo test
188
+ - Other
189
+ - Some updates of documentation and README
@@ -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 research note featuring 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 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.0) [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.2) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
25
25
 
26
26
 
27
27
  ## Installation
@@ -45,6 +45,7 @@ pip install git+https://github.com/geowieland/diffindiff_official.git
45
45
  - Define custom treatment and control groups as well as treatment periods
46
46
  - Create ready-to-fit DiD data objects
47
47
  - Create predictive counterfactuals
48
+ - Create a synthetic control unit
48
49
  - **DiD analysis**:
49
50
  - Perform standard DiD analysis with pre-post data
50
51
  - Perform DiD analysis with two-way fixed effects models
@@ -58,6 +59,7 @@ pip install git+https://github.com/geowieland/diffindiff_official.git
58
59
  - Including after-treatment period
59
60
  - Perform Triple Difference (DDD) analysis
60
61
  - Perform DiD analysis with demeaned data
62
+ - Perform Synthetic DiD analysis
61
63
  - **Diagnosis tools and extensions of analyses**:
62
64
  - Add own counterfactuals or create counterfactuals based on machine learning or OLS regression models
63
65
  - Bonferroni correction for treatment effects
@@ -169,16 +171,11 @@ See the /tests directory for usage examples of most of the included functions.
169
171
  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
172
 
171
173
 
172
- ## What's new (v2.5.0)
174
+ ## What's new (v2.5.2)
173
175
 
174
- - General
175
- - Update of dependencies to be compatible with Python 3.13 (still works with Python 3.11) while avoiding incompatibility conflict of statsmodels and scipy
176
- - Internal changes in some functions to be compatible with Python 3.13 (still works with Python 3.11)
177
176
  - Extensions
178
- - Creating synthetic control units with DiffData.add_synthetic() and conducting a Synthetic DiD analysis
179
- - didanalysis_helper.treatment_diagnostics() now additionally checks unique number of analysis units and time points
180
- - didanalysis_helper.data_diagnostics() now additionally checks whether covariates are constant; didanalysis.did_analysis() and .ddd_analysis() automatically skip such variables from the model analysis
177
+ - New functions DiffData.get_synthetic_control_weightings() and DiffData.get_synthetic_control_fitmetrics() for accessing control unit weightings and fit metrics of the synthetic control unit
181
178
  - Bugfixes
182
- - DiffData.add_own_counterfactual() now works correctly in any case (also when no. of treatments > 1)
183
- - Correct internal processing of treatment data in DiffTreatment and DiffData objects when treatments were added
184
- - Fixed missing treatment group definition in DiffData.add_treatment()
179
+ - DiffModel.placebo() now checks control group size and returns warning when no. of control units is equal to 1, not allowing any placebo test
180
+ - Other
181
+ - Some updates of documentation and README
@@ -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.21
8
- # Last update: 2026-07-03 17:50
7
+ # Version: 1.0.23
8
+ # Last update: 2026-07-14 19:48
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.0"
15
+ PACKAGE_VERSION = "2.5.2"
16
16
 
17
17
  VERBOSE = False
18
18
 
@@ -52,6 +52,7 @@ TREATMENT_PERIOD_DESCRIPTION = f"{TREATMENT_DESCRIPTION} period"
52
52
  STUDY_PERIOD_DESCRIPTION = "Study period"
53
53
  PREPOST_DESCRIPTION = "Pre-post"
54
54
  AFTER_TREATMENT_PERIOD_DESCRIPTION = "After-treatment period"
55
+ FOLLOW_UP_DESCRIPTION = "Follow-up"
55
56
 
56
57
  UNITS_DESCRIPTION = "Units"
57
58
 
@@ -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.1
8
- # Last update: 2026-07-02 20:17
7
+ # Version: 2.4.3
8
+ # Last update: 2026-07-14 19:49
9
9
  # Copyright (c) 2024-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -898,6 +898,49 @@ class DiffModel:
898
898
 
899
899
  treatment_effects = self.treatment_effects()
900
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
+
901
944
  if sort_by_coef:
902
945
  treatment_effects = treatment_effects.sort_values(
903
946
  by = treatment_effects.columns[1],
@@ -1379,10 +1422,15 @@ class DiffModel:
1379
1422
  model_data_c[time_col] = pd.to_datetime(model_data_c[time_col])
1380
1423
  model_data_c[unit_col] = model_data_c[unit_col].astype(str)
1381
1424
 
1382
- units_random_sample = model_data_c[unit_col].sample(
1383
- n = int(round(divide*control_group_N*resample, 0)),
1384
- random_state = random_state
1385
- ).astype(str).tolist()
1425
+ if control_group_N < 2:
1426
+ print(f"WARNING: Model includes one control unit only: '{control_group[0]}'. Placebo analysis requires size of control group >1. Original model is returned.")
1427
+ return self
1428
+
1429
+ else:
1430
+ units_random_sample = model_data_c[unit_col].sample(
1431
+ n = int(round(divide*control_group_N*resample, 0)),
1432
+ random_state = random_state
1433
+ ).astype(str).tolist()
1386
1434
 
1387
1435
  model_data_c[TG_col_] = 0
1388
1436
  model_data_c.loc[(model_data_c[unit_col].isin(units_random_sample)), TG_col_] = 1
@@ -1807,14 +1855,14 @@ class DiffModel:
1807
1855
  model_data_TG_CG["t"] = pd.to_datetime(model_data_TG_CG["t"])
1808
1856
 
1809
1857
  if not model_config["pre_post"]:
1810
- pre_post_barplot = False
1858
+ pre_post_barplot = False
1811
1859
 
1812
1860
  if pre_post_barplot:
1813
1861
 
1814
1862
  x_pos_t1_TG = 0
1815
1863
  x_pos_t1_CG = x_pos_t1_TG + pre_post_bar_width
1816
1864
  x_pos_t2_TG = 1.5
1817
- x_pos_t2_CG = x_pos_t2_TG + pre_post_bar_width
1865
+ x_pos_t2_CG = x_pos_t2_TG + pre_post_bar_width
1818
1866
 
1819
1867
  plt.bar(
1820
1868
  x = x_pos_t1_TG,
@@ -1842,6 +1890,24 @@ class DiffModel:
1842
1890
  color=lines_col[3],
1843
1891
  width = pre_post_bar_width
1844
1892
  )
1893
+
1894
+ if len(model_data_TG_CG) == 3:
1895
+
1896
+ x_pos_t3_TG = 3
1897
+ x_pos_t3_CG = x_pos_t3_TG + pre_post_bar_width
1898
+
1899
+ plt.bar(
1900
+ x = x_pos_t3_TG,
1901
+ height = model_data_TG_CG[outcome_col_expected_TG][2],
1902
+ color = lines_col[2],
1903
+ width = pre_post_bar_width
1904
+ )
1905
+ plt.bar(
1906
+ x = x_pos_t3_CG,
1907
+ height = model_data_TG_CG[outcome_col_expected_CG][2],
1908
+ color=lines_col[3],
1909
+ width = pre_post_bar_width
1910
+ )
1845
1911
 
1846
1912
  plt.xlabel(x_label)
1847
1913
  plt.ylabel(y_label)
@@ -1935,16 +2001,29 @@ class DiffModel:
1935
2001
  ax.xaxis.set_major_formatter(DateFormatter(model_config["date_format"]))
1936
2002
 
1937
2003
  if model_config["pre_post"]:
2004
+
2005
+ ticks_line = model_data_TG_CG["t"].unique()
2006
+ ticks_bars = [0.25, 1.75]
2007
+
2008
+ if len(model_data_TG_CG) == 3:
2009
+
2010
+ if len(pre_post_ticks) < 3:
2011
+ pre_post_ticks.append("Follow-up")
2012
+
2013
+ ticks_bars.append(3.25)
2014
+
1938
2015
  if not pre_post_barplot:
2016
+
1939
2017
  plt.xticks(
1940
- model_data_TG_CG["t"].unique(),
1941
- labels = [pre_post_ticks[0], pre_post_ticks[1]]
2018
+ ticks_line,
2019
+ labels = pre_post_ticks
1942
2020
  )
1943
2021
  else:
1944
2022
  plt.xticks(
1945
- [0.25, 1.75],
1946
- labels = [pre_post_ticks[0], pre_post_ticks[1]]
1947
- )
2023
+ ticks_bars,
2024
+ labels = pre_post_ticks
2025
+ )
2026
+
1948
2027
  else:
1949
2028
  plt.xticks(rotation=90)
1950
2029
 
@@ -2644,6 +2723,8 @@ def did_analysis(
2644
2723
  FE_time_vars = []
2645
2724
  dummy_time_original = []
2646
2725
 
2726
+ drop_first = intercept
2727
+
2647
2728
  if not demean:
2648
2729
 
2649
2730
  if FE_unit:
@@ -2652,7 +2733,7 @@ def did_analysis(
2652
2733
  data = data,
2653
2734
  col = unit_col,
2654
2735
  type = "unit",
2655
- drop_first = intercept,
2736
+ drop_first = drop_first,
2656
2737
  verbose = verbose
2657
2738
  )
2658
2739
 
@@ -2667,7 +2748,7 @@ def did_analysis(
2667
2748
  data = data,
2668
2749
  col = time_col,
2669
2750
  type = "time",
2670
- drop_first = intercept,
2751
+ drop_first = drop_first,
2671
2752
  verbose = verbose
2672
2753
  )
2673
2754
 
@@ -2691,7 +2772,7 @@ def did_analysis(
2691
2772
  data = data,
2692
2773
  col = group_by,
2693
2774
  type = "group",
2694
- drop_first = intercept,
2775
+ drop_first = drop_first,
2695
2776
  verbose = verbose
2696
2777
  )
2697
2778
 
@@ -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.2
8
- # Last update: 2026-07-02 20:18
7
+ # Version: 1.2.3
8
+ # Last update: 2026-07-14 19:44
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.3.0
8
- # Last update: 2026-07-03 18:00
7
+ # Version: 2.3.2
8
+ # Last update: 2026-07-16 18:52
9
9
  # Copyright (c) 2024-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -622,15 +622,36 @@ def create_treatment(
622
622
 
623
623
  if pre_post:
624
624
 
625
- after_treatment_period = False
625
+ if after_treatment_period:
626
+
627
+ if study_period[1] <= treatment_period[1]:
628
+
629
+ print(f"WARNING: after_treatment_period is set True, but end of study period ({study_period[1]}) is <= end of treatment period ({treatment_period[1]}). After treatment period is ignored.")
630
+
631
+ after_treatment_period = False
626
632
 
627
- study_period_range = [study_period[0], study_period[1]]
628
- study_period_N = 2
629
- study_period_counter = [1,2]
633
+ else:
634
+
635
+ study_period_range = [study_period[0], study_period[1]]
636
+ study_period_N = 3
637
+ study_period_counter = [1,2,3]
630
638
 
631
- treatment_period_range = [treatment_period[0], treatment_period[1]]
632
- treatment_period_N = 1
633
- TT_dummies = [0,1]
639
+ treatment_period_N = 1
640
+ TT_dummies = [0,1,0]
641
+ ATT_dummies = [0,0,1]
642
+
643
+ treatment_period_range = [study_period[0], treatment_period[1], study_period[1]]
644
+ after_treatment_period_range = treatment_period_range
645
+
646
+ else:
647
+
648
+ study_period_range = [study_period[0], study_period[1]]
649
+ study_period_N = 2
650
+ study_period_counter = [1,2]
651
+
652
+ treatment_period_range = [treatment_period[0], treatment_period[1]]
653
+ treatment_period_N = 1
654
+ TT_dummies = [0,1]
634
655
 
635
656
  study_period_range = pd.DataFrame (treatment_period_range, columns=[config.TIME_COL])
636
657
  study_period_range[config.TIME_COUNTER_COL] = pd.DataFrame(study_period_counter)
@@ -640,7 +661,7 @@ def create_treatment(
640
661
  TT_col: TT_dummies
641
662
  }
642
663
 
643
- TT_data = pd.DataFrame(TT_data)
664
+ TT_data = pd.DataFrame(TT_data)
644
665
 
645
666
  treatment_period_range = pd.DataFrame(
646
667
  study_period_range
@@ -650,6 +671,24 @@ def create_treatment(
650
671
  TT_data,
651
672
  how = "left"
652
673
  )
674
+
675
+ if after_treatment_period:
676
+
677
+ ATT_data = {
678
+ config.TIME_COL: after_treatment_period_range,
679
+ ATT_col: ATT_dummies
680
+ }
681
+
682
+ ATT_data = pd.DataFrame(ATT_data)
683
+
684
+ after_treatment_period_range = pd.DataFrame (after_treatment_period_range, columns=[config.TIME_COL])
685
+ after_treatment_period_range[config.TIME_COUNTER_COL] = pd.DataFrame(study_period_counter)
686
+
687
+ after_treatment_data_df = after_treatment_period_range.merge(ATT_data, how = "left")
688
+ after_treatment_data_df[ATT_col] = after_treatment_data_df[ATT_col].fillna(0)
689
+ after_treatment_data_df = after_treatment_data_df.drop(columns=[config.TIME_COL, config.TIME_COUNTER_COL])
690
+
691
+ treatment_data_df = pd.concat([treatment_data_df, after_treatment_data_df], axis=1)
653
692
 
654
693
  else:
655
694
 
@@ -689,7 +728,7 @@ def create_treatment(
689
728
 
690
729
  treatment_data_df[TT_col] = treatment_data_df[TT_col].fillna(0)
691
730
 
692
- if after_treatment_period:
731
+ if after_treatment_period and not pre_post:
693
732
 
694
733
  treatment_period_last = datetime.strptime(
695
734
  treatment_period[1],
@@ -720,6 +759,9 @@ def create_treatment(
720
759
 
721
760
  else:
722
761
  after_treatment_period_N = 0
762
+
763
+ if after_treatment_period and pre_post:
764
+ after_treatment_period_N = 1
723
765
 
724
766
  no_treatments = 1
725
767
 
@@ -735,7 +777,7 @@ def create_treatment(
735
777
  "date_format": date_format,
736
778
  "pre_post": pre_post
737
779
  }
738
-
780
+
739
781
  treatment_config = {
740
782
  0: {
741
783
  "treatment_name": treatment_name,
@@ -773,7 +815,8 @@ class DiffData:
773
815
  unit_time_col_original,
774
816
  covariates,
775
817
  treatment_cols,
776
- timestamp
818
+ timestamp,
819
+ synthetic_control
777
820
  ):
778
821
 
779
822
  """
@@ -798,6 +841,8 @@ class DiffData:
798
841
  Mapping of treatment columns metadata.
799
842
  timestamp : dict
800
843
  Timestamp metadata.
844
+ synthetic_control : dict
845
+ If included, data for a synthetic control unit, else None.
801
846
  Returns
802
847
  -------
803
848
  None
@@ -813,7 +858,8 @@ class DiffData:
813
858
  unit_time_col_original,
814
859
  covariates,
815
860
  treatment_cols,
816
- timestamp
861
+ timestamp,
862
+ synthetic_control
817
863
  ]
818
864
 
819
865
  def get_did_modeldata_df (self):
@@ -1018,6 +1064,46 @@ class DiffData:
1018
1064
  """
1019
1065
 
1020
1066
  return self.data[7]
1067
+
1068
+ def get_synthetic_control_weightings(self):
1069
+
1070
+ """
1071
+ Return weightings of the synthetic control unit (if included in the DiffData object).
1072
+
1073
+ Returns
1074
+ -------
1075
+ pandas.DataFrame
1076
+ Weighting by control unit.
1077
+
1078
+ Examples
1079
+ --------
1080
+ >>> my_diffdata.get_synthetic_control_weightings()
1081
+ """
1082
+
1083
+ if len(self.data[8]) > 0:
1084
+ return self.data[8]["weights"]
1085
+ else:
1086
+ print("The DiffData object does not include a synthetic control unit.")
1087
+
1088
+ def get_synthetic_control_fitmetrics(self):
1089
+
1090
+ """
1091
+ Return fit metrics of the synthetic control unit (if included in the DiffData object).
1092
+
1093
+ Returns
1094
+ -------
1095
+ list
1096
+ Fit metrics (dict) and data points with residuals (pandas.DataFrame).
1097
+
1098
+ Examples
1099
+ --------
1100
+ >>> my_diffdata.get_synthetic_control_fitmetrics()
1101
+ """
1102
+
1103
+ if len(self.data[8]) > 0:
1104
+ return self.data[8]["fit_metrics"]
1105
+ else:
1106
+ print("The DiffData object does not include a synthetic control unit.")
1021
1107
 
1022
1108
  def add_covariates(
1023
1109
  self,
@@ -1991,6 +2077,14 @@ class DiffData:
1991
2077
  verbose = False
1992
2078
  )
1993
2079
 
2080
+ self.data[7] = helper.create_timestamp(function="add_synthetic")
2081
+
2082
+ self.data[8] = {
2083
+ "weights": weights_pd,
2084
+ "fit_metrics": synthetic_fit_metrics,
2085
+ "process_unit": process_unit
2086
+ }
2087
+
1994
2088
  return self
1995
2089
 
1996
2090
  def summary(self):
@@ -2050,7 +2144,10 @@ class DiffData:
2050
2144
  else:
2051
2145
  print(f" {config.STUDY_PERIOD_DESCRIPTION:<{config.DIDDATA_SUMMARY_MAX_WIDTH}} {treatment_meta['study_period_start']} - {treatment_meta['study_period_end']} ({treatment_meta['study_period']} {treatment_meta['frequency']})")
2052
2146
  print(f" {config.TREATMENT_PERIOD_DESCRIPTION:<{config.DIDDATA_SUMMARY_MAX_WIDTH}} {treatment_config[key]['treatment_period_start']} - {treatment_config[key]['treatment_period_end']} ({treatment_config[key]['treatment_period']} {treatment_meta['frequency']})")
2053
-
2147
+
2148
+ if value["after_treatment_period"]:
2149
+ print(f" {config.AFTER_TREATMENT_PERIOD_DESCRIPTION:<{config.DIDDATA_SUMMARY_MAX_WIDTH}} {treatment_config[key]['treatment_period_end']} vs. {treatment_meta['study_period_end']} ({config.FOLLOW_UP_DESCRIPTION})")
2150
+
2054
2151
  print("-" * total_width)
2055
2152
 
2056
2153
  print(f"Outcome '{outcome_col_original}'")
@@ -2444,7 +2541,8 @@ def merge_data(
2444
2541
  unit_time_col_original,
2445
2542
  [],
2446
2543
  treatment_cols,
2447
- timestamp
2544
+ timestamp,
2545
+ {}
2448
2546
  )
2449
2547
 
2450
2548
  if verbose:
@@ -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.0.15
8
- # Last update: 2026-05-01 09:54
7
+ # Version: 2.1.0
8
+ # Last update: 2026-07-10 11:35
9
9
  # Copyright (c) 2025-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -75,7 +75,7 @@ curfew_data_prepost=create_data(
75
75
  treatment_period=["2020-03-21", "2020-05-05"],
76
76
  freq="D",
77
77
  pre_post=True,
78
- after_treatment_period = True
78
+ after_treatment_period = False
79
79
  )
80
80
  # Creating DiD treatement dataset by defining groups and
81
81
  # treatment time at once
@@ -98,6 +98,9 @@ print(curfew_model_prepost.covariates())
98
98
  curfew_model_prepost.summary()
99
99
  # Model summary
100
100
 
101
+ print(curfew_model_prepost.didmodel().summary())
102
+ # Access statsmodels OLS object
103
+
101
104
  print(curfew_model_prepost.fit_metrics())
102
105
  # Show model fit metrics
103
106
 
@@ -137,6 +140,59 @@ curfew_model_prepost.plot_treatment_effects(
137
140
  )
138
141
  # plot effects
139
142
 
143
+
144
+ # Same analysis with after-treatment period (follow-up):
145
+
146
+ curfew_data_prepost_AT=create_data(
147
+ outcome_data=curfew_DE,
148
+ unit_id_col="county",
149
+ time_col="infection_date",
150
+ outcome_col="infections_cum_per100000",
151
+ treatment_group=
152
+ curfew_DE.loc[curfew_DE["Bundesland"].isin([9,10,14])]["county"],
153
+ control_group=
154
+ curfew_DE.loc[~curfew_DE["Bundesland"].isin([9,10,14])]["county"],
155
+ study_period=["2020-03-01", "2020-05-15"],
156
+ treatment_period=["2020-03-21", "2020-05-05"],
157
+ freq="D",
158
+ pre_post=True,
159
+ after_treatment_period = True
160
+ )
161
+ # Creating DiD treatement dataset by defining groups and
162
+ # treatment time at once
163
+
164
+ curfew_data_prepost_AT.summary()
165
+ # Summary of created data
166
+
167
+ curfew_model_prepost_AT=curfew_data_prepost_AT.analysis(verbose=True)
168
+ # Model analysis of created data
169
+
170
+ curfew_model_prepost_AT.summary()
171
+ # Model summary
172
+
173
+ curfew_model_prepost_AT.plot(
174
+ x_label="Timepoint",
175
+ y_label="Cumulative infections per 100,000",
176
+ plot_title="Curfew effectiveness pre-post - Groups over time",
177
+ plot_observed=False,
178
+ lines_col=[None,None,"blue","orange"],
179
+ lines_labels=[None,None,"Treatment group","Control group","Treatment group CI","Control group CI"],
180
+ lines_style=[None,None,"solid","solid"]
181
+ )
182
+ # Plot DiD pre vs. post results
183
+ # with user-determined style
184
+
185
+ curfew_model_prepost_AT.plot(
186
+ x_label="Timepoint",
187
+ y_label="Cumulative infections per 100,000",
188
+ plot_title="Curfew effectiveness pre-post - Groups over time",
189
+ lines_col=[None,None,"blue","orange"],
190
+ lines_labels=[None,None,"Treatment group","Control group","Treatment group CI", "Control group CI"],
191
+ pre_post_barplot=True
192
+ )
193
+ # Plot DiD pre vs. post results
194
+ # with user-determined style
195
+
140
196
  counties_DE=pd.read_csv("data/counties_DE.csv", sep=";", decimal=",", encoding='latin1')
141
197
  # Dataset with German county data
142
198
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: diffindiff
3
- Version: 2.5.0
3
+ Version: 2.5.2
4
4
  Summary: diffindiff: Python library for convenient Difference-in-Differences analyses
5
5
  Author: Thomas Wieland
6
6
  Author-email: geowieland@googlemail.com
@@ -22,14 +22,14 @@ Thomas Wieland [ORCID](https://orcid.org/0000-0001-5168-9846) [EMail](mailto:geo
22
22
  - 💻 GitHub Repository: [diffindiff_official](https://github.com/geowieland/diffindiff_official)
23
23
  - 📄 DOI (Zenodo): [10.5281/zenodo.18656820](https://doi.org/10.5281/zenodo.18656820)
24
24
 
25
- A research note featuring a case study that utilizes the diffindiff library is available on [arXiv](https://arxiv.org/abs/2605.21464).
25
+ A case study that utilizes the diffindiff library is available on [arXiv](https://arxiv.org/abs/2605.21464).
26
26
 
27
27
 
28
28
  ## Citation
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.5.0) [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.2) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
33
33
 
34
34
 
35
35
  ## Installation
@@ -53,6 +53,7 @@ pip install git+https://github.com/geowieland/diffindiff_official.git
53
53
  - Define custom treatment and control groups as well as treatment periods
54
54
  - Create ready-to-fit DiD data objects
55
55
  - Create predictive counterfactuals
56
+ - Create a synthetic control unit
56
57
  - **DiD analysis**:
57
58
  - Perform standard DiD analysis with pre-post data
58
59
  - Perform DiD analysis with two-way fixed effects models
@@ -66,6 +67,7 @@ pip install git+https://github.com/geowieland/diffindiff_official.git
66
67
  - Including after-treatment period
67
68
  - Perform Triple Difference (DDD) analysis
68
69
  - Perform DiD analysis with demeaned data
70
+ - Perform Synthetic DiD analysis
69
71
  - **Diagnosis tools and extensions of analyses**:
70
72
  - Add own counterfactuals or create counterfactuals based on machine learning or OLS regression models
71
73
  - Bonferroni correction for treatment effects
@@ -177,16 +179,11 @@ See the /tests directory for usage examples of most of the included functions.
177
179
  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
180
 
179
181
 
180
- ## What's new (v2.5.0)
182
+ ## What's new (v2.5.2)
181
183
 
182
- - General
183
- - Update of dependencies to be compatible with Python 3.13 (still works with Python 3.11) while avoiding incompatibility conflict of statsmodels and scipy
184
- - Internal changes in some functions to be compatible with Python 3.13 (still works with Python 3.11)
185
184
  - Extensions
186
- - Creating synthetic control units with DiffData.add_synthetic() and conducting a Synthetic DiD analysis
187
- - didanalysis_helper.treatment_diagnostics() now additionally checks unique number of analysis units and time points
188
- - didanalysis_helper.data_diagnostics() now additionally checks whether covariates are constant; didanalysis.did_analysis() and .ddd_analysis() automatically skip such variables from the model analysis
185
+ - New functions DiffData.get_synthetic_control_weightings() and DiffData.get_synthetic_control_fitmetrics() for accessing control unit weightings and fit metrics of the synthetic control unit
189
186
  - Bugfixes
190
- - DiffData.add_own_counterfactual() now works correctly in any case (also when no. of treatments > 1)
191
- - Correct internal processing of treatment data in DiffTreatment and DiffData objects when treatments were added
192
- - Fixed missing treatment group definition in DiffData.add_treatment()
187
+ - DiffModel.placebo() now checks control group size and returns warning when no. of control units is equal to 1, not allowing any placebo test
188
+ - Other
189
+ - Some updates of documentation and README
@@ -7,7 +7,7 @@ def read_README():
7
7
 
8
8
  setup(
9
9
  name='diffindiff',
10
- version='2.5.0',
10
+ version='2.5.2',
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