diffindiff 2.5.3__tar.gz → 2.5.5__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.3 → diffindiff-2.5.5}/PKG-INFO +6 -6
  2. {diffindiff-2.5.3 → diffindiff-2.5.5}/README.md +5 -5
  3. {diffindiff-2.5.3 → diffindiff-2.5.5}/diffindiff/config.py +5 -3
  4. {diffindiff-2.5.3 → diffindiff-2.5.5}/diffindiff/didanalysis.py +29 -16
  5. {diffindiff-2.5.3 → diffindiff-2.5.5}/diffindiff/didanalysis_helper.py +104 -19
  6. {diffindiff-2.5.3 → diffindiff-2.5.5}/diffindiff/diddata.py +16 -5
  7. {diffindiff-2.5.3 → diffindiff-2.5.5}/diffindiff/tests/tests_diffindiff.py +90 -6
  8. {diffindiff-2.5.3 → diffindiff-2.5.5}/diffindiff.egg-info/PKG-INFO +6 -6
  9. {diffindiff-2.5.3 → diffindiff-2.5.5}/diffindiff.egg-info/requires.txt +0 -4
  10. {diffindiff-2.5.3 → diffindiff-2.5.5}/setup.py +1 -5
  11. {diffindiff-2.5.3 → diffindiff-2.5.5}/MANIFEST.in +0 -0
  12. {diffindiff-2.5.3 → diffindiff-2.5.5}/diffindiff/__init__.py +0 -0
  13. {diffindiff-2.5.3 → diffindiff-2.5.5}/diffindiff/didtools.py +0 -0
  14. {diffindiff-2.5.3 → diffindiff-2.5.5}/diffindiff/tests/__init__.py +0 -0
  15. {diffindiff-2.5.3 → diffindiff-2.5.5}/diffindiff/tests/data/Corona_Hesse.xlsx +0 -0
  16. {diffindiff-2.5.3 → diffindiff-2.5.5}/diffindiff/tests/data/counties_DE.csv +0 -0
  17. {diffindiff-2.5.3 → diffindiff-2.5.5}/diffindiff/tests/data/curfew_DE.csv +0 -0
  18. {diffindiff-2.5.3 → diffindiff-2.5.5}/diffindiff.egg-info/SOURCES.txt +0 -0
  19. {diffindiff-2.5.3 → diffindiff-2.5.5}/diffindiff.egg-info/dependency_links.txt +0 -0
  20. {diffindiff-2.5.3 → diffindiff-2.5.5}/diffindiff.egg-info/top_level.txt +0 -0
  21. {diffindiff-2.5.3 → diffindiff-2.5.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: diffindiff
3
- Version: 2.5.3
3
+ Version: 2.5.5
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.3) [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.5) [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,9 +180,8 @@ 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.3)
183
+ ## What's new (v2.5.5)
183
184
 
184
- - Extensions
185
- - Added optional saving of plots in DiffModel plot methods
186
185
  - Bugfixes
187
- - Fixed typos in messages
186
+ - didanalysis.did_analysis(): Correct usage of param 'log_outcome' in any case and check whether value of 'log_outcome_add' is appropriate
187
+ - didanalysis.DiffModel.plot() and didanalysis.DiffModel.plot_counterfactual(): Correct detection of the date format via object metadata
@@ -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.3) [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.5) [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,9 +172,8 @@ 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.3)
175
+ ## What's new (v2.5.5)
175
176
 
176
- - Extensions
177
- - Added optional saving of plots in DiffModel plot methods
178
177
  - Bugfixes
179
- - Fixed typos in messages
178
+ - didanalysis.did_analysis(): Correct usage of param 'log_outcome' in any case and check whether value of 'log_outcome_add' is appropriate
179
+ - didanalysis.DiffModel.plot() and didanalysis.DiffModel.plot_counterfactual(): Correct detection of the date format via object metadata
@@ -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.24
8
- # Last update: 2026-07-25 12:31
7
+ # Version: 1.0.26
8
+ # Last update: 2026-09-11 21:46
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.3"
15
+ PACKAGE_VERSION = "2.5.5"
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.4
8
- # Last update: 2026-07-23 19:42
7
+ # Version: 2.4.6
8
+ # Last update: 2026-09-11 21:46
9
9
  # Copyright (c) 2024-2026 Thomas Wieland
10
10
  #-----------------------------------------------------------------------
11
11
 
@@ -1875,8 +1875,12 @@ class DiffModel:
1875
1875
  fig, ax = plt.subplots(figsize=(12, 6))
1876
1876
  else:
1877
1877
  fig, ax = plt.subplots(figsize=(plot_size[0], plot_size[1]))
1878
-
1879
- model_data_TG_CG["t"] = pd.to_datetime(model_data_TG_CG["t"])
1878
+
1879
+ date_format = model_config["date_format"]
1880
+ model_data_TG_CG["t"] = pd.to_datetime(
1881
+ model_data_TG_CG["t"],
1882
+ format = date_format
1883
+ )
1880
1884
 
1881
1885
  if not model_config["pre_post"]:
1882
1886
  pre_post_barplot = False
@@ -2022,7 +2026,7 @@ class DiffModel:
2022
2026
  plt.xlabel(x_label)
