diffindiff 2.4.2__tar.gz → 2.5.0__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.0}/PKG-INFO +14 -5
  2. {diffindiff-2.4.2 → diffindiff-2.5.0}/README.md +13 -4
  3. {diffindiff-2.4.2 → diffindiff-2.5.0}/diffindiff/config.py +14 -4
  4. {diffindiff-2.4.2 → diffindiff-2.5.0}/diffindiff/didanalysis.py +72 -17
  5. {diffindiff-2.4.2 → diffindiff-2.5.0}/diffindiff/didanalysis_helper.py +23 -5
  6. {diffindiff-2.4.2 → diffindiff-2.5.0}/diffindiff/diddata.py +313 -77
  7. {diffindiff-2.4.2 → diffindiff-2.5.0}/diffindiff/tests/tests_diffindiff.py +2 -1
  8. {diffindiff-2.4.2 → diffindiff-2.5.0}/diffindiff.egg-info/PKG-INFO +14 -5
  9. diffindiff-2.5.0/diffindiff.egg-info/requires.txt +14 -0
  10. {diffindiff-2.4.2 → diffindiff-2.5.0}/setup.py +14 -9
  11. diffindiff-2.4.2/diffindiff.egg-info/requires.txt +0 -9
  12. {diffindiff-2.4.2 → diffindiff-2.5.0}/MANIFEST.in +0 -0
  13. {diffindiff-2.4.2 → diffindiff-2.5.0}/diffindiff/__init__.py +0 -0
  14. {diffindiff-2.4.2 → diffindiff-2.5.0}/diffindiff/didtools.py +0 -0
  15. {diffindiff-2.4.2 → diffindiff-2.5.0}/diffindiff/tests/__init__.py +0 -0
  16. {diffindiff-2.4.2 → diffindiff-2.5.0}/diffindiff/tests/data/Corona_Hesse.xlsx +0 -0
  17. {diffindiff-2.4.2 → diffindiff-2.5.0}/diffindiff/tests/data/counties_DE.csv +0 -0
  18. {diffindiff-2.4.2 → diffindiff-2.5.0}/diffindiff/tests/data/curfew_DE.csv +0 -0
  19. {diffindiff-2.4.2 → diffindiff-2.5.0}/diffindiff.egg-info/SOURCES.txt +0 -0
  20. {diffindiff-2.4.2 → diffindiff-2.5.0}/diffindiff.egg-info/dependency_links.txt +0 -0
  21. {diffindiff-2.4.2 → diffindiff-2.5.0}/diffindiff.egg-info/top_level.txt +0 -0
  22. {diffindiff-2.4.2 → diffindiff-2.5.0}/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.0
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.0) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
33
33
 
34
34
 
35
35
  ## Installation
@@ -177,7 +177,16 @@ 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.0)
181
181
 
182
- - Bugfixes:
183
- - DiffData.add_own_counterfactual() now works correctly in any case
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
+ - 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
189
+ - 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()
@@ -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.0) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
25
25
 
26
26
 
27
27
  ## Installation
@@ -169,7 +169,16 @@ 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.0)
173
173
 
174
- - Bugfixes:
175
- - DiffData.add_own_counterfactual() now works correctly in any case
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
+ - 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
181
+ - 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()
@@ -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.21
8
+ # Last update: 2026-07-03 17:50
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.0"
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:
@@ -96,6 +101,7 @@ LOG_PREFIX = "log"
96
101
  OBSERVED_SUFFIX = "observed"
97
102
  EXPECTED_SUFFIX = "expected"
98
103
  DEMEAN_SUFFIX = "demean"
104
+ WEIGHTED_SUFFIX = "_weighted"
99
105
  PREDICTED_SUFFIX = "pred"
100
106
  CI_LOWER_SUFFIX = "CI_lower"
101
107
  CI_UPPER_SUFFIX = "CI_upper"
@@ -500,7 +506,11 @@ DATA_DIAGNOSTICS = {
500
506
  "description": N_DESCRIPTION,
501
507
  "show_in_summary": True
502
508
  },
503
- }
509
+ "cols_constants": {
510
+ "description": "Columns with constant values",
511
+ "show_in_summary": False
512
+ },
513
+ }
504
514
 
505
515
  DIAGNOSTICS_COLUMN = "Result"
506
516
 
@@ -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.1
8
+ # Last update: 2026-07-02 20:17
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
 
@@ -2389,16 +2393,37 @@ def did_analysis(
2389
2393
  verbose=verbose
2390
2394
  )
2391
2395
  treatment_diagnostics = treatment_diagnostics_results[0]
2392
- staggered_adoption = treatment_diagnostics_results[1]
2396
+ staggered_adoption = treatment_diagnostics_results[1]
2397
+ unique_units = treatment_diagnostics_results[3]
2398
+ unique_time_points = treatment_diagnostics_results[4]
2393
2399
 
2394
2400
  if no_treatments > 1:
