trop 0.1.0__tar.gz → 0.1.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.
trop-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 zhaonanq
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
trop-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: trop
3
+ Version: 0.1.1
4
+ Summary: Triply Robust Panel (TROP) estimator: weighted TWFE with optional low-rank adjustment.
5
+ Author: Susan Athey, Guido Imbens, Zhaonan Qu, Davide Viviano
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/zhaonanq/TROP
8
+ Project-URL: Repository, https://github.com/zhaonanq/TROP
9
+ Project-URL: Issues, https://github.com/zhaonanq/TROP/issues
10
+ Keywords: causal-inference,panel-data,factor-models,difference-in-differences,synthetic-control,synthetic-controls,trop,twfe
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering
21
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: numpy>=1.23
26
+ Requires-Dist: cvxpy>=1.4
27
+ Requires-Dist: osqp>=0.6.5
28
+ Requires-Dist: scs>=3.2.4
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0; extra == "dev"
31
+ Requires-Dist: ruff>=0.5.0; extra == "dev"
32
+ Requires-Dist: black>=24.0.0; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # TROP: Triply Robust Panel Estimator
36
+
37
+ This package provides a Python implementation of the **Triply Robust Panel (TROP)** estimator introduced in:
38
+
39
+ > Susan Athey, Guido Imbens, Zhaonan Qu, Davide Viviano (2025).
40
+ > *Triply Robust Panel Estimators*.
41
+ > arXiv:2508.21536.
42
+
43
+ The initial release (v0.1.0) exposes the function:
44
+
45
+ - `TROP_TWFE_average(Y, W, treated_units, lambda_unit, lambda_time, lambda_nn, treated_periods=..., solver=...)`
46
+
47
+ which estimates an average treatment effect `tau` in panel settings using a weighted TWFE objective with optional low-rank adjustment.
48
+
49
+ ---
50
+
51
+ ## Installation
52
+
53
+ ```
54
+ pip install trop
55
+ ```
trop-0.1.1/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # TROP: Triply Robust Panel Estimator
2
+
3
+ This package provides a Python implementation of the **Triply Robust Panel (TROP)** estimator introduced in:
4
+
5
+ > Susan Athey, Guido Imbens, Zhaonan Qu, Davide Viviano (2025).
6
+ > *Triply Robust Panel Estimators*.
7
+ > arXiv:2508.21536.
8
+
9
+ The initial release (v0.1.0) exposes the function:
10
+
11
+ - `TROP_TWFE_average(Y, W, treated_units, lambda_unit, lambda_time, lambda_nn, treated_periods=..., solver=...)`
12
+
13
+ which estimates an average treatment effect `tau` in panel settings using a weighted TWFE objective with optional low-rank adjustment.
14
+
15
+ ---
16
+
17
+ ## Installation
18
+
19
+ ```
20
+ pip install trop
21
+ ```
@@ -0,0 +1,74 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "trop"
7
+ version = "0.1.1"
8
+ description = "Triply Robust Panel (TROP) estimator: weighted TWFE with optional low-rank adjustment."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+
14
+ authors = [
15
+ { name = "Susan Athey" },
16
+ { name = "Guido Imbens" },
17
+ { name = "Zhaonan Qu" },
18
+ { name = "Davide Viviano" }
19
+ ]
20
+
21
+ keywords = [
22
+ "causal-inference",
23
+ "panel-data",
24
+ "factor-models",
25
+ "difference-in-differences",
26
+ "synthetic-control",
27
+ "synthetic-controls",
28
+ "trop",
29
+ "twfe"
30
+ ]
31
+
32
+ classifiers = [
33
+ "Development Status :: 3 - Alpha",
34
+ "Intended Audience :: Science/Research",
35
+ "Operating System :: OS Independent",
36
+ "Programming Language :: Python :: 3",
37
+ "Programming Language :: Python :: 3 :: Only",
38
+ "Programming Language :: Python :: 3.9",
39
+ "Programming Language :: Python :: 3.10",
40
+ "Programming Language :: Python :: 3.11",
41
+ "Programming Language :: Python :: 3.12",
42
+ "Topic :: Scientific/Engineering",
43
+ "Topic :: Scientific/Engineering :: Mathematics",
44
+ ]
45
+
46
+ dependencies = [
47
+ "numpy>=1.23",
48
+ "cvxpy>=1.4",
49
+ # These solvers are commonly used by CVXPY. Including explicitly makes installs more reliable.
50
+ "osqp>=0.6.5",
51
+ "scs>=3.2.4",
52
+ ]
53
+
54
+ [project.urls]
55
+ Homepage = "https://github.com/zhaonanq/TROP"
56
+ Repository = "https://github.com/zhaonanq/TROP"
57
+ Issues = "https://github.com/zhaonanq/TROP/issues"
58
+
59
+ [project.optional-dependencies]
60
+ dev = [
61
+ "pytest>=7.0",
62
+ "ruff>=0.5.0",
63
+ "black>=24.0.0",
64
+ ]
65
+
66
+ [tool.setuptools]
67
+ package-dir = {"" = "src"}
68
+
69
+ [tool.setuptools.packages.find]
70
+ where = ["src"]
71
+
72
+ [tool.pytest.ini_options]
73
+ testpaths = ["tests"]
74
+ addopts = "-q"
trop-0.1.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .estimator import TROP_TWFE_average
2
+
3
+ __all__ = ["TROP_TWFE_average"]
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from typing import Iterable, Optional, Sequence, Union
5
+
6
+ import numpy as np
7
+ import cvxpy as cp
8
+
9
+
10
+ ArrayLike = Union[np.ndarray, Sequence[Sequence[float]]]
11
+
12
+
13
+ def TROP_TWFE_average(
14
+ Y: ArrayLike,
15
+ W: ArrayLike,
16
+ treated_units: Sequence[int],
17
+ lambda_unit: float,
18
+ lambda_time: float,
19
+ lambda_nn: float,
20
+ treated_periods: int = 10,
21
+ solver: Optional[str] = None,
22
+ verbose: bool = False,
23
+ ) -> float:
24
+ """
25
+ Triply Robust Panel (TROP) estimator in a TWFE framework with:
26
+ - distance-based unit weights (lambda_unit)
27
+ - distance-based time weights (lambda_time)
28
+ - optional low-rank regression adjustment (nuclear norm penalty, lambda_nn)
29
+
30
+ Parameters
31
+ ----------
32
+ Y:
33
+ Outcome matrix of shape (N, T).
34
+ W:
35
+ Treatment indicator matrix of shape (N, T). Typically binary {0,1}.
36
+ Convention in the provided codebase: treated units are treated in the final
37
+ `treated_periods` columns, but the function will run for any W.
38
+ treated_units:
39
+ Indices of treated units (row indices into Y/W).
40
+ lambda_unit:
41
+ Nonnegative tuning parameter controlling unit-weight decay.
42
+ lambda_time:
43
+ Nonnegative tuning parameter controlling time-weight decay.
44
+ lambda_nn:
45
+ Nuclear norm penalty weight for the low-rank component L.
46
+ Use np.inf to disable low-rank adjustment (i.e., no L term).
47
+ treated_periods:
48
+ Number of treated (post) periods at the end of the panel used to define:
49
+ - the time-distance center
50
+ - the pre-period mask for unit distance computation
51
+ solver:
52
+ Optional CVXPY solver name. If None, chooses a reasonable default:
53
+ - if lambda_nn is finite: uses SCS (supports nuclear norm / SDP forms)
54
+ - if lambda_nn is infinite: uses OSQP (fast for pure quadratic problems)
55
+ verbose:
56
+ Passed to CVXPY solve().
57
+
58
+ Returns
59
+ -------
60
+ float
61
+ Estimated average treatment effect tau.
62
+
63
+ """
64
+ Y = np.asarray(Y, dtype=float)
65
+ W = np.asarray(W, dtype=float)
66
+
67
+ if Y.ndim != 2 or W.ndim != 2:
68
+ raise ValueError(f"Y and W must be 2D arrays. Got Y.ndim={Y.ndim}, W.ndim={W.ndim}.")
69
+ if Y.shape != W.shape:
70
+ raise ValueError(f"Y and W must have the same shape. Got Y={Y.shape}, W={W.shape}.")
71
+
72
+ N, T = Y.shape
73
+
74
+ if not isinstance(treated_periods, int) or treated_periods <= 0:
75
+ raise ValueError("treated_periods must be a positive integer.")
76
+ if treated_periods >= T:
77
+ raise ValueError(f"treated_periods must be < T. Got treated_periods={treated_periods}, T={T}.")
78
+
79
+ treated_units_arr = np.asarray(treated_units, dtype=int)
80
+ if treated_units_arr.size == 0:
81
+ raise ValueError("treated_units must contain at least one unit index.")
82
+ if np.any(treated_units_arr < 0) or np.any(treated_units_arr >= N):
83
+ raise ValueError(f"treated_units contains out-of-range indices for N={N}: {treated_units_arr}")
84
+
85
+ if lambda_unit < 0 or lambda_time < 0:
86
+ raise ValueError("lambda_unit and lambda_time should be nonnegative.")
87
+
88
+ # ---------------------------------------------------------------------
89
+ # Distance-based time weights
90
+ # ---------------------------------------------------------------------
91
+ # Distance to the center of the treated block near the end of the panel.
92
+ # dist_time = abs(arange(T) - (T - treated_periods/2))
93
+ center = T - treated_periods / 2.0
94
+ dist_time = np.abs(np.arange(T, dtype=float) - center)
95
+
96
+ # ---------------------------------------------------------------------
97
+ # Distance-based unit weights
98
+ # ---------------------------------------------------------------------
99
+ average_treated = np.mean(Y[treated_units_arr, :], axis=0)
100
+
101
+ # Pre-period mask: 1 in pre, 0 in treated/post
102
+ mask = np.ones((N, T), dtype=float)
103
+ mask[:, -treated_periods:] = 0.0
104
+
105
+ # RMS distance to average treated trajectory over pre-periods
106
+ # dist_unit[i] = sqrt( sum_pre (avg_tr - Y_i)^2 / (#pre) )
107
+ A = np.sum(((average_treated - Y) ** 2) * mask, axis=1)
108
+ B = np.sum(mask, axis=1)
109
+
110
+ if np.any(B == 0):
111
+ raise ValueError(
112
+ "Pre-period mask has zero pre-periods for at least one unit."
113
+ )
114
+
115
+ dist_unit = np.sqrt(A / B)
116
+
117
+ # Convert distances to weights
118
+ delta_unit = np.exp(-lambda_unit * dist_unit) # shape (N,)
119
+ delta_time = np.exp(-lambda_time * dist_time) # shape (T,)
120
+ delta = np.outer(delta_unit, delta_time) # shape (N, T)
121
+
122
+ # ---------------------------------------------------------------------
123
+ # CVXPY problem: weighted TWFE
124
+ # ---------------------------------------------------------------------
125
+ unit_effects = cp.Variable((1, N))
126
+ time_effects = cp.Variable((1, T))
127
+ mu = cp.Variable() # intercept
128
+ tau = cp.Variable() # treatment effect
129
+
130
+ # Broadcast TWFE components to N x T
131
+ unit_factor = cp.kron(np.ones((T, 1)), unit_effects).T
132
+ time_factor = cp.kron(np.ones((N, 1)), time_effects)
133
+
134
+ is_low_rank = not math.isinf(float(lambda_nn))
135
+
136
+ if is_low_rank:
137
+ L = cp.Variable((N, T))
138
+ residual = Y - mu - unit_factor - time_factor - L - W * tau
139
+ loss = cp.sum_squares(cp.multiply(residual, delta)) + float(lambda_nn) * cp.norm(L, "nuc")
140
+ default_solver = "SCS" # robust choice for nuclear norm problems
141
+ else:
142
+ residual = Y - mu - unit_factor - time_factor - W * tau
143
+ loss = cp.sum_squares(cp.multiply(residual, delta))
144
+ default_solver = "OSQP" # fast for pure quadratic objective
145
+
146
+ prob = cp.Problem(cp.Minimize(loss))
147
+
148
+ chosen_solver = solver or default_solver
149
+ prob.solve(solver=chosen_solver, verbose=verbose)
150
+
151
+ if tau.value is None or not np.isfinite(tau.value):
152
+ raise RuntimeError(
153
+ "Optimization did not return a valid tau. "
154
+ f"Solver={chosen_solver}, status={prob.status}."
155
+ )
156
+
157
+ return float(tau.value)
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: trop
3
+ Version: 0.1.1
4
+ Summary: Triply Robust Panel (TROP) estimator: weighted TWFE with optional low-rank adjustment.
5
+ Author: Susan Athey, Guido Imbens, Zhaonan Qu, Davide Viviano
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/zhaonanq/TROP
8
+ Project-URL: Repository, https://github.com/zhaonanq/TROP
9
+ Project-URL: Issues, https://github.com/zhaonanq/TROP/issues
10
+ Keywords: causal-inference,panel-data,factor-models,difference-in-differences,synthetic-control,synthetic-controls,trop,twfe
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering
21
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: numpy>=1.23
26
+ Requires-Dist: cvxpy>=1.4
27
+ Requires-Dist: osqp>=0.6.5
28
+ Requires-Dist: scs>=3.2.4
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0; extra == "dev"
31
+ Requires-Dist: ruff>=0.5.0; extra == "dev"
32
+ Requires-Dist: black>=24.0.0; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # TROP: Triply Robust Panel Estimator
36
+
37
+ This package provides a Python implementation of the **Triply Robust Panel (TROP)** estimator introduced in:
38
+
39
+ > Susan Athey, Guido Imbens, Zhaonan Qu, Davide Viviano (2025).
40
+ > *Triply Robust Panel Estimators*.
41
+ > arXiv:2508.21536.
42
+
43
+ The initial release (v0.1.0) exposes the function:
44
+
45
+ - `TROP_TWFE_average(Y, W, treated_units, lambda_unit, lambda_time, lambda_nn, treated_periods=..., solver=...)`
46
+
47
+ which estimates an average treatment effect `tau` in panel settings using a weighted TWFE objective with optional low-rank adjustment.
48
+
49
+ ---
50
+
51
+ ## Installation
52
+
53
+ ```
54
+ pip install trop
55
+ ```
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/trop/__init__.py
5
+ src/trop/estimator.py
6
+ src/trop.egg-info/PKG-INFO
7
+ src/trop.egg-info/SOURCES.txt
8
+ src/trop.egg-info/dependency_links.txt
9
+ src/trop.egg-info/requires.txt
10
+ src/trop.egg-info/top_level.txt
11
+ tests/test_smoke_trop.py
@@ -0,0 +1,9 @@
1
+ numpy>=1.23
2
+ cvxpy>=1.4
3
+ osqp>=0.6.5
4
+ scs>=3.2.4
5
+
6
+ [dev]
7
+ pytest>=7.0
8
+ ruff>=0.5.0
9
+ black>=24.0.0
@@ -0,0 +1 @@
1
+ trop
@@ -0,0 +1,46 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from trop import TROP_TWFE_average
5
+
6
+
7
+ def test_trop_twfe_average_smoke():
8
+ # Small synthetic panel
9
+ N, T = 8, 20
10
+ treated_periods = 5
11
+ treated_units = [0, 1]
12
+
13
+ rng = np.random.default_rng(0)
14
+ Y = rng.normal(size=(N, T))
15
+
16
+ # Treatment: treated_units get treated in last treated_periods
17
+ W = np.zeros((N, T))
18
+ W[treated_units, -treated_periods:] = 1.0
19
+
20
+ tau = TROP_TWFE_average(
21
+ Y=Y,
22
+ W=W,
23
+ treated_units=treated_units,
24
+ lambda_unit=0.1,
25
+ lambda_time=0.1,
26
+ lambda_nn=np.inf, # simplest path: no nuclear-norm component
27
+ treated_periods=treated_periods,
28
+ )
29
+
30
+ assert np.isfinite(tau), "tau should be a finite scalar"
31
+ assert isinstance(tau, float)
32
+
33
+
34
+ def test_invalid_shapes_raises():
35
+ Y = np.zeros((5, 10))
36
+ W = np.zeros((5, 9)) # mismatched
37
+ with pytest.raises(ValueError):
38
+ TROP_TWFE_average(
39
+ Y=Y,
40
+ W=W,
41
+ treated_units=[0],
42
+ lambda_unit=0.1,
43
+ lambda_time=0.1,
44
+ lambda_nn=np.inf,
45
+ treated_periods=2,
46
+ )
trop-0.1.0/PKG-INFO DELETED
@@ -1,24 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: trop
3
- Version: 0.1.0
4
- Summary: Package that implements the Triply Robust Panel Estimator (TROP)
5
- License: MIT
6
- Author: meganndare, zhaonanq
7
- Requires-Python: >=3.9
8
- Classifier: License :: OSI Approved :: MIT License
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3.9
11
- Classifier: Programming Language :: Python :: 3.10
12
- Classifier: Programming Language :: Python :: 3.11
13
- Classifier: Programming Language :: Python :: 3.12
14
- Classifier: Programming Language :: Python :: 3.13
15
- Classifier: Programming Language :: Python :: 3.14
16
- Requires-Dist: cvxpy (==1.4.1)
17
- Requires-Dist: joblib (==1.3.2)
18
- Requires-Dist: numpy (==1.26.2)
19
- Requires-Dist: pandas (==2.1.3)
20
- Description-Content-Type: text/markdown
21
-
22
- # Triply Robust Panel Estimators (TROP)
23
-
24
- This package will soon contain the replication files and implementation of the TROP estimator.
trop-0.1.0/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Triply Robust Panel Estimators (TROP)
2
-
3
- This package will soon contain the replication files and implementation of the TROP estimator.
trop-0.1.0/pyproject.toml DELETED
@@ -1,21 +0,0 @@
1
- [project]
2
- name = "trop"
3
- version = "0.1.0"
4
- description = "Package that implements the Triply Robust Panel Estimator (TROP)"
5
- authors = [
6
- {name = "meganndare, zhaonanq"}
7
- ]
8
- license = {text = "MIT"}
9
- readme = "README.md"
10
- requires-python = ">=3.9"
11
- dependencies = [
12
- "numpy (==1.26.2)",
13
- "pandas (==2.1.3)",
14
- "cvxpy (==1.4.1)",
15
- "joblib (==1.3.2)"
16
- ]
17
-
18
-
19
- [build-system]
20
- requires = ["poetry-core>=2.0.0,<3.0.0"]
21
- build-backend = "poetry.core.masonry.api"
@@ -1 +0,0 @@
1
- # placeholder to test package publishing