diff-diff 2.0.0__tar.gz → 2.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {diff_diff-2.0.0 → diff_diff-2.0.1}/PKG-INFO +1 -1
  2. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/__init__.py +1 -1
  3. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/bacon.py +12 -60
  4. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/estimators.py +9 -8
  5. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/sun_abraham.py +2 -22
  6. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/twfe.py +2 -18
  7. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/utils.py +137 -0
  8. {diff_diff-2.0.0 → diff_diff-2.0.1}/pyproject.toml +1 -1
  9. {diff_diff-2.0.0 → diff_diff-2.0.1}/rust/Cargo.lock +2 -2
  10. {diff_diff-2.0.0 → diff_diff-2.0.1}/README.md +0 -0
  11. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/_backend.py +0 -0
  12. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/diagnostics.py +0 -0
  13. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/honest_did.py +0 -0
  14. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/linalg.py +0 -0
  15. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/power.py +0 -0
  16. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/prep.py +0 -0
  17. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/pretrends.py +0 -0
  18. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/results.py +0 -0
  19. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/staggered.py +0 -0
  20. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/synthetic_did.py +0 -0
  21. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/triple_diff.py +0 -0
  22. {diff_diff-2.0.0 → diff_diff-2.0.1}/diff_diff/visualization.py +0 -0
  23. {diff_diff-2.0.0 → diff_diff-2.0.1}/rust/Cargo.toml +0 -0
  24. {diff_diff-2.0.0 → diff_diff-2.0.1}/rust/src/bootstrap.rs +0 -0
  25. {diff_diff-2.0.0 → diff_diff-2.0.1}/rust/src/lib.rs +0 -0
  26. {diff_diff-2.0.0 → diff_diff-2.0.1}/rust/src/linalg.rs +0 -0
  27. {diff_diff-2.0.0 → diff_diff-2.0.1}/rust/src/weights.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diff-diff
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: Operating System :: OS Independent
@@ -113,7 +113,7 @@ from diff_diff.visualization import (
113
113
  plot_sensitivity,
114
114
  )
115
115
 
116
- __version__ = "2.0.0"
116
+ __version__ = "2.0.1"
117
117
  __all__ = [
118
118
  # Estimators
119
119
  "DifferenceInDifferences",
@@ -17,6 +17,8 @@ from typing import Any, Dict, List, Optional, Tuple
17
17
  import numpy as np
18
18
  import pandas as pd
19
19
 
20
+ from diff_diff.utils import within_transform as _within_transform_util
21
+
20
22
 
21
23
  @dataclass
22
24
  class Comparison2x2:
@@ -573,66 +575,16 @@ class BaconDecomposition:
573
575
  treat_col: str = '__bacon_treated_internal__',
574
576
  ) -> float:
575
577
  """Compute TWFE estimate using within-transformation."""
576
- # Demean by unit and time
577
- y = df[outcome].values
578
- d = df[treat_col].astype(float).values
579
-
580
- # Create unit and time dummies for demeaning
581
- units = df[unit].values
582
- times = df[time].values
583
-
584
- # Unit means
585
- unit_map = {u: i for i, u in enumerate(df[unit].unique())}
586
- unit_idx = np.array([unit_map[u] for u in units])
587
- n_units = len(unit_map)
588
-
589
- # Time means
590
- time_map = {t: i for i, t in enumerate(df[time].unique())}
591
- time_idx = np.array([time_map[t] for t in times])
592
- n_times = len(time_map)
593
-
594
- # Compute means
595
- y_unit_mean = np.zeros(n_units)
596
- d_unit_mean = np.zeros(n_units)
597
- unit_counts = np.zeros(n_units)
598
-
599
- for i in range(len(y)):
600
- u = unit_idx[i]
601
- y_unit_mean[u] += y[i]
602
- d_unit_mean[u] += d[i]
603
- unit_counts[u] += 1
604
-
605
- y_unit_mean /= np.maximum(unit_counts, 1)
606
- d_unit_mean /= np.maximum(unit_counts, 1)
607
-
608
- y_time_mean = np.zeros(n_times)
609
- d_time_mean = np.zeros(n_times)
610
- time_counts = np.zeros(n_times)
611
-
612
- for i in range(len(y)):
613
- t = time_idx[i]
614
- y_time_mean[t] += y[i]
615
- d_time_mean[t] += d[i]
616
- time_counts[t] += 1
617
-
618
- y_time_mean /= np.maximum(time_counts, 1)
619
- d_time_mean /= np.maximum(time_counts, 1)
620
-
621
- # Overall mean
622
- y_mean = np.mean(y)
623
- d_mean = np.mean(d)
624
-
625
- # Within transformation: y_it - y_i - y_t + y
626
- y_within = np.zeros(len(y))
627
- d_within = np.zeros(len(d))
628
-
629
- for i in range(len(y)):
630
- u = unit_idx[i]
631
- t = time_idx[i]
632
- y_within[i] = y[i] - y_unit_mean[u] - y_time_mean[t] + y_mean
633
- d_within[i] = d[i] - d_unit_mean[u] - d_time_mean[t] + d_mean
634
-
635
- # OLS on demeaned data
578
+ # Apply two-way within transformation
579
+ df_dm = _within_transform_util(
580
+ df, [outcome, treat_col], unit, time, suffix="_within"
581
+ )
582
+
583
+ # Extract within-transformed values
584
+ y_within = df_dm[f"{outcome}_within"].values
585
+ d_within = df_dm[f"{treat_col}_within"].values
586
+
587
+ # OLS on demeaned data: beta = sum(d * y) / sum(d^2)
636
588
  d_var = np.sum(d_within ** 2)
637
589
  if d_var > 0:
638
590
  beta = np.sum(d_within * y_within) / d_var
@@ -23,6 +23,7 @@ from diff_diff.utils import (
23
23
  WildBootstrapResults,
24
24
  compute_confidence_interval,
25
25
  compute_p_value,
26
+ demean_by_group,
26
27
  validate_binary,
27
28
  wild_bootstrap_se,
28
29
  )
@@ -227,10 +228,10 @@ class DifferenceInDifferences:
227
228
  # unit-invariant, so demeaning them would create multicollinearity
228
229
  vars_to_demean = [outcome] + (covariates or [])
229
230
  for ab_var in absorb:
230
- n_absorbed_effects += working_data[ab_var].nunique() - 1
231
- for var in vars_to_demean:
232
- group_means = working_data.groupby(ab_var)[var].transform("mean")
233
- working_data[var] = working_data[var] - group_means
231
+ working_data, n_fe = demean_by_group(
232
+ working_data, vars_to_demean, ab_var, inplace=True
233
+ )
234
+ n_absorbed_effects += n_fe
234
235
  absorbed_vars.append(ab_var)
235
236
 
236
237
  # Extract variables (may be demeaned if absorb was used)
@@ -828,10 +829,10 @@ class MultiPeriodDiD(DifferenceInDifferences):
828
829
  if absorb:
829
830
  vars_to_demean = [outcome] + (covariates or [])
830
831
  for ab_var in absorb:
831
- n_absorbed_effects += working_data[ab_var].nunique() - 1
832
- for var in vars_to_demean:
833
- group_means = working_data.groupby(ab_var)[var].transform("mean")
834
- working_data[var] = working_data[var] - group_means
832
+ working_data, n_fe = demean_by_group(
833
+ working_data, vars_to_demean, ab_var, inplace=True
834
+ )
835
+ n_absorbed_effects += n_fe
835
836
 
836
837
  # Extract outcome and treatment
837
838
  y = working_data[outcome].values.astype(float)
@@ -21,6 +21,7 @@ from diff_diff.results import _get_significance_stars
21
21
  from diff_diff.utils import (
22
22
  compute_confidence_interval,
23
23
  compute_p_value,
24
+ within_transform as _within_transform_util,
24
25
  )
25
26
 
26
27
 
@@ -789,28 +790,7 @@ class SunAbraham:
789
790
 
790
791
  y_it - y_i. - y_.t + y_..
791
792
  """
792
- df = df.copy()
793
-
794
- # Build all demeaned columns at once to avoid fragmentation
795
- demeaned_data = {}
796
- for var in variables:
797
- # Unit means
798
- unit_means = df.groupby(unit)[var].transform("mean")
799
- # Time means
800
- time_means = df.groupby(time)[var].transform("mean")
801
- # Grand mean
802
- grand_mean = df[var].mean()
803
-
804
- # Within transformation
805
- demeaned_data[f"{var}_dm"] = (
806
- df[var] - unit_means - time_means + grand_mean
807
- ).values
808
-
809
- # Add all demeaned columns at once
810
- demeaned_df = pd.DataFrame(demeaned_data, index=df.index)
811
- df = pd.concat([df, demeaned_df], axis=1)
812
-
813
- return df
793
+ return _within_transform_util(df, variables, unit, time, suffix="_dm")
814
794
 
815
795
  def _compute_iw_effects(
816
796
  self,
@@ -17,6 +17,7 @@ from diff_diff.results import DiDResults
17
17
  from diff_diff.utils import (
18
18
  compute_confidence_interval,
19
19
  compute_p_value,
20
+ within_transform as _within_transform_util,
20
21
  )
21
22
 
22
23
 
@@ -211,25 +212,8 @@ class TwoWayFixedEffects(DifferenceInDifferences):
211
212
  pd.DataFrame
212
213
  Data with demeaned variables.
213
214
  """
214
- data = data.copy()
215
215
  variables = [outcome] + (covariates or [])
216
-
217
- # Cache groupby objects for efficiency (avoids re-computing group indexes)
218
- unit_grouper = data.groupby(unit, sort=False)
219
- time_grouper = data.groupby(time, sort=False)
220
-
221
- for var in variables:
222
- # Unit means (using cached grouper)
223
- unit_means = unit_grouper[var].transform("mean")
224
- # Time means (using cached grouper)
225
- time_means = time_grouper[var].transform("mean")
226
- # Grand mean
227
- grand_mean = data[var].mean()
228
-
229
- # Within transformation
230
- data[f"{var}_demeaned"] = data[var] - unit_means - time_means + grand_mean
231
-
232
- return data
216
+ return _within_transform_util(data, variables, unit, time, suffix="_demeaned")
233
217
 
234
218
  def _check_staggered_treatment(
235
219
  self,
@@ -1342,3 +1342,140 @@ def compute_placebo_effects(
1342
1342
  placebo_effects.append(placebo_tau)
1343
1343
 
1344
1344
  return np.asarray(placebo_effects)
1345
+
1346
+
1347
+ def demean_by_group(
1348
+ data: pd.DataFrame,
1349
+ variables: List[str],
1350
+ group_var: str,
1351
+ inplace: bool = False,
1352
+ suffix: str = "",
1353
+ ) -> Tuple[pd.DataFrame, int]:
1354
+ """
1355
+ Demean variables by a grouping variable (one-way within transformation).
1356
+
1357
+ For each variable, computes: x_ig - mean(x_g) where g is the group.
1358
+
1359
+ Parameters
1360
+ ----------
1361
+ data : pd.DataFrame
1362
+ DataFrame containing the variables to demean.
1363
+ variables : list of str
1364
+ Column names to demean.
1365
+ group_var : str
1366
+ Column name for the grouping variable.
1367
+ inplace : bool, default False
1368
+ If True, modifies the original columns. If False, leaves original
1369
+ columns unchanged (demeaning is still applied to return value).
1370
+ suffix : str, default ""
1371
+ Suffix to add to demeaned column names (only used when inplace=False
1372
+ and you want to keep both original and demeaned columns).
1373
+
1374
+ Returns
1375
+ -------
1376
+ data : pd.DataFrame
1377
+ DataFrame with demeaned variables.
1378
+ n_effects : int
1379
+ Number of absorbed fixed effects (nunique - 1).
1380
+
1381
+ Examples
1382
+ --------
1383
+ >>> df, n_fe = demean_by_group(df, ['y', 'x1', 'x2'], 'unit')
1384
+ >>> # df['y'], df['x1'], df['x2'] are now demeaned by unit
1385
+ """
1386
+ if not inplace:
1387
+ data = data.copy()
1388
+
1389
+ # Count fixed effects (categories - 1 for identification)
1390
+ n_effects = data[group_var].nunique() - 1
1391
+
1392
+ # Cache the groupby object for efficiency
1393
+ grouper = data.groupby(group_var, sort=False)
1394
+
1395
+ for var in variables:
1396
+ col_name = var if not suffix else f"{var}{suffix}"
1397
+ group_means = grouper[var].transform("mean")
1398
+ data[col_name] = data[var] - group_means
1399
+
1400
+ return data, n_effects
1401
+
1402
+
1403
+ def within_transform(
1404
+ data: pd.DataFrame,
1405
+ variables: List[str],
1406
+ unit: str,
1407
+ time: str,
1408
+ inplace: bool = False,
1409
+ suffix: str = "_demeaned",
1410
+ ) -> pd.DataFrame:
1411
+ """
1412
+ Apply two-way within transformation to remove unit and time fixed effects.
1413
+
1414
+ Computes: y_it - y_i. - y_.t + y_.. for each variable.
1415
+
1416
+ This is the standard fixed effects transformation for panel data that
1417
+ removes both unit-specific and time-specific effects.
1418
+
1419
+ Parameters
1420
+ ----------
1421
+ data : pd.DataFrame
1422
+ Panel data containing the variables to transform.
1423
+ variables : list of str
1424
+ Column names to transform.
1425
+ unit : str
1426
+ Column name for unit identifier.
1427
+ time : str
1428
+ Column name for time period identifier.
1429
+ inplace : bool, default False
1430
+ If True, modifies the original columns. If False, creates new columns
1431
+ with the specified suffix.
1432
+ suffix : str, default "_demeaned"
1433
+ Suffix for new column names when inplace=False.
1434
+
1435
+ Returns
1436
+ -------
1437
+ pd.DataFrame
1438
+ DataFrame with within-transformed variables.
1439
+
1440
+ Notes
1441
+ -----
1442
+ The within transformation removes variation that is constant within units
1443
+ (unit fixed effects) and constant within time periods (time fixed effects).
1444
+ The resulting estimates are equivalent to including unit and time dummies
1445
+ but is computationally more efficient for large panels.
1446
+
1447
+ Examples
1448
+ --------
1449
+ >>> df = within_transform(df, ['y', 'x'], 'unit_id', 'year')
1450
+ >>> # df now has 'y_demeaned' and 'x_demeaned' columns
1451
+ """
1452
+ if not inplace:
1453
+ data = data.copy()
1454
+
1455
+ # Cache groupby objects for efficiency
1456
+ unit_grouper = data.groupby(unit, sort=False)
1457
+ time_grouper = data.groupby(time, sort=False)
1458
+
1459
+ if inplace:
1460
+ # Modify columns in place
1461
+ for var in variables:
1462
+ unit_means = unit_grouper[var].transform("mean")
1463
+ time_means = time_grouper[var].transform("mean")
1464
+ grand_mean = data[var].mean()
1465
+ data[var] = data[var] - unit_means - time_means + grand_mean
1466
+ else:
1467
+ # Build all demeaned columns at once to avoid DataFrame fragmentation
1468
+ demeaned_data = {}
1469
+ for var in variables:
1470
+ unit_means = unit_grouper[var].transform("mean")
1471
+ time_means = time_grouper[var].transform("mean")
1472
+ grand_mean = data[var].mean()
1473
+ demeaned_data[f"{var}{suffix}"] = (
1474
+ data[var] - unit_means - time_means + grand_mean
1475
+ ).values
1476
+
1477
+ # Add all columns at once
1478
+ demeaned_df = pd.DataFrame(demeaned_data, index=data.index)
1479
+ data = pd.concat([data, demeaned_df], axis=1)
1480
+
1481
+ return data
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "diff-diff"
7
- version = "2.0.0"
7
+ version = "2.0.1"
8
8
  description = "A library for Difference-in-Differences causal inference analysis"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2391,6 +2391,6 @@ dependencies = [
2391
2391
 
2392
2392
  [[package]]
2393
2393
  name = "zmij"
2394
- version = "1.0.13"
2394
+ version = "1.0.14"
2395
2395
  source = "registry+https://github.com/rust-lang/crates.io-index"
2396
- checksum = "ac93432f5b761b22864c774aac244fa5c0fd877678a4c37ebf6cf42208f9c9ec"
2396
+ checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes