diffindiff 2.5.2__tar.gz → 2.5.4__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.2 → diffindiff-2.5.4}/PKG-INFO +9 -7
  2. {diffindiff-2.5.2 → diffindiff-2.5.4}/README.md +8 -6
  3. {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/config.py +5 -3
  4. {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/didanalysis.py +45 -9
  5. {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/didanalysis_helper.py +104 -19
  6. {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/diddata.py +16 -5
  7. {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/didtools.py +4 -4
  8. {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/tests/tests_diffindiff.py +90 -6
  9. {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff.egg-info/PKG-INFO +9 -7
  10. {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff.egg-info/requires.txt +0 -4
  11. {diffindiff-2.5.2 → diffindiff-2.5.4}/setup.py +1 -5
  12. {diffindiff-2.5.2 → diffindiff-2.5.4}/MANIFEST.in +0 -0
  13. {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/__init__.py +0 -0
  14. {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/tests/__init__.py +0 -0
  15. {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/tests/data/Corona_Hesse.xlsx +0 -0
  16. {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/tests/data/counties_DE.csv +0 -0
  17. {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff/tests/data/curfew_DE.csv +0 -0
  18. {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff.egg-info/SOURCES.txt +0 -0
  19. {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff.egg-info/dependency_links.txt +0 -0
  20. {diffindiff-2.5.2 → diffindiff-2.5.4}/diffindiff.egg-info/top_level.txt +0 -0
  21. {diffindiff-2.5.2 → diffindiff-2.5.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: diffindiff
3
- Version: 2.5.2
3
+ Version: 2.5.4
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 case study that utilizes the diffindiff library is available on [arXiv](https:
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.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.4) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
33
33
 
34
34
 
35
35
  ## Installation
@@ -164,6 +164,7 @@ See the /tests directory for usage examples of most of the included functions.
164
164
  - Goldfarb A, Tucker C, Wang Y (2022) Conducting Research in Marketing with Quasi-Experiments. *Journal of Marketing* 86(3): 1-19. [10.1177/00222429221082977](https://doi.org/10.1177/00222429221082977)
165
165
  - Isporhing IE, Lipfert M, Pestel N (2021) Does re-opening schools contribute to the spread of SARS-CoV-2? Evidence from staggered summer breaks in Germany. *Journal of Public Economics* 198: 104426. [10.1016/j.jpubeco.2021.104426](https://doi.org/10.1016/j.jpubeco.2021.104426)
166
166
  - Li KT, Luo L, Pattabhiramaiah A (2024) Causal Inference with Quasi-Experimental Data. *IMPACT at JMR* November 13, 2024. [AMA](https://www.ama.org/marketing-news/causal-inference-with-quasi-experimental-data/)
167
+ - Mitze T, Kosfeld R, Rode J, Wälde K (2020) Face masks considerably reduce COVID-19 cases in Germany. *Proceedings of the National Academy of Sciences of the United States of America* 117(51): 32293-32301. [10.1073/pnas.2015954117](https://doi.org/10.1073/pnas.2015954117)
167
168
  - Olden A (2018) What do you buy when no one's watching? The effect of self-service checkouts on the composition of sales in retail. Discussion paper FOR 3/18, Norwegian School of Economics, Norway. [http://hdl.handle.net/11250/2490886](http://hdl.handle.net/11250/2490886)
168
169
  - Olden A, Moen J (2022) The triple difference estimator. *The Econometrics Journal* 25(3): 531-553. [10.1093/ectj/utac010](https://doi.org/10.1093/ectj/utac010)
169
170
  - Strassmann A, Çolak Y, Serra-Burriel M, Nordestgaard BG, Turk A, Afzal S, Puhan MA (2023) Nationwide indoor smoking ban and impact on smoking behaviour and lung function: a two-population natural experiment. *Thorax* 78(2): 144-150. [10.1136/thoraxjnl-2021-218436](https://doi.org/10.1136/thoraxjnl-2021-218436)
@@ -179,11 +180,12 @@ See the /tests directory for usage examples of most of the included functions.
179
180
  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).
180
181
 
181
182
 
182
- ## What's new (v2.5.2)
183
+ ## What's new (v2.5.4)
183
184
 
184
- - Extensions
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
186
185
  - Bugfixes
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
186
+ - Correction of the nonsensical skipping of the treatment group dummy in the case of two observation units in didanalysis.did_analysis()
187
+ - Automatical drop of duplicates in results dictionaries in didanalysis_helper.extract_model_results()
188
+ - Inefficient successive data insert in diddata.DiffData.add_synthetic() replaced by efficient concatenating
188
189
  - Other
189
- - Some updates of documentation and README
190
+ - Test script extended by another example
191
+ - Cleaned dependencies
@@ -21,7 +21,7 @@ A case study that utilizes the diffindiff library is available on [arXiv](https:
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.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.4) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
25
25
 
26
26
 
27
27
  ## Installation
@@ -156,6 +156,7 @@ See the /tests directory for usage examples of most of the included functions.
156
156
  - Goldfarb A, Tucker C, Wang Y (2022) Conducting Research in Marketing with Quasi-Experiments. *Journal of Marketing* 86(3): 1-19. [10.1177/00222429221082977](https://doi.org/10.1177/00222429221082977)
157
157
  - Isporhing IE, Lipfert M, Pestel N (2021) Does re-opening schools contribute to the spread of SARS-CoV-2? Evidence from staggered summer breaks in Germany. *Journal of Public Economics* 198: 104426. [10.1016/j.jpubeco.2021.104426](https://doi.org/10.1016/j.jpubeco.2021.104426)
158
158
  - Li KT, Luo L, Pattabhiramaiah A (2024) Causal Inference with Quasi-Experimental Data. *IMPACT at JMR* November 13, 2024. [AMA](https://www.ama.org/marketing-news/causal-inference-with-quasi-experimental-data/)
159
+ - Mitze T, Kosfeld R, Rode J, Wälde K (2020) Face masks considerably reduce COVID-19 cases in Germany. *Proceedings of the National Academy of Sciences of the United States of America* 117(51): 32293-32301. [10.1073/pnas.2015954117](https://doi.org/10.1073/pnas.2015954117)
159
160
  - Olden A (2018) What do you buy when no one's watching? The effect of self-service checkouts on the composition of sales in retail. Discussion paper FOR 3/18, Norwegian School of Economics, Norway. [http://hdl.handle.net/11250/2490886](http://hdl.handle.net/11250/2490886)
160
161
  - Olden A, Moen J (2022) The triple difference estimator. *The Econometrics Journal* 25(3): 531-553. [10.1093/ectj/utac010](https://doi.org/10.1093/ectj/utac010)
161
162
  - Strassmann A, Çolak Y, Serra-Burriel M, Nordestgaard BG, Turk A, Afzal S, Puhan MA (2023) Nationwide indoor smoking ban and impact on smoking behaviour and lung function: a two-population natural experiment. *Thorax* 78(2): 144-150. [10.1136/thoraxjnl-2021-218436](https://doi.org/10.1136/thoraxjnl-2021-218436)
@@ -171,11 +172,12 @@ See the /tests directory for usage examples of most of the included functions.
171
172
  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).
172
173
 
173
174
 
174
- ## What's new (v2.5.2)
175
+ ## What's new (v2.5.4)
175
176
 
176
- - Extensions
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
178
177
  - Bugfixes
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
178
+ - Correction of the nonsensical skipping of the treatment group dummy in the case of two observation units in didanalysis.did_analysis()
179
+ - Automatical drop of duplicates in results dictionaries in didanalysis_helper.extract_model_results()
180
+ - Inefficient successive data insert in diddata.DiffData.add_synthetic() replaced by efficient concatenating
180
181
  - Other
181
- - Some updates of documentation and README
182
+ - Test script extended by another example
183
+ - Cleaned dependencies
@@ -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.23
8
- # Last update: 2026-07-14 19:48
7
+ # Version: 1.0.25
8
+ # Last update: 2026-08-04 18:17
9
9
  # Copyright (c) 2025-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
12
12
  # Basic config:
13
13
 
14
14
  PACKAGE_NAME = "diffindiff"
15
- PACKAGE_VERSION = "2.5.2"
15
+ PACKAGE_VERSION = "2.5.4"
16
16
 
17
17
  VERBOSE = False
18
18
 
@@ -26,6 +26,8 @@ FIXED_EFFECTS_THRESHOLD = 3
26
26
 
27
27
  AUTO_SKIP_CONSTANT_COLUMNS = True
28
28
 
29
+ REMOVE_DUPLICATES_FROM_RESULTS_DICT = True
30
+
29
31
  ACCEPT_CONTINUOUS_TREATMENTS = True
30
32
 
31
33
  # Description texts:
@@ -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.3
8
- # Last update: 2026-07-14 19:49
7
+ # Version: 2.4.5
8
+ # Last update: 2026-08-03 20:25
9
9
  # Copyright (c) 2024-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -782,7 +782,7 @@ class DiffModel:
782
782
 
783
783
  if len(no_control_conditions) > 0:
784
784
  if len(no_control_conditions) == 1:
785
- print(f"NOTE: Treatment {no_control_conditions[0]} has no control conditions.")
785
+ print(f"NOTE: Treatment '{no_control_conditions[0]}' has no control conditions.")
786
786
  else:
787
787
  print(f"NOTE: Treatments {', '.join(no_control_conditions)} have no control conditions.")
788
788
 
@@ -829,7 +829,9 @@ class DiffModel:
829
829
  plot_size: list = [7, 6],
830
830
  scale_plot: bool = True,
831
831
  show_central_tendency: bool = False,
832
- central_tendency: str = "mean"
832
+ central_tendency: str = "mean",
833
+ save_fig: str = None,
834
+ save_fig_kwargs: dict = {}
833
835
  ):
834
836
 
835
837
  """
@@ -869,6 +871,10 @@ class DiffModel:
869
871
  Show mean/median line for estimates. Default is False.
870
872
  central_tendency : str, optional
871
873
  'mean' or 'median' for central tendency if shown. Default is 'mean'.
874
+ save_fig : str, optional
875
+ If not none, filename of plot to be saved.
876
+ save_fig_kwargs : dict, optional
877
+ Optional arguments for plt.savefig() if save_fig is not None.
872
878
 
873
879
  Returns
874
880
  -------
@@ -1006,6 +1012,9 @@ class DiffModel:
1006
1012
  if plot_grid:
1007
1013
  plt.grid(True)
1008
1014
 
1015
+ if save_fig is not None and isinstance(save_fig, str):
1016
+ plt.savefig(save_fig, **save_fig_kwargs)
1017
+
1009
1018
  plt.show()
1010
1019
 
1011
1020
  return self
@@ -1473,7 +1482,9 @@ class DiffModel:
1473
1482
  y_lim = None,
1474
1483
  plot_title: str = "Treatment time",
1475
1484
  plot_symbol: str = "o",
1476
- treatment_group_only: bool = True
1485
+ treatment_group_only: bool = True,
1486
+ save_fig: str = None,
1487
+ save_fig_kwargs: dict = {}
1477
1488
  ):
1478
1489
 
1479
1490
  """
@@ -1497,6 +1508,10 @@ class DiffModel:
1497
1508
  Symbol used for treatment timing points. Default is 'o'.
1498
1509
  treatment_group_only : bool, optional
1499
1510
  If True, only plot treated units.
1511
+ save_fig : str, optional
1512
+ If not none, filename of plot to be saved.
1513
+ save_fig_kwargs : dict, optional
1514
+ Optional arguments for plt.savefig() if save_fig is not None.
1500
1515
 
1501
1516
  Returns
1502
1517
  -------
@@ -1570,6 +1585,9 @@ class DiffModel:
1570
1585
 
1571
1586
  if y_lim is not None:
1572
1587
  ax.set_ylim(y_lim)
1588
+
1589
+ if save_fig is not None and isinstance(save_fig, str):
1590
+ plt.savefig(save_fig, **save_fig_kwargs)
1573
1591
 
1574
1592
  plt.show()
1575
1593
 
@@ -1595,7 +1613,9 @@ class DiffModel:
1595
1613
  pre_post_ticks: list = ["Pre", "Post"],
1596
1614
  pre_post_barplot = False,
1597
1615
  pre_post_bar_width = 0.5,
1598
- retransform_log_outcome: bool = False
1616
+ retransform_log_outcome: bool = False,
1617
+ save_fig: str = None,
1618
+ save_fig_kwargs: dict = {}
1599
1619
  ):
1600
1620
 
1601
1621
  """
@@ -1642,6 +1662,10 @@ class DiffModel:
1642
1662
  retransform_log_outcome : bool, optional
1643
1663
  If outcome was log-transformed, retransform to original scale for plotting.
1644
1664
  Default is False.
1665
+ save_fig : str, optional
1666
+ If not none, filename of plot to be saved.
1667
+ save_fig_kwargs : dict, optional
1668
+ Optional arguments for plt.savefig() if save_fig is not None.
1645
1669
 
1646
1670
  Returns
1647
1671
  -------
@@ -2044,7 +2068,10 @@ class DiffModel:
2044
2068
 
2045
2069
  if y_lim is not None:
2046
2070
  ax.set_ylim(y_lim)
2047
-
2071
+
2072
+ if save_fig is not None and isinstance(save_fig, str):
2073
+ plt.savefig(save_fig, **save_fig_kwargs)
2074
+
2048
2075
  plt.show()
2049
2076
 
2050
2077
  return model_data_TG_CG
@@ -2063,7 +2090,9 @@ class DiffModel:
2063
2090
  plot_legend: bool = True,
2064
2091
  plot_grid: bool = True,
2065
2092
  plot_size: list = [12, 6],
2066
- retransform_log_outcome: bool = False
2093
+ retransform_log_outcome: bool = False,
2094
+ save_fig: str = None,
2095
+ save_fig_kwargs: dict = {}
2067
2096
  ):
2068
2097
 
2069
2098
  """
@@ -2098,6 +2127,10 @@ class DiffModel:
2098
2127
  retransform_log_outcome : bool, optional
2099
2128
  If outcome was log-transformed, retransform to original scale for plotting.
2100
2129
  Default is False.
2130
+ save_fig : str, optional
2131
+ If not none, filename of plot to be saved.
2132
+ save_fig_kwargs : dict, optional
2133
+ Optional arguments for plt.savefig() if save_fig is not None.
2101
2134
 
2102
2135
  Returns
2103
2136
  -------
@@ -2227,6 +2260,9 @@ class DiffModel:
2227
2260
 
2228
2261
  if y_lim is not None:
2229
2262
  ax.set_ylim(y_lim)
2263
+
2264
+ if save_fig is not None and isinstance(save_fig, str):
2265
+ plt.savefig(save_fig, **save_fig_kwargs)
2230
2266
 
2231
2267
  plt.show()
2232
2268
 
@@ -2476,7 +2512,7 @@ def did_analysis(
2476
2512
  unique_units = treatment_diagnostics_results[3]
2477
2513
  unique_time_points = treatment_diagnostics_results[4]
2478
2514
 
2479
- if no_treatments > 1:
2515
+ if no_treatments > 1 and unique_units >= config.FIXED_EFFECTS_THRESHOLD:
2480
2516
 
2481
2517
  intercept = False
2482
2518
  TG_col = []
@@ -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.3
8
- # Last update: 2026-07-14 19:44
7
+ # Version: 1.2.4
8
+ # Last update: 2026-08-04 17:35
9
9
  # Copyright (c) 2025-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -25,7 +25,7 @@ def create_fixed_effects(
25
25
  type: str = "unit",
26
26
  drop_first: bool = False,
27
27
  verbose: bool = config.VERBOSE
28
- ):
28
+ ) -> list:
29
29
 
30
30
  """
31
31
  Create dummy variables for fixed effects and attach them to the data.
@@ -118,7 +118,7 @@ def demean_variables(
118
118
  max_iter=10,
119
119
  tol=1e-8,
120
120
  verbose: bool = config.VERBOSE
121
- ):
121
+ ) -> tuple:
122
122
 
123
123
  """
124
124
  Demean numeric variables by removing unit and/or time means.
@@ -399,7 +399,7 @@ def create_specific_time_trends(
399
399
  FE_vars: list,
400
400
  type: str = "ITT",
401
401
  verbose: bool = config.VERBOSE
402
- ):
402
+ ) -> list:
403
403
 
404
404
  """
405
405
  Create unit-specific time trend variables (interactions with a time counter).
@@ -482,7 +482,7 @@ def create_specific_treatment_effects(
482
482
  FE_vars: list,
483
483
  type: str = "ITE",
484
484
  verbose: bool = config.VERBOSE
485
- ):
485
+ ) -> list:
486
486
 
487
487
  """
488
488
  Create unit-specific treatment effect interaction variables.
@@ -566,7 +566,7 @@ def create_spillover(
566
566
  spillover_treatment: list = None,
567
567
  spillover_units: list = None,
568
568
  verbose: bool = config.VERBOSE
569
- ):
569
+ ) -> list:
570
570
 
571
571
  """
572
572
  Create spillover indicator variables for given treatments and units.
@@ -667,7 +667,7 @@ def create_interactions(
667
667
  data: pd.DataFrame,
668
668
  interactions: dict = None,
669
669
  verbose: bool = config.VERBOSE
670
- ):
670
+ ) -> list:
671
671
 
672
672
  """
673
673
  Create interaction variables by multiplying specified treatment columns.
@@ -787,7 +787,7 @@ def data_diagnostics(
787
787
  drop_missing: bool = True,
788
788
  missing_replace_by_zero: bool = False,
789
789
  verbose: bool = config.VERBOSE
790
- ):
790
+ ) -> dict:
791
791
 
792
792
  """
793
793
  Run diagnostics on model data: missingness, balance and basic stats.
@@ -906,7 +906,7 @@ def treatment_diagnostics(
906
906
  pre_post: bool = False,
907
907
  confint_alpha = 0.05,
908
908
  verbose: bool = config.VERBOSE
909
- ):
909
+ ) -> list:
910
910
 
911
911
  """
912
912
  Compute diagnostics for each treatment: simultaneity, parallel trends,
@@ -1057,7 +1057,7 @@ def ols_fit(
1057
1057
  confint_alpha = 0.05,
1058
1058
  cluster_SE_by: str = None,
1059
1059
  verbose: bool = config.VERBOSE
1060
- ):
1060
+ ) -> list:
1061
1061
 
1062
1062
  """
1063
1063
  Estimate an OLS model using a formula and return coefficients and predictions.
@@ -1130,7 +1130,7 @@ def ml_fit(
1130
1130
  family=sm.families.Gaussian(),
1131
1131
  link=sm.families.links.Identity(),
1132
1132
  verbose: bool = config.VERBOSE
1133
- ):
1133
+ ) -> list:
1134
1134
 
1135
1135
  """
1136
1136
  Estimate a model by maximum likelihood (GLM) and return coefficients and predictions.
@@ -1217,7 +1217,7 @@ def extract_model_results(
1217
1217
  BG_x_TT_col: list = None,
1218
1218
  covariates: list = None,
1219
1219
  verbose: bool = config.VERBOSE
1220
- ):
1220
+ ) -> dict:
1221
1221
 
1222
1222
  """
1223
1223
  Compile and format model coefficient results into a structured dict.
@@ -1311,6 +1311,9 @@ def extract_model_results(
1311
1311
  "CI_lower": float(coef_conf_intervals.loc[treatment, 0]),
1312
1312
  "CI_upper": float(coef_conf_intervals.loc[treatment, 1]),
1313
1313
  }
1314
+
1315
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1316
+ ATE = remove_duplicates_from_dict(ATE)
1314
1317
 
1315
1318
  model_results = {config.EFFECTS_TYPES["ATE"]["model_results_key"]: ATE}
1316
1319
 
@@ -1329,6 +1332,9 @@ def extract_model_results(
1329
1332
  "CI_upper": float(coef_conf_intervals.loc[TG_, 1]),
1330
1333
  }
1331
1334
 
1335
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1336
+ beta_1 = remove_duplicates_from_dict(beta_1)
1337
+
1332
1338
  model_results[config.EFFECTS_TYPES["beta_1"]["model_results_key"]] = beta_1
1333
1339
 
1334
1340
  if (len(TT_col) > 0) and (any(col in coefficients for col in TT_col)):
@@ -1346,6 +1352,9 @@ def extract_model_results(
1346
1352
  "CI_upper": float(coef_conf_intervals.loc[TT_, 1]),
1347
1353
  }
1348
1354
 
1355
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1356
+ delta_0 = remove_duplicates_from_dict(delta_0)
1357
+
1349
1358
  model_results[config.EFFECTS_TYPES["delta_0"]["model_results_key"]] = delta_0
1350
1359
 
1351
1360
  if "Intercept" in coefficients:
@@ -1377,6 +1386,9 @@ def extract_model_results(
1377
1386
  "CI_upper": float(coef_conf_intervals.loc[AATE_, 1]),
1378
1387
  }
1379
1388
 
1389
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1390
+ AATE = remove_duplicates_from_dict(AATE)
1391
+
1380
1392
  model_results[config.EFFECTS_TYPES["AATE"]["model_results_key"]] = AATE
1381
1393
 
1382
1394
  if (len(ATT_col) > 0) and (any(col in coefficients for col in ATT_col)):
@@ -1394,6 +1406,9 @@ def extract_model_results(
1394
1406
  "CI_upper": float(coef_conf_intervals.loc[ATT_, 1]),
1395
1407
  }
1396
1408
 
1409
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1410
+ ATT = remove_duplicates_from_dict(ATT)
1411
+
1397
1412
  model_results[config.EFFECTS_TYPES["ATT"]["model_results_key"]] = ATT
1398
1413
 
1399
1414
  if (len(spillover_vars) > 0) and (any(col in coefficients for col in spillover_vars)):
@@ -1411,6 +1426,9 @@ def extract_model_results(
1411
1426
  "CI_upper": float(coef_conf_intervals.loc[spillover_var, 1]),
1412
1427
  }
1413
1428
 
1429
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1430
+ spillover_coef = remove_duplicates_from_dict(spillover_coef)
1431
+
1414
1432
  model_results[config.EFFECTS_TYPES["spillover"]["model_results_key"]] = spillover_coef
1415
1433
 
1416
1434
  fixed_effects = [
@@ -1435,6 +1453,9 @@ def extract_model_results(
1435
1453
  "Coefficient_type": config.EFFECTS_TYPES["FE"]["types"][0]["description"]
1436
1454
  }
1437
1455
 
1456
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1457
+ FE_unit_coef = remove_duplicates_from_dict(FE_unit_coef)
1458
+
1438
1459
  fixed_effects[0] = {config.EFFECTS_TYPES["FE"]["types"][0]["model_results_key"]: FE_unit_coef}
1439
1460
 
1440
1461
  if (len(FE_time_vars) > 0) and (any(col in coefficients for col in FE_time_vars)):
@@ -1452,7 +1473,10 @@ def extract_model_results(
1452
1473
  "CI_upper": float(coef_conf_intervals.loc[time_dummy, 1]),
1453
1474
  "Coefficient_type": config.EFFECTS_TYPES["FE"]["types"][1]["description"]
1454
1475
  }
1455
-
1476
+
1477
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1478
+ FE_time_coef = remove_duplicates_from_dict(FE_time_coef)
1479
+
1456
1480
  fixed_effects[1] = {config.EFFECTS_TYPES["FE"]["types"][1]["model_results_key"]: FE_time_coef}
1457
1481
 
1458
1482
  if (len(FE_group_vars) > 0) and (any(col in coefficients for col in FE_group_vars)):
@@ -1470,7 +1494,10 @@ def extract_model_results(
1470
1494
  "CI_upper": float(coef_conf_intervals.loc[group_dummy, 1]),
1471
1495
  "Coefficient_type": config.EFFECTS_TYPES["FE"]["types"][2]["description"]
1472
1496
  }
1473
-
1497
+
1498
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1499
+ FE_group_coef = remove_duplicates_from_dict(FE_group_coef)
1500
+
1474
1501
  fixed_effects[2] = {config.EFFECTS_TYPES["FE"]["types"][2]["model_results_key"]: FE_group_coef}
1475
1502
 
1476
1503
  model_results[config.EFFECTS_TYPES["FE"]["model_results_key"]] = fixed_effects
@@ -1491,6 +1518,9 @@ def extract_model_results(
1491
1518
  "CI_upper": float(coef_conf_intervals.loc[ITT_var, 1]),
1492
1519
  }
1493
1520
 
1521
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1522
+ ITT_coef = remove_duplicates_from_dict(ITT_coef)
1523
+
1494
1524
  model_results["individual_time_trends"] = ITT_coef
1495
1525
 
1496
1526
  if (len(ITE_vars) > 0) and (any(col in coefficients for col in ITE_vars)):
@@ -1516,6 +1546,9 @@ def extract_model_results(
1516
1546
  "CI_upper": float(coef_conf_intervals.loc[ITE_var, 1]),
1517
1547
  }
1518
1548
 
1549
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1550
+ ITE_coef = remove_duplicates_from_dict(ITE_coef)
1551
+
1519
1552
  model_results["individual_treatment_effects"] = ITE_coef
1520
1553
 
1521
1554
  if (len(GTT_vars) > 0) and (any(col in coefficients for col in GTT_vars)):
@@ -1534,6 +1567,9 @@ def extract_model_results(
1534
1567
  "CI_upper": float(coef_conf_intervals.loc[GTT_var, 1]),
1535
1568
  }
1536
1569
 
1570
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1571
+ GTT_coef = remove_duplicates_from_dict(GTT_coef)
1572
+
1537
1573
  model_results["group_time_trends"] = GTT_coef
1538
1574
 
1539
1575
  if (len(GTE_vars) > 0) and (any(col in coefficients for col in GTE_vars)):
@@ -1558,6 +1594,9 @@ def extract_model_results(
1558
1594
  "CI_upper": float(coef_conf_intervals.loc[GTE_var, 1]),
1559
1595
  }
1560
1596
 
1597
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1598
+ GTE_coef = remove_duplicates_from_dict(GTE_coef)
1599
+
1561
1600
  model_results["group_treatment_effects"] = GTE_coef
1562
1601
 
1563
1602
  if (len(covariates) > 0) and (any(col in coefficients for col in covariates)):
@@ -1577,6 +1616,9 @@ def extract_model_results(
1577
1616
  "CI_lower": float(coef_conf_intervals.loc[covariate, 0]),
1578
1617
  "CI_upper": float(coef_conf_intervals.loc[covariate, 1]),
1579
1618
  }
1619
+
1620
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1621
+ covariates_effects = remove_duplicates_from_dict(covariates_effects)
1580
1622
 
1581
1623
  model_results["covariates_effects"] = covariates_effects
1582
1624
 
@@ -1595,6 +1637,9 @@ def extract_model_results(
1595
1637
  "CI_upper": float(coef_conf_intervals.loc[TG_x_BG_x_TT_, 1]),
1596
1638
  }
1597
1639
 
1640
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1641
+ TDATE = remove_duplicates_from_dict(TDATE)
1642
+
1598
1643
  model_results = {config.EFFECTS_TYPES_DDD["TDATE"]["model_results_key"]: TDATE}
1599
1644
 
1600
1645
  if (len(BG_col) > 0) and (any(col in coefficients for col in BG_col)):
@@ -1612,6 +1657,9 @@ def extract_model_results(
1612
1657
  "CI_upper": float(coef_conf_intervals.loc[BG_, 1]),
1613
1658
  }
1614
1659
 
1660
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1661
+ BG = remove_duplicates_from_dict(BG)
1662
+
1615
1663
  model_results[config.EFFECTS_TYPES_DDD["beta_2"]["model_results_key"]] = BG
1616
1664
 
1617
1665
  if (len(TG_x_BG_col) > 0) and (any(col in coefficients for col in TG_x_BG_col)):
@@ -1627,7 +1675,10 @@ def extract_model_results(
1627
1675
  "p": float(coef_p[TG_x_BG_]),
1628
1676
  "CI_lower": float(coef_conf_intervals.loc[TG_x_BG_, 0]),
1629
1677
  "CI_upper": float(coef_conf_intervals.loc[TG_x_BG_, 1]),
1630
- }
1678
+ }
1679
+
1680
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1681
+ TG_x_BG = remove_duplicates_from_dict(TG_x_BG)
1631
1682
 
1632
1683
  model_results[config.EFFECTS_TYPES_DDD["beta_4"]["model_results_key"]] = TG_x_BG
1633
1684
 
@@ -1646,6 +1697,9 @@ def extract_model_results(
1646
1697
  "CI_upper": float(coef_conf_intervals.loc[BG_x_TT_, 1]),
1647
1698
  }
1648
1699
 
1700
+ if config.REMOVE_DUPLICATES_FROM_RESULTS_DICT:
1701
+ BG_x_TT = remove_duplicates_from_dict(BG_x_TT)
1702
+
1649
1703
  model_results[config.EFFECTS_TYPES_DDD["beta_6"]["model_results_key"]] = BG_x_TT
1650
1704
 
1651
1705
  if verbose:
@@ -1658,7 +1712,7 @@ def fit_metrics(
1658
1712
  outcome_col,
1659
1713
  model_predictions,
1660
1714
  indep_vars_no: int = None
1661
- ):
1715
+ ) -> list:
1662
1716
 
1663
1717
  """
1664
1718
  Compute fit metrics for a model given data and predictions.
@@ -1704,7 +1758,7 @@ def fit_metrics(
1704
1758
 
1705
1759
  return fit_metrics_result
1706
1760
 
1707
- def create_timestamp(function):
1761
+ def create_timestamp(function) -> dict:
1708
1762
 
1709
1763
  """
1710
1764
  Create a standard timestamp dictionary for logging or metadata.
@@ -1743,4 +1797,35 @@ def create_timestamp(function):
1743
1797
  "datetime": now.strftime("%Y-%m-%d %H-%M-%S")
1744
1798
  }
1745
1799
 
1746
- return timestamp_dict
1800
+ return timestamp_dict
1801
+
1802
+ def remove_duplicates_from_dict(
1803
+ any_dict: dict
1804
+ ) -> dict:
1805
+
1806
+ """
1807
+ Remove duplicate values from a dictionary.
1808
+
1809
+ Parameters
1810
+ ----------
1811
+ any_dict : dict
1812
+ Dictionary from which to remove duplicates.
1813
+
1814
+ Returns
1815
+ -------
1816
+ dict
1817
+ Dictionary with unique values.
1818
+ """
1819
+
1820
+ any_dict_unique = {}
1821
+ seen = set()
1822
+
1823
+ for key, value in any_dict.items():
1824
+
1825
+ identifier = tuple(sorted(value.items()))
1826
+
1827
+ if identifier not in seen:
1828
+ seen.add(identifier)
1829
+ any_dict_unique[key] = value
1830
+
1831
+ return any_dict_unique
@@ -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.2
8
- # Last update: 2026-07-16 18:52
7
+ # Version: 2.3.3
8
+ # Last update: 2026-08-04 17:51
9
9
  # Copyright (c) 2024-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -1986,10 +1986,11 @@ class DiffData:
1986
1986
  }
1987
1987
  )
1988
1988
 
1989
+ columns = {}
1989
1990
  units_not_included = []
1991
+ valid_units = []
1990
1992
 
1991
1993
  for unit in units:
1992
-
1993
1994
  did_modeldata_unit = did_modeldata.loc[
1994
1995
  (did_modeldata[config.UNIT_COL].astype(str) == str(unit))
1995
1996
  & (did_modeldata[outcome_col].notna()),
@@ -1998,9 +1999,19 @@ class DiffData:
1998
1999
 
1999
2000
  if len(did_modeldata_unit) != len(outcome_matrix):
2000
2001
  units_not_included.append(unit)
2001
- units.remove(unit)
2002
2002
  else:
2003
- outcome_matrix[tools.clean_column_name(unit)] = did_modeldata_unit.reset_index(drop=True)
2003
+ valid_units.append(unit)
2004
+ columns[tools.clean_column_name(unit)] = did_modeldata_unit.reset_index(drop=True)
2005
+
2006
+ outcome_matrix = pd.concat(
2007
+ [
2008
+ outcome_matrix,
2009
+ pd.DataFrame(columns)
2010
+ ],
2011
+ axis=1
2012
+ )
2013
+
2014
+ units = valid_units
2004
2015
 
2005
2016
  if len(units_not_included) > 0:
2006
2017
  print(f"WARNING: {len(units_not_included)} analysis units were not included due to NaN data.")
@@ -4,8 +4,8 @@
4
4
  # Author: Thomas Wieland
5
5
  # ORCID: 0000-0001-5168-9846
6
6
  # mail: geowieland@googlemail.com
7
- # Version: 2.2.4
8
- # Last update: 2026-04-28 21:45
7
+ # Version: 2.2.5
8
+ # Last update: 2026-07-23 19:34
9
9
  # Copyright (c) 2025-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -540,10 +540,10 @@ def is_simultaneous(
540
540
  print("OK")
541
541
 
542
542
  if not simultaneous and data_isnotreatment[0]:
543
- print(f"NOTE: treatment '{treatment_col}' is not simultaneous.")
543
+ print(f"NOTE: Treatment '{treatment_col}' is not simultaneous.")
544
544
 
545
545
  if simultaneous and not data_isnotreatment[0]:
546
- print(f"WARNING: treatment '{treatment_col}' is simultaneous and does not include a {config.NO_TREATMENT_CG_DESCRIPTION}")
546
+ print(f"WARNING: Treatment '{treatment_col}' is simultaneous and does not include a {config.NO_TREATMENT_CG_DESCRIPTION}")
547
547
 
548
548
  return simultaneous
549
549
 
@@ -4,16 +4,18 @@
4
4
  # Author: Thomas Wieland
5
5
  # ORCID: 0000-0001-5168-9846
6
6
  # mail: geowieland@googlemail.com
7
- # Version: 2.1.0
8
- # Last update: 2026-07-10 11:35
7
+ # Version: 2.2.0
8
+ # Last update: 2026-08-04 18:42
9
9
  # Copyright (c) 2025-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
12
+
12
13
  """
13
14
  Please note that this tests script is a collection of examples for the use of most functions of the diffindiff package.
14
15
  Although the data is real (i.e., non-simulated), no results relevant to the real world can be derived from it.
15
16
  """
16
17
 
18
+ from pathlib import Path
17
19
  import pandas as pd
18
20
  from diffindiff.didanalysis import did_analysis
19
21
  from diffindiff.diddata import create_groups, create_treatment, merge_data, create_data
@@ -22,7 +24,11 @@ from diffindiff.diddata import create_groups, create_treatment, merge_data, crea
22
24
  # Example 1: Effect of a curfew in German counties in the first
23
25
  # wave of the COVID-19 pandemic (DiD pre-post analysis)
24
26
 
25
- curfew_DE=pd.read_csv("data/curfew_DE.csv", sep=";", decimal=",")
27
+ curfew_DE = pd.read_csv(
28
+ Path(__file__).parent / "data" / "curfew_DE.csv",
29
+ sep=";",
30
+ decimal=","
31
+ )
26
32
  # Dataset with daily and cumulative SARS-CoV-2 infections of German counties
27
33
  # Data source: Wieland (2020) https://doi.org/10.18335/region.v7i2.324
28
34
 
@@ -193,7 +199,12 @@ curfew_model_prepost_AT.plot(
193
199
  # Plot DiD pre vs. post results
194
200
  # with user-determined style
195
201
 
196
- counties_DE=pd.read_csv("data/counties_DE.csv", sep=";", decimal=",", encoding='latin1')
202
+ counties_DE = pd.read_csv(
203
+ Path(__file__).parent / "data" / "counties_DE.csv",
204
+ sep=";",
205
+ decimal=",",
206
+ encoding='latin1'
207
+ )
197
208
  # Dataset with German county data
198
209
 
199
210
  curfew_data_prepost_withcov = curfew_data_prepost.add_covariates(
@@ -405,7 +416,9 @@ curfew_model_extended.plot(
405
416
  # Example 3: Nighttime curfew and other NPI in Hesse
406
417
  # (Staggered adoption)
407
418
 
408
- Corona_Hesse=pd.read_excel("data/Corona_Hesse.xlsx")
419
+ Corona_Hesse = pd.read_excel(
420
+ Path(__file__).parent / "data" / "Corona_Hesse.xlsx",
421
+ )
409
422
  # Test data effective reproduction number and Corona NPI Hesse
410
423
  # Data source: Wieland (2025) https://doi.org/10.1007/s10389-024-02218-x
411
424
 
@@ -530,4 +543,75 @@ Hesse_model6=did_analysis(
530
543
  # Model with two interventions and one interaction of the two treatments
531
544
 
532
545
  Hesse_model6.summary()
533
- # Model summary
546
+ # Model summary
547
+
548
+
549
+ # Example 4: Mandatory face masks in Jena (Germany) during the first Corona wave
550
+ # data: German counties during the first Corona wave
551
+
552
+ facemasks_data=create_data(
553
+ outcome_data=curfew_DE,
554
+ unit_id_col="county",
555
+ time_col="infection_date",
556
+ outcome_col="infections_cum_per100000",
557
+ treatment_group=
558
+ curfew_DE.loc[curfew_DE["REG_NAME"] == "Jena"]["county"],
559
+ control_group=
560
+ curfew_DE.loc[curfew_DE["REG_NAME"] != "Jena"]["county"],
561
+ treatment_name="Mandatory face masks",
562
+ study_period=["2020-03-01", "2020-04-26"],
563
+ treatment_period=["2020-04-06", "2020-04-26"],
564
+ freq="D"
565
+ )
566
+ # Creating DiD dataset by defining groups and treatment time at once
567
+ # Treatment mandatory face masks in Jena beginning April 6, 2020 (first German city with mandatory face masks)
568
+
569
+ facemasks_data.summary()
570
+ # Summary of created treatment data
571
+
572
+ facemasks_data_analysis = facemasks_data.analysis(
573
+ FE_unit=True,
574
+ FE_time=True,
575
+ intercept=False,
576
+ verbose=True
577
+ )
578
+ # DiD analysis as two-way fixed effects model
579
+
580
+ facemasks_data_analysis.summary()
581
+ # Summary of model results
582
+
583
+
584
+ facemasks_data_analysis_demean = facemasks_data.analysis(
585
+ FE_unit=True,
586
+ FE_time=True,
587
+ intercept=False,
588
+ demean=True,
589
+ verbose=True
590
+ )
591
+ # DiD analysis as model with demeaened variables instead of fixed effects
592
+
593
+ facemasks_data_analysis_demean.summary()
594
+ # Summary of model results
595
+
596
+
597
+ facemasks_data_synth = facemasks_data.add_synthetic(process_unit="Jena")
598
+ # Add synthetic control for Jena
599
+
600
+ facemasks_data_synth.summary()
601
+ # Summary of synthetic DiD data
602
+
603
+ print(facemasks_data_synth.get_synthetic_control_weightings())
604
+ # Weights
605
+
606
+ print(facemasks_data_synth.data[0].head)
607
+ # Treatment unit with counterfactual (synthetic) control unit
608
+
609
+ facemasks_data_synth_analysis = facemasks_data_synth.analysis(
610
+ FE_time=True,
611
+ log_outcome=True,
612
+ verbose=True
613
+ )
614
+ # Synthetic DiD analysis
615
+
616
+ facemasks_data_synth_analysis.summary()
617
+ # Summary of model results
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: diffindiff
3
- Version: 2.5.2
3
+ Version: 2.5.4
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 case study that utilizes the diffindiff library is available on [arXiv](https:
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.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.4) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18656820
33
33
 
34
34
 
35
35
  ## Installation
@@ -164,6 +164,7 @@ See the /tests directory for usage examples of most of the included functions.
164
164
  - Goldfarb A, Tucker C, Wang Y (2022) Conducting Research in Marketing with Quasi-Experiments. *Journal of Marketing* 86(3): 1-19. [10.1177/00222429221082977](https://doi.org/10.1177/00222429221082977)
165
165
  - Isporhing IE, Lipfert M, Pestel N (2021) Does re-opening schools contribute to the spread of SARS-CoV-2? Evidence from staggered summer breaks in Germany. *Journal of Public Economics* 198: 104426. [10.1016/j.jpubeco.2021.104426](https://doi.org/10.1016/j.jpubeco.2021.104426)
166
166
  - Li KT, Luo L, Pattabhiramaiah A (2024) Causal Inference with Quasi-Experimental Data. *IMPACT at JMR* November 13, 2024. [AMA](https://www.ama.org/marketing-news/causal-inference-with-quasi-experimental-data/)
167
+ - Mitze T, Kosfeld R, Rode J, Wälde K (2020) Face masks considerably reduce COVID-19 cases in Germany. *Proceedings of the National Academy of Sciences of the United States of America* 117(51): 32293-32301. [10.1073/pnas.2015954117](https://doi.org/10.1073/pnas.2015954117)
167
168
  - Olden A (2018) What do you buy when no one's watching? The effect of self-service checkouts on the composition of sales in retail. Discussion paper FOR 3/18, Norwegian School of Economics, Norway. [http://hdl.handle.net/11250/2490886](http://hdl.handle.net/11250/2490886)
168
169
  - Olden A, Moen J (2022) The triple difference estimator. *The Econometrics Journal* 25(3): 531-553. [10.1093/ectj/utac010](https://doi.org/10.1093/ectj/utac010)
169
170
  - Strassmann A, Çolak Y, Serra-Burriel M, Nordestgaard BG, Turk A, Afzal S, Puhan MA (2023) Nationwide indoor smoking ban and impact on smoking behaviour and lung function: a two-population natural experiment. *Thorax* 78(2): 144-150. [10.1136/thoraxjnl-2021-218436](https://doi.org/10.1136/thoraxjnl-2021-218436)
@@ -179,11 +180,12 @@ See the /tests directory for usage examples of most of the included functions.
179
180
  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).
180
181
 
181
182
 
182
- ## What's new (v2.5.2)
183
+ ## What's new (v2.5.4)
183
184
 
184
- - Extensions
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
186
185
  - Bugfixes
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
186
+ - Correction of the nonsensical skipping of the treatment group dummy in the case of two observation units in didanalysis.did_analysis()
187
+ - Automatical drop of duplicates in results dictionaries in didanalysis_helper.extract_model_results()
188
+ - Inefficient successive data insert in diddata.DiffData.add_synthetic() replaced by efficient concatenating
188
189
  - Other
189
- - Some updates of documentation and README
190
+ - Test script extended by another example
191
+ - Cleaned dependencies
@@ -1,4 +1,3 @@
1
- geopandas
2
1
  pandas
3
2
  numpy
4
3
  statsmodels>=0.14.5
@@ -6,9 +5,6 @@ scipy>=1.17
6
5
  scikit-learn
7
6
  xgboost
8
7
  lightgbm
9
- shapely
10
- requests<3.0
11
- contextily
12
8
  openpyxl
13
9
  matplotlib
14
10
  patsy
@@ -7,7 +7,7 @@ def read_README():
7
7
 
8
8
  setup(
9
9
  name='diffindiff',
10
- version='2.5.2',
10
+ version='2.5.4',
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,7 +20,6 @@ setup(
20
20
  'diffindiff': ['tests/data/*'],
21
21
  },
22
22
  install_requires=[
23
- 'geopandas',
24
23
  'pandas',
25
24
  'numpy',
26
25
  'statsmodels>=0.14.5',
@@ -28,9 +27,6 @@ setup(
28
27
  'scikit-learn',
29
28
  'xgboost',
30
29
  'lightgbm',
31
- 'shapely',
32
- 'requests<3.0',
33
- 'contextily',
34
30
  'openpyxl',
35
31
  'matplotlib',
36
32
  'patsy',
File without changes
File without changes