diff-diff 2.0.1__tar.gz → 2.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {diff_diff-2.0.1 → diff_diff-2.0.2}/PKG-INFO +1 -1
  2. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/__init__.py +1 -1
  3. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/staggered.py +157 -6
  4. {diff_diff-2.0.1 → diff_diff-2.0.2}/pyproject.toml +1 -1
  5. {diff_diff-2.0.1 → diff_diff-2.0.2}/rust/Cargo.lock +12 -12
  6. {diff_diff-2.0.1 → diff_diff-2.0.2}/README.md +0 -0
  7. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/_backend.py +0 -0
  8. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/bacon.py +0 -0
  9. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/diagnostics.py +0 -0
  10. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/estimators.py +0 -0
  11. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/honest_did.py +0 -0
  12. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/linalg.py +0 -0
  13. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/power.py +0 -0
  14. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/prep.py +0 -0
  15. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/pretrends.py +0 -0
  16. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/results.py +0 -0
  17. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/sun_abraham.py +0 -0
  18. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/synthetic_did.py +0 -0
  19. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/triple_diff.py +0 -0
  20. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/twfe.py +0 -0
  21. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/utils.py +0 -0
  22. {diff_diff-2.0.1 → diff_diff-2.0.2}/diff_diff/visualization.py +0 -0
  23. {diff_diff-2.0.1 → diff_diff-2.0.2}/rust/Cargo.toml +0 -0
  24. {diff_diff-2.0.1 → diff_diff-2.0.2}/rust/src/bootstrap.rs +0 -0
  25. {diff_diff-2.0.1 → diff_diff-2.0.2}/rust/src/lib.rs +0 -0
  26. {diff_diff-2.0.1 → diff_diff-2.0.2}/rust/src/linalg.rs +0 -0
  27. {diff_diff-2.0.1 → diff_diff-2.0.2}/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.1