2023
2027
  plt.ylabel(y_label)
2024
2028
  plt.title(plot_title)
2025
- ax.xaxis.set_major_formatter(DateFormatter(model_config["date_format"]))
2029
+ ax.xaxis.set_major_formatter(DateFormatter(date_format))
2026
2030
 
2027
2031
  if model_config["pre_post"]:
2028
2032
 
@@ -2222,7 +2226,12 @@ class DiffModel:
2222
2226
  model_data_TG_mean_pred_counterfac.reset_index()
2223
2227
  ],
2224
2228
  axis = 1)
2225
- model_data_TG_mean[time_col] = pd.to_datetime(model_data_TG_mean[time_col])
2229
+
2230
+ date_format = model_config["date_format"]
2231
+ model_data_TG_mean[time_col] = pd.to_datetime(
2232
+ model_data_TG_mean[time_col],
2233
+ format=date_format
2234
+ )
2226
2235
 
2227
2236
  fig, ax = plt.subplots(figsize=(plot_size[0], plot_size[1]))
2228
2237
 
@@ -2512,7 +2521,7 @@ def did_analysis(
2512
2521
  unique_units = treatment_diagnostics_results[3]
2513
2522
  unique_time_points = treatment_diagnostics_results[4]
2514
2523
 
2515
- if no_treatments > 1:
2524
+ if no_treatments > 1 and unique_units >= config.FIXED_EFFECTS_THRESHOLD:
2516
2525
 
2517
2526
  intercept = False
2518
2527
  TG_col = []
@@ -2725,13 +2734,15 @@ def did_analysis(
2725
2734
  print(f"NOTE: Input is {config.MULTIPERIOD_PANELDATA_DESCRIPTION}. Consider including {config.EFFECTS_TYPES['FE']['description']}.")
2726
2735
 
2727
2736
  if log_outcome:
2728
-
2729
- if missing_replace_by_zero:
2730
- data[f"{config.LOG_PREFIX}{config.DELIMITER}{outcome_col}"] = np.log(data[outcome_col]+log_outcome_add)
2731
-
2737
+
2738
+ if any(data[outcome_col] <= 0):
2739
+ data[f"{config.LOG_PREFIX}{config.DELIMITER}{outcome_col}"] = np.log(data[outcome_col]+log_outcome_add)
2732
2740
  else:
2733
2741
  data[f"{config.LOG_PREFIX}{config.DELIMITER}{outcome_col}"] = np.log(data[outcome_col])
2734
-
2742
+
2743
+ if not np.isfinite(data[f"{config.LOG_PREFIX}{config.DELIMITER}{outcome_col}"]).all():
2744
+ print(f"WARNING: Outcome column '{outcome_col}' in input data contains values <= 0, which is not fully compensated by log_outcome_add = {log_outcome_add}. This may lead to NaN Inf values in the log-transformed outcome. Consider using a larger constant for log_outcome_add.")
2745
+
2735
2746
  outcome_col = f"{config.LOG_PREFIX}{config.DELIMITER}{outcome_col}"
2736
2747
 
2737
2748
  if not ITE and not GTE:
@@ -3320,12 +3331,14 @@ def ddd_analysis(
3320
3331
  pre_post = True
3321
3332
 
3322
3333
  if log_outcome:
3323
-
3324
- if missing_replace_by_zero:
3325
- data[f"{config.LOG_PREFIX}{config.DELIMITER}{outcome_col}"] = np.log(data[outcome_col]+log_outcome_add)
3326
-
3334
+
3335
+ if any(data[outcome_col] <= 0):
3336
+ data[f"{config.LOG_PREFIX}{config.DELIMITER}{outcome_col}"] = np.log(data[outcome_col]+log_outcome_add)
3327
3337
  else:
3328
3338
  data[f"{config.LOG_PREFIX}{config.DELIMITER}{outcome_col}"] = np.log(data[outcome_col])
3339
+
3340
+ if not np.isfinite(data[f"{config.LOG_PREFIX}{config.DELIMITER}{outcome_col}"]).all():
3341
+ print(f"WARNING: Outcome column '{outcome_col}' in input data contains values <= 0, which is not fully compensated by log_outcome_add = {log_outcome_add}. This may lead to NaN Inf values in the log-transformed outcome. Consider using a larger constant for log_outcome_add.")
3329
3342
 
3330
3343
  outcome_col = f"{config.LOG_PREFIX}{config.DELIMITER}{outcome_col}"
3331
3344
 
@@ -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,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.3
3
+ Version: 2.5.5
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.3) [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.5) [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,9 +180,8 @@ 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.3)
183
+ ## What's new (v2.5.5)
183
184
 
184
- - Extensions
185
- - Added optional saving of plots in DiffModel plot methods
186
185
  - Bugfixes
187
- - Fixed typos in messages
186
+ - didanalysis.did_analysis(): Correct usage of param 'log_outcome' in any case and check whether value of 'log_outcome_add' is appropriate
187
+ - didanalysis.DiffModel.plot() and didanalysis.DiffModel.plot_counterfactual(): Correct detection of the date format via object metadata
@@ -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.3',
10
+ version='2.5.5',
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