2395
2401
 
2396
2402
  intercept = False
2397
2403
  TG_col = []
2398
2404
 
2399
- if not FE_unit:
2405
+ if not FE_unit and config.AUTO_SWITCH_TO_FIXED_EFFECTS:
2400
2406
  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.")
2407
+ 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.")
2408
+
2409
+ if unique_units < config.FIXED_EFFECTS_THRESHOLD:
2410
+
2411
+ FE_unit = False
2412
+ FE_group = False
2413
+ ITE = False
2414
+ ITT = False
2415
+ intercept = True
2416
+
2417
+ print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} units. Unit or group fixed effects are skipped.")
2418
+
2419
+ if unique_time_points < config.FIXED_EFFECTS_THRESHOLD:
2420
+
2421
+ FE_time = False
2422
+ ITT = False
2423
+ GTT = False
2424
+ intercept = True
2425
+
2426
+ print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} time points. Time fixed effects are skipped.")
2402
2427
 
2403
2428
  if demean and FE_unit:
2404
2429
 
@@ -2435,7 +2460,7 @@ def did_analysis(
2435
2460
  GTT = False
2436
2461
  print("NOTE: Both group and individual time trends were stated. Switching to individual time trends only.")
2437
2462
 
2438
- if staggered_adoption:
2463
+ if staggered_adoption and config.AUTO_SWITCH_TO_FIXED_EFFECTS:
2439
2464
 
2440
2465
  if not FE_unit or not FE_time:
2441
2466
  print("NOTE: Quasi-experiment includes one or more staggered treatments. Two-way fixed effects model is used.")
@@ -2562,7 +2587,15 @@ def did_analysis(
2562
2587
  missing_replace_by_zero = missing_replace_by_zero,
2563
2588
  verbose = verbose
2564
2589
  )
2590
+
2591
+ if len(data_diagnostics["cols_constants"]) > 0:
2592
+
2593
+ if config.AUTO_SKIP_CONSTANT_COLUMNS:
2565
2594
 
2595
+ print(f"NOTE: The following columns are constant and dropped from the analysis: {', '.join(data_diagnostics['cols_constants'])}.")
2596
+
2597
+ covariates = [col for col in covariates if col not in data_diagnostics["cols_constants"]]
2598
+
2566
2599
  if data_diagnostics["is_prepost"] and config.AUTO_SWITCH_TO_PREPOST:
2567
2600
 
2568
2601
  print(f"NOTE: Input is {config.PREPOST_PANELDATA_DESCRIPTION}. Data processing and model estimation will treat data as pre-post.")
@@ -3115,7 +3148,21 @@ def ddd_analysis(
3115
3148
  verbose=verbose
3116
3149
  )
3117
3150
  treatment_diagnostics = treatment_diagnostics_results[0]
3118
-
3151
+ unique_units = treatment_diagnostics_results[3]
3152
+ unique_time_points = treatment_diagnostics_results[4]
3153
+
3154
+ if unique_units < config.FIXED_EFFECTS_THRESHOLD:
3155
+
3156
+ FE_unit = False
3157
+
3158
+ print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} units. Unit fixed effects are skipped.")
3159
+
3160
+ if unique_time_points < config.FIXED_EFFECTS_THRESHOLD:
3161
+
3162
+ FE_time = False
3163
+
3164
+ print(f"NOTE: Quasi-experiment includes less than {config.FIXED_EFFECTS_THRESHOLD} time points. Time fixed effects are skipped.")
3165
+
3119
3166
  if FE_unit:
3120
3167
  TG_col = None
3121
3168
  if FE_time:
@@ -3140,7 +3187,15 @@ def ddd_analysis(
3140
3187
  missing_replace_by_zero = missing_replace_by_zero,
3141
3188
  verbose = verbose
3142
3189
  )
3143
-
3190
+
3191
+ if len(data_diagnostics["cols_constants"]) > 0:
3192
+
3193
+ if config.AUTO_SKIP_CONSTANT_COLUMNS:
3194
+
3195
+ print(f"NOTE: The following columns are constant and dropped from the analysis: {', '.join(data_diagnostics['cols_constants'])}.")
3196
+
3197
+ covariates = [col for col in covariates if col not in data_diagnostics["cols_constants"]]
3198
+
3144
3199
  if data_diagnostics["is_prepost"] and config.AUTO_SWITCH_TO_PREPOST:
3145
3200
 
3146
3201
  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(
@@ -4,14 +4,15 @@
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-06-28 13:18
7
+ # Version: 2.3.0
8
+ # Last update: 2026-07-03 18:00
9
9
  # Copyright (c) 2024-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
12
12
 
13
13
  import pandas as pd
14
14
  import numpy as np
15
+ from scipy.optimize import minimize
15
16
  from datetime import datetime, timedelta
16
17
  import diffindiff.didanalysis as didanalysis
17
18
  import diffindiff.didtools as tools
@@ -362,8 +363,8 @@ class DiffTreatment:
362
363
 
363
364
  Parameters
364
365
  ----------
365
- treatment_data_df : pandas.DataFrame
366
- Data frame describing treatment timing (TT/ATT).
366
+ treatment_data_df : list
367
+ List with data frames describing treatment timing (TT/ATT).
367
368
  treatment_config_dict : dict
368
369
  Configuration dictionary for the treatment.
369
370
  treatment_meta : dict
@@ -375,7 +376,6 @@ class DiffTreatment:
375
376
  -------
376
377
  None
377
378
  Constructor does not return a value; instance is initialized in-place.
378
-
379
379
  """
380
380
 
381
381
  self.data = [
@@ -542,7 +542,6 @@ def create_treatment(
542
542
  ):
543
543
 
544
544
  """
545
-
546
545
  Create a DiffTreatment object describing treatment timepoints.
547
546
 
548
547
  Parameters
@@ -647,7 +646,10 @@ def create_treatment(
647
646
  study_period_range
648
647
  )
649
648
 
650
- treatment_data = treatment_period_range.merge(TT_data, how = "left")
649
+ treatment_data_df = treatment_period_range.merge(
650
+ TT_data,
651
+ how = "left"
652
+ )
651
653
 
652
654
  else:
653
655
 
@@ -680,12 +682,12 @@ def create_treatment(
680
682
  }
681
683
  TT_data = pd.DataFrame(TT_data)
682
684
 
683
- treatment_data = study_period_range.merge(
685
+ treatment_data_df = study_period_range.merge(
684
686
  TT_data,
685
687
  how = "left"
686
688
  )
687
689
 
688
- treatment_data[TT_col] = treatment_data[TT_col].fillna(0)
690
+ treatment_data_df[TT_col] = treatment_data_df[TT_col].fillna(0)
689
691
 
690
692
  if after_treatment_period:
691
693
 
@@ -710,17 +712,20 @@ def create_treatment(
710
712
  }
711
713
  ATT_data = pd.DataFrame(ATT_data)
712
714
 
713
- after_treatment_data = study_period_range.merge(ATT_data, how = "left")
714
- after_treatment_data[ATT_col] = after_treatment_data[ATT_col].fillna(0)
715
- after_treatment_data = after_treatment_data.drop(columns=[config.TIME_COL, config.TIME_COUNTER_COL])
715
+ after_treatment_data_df = study_period_range.merge(ATT_data, how = "left")
716
+ after_treatment_data_df[ATT_col] = after_treatment_data_df[ATT_col].fillna(0)
717
+ after_treatment_data_df = after_treatment_data_df.drop(columns=[config.TIME_COL, config.TIME_COUNTER_COL])
716
718
 
717
- treatment_data = pd.concat([treatment_data, after_treatment_data], axis=1)
719
+ treatment_data_df = pd.concat([treatment_data_df, after_treatment_data_df], axis=1)
718
720
 
719
721
  else:
720
722
  after_treatment_period_N = 0
721
723
 
722
724
  no_treatments = 1
723
725
 
726
+ treatment_data = [0] * no_treatments
727
+ treatment_data[0] = treatment_data_df
728
+
724
729
  treatment_meta = {
725
730
  "no_treatments": 1,
726
731
  "study_period_start": study_period[0],
@@ -732,19 +737,18 @@ def create_treatment(
732
737
  }
733
738
 
734
739
  treatment_config = {
735
- 0:
736
- {
737
- "treatment_name": treatment_name,
738
- "treatment_period_start": treatment_period[0],
739
- "treatment_period_end": treatment_period[1],
740
- "treatment_period": treatment_period_N,
741
- "after_treatment_period": after_treatment_period,
742
- "after_treatment_period_N": after_treatment_period_N,
743
- "no_treatments": no_treatments,
744
- "TT_col": TT_col,
745
- "ATT_col": ATT_col,
746
- }
740
+ 0: {
741
+ "treatment_name": treatment_name,
742
+ "treatment_period_start": treatment_period[0],
743
+ "treatment_period_end": treatment_period[1],
744
+ "treatment_period": treatment_period_N,
745
+ "after_treatment_period": after_treatment_period,
746
+ "after_treatment_period_N": after_treatment_period_N,
747
+ "no_treatments": no_treatments,
748
+ "TT_col": TT_col,
749
+ "ATT_col": ATT_col,
747
750
  }
751
+ }
748
752
 
749
753
  treatment = DiffTreatment(
750
754
  treatment_data,
@@ -1249,6 +1253,7 @@ class DiffData:
1249
1253
  did_treatment_old = self.get_did_treatment()
1250
1254
  treatment_config_old = did_treatment_old.get_config()
1251
1255
  treatment_meta_old = did_treatment_old.get_metadata()
1256
+ treatment_data_old = did_treatment_old.get_data()
1252
1257
  if treatment_meta_old["pre_post"]:
1253
1258
  raise ValueError ("Adding treatments in a pre-post design is currently not possible.")
1254
1259
  no_treatments_old = treatment_meta_old["no_treatments"]
@@ -1288,7 +1293,8 @@ class DiffData:
1288
1293
  after_treatment_period = after_treatment_period,
1289
1294
  verbose=False
1290
1295
  )
1291
- new_treatment_data_df = new_treatment.get_data()
1296
+ new_treatment_data = new_treatment.get_data()
1297
+ new_treatment_data_df = new_treatment_data[0]
1292
1298
 
1293
1299
  new_treatment_config = new_treatment.get_config()
1294
1300
  TT_col = new_treatment_config[0]["TT_col"]
@@ -1336,7 +1342,8 @@ class DiffData:
1336
1342
 
1337
1343
  treatment_cols_new[key_counter] = {
1338
1344
  "TT_col": TT_col,
1339
- "ATT_col": ATT_col,
1345
+ "ATT_col": ATT_col,
1346
+ "TG_col": TG_col,
1340
1347
  "treatment_name": treatment_name,
1341
1348
  "after_treatment_name": after_treatment_name
1342
1349
  }
@@ -1355,9 +1362,12 @@ class DiffData:
1355
1362
  treatment_meta_new["no_treatments"] = no_treatments
1356
1363
  treatment_config_new = treatment_config_old
1357
1364
  treatment_config_new[key_counter] = new_treatment_config[0]
1365
+
1366
+ treatment_data_new = treatment_data_old.copy()
1367
+ treatment_data_new.append(new_treatment_data_df)
1358
1368
 
1359
1369
  treatment_new = DiffTreatment(
1360
- new_treatment_data_df,
1370
+ treatment_data_new,
1361
1371
  treatment_config_new,
1362
1372
  treatment_meta_new,
1363
1373
  timestamp = helper.create_timestamp(function="add_treatment")
@@ -1648,80 +1658,79 @@ class DiffData:
1648
1658
  raise ValueError("Parameters 'time_col' and 'counterfactual_outcome_col' must be stated")
1649
1659
 
1650
1660
  necessary_cols = [time_col, counterfactual_outcome_col]
1651
- covariates = self.data[5]
1661
+ covariates = self.data[5].copy()
1652
1662
  if len(covariates) > 0:
1653
1663
  necessary_cols.extend(covariates)
1654
1664
 
1655
1665
  tools.check_columns(
1656
1666
  df = additional_df,
1657
- columns = necessary_cols
1667
+ columns = necessary_cols,
1668
+ verbose = verbose
1658
1669
  )
1670
+
1671
+ additional_df = additional_df[necessary_cols].copy()
1659
1672
 
1660
- if verbose:
1661
- print("Collecting treatment information", end = " ... ")
1673
+ did_modeldata = self.data[0].copy()
1662
1674
 
1663
- did_modeldata = self.data[0]
1664
-
1665
- treatment_cols = self.get_treatment_cols()
1675
+ treatment_cols = self.get_treatment_cols().copy()
1666
1676
  TG_col = treatment_cols[0]["TG_col"]
1667
1677
  TT_col = treatment_cols[0]["TT_col"]
1668
1678
  treatment_name = treatment_cols[0]["treatment_name"]
1669
1679
 
1670
- groups_data = self.data[1].get_data()
1671
- groups_config = self.data[1].get_config()
1672
-
1673
- treatment_group = did_modeldata.loc[did_modeldata[TG_col] == 1, config.UNIT_COL].values
1674
-
1675
- treatment_config = self.data[2].get_config()
1676
-
1677
- outcome_col_original = self.data[3]
1680
+ if len(treatment_cols) > 1:
1681
+ print(f"NOTE: Treatment data contains {len(treatment_cols)} treatments. Taking first treatment '{treatment_name}' for identifying treatment group and time.")
1678
1682
 
1679
- treatment_data = self.data[2].get_data()
1683
+ if verbose:
1684
+ print("Collecting treatment information", end = " ... ")
1680
1685
 
1681
- additional_df = additional_df[necessary_cols].copy()
1686
+ groups_data = self.data[1].get_data().copy()
1687
+ groups_config = self.data[1].get_config().copy()
1688
+
1689
+ treatment_data = self.data[2].get_data().copy()
1690
+ treatment_data_df = treatment_data[0]
1691
+ treatment_config = self.data[2].get_config().copy()
1682
1692
 
1693
+ treatment_group = (did_modeldata.loc[did_modeldata[TG_col] == 1, config.UNIT_COL].astype(str).unique()).tolist()
1683
1694
  did_modeldata_TG = did_modeldata[did_modeldata[config.UNIT_COL].astype(str).isin(treatment_group)].copy()
1695
+
1696
+ outcome_col_original = self.data[3]
1684
1697
 
1685
1698
  if verbose:
1686
1699
  print("OK")
1687
1700
  print("Compiling and merging counterfactual data", end = " ... ")
1688
1701
 
1689
- did_modeldata_counterfac = pd.DataFrame(columns=did_modeldata_TG.columns, index=range(len(treatment_data)))
1702
+ did_modeldata_counterfac = pd.DataFrame(columns=did_modeldata_TG.columns, index=range(len(treatment_data_df)))
1690
1703
 
1691
1704
  did_modeldata_counterfac[config.UNIT_COL] = counterfactual_UID
1692
1705
  did_modeldata_counterfac[config.UNIT_COL] = did_modeldata_counterfac[config.UNIT_COL].astype(str)
1693
1706
 
1694
1707
  did_modeldata_counterfac[TG_col] = 0
1695
1708
 
1696
- did_modeldata_counterfac[config.TIME_COL] = treatment_data[config.TIME_COL].values
1709
+ did_modeldata_counterfac[config.TIME_COL] = treatment_data_df[config.TIME_COL].values
1697
1710
  did_modeldata_counterfac[config.TIME_COL] = did_modeldata_counterfac[config.TIME_COL].astype(str)
1698
1711
 
1699
- did_modeldata_counterfac[config.TIME_COUNTER_COL] = treatment_data[config.TIME_COUNTER_COL].values
1700
-
1701
- did_modeldata_counterfac[TT_col] = treatment_data[TT_col].values
1702
-
1703
- did_modeldata_counterfac[treatment_name] = did_modeldata_counterfac[TG_col] * did_modeldata_counterfac[TT_col]
1712
+ did_modeldata_counterfac[config.TIME_COUNTER_COL] = treatment_data_df[config.TIME_COUNTER_COL].values
1704
1713
 
1714
+ for treatment_col_key, treatment_col_data in treatment_cols.items():
1715
+ did_modeldata_counterfac[treatment_cols[treatment_col_key]["treatment_name"]] = 0
1716
+ if treatment_col_data["TG_col"] in did_modeldata_counterfac.columns:
1717
+ did_modeldata_counterfac[treatment_col_data["TG_col"]] = 0
1718
+ if treatment_col_data["TT_col"] in did_modeldata_counterfac.columns:
1719
+ did_modeldata_counterfac[treatment_col_data["TT_col"]] = treatment_data_df[TT_col].values
1720
+
1705
1721
  did_modeldata_counterfac[config.UNIT_TIME_COL] = did_modeldata_counterfac[config.UNIT_COL]+config.DELIMITER+did_modeldata_counterfac[config.TIME_COL]
1706
1722
 
1707
1723
  if treatment_config[0]["after_treatment_period"]:
1708
- did_modeldata_counterfac["ATT"] = treatment_data["ATT"].values
1724
+ did_modeldata_counterfac["ATT"] = treatment_data_df["ATT"].values
1709
1725
 
1710
1726
  if counterfactual_outcome_col == outcome_col_original:
1711
1727
  additional_df = additional_df.rename(columns={counterfactual_outcome_col: counterfactual_outcome_col+"_cf"})
1712
1728
  counterfactual_outcome_col = counterfactual_outcome_col+"_cf"
1713
1729
 
1714
- did_modeldata_counterfac = pd.merge(
1715
- did_modeldata_counterfac,
1716
- additional_df,
1717
- left_on = config.TIME_COL,
1718
- right_on = time_col,
1719
- how = "left"
1720
- )
1721
-
1722
- did_modeldata_counterfac[outcome_col_original] = did_modeldata_counterfac[counterfactual_outcome_col]
1723
- did_modeldata_counterfac = did_modeldata_counterfac.drop(counterfactual_outcome_col, axis = 1)
1724
-
1730
+ did_modeldata_counterfac[outcome_col_original] = additional_df[counterfactual_outcome_col]
1731
+ for covariate in covariates:
1732
+ did_modeldata_counterfac[covariate] = additional_df[covariate]
1733
+
1725
1734
  if time_col in did_modeldata_counterfac.columns and time_col != config.TIME_COL:
1726
1735
  did_modeldata_counterfac = did_modeldata_counterfac.drop(time_col, axis = 1)
1727
1736
 
@@ -1732,21 +1741,25 @@ class DiffData:
1732
1741
  ],
1733
1742
  ignore_index=True
1734
1743
  )
1744
+
1745
+ groups_config_new = groups_config.copy()
1746
+ for groups_key in groups_config.keys():
1735
1747
 
1736
- groups_config[0]["own_counterfactual"] = True
1737
- groups_config[0]["control_group"] = 1
1738
- groups_config[0]["full_sample"] = 2
1739
-
1740
- groups_data = groups_data[0][groups_data[0][TG_col] == 1]
1741
- groups_data_cf = {
1742
- config.UNIT_COL: counterfactual_UID,
1743
- config.TG_COL: 0
1744
- }
1745
- groups_data = pd.concat([groups_data, pd.DataFrame([groups_data_cf])], ignore_index=True)
1748
+ groups_config_new[groups_key]["own_counterfactual"] = True
1749
+ groups_config_new[groups_key]["treatment_group"] = len(treatment_group)
1750
+ groups_config_new[groups_key]["control_group"] = 1
1751
+ groups_config_new[groups_key]["full_sample"] = len(treatment_group)+1
1752
+
1753
+ groups_data_new = groups_data.copy()
1746
1754
 
1755
+ for groups_key, entry in enumerate(groups_data_new):
1756
+
1757
+ groups_data_new[groups_key] = groups_data[groups_key][groups_data[groups_key].iloc[:, 1] == 1].copy()
1758
+ groups_data_new[groups_key].loc[len(groups_data_new[groups_key])] = [counterfactual_UID, 0]
1759
+
1747
1760
  groups = DiffGroups(
1748
- groups_data,
1749
- groups_config,
1761
+ groups_data = groups_data_new,
1762
+ groups_config_dict = groups_config_new,
1750
1763
  timestamp = helper.create_timestamp(function="add_own_counterfactual")
1751
1764
  )
1752
1765
 
@@ -1756,8 +1769,230 @@ class DiffData:
1756
1769
  if verbose:
1757
1770
  print("OK")
1758
1771
 
1772
+ print("NOTE: DiffData object was changed with counterfactual.")
1773
+
1759
1774
  return self
1760
1775
 
1776
+ def add_synthetic(
1777
+ self,
1778
+ process_unit: str = None,
1779
+ SCU_UID: str = "synthetic_control",
1780
+ log_outcome: bool = False,
1781
+ log_outcome_add = 0.01,
1782
+ verbose: bool = False
1783
+ ):
1784
+
1785
+ """
1786
+ Create a synthetic control unit (SCU) and add it as a counterfactual.
1787
+
1788
+ Parameters
1789
+ ----------
1790
+ process_unit : str, optional
1791
+ Identifier of the treatment unit for which the synthetic control unit must be created.
1792
+ SCU_UID : str, optional
1793
+ Identifier assigned to the synthetic control unit in the counterfactual data.
1794
+ log_outcome : bool, optional
1795
+ If True, transform the outcome by natural logarithm before fitting the synthetic control weights.
1796
+ log_outcome_add : float, optional
1797
+ Constant added before applying the natural logarithm when log_outcome is True.
1798
+ verbose : bool, optional
1799
+ If True, print progress messages.
1800
+
1801
+ Returns
1802
+ -------
1803
+ DiffData
1804
+ The updated DiffData object including the synthetic control unit as counterfactual.
1805
+
1806
+ Raises
1807
+ ------
1808
+ ValueError
1809
+ If the treatment group contains multiple analysis units and no process unit is specified, if the specified process unit is not part of the model data, or if the control group contains fewer than two units.
1810
+
1811
+ Examples
1812
+ --------
1813
+ >>> curfew_data_prepost = create_data(
1814
+ ... outcome_data=curfew_DE,
1815
+ ... unit_id_col="county",
1816
+ ... time_col="infection_date",
1817
+ ... outcome_col="infections_cum_per100000",
1818
+ ... treatment_group=curfew_DE.loc[curfew_DE["Bundesland"].isin([9,10,14])]["county"],
1819
+ ... control_group=curfew_DE.loc[~curfew_DE["Bundesland"].isin([9,10,14])]["county"],
1820
+ ... study_period=["2020-03-01", "2020-05-15"],
1821
+ ... treatment_period=["2020-03-21", "2020-05-05"],
1822
+ ... freq="D",
1823
+ ... pre_post=True,
1824
+ ... after_treatment_period=True
1825
+ ... )
1826
+ >>> curfew_data_prepost.add_synthetic(process_unit="Aachen")
1827
+ """
1828
+
1829
+ did_modeldata = self.data[0].copy()
1830
+
1831
+ covariates = self.data[5].copy()
1832
+ if len(covariates) > 0:
1833
+ tools.check_columns(
1834
+ df = did_modeldata,
1835
+ columns = covariates,
1836
+ verbose = verbose
1837
+ )
1838
+
1839
+ treatment_cols = self.get_treatment_cols().copy()
1840
+ treatment_name = treatment_cols[0]["treatment_name"]
1841
+ TG_col = treatment_cols[0]["TG_col"]
1842
+
1843
+ if len(treatment_cols) > 1:
1844
+ print(f"NOTE: Treatment data contains {len(treatment_cols)} treatments. Taking first treatment '{treatment_name}' for identifying treatment group and time.")
1845
+
1846
+ if verbose:
1847
+ print("Collecting treatment information", end = " ... ")
1848
+
1849
+ groups_config = self.data[1].get_config().copy()
1850
+ treatment_group_size = groups_config[0]["treatment_group"]
1851
+ control_group_size = groups_config[0]["control_group"]
1852
+
1853
+ if verbose:
1854
+ print("OK")
1855
+
1856
+ if treatment_group_size > 1 and process_unit is None:
1857
+ raise ValueError(f"Treatment group contains {treatment_group_size} analysis units and process_unit is set to None. Specify for which unit the SCU must be created.")
1858
+ if control_group_size < 2:
1859
+ raise ValueError(f"Control group contains only {control_group_size} units, which does not allow for creating a SCU.")
1860
+
1861
+ if verbose:
1862
+ print("Extracting control group data", end = " ... ")
1863
+
1864
+ units = list(did_modeldata[config.UNIT_COL].unique())
1865
+
1866
+ process_unit = did_modeldata.loc[did_modeldata[TG_col] == 1, config.UNIT_COL][0]
1867
+
1868
+ if treatment_group_size > 1 and process_unit is not None:
1869
+
1870
+ if process_unit not in units:
1871
+ raise ValueError(f"Diff-in-diff model data does not contain specified treatment unit '{process_unit}'.")
1872
+
1873
+ units.remove(process_unit)
1874
+ units_size = len(units)
1875
+
1876
+ if verbose:
1877
+ print("OK")
1878
+
1879
+ outcome_col_original = self.data[3]
1880
+ outcome_col = outcome_col_original
1881
+
1882
+ if log_outcome:
1883
+
1884
+ print(f"Compiling outcome data for natural logarithm of outcome '{outcome_col}'", end = " ... ")
1885
+
1886
+ outcome_col = f"{config.LOG_PREFIX}{config.DELIMITER}{outcome_col_original}"
1887
+ did_modeldata[outcome_col] = np.log(did_modeldata[outcome_col_original]+log_outcome_add)
1888
+
1889
+ else:
1890
+ if verbose:
1891
+ print(f"Compiling outcome data for outcome '{outcome_col}'", end = " ... ")
1892
+
1893
+ process_unit_time = pd.Series(did_modeldata.loc[did_modeldata[config.UNIT_COL] == process_unit, config.TIME_COL])
1894
+ process_unit_outcome = pd.Series(did_modeldata.loc[did_modeldata[config.UNIT_COL] == process_unit, outcome_col])
1895
+
1896
+ outcome_matrix = pd.DataFrame(
1897
+ {
1898
+ config.TIME_COL: process_unit_time.reset_index(drop=True),
1899
+ tools.clean_column_name(process_unit): process_unit_outcome.reset_index(drop=True)
1900
+ }
1901
+ )
1902
+
1903
+ units_not_included = []
1904
+
1905
+ for unit in units:
1906
+
1907
+ did_modeldata_unit = did_modeldata.loc[
1908
+ (did_modeldata[config.UNIT_COL].astype(str) == str(unit))
1909
+ & (did_modeldata[outcome_col].notna()),
1910
+ outcome_col
1911
+ ]
1912
+
1913
+ if len(did_modeldata_unit) != len(outcome_matrix):
1914
+ units_not_included.append(unit)
1915
+ units.remove(unit)
1916
+ else:
1917
+ outcome_matrix[tools.clean_column_name(unit)] = did_modeldata_unit.reset_index(drop=True)
1918
+
1919
+ if len(units_not_included) > 0:
1920
+ print(f"WARNING: {len(units_not_included)} analysis units were not included due to NaN data.")
1921
+
1922
+ if verbose:
1923
+ print("OK")
1924
+ print(f"Calculating weights based on {len(units)} analysis units", end = " ... ")
1925
+
1926
+ outcome_matrix_Y = outcome_matrix.iloc[:, :2]
1927
+ outcome_matrix_X = outcome_matrix.iloc[:, 2:]
1928
+
1929
+ y = outcome_matrix_Y[tools.clean_column_name(process_unit)]
1930
+ X = outcome_matrix_X.values
1931
+
1932
+ def objective(w):
1933
+ return np.sum((y - X @ w)**2)
1934
+
1935
+ n = X.shape[1]
1936
+ w0 = np.ones(n) / n
1937
+ constraints = {"type": "eq", "fun": lambda w: np.sum(w) - 1}
1938
+ bounds = [(0, 1)] * n
1939
+
1940
+ result = minimize(
1941
+ objective,
1942
+ w0,
1943
+ bounds=bounds,
1944
+ constraints=constraints
1945
+ )
1946
+
1947
+ weights = result.x
1948
+ weights_pd = pd.Series(weights, index=units)
1949
+
1950
+ synthetic_pre = X @ weights
1951
+
1952
+ synthetic_fit_metrics = tools.fit_metrics(
1953
+ observed = y,
1954
+ expected = synthetic_pre
1955
+ )
1956
+
1957
+ if verbose:
1958
+ print("OK")
1959
+ print(f"Creating synthetic control unit", end = " ... ")
1960
+
1961
+ control_units_data = did_modeldata[did_modeldata[config.UNIT_COL].isin(weights_pd.index)].copy()
1962
+ control_units_data["weights"] = control_units_data[config.UNIT_COL].map(weights_pd)
1963
+ control_units_data[outcome_col] = control_units_data[outcome_col].astype(float)
1964
+ control_units_data.loc[:, outcome_col] = (control_units_data[outcome_col] * control_units_data["weights"])
1965
+
1966
+ variables_dict = {
1967
+ outcome_col: "sum"
1968
+ }
1969
+ for covariate in covariates:
1970
+ control_units_data[covariate] = control_units_data[covariate].astype(float)
1971
+ control_units_data.loc[:, covariate] = control_units_data[covariate]*control_units_data["weights"]
1972
+ variables_dict[covariate] = "sum"
1973
+
1974
+ synthetic = (
1975
+ control_units_data
1976
+ .groupby(config.TIME_COL, as_index=False)
1977
+ .agg(variables_dict)
1978
+ )
1979
+
1980
+ if log_outcome:
1981
+ synthetic[outcome_col_original] = np.exp(synthetic[outcome_col])
1982
+
1983
+ if verbose:
1984
+ print("OK")
1985
+
1986
+ self.add_own_counterfactual(
1987
+ additional_df=synthetic,
1988
+ counterfactual_outcome_col=outcome_col_original,
1989
+ counterfactual_UID=SCU_UID,
1990
+ time_col=config.TIME_COL,
1991
+ verbose = False
1992
+ )
1993
+
1994
+ return self
1995
+
1761
1996
  def summary(self):
1762
1997
 
1763
1998
  """
@@ -2112,7 +2347,8 @@ def merge_data(
2112
2347
  groups_config = diff_groups.get_config()
2113
2348
  groups_config = groups_config[0]
2114
2349
 
2115
- treatment_data_df = diff_treatment.get_data()
2350
+ treatment_data = diff_treatment.get_data()
2351
+ treatment_data_df = treatment_data[0]
2116
2352
  treatment_config = diff_treatment.get_config()
2117
2353
  treatment_config = treatment_config[0]
2118
2354
 
@@ -74,7 +74,8 @@ curfew_data_prepost=create_data(
74
74
  study_period=["2020-03-01", "2020-05-15"],
75
75
  treatment_period=["2020-03-21", "2020-05-05"],
76
76
  freq="D",
77
- pre_post=True
77
+ pre_post=True,
78
+ after_treatment_period = True
78
79
  )
79
80
  # Creating DiD treatement dataset by defining groups and
80
81
  # treatment time at once
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: diffindiff
3
- Version: 2.4.2
3
+ Version: 2.5.0
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.0) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
33
33
 
34
34
 
35
35
  ## Installation
@@ -177,7 +177,16 @@ 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.0)
181
181
 
182
- - Bugfixes:
183
- - DiffData.add_own_counterfactual() now works correctly in any case
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
+ - 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
189
+ - 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()
@@ -0,0 +1,14 @@
1
+ geopandas
2
+ pandas
3
+ numpy
4
+ statsmodels>=0.14.5
5
+ scipy>=1.17
6
+ scikit-learn
7
+ xgboost
8
+ lightgbm
9
+ shapely
10
+ requests<3.0
11
+ contextily
12
+ openpyxl
13
+ matplotlib
14
+ patsy
@@ -7,7 +7,7 @@ def read_README():
7
7
 
8
8
  setup(
9
9
  name='diffindiff',
10
- version='2.4.2',
10
+ version='2.5.0',
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,
@@ -20,15 +20,20 @@ setup(
20
20
  'diffindiff': ['tests/data/*'],
21
21
  },
22
22
  install_requires=[
23
- 'numpy>=1.26.1,<=1.26.4',
24
- 'pandas>=2.2.2,<3.0',
25
- 'statsmodels==0.14.2',
26
- 'scipy==1.15.3',
23
+ 'geopandas',
24
+ 'pandas',
25
+ 'numpy',
26
+ 'statsmodels>=0.14.5',
27
+ 'scipy>=1.17',
28
+ 'scikit-learn',
29
+ 'xgboost',
30
+ 'lightgbm',
31
+ 'shapely',
32
+ 'requests<3.0',
33
+ 'contextily',
34
+ 'openpyxl',
27
35
  'matplotlib',
28
- 'scikit-learn>=1.3,<1.6',
29
- 'xgboost>=1.7,<2.1',
30
- 'lightgbm>=4.0,<4.5',
31
- 'patsy>=0.5.6',
36
+ 'patsy',
32
37
  ],
33
38
  test_suite='tests',
34
39
  )
@@ -1,9 +0,0 @@
1
- numpy<=1.26.4,>=1.26.1
2
- pandas<3.0,>=2.2.2
3
- statsmodels==0.14.2
4
- scipy==1.15.3
5
- matplotlib
6
- scikit-learn<1.6,>=1.3
7
- xgboost<2.1,>=1.7
8
- lightgbm<4.5,>=4.0
9
- patsy>=0.5.6
File without changes
File without changes