3
+ Version: 2.0.2
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.1"
116
+ __version__ = "2.0.2"
117
117
  __all__ = [
118
118
  # Estimators
119
119
  "DifferenceInDifferences",
@@ -1479,29 +1479,36 @@ class CallawaySantAnna:
1479
1479
 
1480
1480
  Standard errors are computed using influence function aggregation,
1481
1481
  which properly accounts for covariances across (g,t) pairs due to
1482
- shared control units. This matches R's `did` package approach.
1482
+ shared control units. This includes the wif (weight influence function)
1483
+ adjustment from R's `did` package that accounts for uncertainty in
1484
+ estimating the group-size weights.
1483
1485
  """
1484
1486
  effects = []
1485
1487
  weights_list = []
1486
1488
  gt_pairs = []
1489
+ groups_for_gt = []
1487
1490
 
1488
1491
  for (g, t), data in group_time_effects.items():
1489
1492
  effects.append(data['effect'])
1490
1493
  weights_list.append(data['n_treated'])
1491
1494
  gt_pairs.append((g, t))
1495
+ groups_for_gt.append(g)
1492
1496
 
1493
1497
  effects = np.array(effects)
1494
1498
  weights = np.array(weights_list, dtype=float)
1499
+ groups_for_gt = np.array(groups_for_gt)
1495
1500
 
1496
1501
  # Normalize weights
1497
- weights = weights / np.sum(weights)
1502
+ total_weight = np.sum(weights)
1503
+ weights_norm = weights / total_weight
1498
1504
 
1499
1505
  # Weighted average
1500
- overall_att = np.sum(weights * effects)
1506
+ overall_att = np.sum(weights_norm * effects)
1501
1507
 
1502
- # Compute SE using influence function aggregation
1503
- overall_se = self._compute_aggregated_se(
1504
- gt_pairs, weights, influence_func_info
1508
+ # Compute SE using influence function aggregation with wif adjustment
1509
+ overall_se = self._compute_aggregated_se_with_wif(
1510
+ gt_pairs, weights_norm, effects, groups_for_gt,
1511
+ influence_func_info, df, unit
1505
1512
  )
1506
1513
 
1507
1514
  return overall_att, overall_se
@@ -1566,6 +1573,150 @@ class CallawaySantAnna:
1566
1573
  variance = np.sum(psi_overall ** 2)
1567
1574
  return np.sqrt(variance)
1568
1575
 
1576
+ def _compute_aggregated_se_with_wif(
1577
+ self,
1578
+ gt_pairs: List[Tuple[Any, Any]],
1579
+ weights: np.ndarray,
1580
+ effects: np.ndarray,
1581
+ groups_for_gt: np.ndarray,
1582
+ influence_func_info: Dict,
1583
+ df: pd.DataFrame,
1584
+ unit: str,
1585
+ ) -> float:
1586
+ """
1587
+ Compute SE with weight influence function (wif) adjustment.
1588
+
1589
+ This matches R's `did` package approach for "simple" aggregation,
1590
+ which accounts for uncertainty in estimating group-size weights.
1591
+
1592
+ The wif adjustment adds variance due to the fact that aggregation
1593
+ weights w_g = n_g / N depend on estimated group sizes.
1594
+
1595
+ Formula (matching R's did::aggte):
1596
+ agg_inf_i = Σ_k w_k × inf_i_k + wif_i × ATT_k
1597
+ se = sqrt(mean(agg_inf^2) / n)
1598
+
1599
+ where:
1600
+ - k indexes "keepers" (post-treatment (g,t) pairs)
1601
+ - w_k = pg[k] / sum(pg[keepers]) where pg = n_g / n_all
1602
+ - wif captures how unit i influences the weight estimation
1603
+ """
1604
+ if not influence_func_info:
1605
+ return 0.0
1606
+
1607
+ # Build unit index mapping
1608
+ all_units = set()
1609
+ for (g, t) in gt_pairs:
1610
+ if (g, t) in influence_func_info:
1611
+ info = influence_func_info[(g, t)]
1612
+ all_units.update(info['treated_units'])
1613
+ all_units.update(info['control_units'])
1614
+
1615
+ if not all_units:
1616
+ return 0.0
1617
+
1618
+ all_units = sorted(all_units)
1619
+ n_units = len(all_units)
1620
+ unit_to_idx = {u: i for i, u in enumerate(all_units)}
1621
+
1622
+ # Get unique groups and their information
1623
+ unique_groups = sorted(set(groups_for_gt))
1624
+ group_to_idx = {g: i for i, g in enumerate(unique_groups)}
1625
+
1626
+ # Compute group-level probabilities matching R's formula:
1627
+ # pg[g] = n_g / n_all (fraction of ALL units in group g)
1628
+ # This differs from our old formula which used n_g / total_treated
1629
+ group_sizes = {}
1630
+ for g in unique_groups:
1631
+ treated_in_g = df[df['first_treat'] == g][unit].nunique()
1632
+ group_sizes[g] = treated_in_g
1633
+
1634
+ # pg indexed by group
1635
+ pg_by_group = np.array([group_sizes[g] / n_units for g in unique_groups])
1636
+
1637
+ # pg indexed by keeper (each (g,t) pair gets its group's pg)
1638
+ # This matches R's: pg <- pgg[match(group, originalglist)]
1639
+ pg_keepers = np.array([pg_by_group[group_to_idx[g]] for g in groups_for_gt])
1640
+ sum_pg_keepers = np.sum(pg_keepers)
1641
+
1642
+ # Standard aggregated influence (without wif)
1643
+ psi_standard = np.zeros(n_units)
1644
+
1645
+ for j, (g, t) in enumerate(gt_pairs):
1646
+ if (g, t) not in influence_func_info:
1647
+ continue
1648
+
1649
+ info = influence_func_info[(g, t)]
1650
+ w = weights[j]
1651
+
1652
+ for i, uid in enumerate(info['treated_units']):
1653
+ idx = unit_to_idx[uid]
1654
+ psi_standard[idx] += w * info['treated_inf'][i]
1655
+
1656
+ for i, uid in enumerate(info['control_units']):
1657
+ idx = unit_to_idx[uid]
1658
+ psi_standard[idx] += w * info['control_inf'][i]
1659
+
1660
+ # Build unit-group membership indicator
1661
+ unit_groups = {}
1662
+ for uid in all_units:
1663
+ unit_first_treat = df[df[unit] == uid]['first_treat'].iloc[0]
1664
+ if unit_first_treat in unique_groups:
1665
+ unit_groups[uid] = unit_first_treat
1666
+ else:
1667
+ unit_groups[uid] = None # Never-treated or other
1668
+
1669
+ # Compute wif using R's exact formula (iterate over keepers, not groups)
1670
+ # R's wif function:
1671
+ # if1[i,k] = (indicator(G_i == group_k) - pg[k]) / sum(pg[keepers])
1672
+ # if2[i,k] = indicator_sum[i] * pg[k] / sum(pg[keepers])^2
1673
+ # wif[i,k] = if1[i,k] - if2[i,k]
1674
+ #
1675
+ # Then: wif_contrib[i] = sum_k(wif[i,k] * att[k])
1676
+
1677
+ n_keepers = len(gt_pairs)
1678
+ wif_contrib = np.zeros(n_units)
1679
+
1680
+ # Pre-compute indicator_sum for each unit
1681
+ # indicator_sum[i] = sum_k(indicator(G_i == group_k) - pg[k])
1682
+ indicator_sum = np.zeros(n_units)
1683
+ for j, g in enumerate(groups_for_gt):
1684
+ pg_k = pg_keepers[j]
1685
+ for uid in all_units:
1686
+ i = unit_to_idx[uid]
1687
+ unit_g = unit_groups[uid]
1688
+ indicator = 1.0 if unit_g == g else 0.0
1689
+ indicator_sum[i] += (indicator - pg_k)
1690
+
1691
+ # Compute wif contribution for each keeper
1692
+ for j, (g, t) in enumerate(gt_pairs):
1693
+ pg_k = pg_keepers[j]
1694
+ att_k = effects[j]
1695
+
1696
+ for uid in all_units:
1697
+ i = unit_to_idx[uid]
1698
+ unit_g = unit_groups[uid]
1699
+ indicator = 1.0 if unit_g == g else 0.0
1700
+
1701
+ # R's formula for wif
1702
+ if1_ik = (indicator - pg_k) / sum_pg_keepers
1703
+ if2_ik = indicator_sum[i] * pg_k / (sum_pg_keepers ** 2)
1704
+ wif_ik = if1_ik - if2_ik
1705
+
1706
+ # Add contribution: wif[i,k] * att[k]
1707
+ wif_contrib[i] += wif_ik * att_k
1708
+
1709
+ # Scale by 1/n_units to match R's getSE formula: sqrt(mean(IF^2)/n)
1710
+ psi_wif = wif_contrib / n_units
1711
+
1712
+ # Combine standard and wif terms
1713
+ psi_total = psi_standard + psi_wif
1714
+
1715
+ # Compute variance and SE
1716
+ # R's formula: sqrt(mean(IF^2) / n) = sqrt(sum(IF^2) / n^2)
1717
+ variance = np.sum(psi_total ** 2)
1718
+ return np.sqrt(variance)
1719
+
1569
1720
  def _aggregate_event_study(
1570
1721
  self,
1571
1722
  group_time_effects: Dict,
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "diff-diff"
7
- version = "2.0.1"
7
+ version = "2.0.2"
8
8
  description = "A library for Difference-in-Differences causal inference analysis"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -131,9 +131,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
131
131
 
132
132
  [[package]]
133
133
  name = "chrono"
134
- version = "0.4.42"
134
+ version = "0.4.43"
135
135
  source = "registry+https://github.com/rust-lang/crates.io-index"
136
- checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
136
+ checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
137
137
  dependencies = [
138
138
  "iana-time-zone",
139
139
  "js-sys",
@@ -709,9 +709,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
709
709
 
710
710
  [[package]]
711
711
  name = "js-sys"
712
- version = "0.3.83"
712
+ version = "0.3.85"
713
713
  source = "registry+https://github.com/rust-lang/crates.io-index"
714
- checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
714
+ checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
715
715
  dependencies = [
716
716
  "once_cell",
717
717
  "wasm-bindgen",
@@ -1895,9 +1895,9 @@ dependencies = [
1895
1895
 
1896
1896
  [[package]]
1897
1897
  name = "wasm-bindgen"
1898
- version = "0.2.106"
1898
+ version = "0.2.108"
1899
1899
  source = "registry+https://github.com/rust-lang/crates.io-index"
1900
- checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
1900
+ checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
1901
1901
  dependencies = [
1902
1902
  "cfg-if",
1903
1903
  "once_cell",
@@ -1908,9 +1908,9 @@ dependencies = [
1908
1908
 
1909
1909
  [[package]]
1910
1910
  name = "wasm-bindgen-macro"
1911
- version = "0.2.106"
1911
+ version = "0.2.108"
1912
1912
  source = "registry+https://github.com/rust-lang/crates.io-index"
1913
- checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
1913
+ checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
1914
1914
  dependencies = [
1915
1915
  "quote",
1916
1916
  "wasm-bindgen-macro-support",
@@ -1918,9 +1918,9 @@ dependencies = [
1918
1918
 
1919
1919
  [[package]]
1920
1920
  name = "wasm-bindgen-macro-support"
1921
- version = "0.2.106"
1921
+ version = "0.2.108"
1922
1922
  source = "registry+https://github.com/rust-lang/crates.io-index"
1923
- checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
1923
+ checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
1924
1924
  dependencies = [
1925
1925
  "bumpalo",
1926
1926
  "proc-macro2",
@@ -1931,9 +1931,9 @@ dependencies = [
1931
1931
 
1932
1932
  [[package]]
1933
1933
  name = "wasm-bindgen-shared"
1934
- version = "0.2.106"
1934
+ version = "0.2.108"
1935
1935
  source = "registry+https://github.com/rust-lang/crates.io-index"
1936
- checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
1936
+ checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
1937
1937
  dependencies = [
1938
1938
  "unicode-ident",
1939
1939
  ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes