trop 0.1.1__tar.gz → 0.1.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trop
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Triply Robust Panel (TROP) estimator: weighted TWFE with optional low-rank adjustment.
5
5
  Author: Susan Athey, Guido Imbens, Zhaonan Qu, Davide Viviano
6
6
  License-Expression: MIT
@@ -24,6 +24,7 @@ Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: numpy>=1.23
26
26
  Requires-Dist: cvxpy>=1.4
27
+ Requires-Dist: joblib>=1.2
27
28
  Requires-Dist: osqp>=0.6.5
28
29
  Requires-Dist: scs>=3.2.4
29
30
  Provides-Extra: dev
@@ -34,18 +35,15 @@ Dynamic: license-file
34
35
 
35
36
  # TROP: Triply Robust Panel Estimator
36
37
 
37
- This package provides a Python implementation of the **Triply Robust Panel (TROP)** estimator introduced in:
38
+ `trop` is a Python package implementing the **Triply Robust Panel (TROP)** estimator for average treatment effects (ATEs) in panel data. The core estimator is expressed as a weighted two-way fixed effects (TWFE) objective, with an optional low-rank regression adjustment via a nuclear-norm penalty.
39
+
40
+
41
+ Reference:
38
42
 
39
43
  > Susan Athey, Guido Imbens, Zhaonan Qu, Davide Viviano (2025).
40
44
  > *Triply Robust Panel Estimators*.
41
45
  > arXiv:2508.21536.
42
46
 
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
47
  ---
50
48
 
51
49
  ## Installation
trop-0.1.3/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # TROP: Triply Robust Panel Estimator
2
+
3
+ `trop` is a Python package implementing the **Triply Robust Panel (TROP)** estimator for average treatment effects (ATEs) in panel data. The core estimator is expressed as a weighted two-way fixed effects (TWFE) objective, with an optional low-rank regression adjustment via a nuclear-norm penalty.
4
+
5
+
6
+ Reference:
7
+
8
+ > Susan Athey, Guido Imbens, Zhaonan Qu, Davide Viviano (2025).
9
+ > *Triply Robust Panel Estimators*.
10
+ > arXiv:2508.21536.
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ ```
17
+ pip install trop
18
+ ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trop"
7
- version = "0.1.1"
7
+ version = "0.1.3"
8
8
  description = "Triply Robust Panel (TROP) estimator: weighted TWFE with optional low-rank adjustment."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -46,7 +46,7 @@ classifiers = [
46
46
  dependencies = [
47
47
  "numpy>=1.23",
48
48
  "cvxpy>=1.4",
49
- # These solvers are commonly used by CVXPY. Including explicitly makes installs more reliable.
49
+ "joblib>=1.2",
50
50
  "osqp>=0.6.5",
51
51
  "scs>=3.2.4",
52
52
  ]
@@ -0,0 +1,9 @@
1
+ from .estimator import TROP_TWFE_average
2
+ from .cv import TROP_cv_single, TROP_cv_cycle, TROP_cv_joint
3
+
4
+ __all__ = [
5
+ "TROP_TWFE_average",
6
+ "TROP_cv_single",
7
+ "TROP_cv_cycle",
8
+ "TROP_cv_joint",
9
+ ]
@@ -0,0 +1,411 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Iterable, Optional, Sequence, Tuple, Union, List
5
+
6
+ import numpy as np
7
+ from joblib import Parallel, delayed
8
+
9
+ from .estimator import TROP_TWFE_average
10
+
11
+
12
+ ArrayLike = Union[np.ndarray, Sequence[Sequence[float]]]
13
+
14
+
15
+ def _validate_panel(Y: np.ndarray, treated_periods: int, n_treated_units: int) -> None:
16
+ """Validate panel dimensions and placebo CV inputs."""
17
+ if Y.ndim != 2:
18
+ raise ValueError("Y must be a 2D array of shape (N, T).")
19
+ N, T = Y.shape
20
+ if treated_periods <= 0 or treated_periods >= T:
21
+ raise ValueError(f"treated_periods must be in [1, T-1]. Got treated_periods={treated_periods}, T={T}.")
22
+ if n_treated_units <= 0 or n_treated_units >= N:
23
+ raise ValueError(f"n_treated_units must be in [1, N-1]. Got n_treated_units={n_treated_units}, N={N}.")
24
+
25
+
26
+ def _as_list(grid: Iterable[float]) -> List[float]:
27
+ """Convert an iterable of grid values to a non-empty list of floats."""
28
+ grid_list = list(grid)
29
+ if len(grid_list) == 0:
30
+ raise ValueError("lambda_grid must be non-empty.")
31
+ grid_list = [float(x) for x in grid_list]
32
+ return grid_list
33
+
34
+
35
+
36
+ def _simulate_ate(
37
+ seed: int,
38
+ Y: np.ndarray,
39
+ n_treated_units: int,
40
+ treated_periods: int,
41
+ lambda_unit: float,
42
+ lambda_time: float,
43
+ lambda_nn: float,
44
+ solver: Optional[str] = None,
45
+ verbose: bool = False,
46
+ ) -> float:
47
+ """
48
+ Simulate a single placebo ATE by randomly selecting treated units.
49
+ """
50
+ rng = np.random.default_rng(seed)
51
+ N, _ = Y.shape
52
+ treated_units = rng.choice(N, size=n_treated_units, replace=False)
53
+
54
+ W = np.zeros_like(Y, dtype=float)
55
+ W[treated_units, -treated_periods:] = 1.0
56
+
57
+ return TROP_TWFE_average(
58
+ Y=Y,
59
+ W=W,
60
+ treated_units=treated_units,
61
+ lambda_unit=lambda_unit,
62
+ lambda_time=lambda_time,
63
+ lambda_nn=lambda_nn,
64
+ treated_periods=treated_periods,
65
+ solver=solver,
66
+ verbose=verbose,
67
+ )
68
+
69
+
70
+ def TROP_cv_single(
71
+ Y_control: ArrayLike,
72
+ n_treated_units: int,
73
+ treated_periods: int,
74
+ fixed_lambdas: Tuple[float, float] = (0.0, 0.0),
75
+ lambda_grid: Optional[Iterable[float]] = None,
76
+ lambda_cv: str = "unit",
77
+ *,
78
+ n_trials: int = 200,
79
+ n_jobs: int = -1,
80
+ prefer: str = "threads",
81
+ random_seed: int = 0,
82
+ solver: Optional[str] = None,
83
+ verbose: bool = False,
84
+ ) -> float:
85
+ """
86
+ Tune one TROP tuning parameter via placebo cross-validation on a control-only panel.
87
+
88
+ For each candidate value in `lambda_grid`, this routine repeatedly assigns a placebo
89
+ treatment to random units in the last `treated_periods` columns, computes the
90
+ corresponding TROP estimate, and selects the lambda that minimizes the RMSE of
91
+ placebo effects.
92
+
93
+ Parameters
94
+ ----------
95
+ Y_control : array_like of shape (N, T)
96
+ Control-only outcome panel used for placebo cross-validation.
97
+ n_treated_units : int
98
+ Number of placebo treated units sampled (without replacement) per trial.
99
+ treated_periods : int
100
+ Number of placebo treated (post) periods, taken as the final columns.
101
+ fixed_lambdas : tuple of float, default=(0.0, 0.0)
102
+ Values held fixed for the two lambdas not being tuned. Interpretation depends on
103
+ `lambda_cv`:
104
+ - 'unit': (lambda_time, lambda_nn)
105
+ - 'time': (lambda_unit, lambda_nn)
106
+ - 'nn' : (lambda_unit, lambda_time)
107
+ lambda_grid : iterable of float or None, default=None
108
+ Candidate values for the lambda being tuned. If None, uses ``np.arange(0, 2, 0.2)``.
109
+ lambda_cv : {'unit', 'time', 'nn'}, default='unit'
110
+ Which lambda to tune.
111
+ n_trials : int, default=200
112
+ Number of placebo trials per candidate lambda.
113
+ n_jobs : int, default=-1
114
+ Number of parallel jobs for placebo trials. ``-1`` uses all available cores.
115
+ prefer : {'threads', 'processes'}, default='threads'
116
+ joblib backend preference.
117
+ random_seed : int, default=0
118
+ Seed for generating trial seeds (deterministic tuning).
119
+ solver : str or None, default=None
120
+ CVXPY solver passed to ``TROP_TWFE_average``.
121
+ verbose : bool, default=False
122
+ Verbosity flag passed to ``TROP_TWFE_average``.
123
+
124
+ Returns
125
+ -------
126
+ float
127
+ Selected lambda value minimizing the RMSE of placebo estimates.
128
+ """
129
+ Y = np.asarray(Y_control, dtype=float)
130
+ _validate_panel(Y, treated_periods, n_treated_units)
131
+
132
+ if lambda_cv not in {"unit", "time", "nn"}:
133
+ raise ValueError("lambda_cv must be one of {'unit','time','nn'}.")
134
+
135
+ if lambda_grid is None:
136
+ lambda_grid_list = _as_list(np.arange(0.0, 2.0, 0.2))
137
+ else:
138
+ lambda_grid_list = _as_list(lambda_grid)
139
+
140
+ if n_trials <= 0:
141
+ raise ValueError("n_trials must be positive.")
142
+ if n_jobs == 0 or n_jobs < -1:
143
+ raise ValueError("n_jobs must be -1 or a positive integer.")
144
+
145
+ base_rng = np.random.default_rng(random_seed)
146
+ seeds = base_rng.integers(0, 2**32 - 1, size=n_trials, dtype=np.uint32)
147
+
148
+ scores: List[float] = []
149
+
150
+ for lamb in lambda_grid_list:
151
+ if lamb < 0:
152
+ raise ValueError("Lambda values must be nonnegative.")
153
+
154
+ if lambda_cv == "unit":
155
+ lambda_unit, lambda_time, lambda_nn = lamb, float(fixed_lambdas[0]), float(fixed_lambdas[1])
156
+ elif lambda_cv == "time":
157
+ lambda_unit, lambda_time, lambda_nn = float(fixed_lambdas[0]), lamb, float(fixed_lambdas[1])
158
+ else: # 'nn'
159
+ lambda_unit, lambda_time, lambda_nn = float(fixed_lambdas[0]), float(fixed_lambdas[1]), lamb
160
+
161
+ ates = Parallel(n_jobs=n_jobs, prefer=prefer)(
162
+ delayed(_simulate_ate)(
163
+ int(seed),
164
+ Y,
165
+ n_treated_units,
166
+ treated_periods,
167
+ lambda_unit,
168
+ lambda_time,
169
+ lambda_nn,
170
+ solver,
171
+ verbose,
172
+ )
173
+ for seed in seeds
174
+ )
175
+
176
+ ates_arr = np.asarray(ates, dtype=float)
177
+ ates_arr = ates_arr[np.isfinite(ates_arr)]
178
+
179
+ if ates_arr.size == 0:
180
+ raise RuntimeError(
181
+ f"All placebo trials failed or returned non-finite ATEs for lambda={lamb} "
182
+ f"(lambda_cv='{lambda_cv}'). Consider changing solver/settings."
183
+ )
184
+
185
+ scores.append(float(np.sqrt(np.mean(ates_arr**2))))
186
+
187
+ best_idx = int(np.argmin(scores))
188
+ return float(lambda_grid_list[best_idx])
189
+
190
+
191
+ def TROP_cv_cycle(
192
+ Y_control: ArrayLike,
193
+ n_treated_units: int,
194
+ treated_periods: int,
195
+ unit_grid: Sequence[float],
196
+ time_grid: Sequence[float],
197
+ nn_grid: Sequence[float],
198
+ lambdas_init: Optional[Tuple[float, float, float]] = None,
199
+ *,
200
+ max_iter: int = 50,
201
+ n_trials: int = 200,
202
+ n_jobs: int = -1,
203
+ prefer: str = "threads",
204
+ random_seed: int = 0,
205
+ solver: Optional[str] = None,
206
+ verbose: bool = False,
207
+ ) -> Tuple[float, float, float]:
208
+ """
209
+ Tune (lambda_unit, lambda_time, lambda_nn) by coordinate-descent placebo cross-validation.
210
+
211
+ Iteratively updates one tuning parameter at a time using `TROP_cv_single` (holding the
212
+ other two fixed) until the selected triplet stops changing or `max_iter` is reached.
213
+ Each update minimizes the RMSE of placebo effects on a control-only panel.
214
+
215
+ Parameters
216
+ ----------
217
+ Y_control : array_like of shape (N, T)
218
+ Control-only outcome panel used for placebo cross-validation.
219
+ n_treated_units : int
220
+ Number of placebo treated units sampled (without replacement) per trial.
221
+ treated_periods : int
222
+ Number of placebo treated (post) periods, taken as the final columns.
223
+ unit_grid : sequence of float
224
+ Candidate values for `lambda_unit` (unit-distance decay).
225
+ time_grid : sequence of float
226
+ Candidate values for `lambda_time` (time-distance decay).
227
+ nn_grid : sequence of float
228
+ Candidate values for `lambda_nn` (nuclear-norm penalty).
229
+ lambdas_init : tuple of float or None, default=None
230
+ Initial values (lambda_unit, lambda_time, lambda_nn). If None, initializes each
231
+ parameter to the mean of its grid.
232
+ max_iter : int, default=50
233
+ Maximum number of coordinate-descent iterations.
234
+ n_trials : int, default=200
235
+ Number of placebo trials per grid point in each coordinate update.
236
+ n_jobs : int, default=-1
237
+ Number of parallel jobs for placebo trials. ``-1`` uses all available cores.
238
+ prefer : {'threads', 'processes'}, default='threads'
239
+ joblib backend preference.
240
+ random_seed : int, default=0
241
+ Seed for generating trial seeds (deterministic tuning).
242
+ solver : str or None, default=None
243
+ CVXPY solver passed to ``TROP_TWFE_average``.
244
+ verbose : bool, default=False
245
+ Verbosity flag passed to ``TROP_TWFE_average``.
246
+
247
+ Returns
248
+ -------
249
+ tuple of float
250
+ (lambda_unit, lambda_time, lambda_nn) at the fixed point of the coordinate updates.
251
+
252
+ Raises
253
+ ------
254
+ RuntimeError
255
+ If the procedure does not converge within `max_iter`.
256
+ """
257
+ Y = np.asarray(Y_control, dtype=float)
258
+ _validate_panel(Y, treated_periods, n_treated_units)
259
+
260
+ unit_grid_list = _as_list(unit_grid)
261
+ time_grid_list = _as_list(time_grid)
262
+ nn_grid_list = _as_list(nn_grid)
263
+
264
+ if lambdas_init is None:
265
+ lambda_unit = float(np.mean(unit_grid_list))
266
+ lambda_time = float(np.mean(time_grid_list))
267
+ lambda_nn = float(np.mean(nn_grid_list))
268
+ else:
269
+ lambda_unit, lambda_time, lambda_nn = map(float, lambdas_init)
270
+
271
+ for _ in range(max_iter):
272
+ old = (lambda_unit, lambda_time, lambda_nn)
273
+
274
+ lambda_unit = TROP_cv_single(
275
+ Y, n_treated_units, treated_periods,
276
+ fixed_lambdas=(lambda_time, lambda_nn),
277
+ lambda_grid=unit_grid_list,
278
+ lambda_cv="unit",
279
+ n_trials=n_trials, n_jobs=n_jobs, prefer=prefer,
280
+ random_seed=random_seed, solver=solver, verbose=verbose
281
+ )
282
+
283
+ lambda_time = TROP_cv_single(
284
+ Y, n_treated_units, treated_periods,
285
+ fixed_lambdas=(lambda_unit, lambda_nn),
286
+ lambda_grid=time_grid_list,
287
+ lambda_cv="time",
288
+ n_trials=n_trials, n_jobs=n_jobs, prefer=prefer,
289
+ random_seed=random_seed, solver=solver, verbose=verbose
290
+ )
291
+
292
+ lambda_nn = TROP_cv_single(
293
+ Y, n_treated_units, treated_periods,
294
+ fixed_lambdas=(lambda_unit, lambda_time),
295
+ lambda_grid=nn_grid_list,
296
+ lambda_cv="nn",
297
+ n_trials=n_trials, n_jobs=n_jobs, prefer=prefer,
298
+ random_seed=random_seed, solver=solver, verbose=verbose
299
+ )
300
+
301
+ new = (lambda_unit, lambda_time, lambda_nn)
302
+ if new == old:
303
+ return new
304
+
305
+ raise RuntimeError("TROP_cv_cycle did not converge (no fixed point) within max_iter.")
306
+
307
+
308
+ def TROP_cv_joint(
309
+ Y_control: ArrayLike,
310
+ n_treated_units: int,
311
+ treated_periods: int,
312
+ unit_grid: Sequence[float],
313
+ time_grid: Sequence[float],
314
+ nn_grid: Sequence[float],
315
+ *,
316
+ n_trials: int = 200,
317
+ n_jobs: int = -1,
318
+ prefer: str = "threads",
319
+ random_seed: int = 0,
320
+ solver: Optional[str] = None,
321
+ verbose: bool = False,
322
+ ) -> Tuple[float, float, float]:
323
+ """
324
+ Select (lambda_unit, lambda_time, lambda_nn) by joint placebo cross-validation.
325
+
326
+ Performs a full grid search over `unit_grid` × `time_grid` × `nn_grid`. For each
327
+ candidate triple, repeatedly assigns a placebo treatment to random units in the
328
+ last `treated_periods` columns and selects the triple that minimizes the RMSE of
329
+ placebo effects on the control-only panel.
330
+
331
+ Parameters
332
+ ----------
333
+ Y_control : array_like of shape (N, T)
334
+ Control-only outcome panel used for placebo cross-validation.
335
+ n_treated_units : int
336
+ Number of placebo treated units sampled (without replacement) per trial.
337
+ treated_periods : int
338
+ Number of placebo treated (post) periods, taken as the final columns.
339
+ unit_grid : sequence of float
340
+ Candidate values for `lambda_unit` (unit-distance decay).
341
+ time_grid : sequence of float
342
+ Candidate values for `lambda_time` (time-distance decay).
343
+ nn_grid : sequence of float
344
+ Candidate values for `lambda_nn` (nuclear-norm penalty).
345
+ n_trials : int, default=200
346
+ Number of placebo trials per candidate triple.
347
+ n_jobs : int, default=-1
348
+ Number of parallel jobs for placebo trials. ``-1`` uses all available cores.
349
+ prefer : {'threads', 'processes'}, default='threads'
350
+ joblib backend preference.
351
+ random_seed : int, default=0
352
+ Seed for generating trial seeds (deterministic tuning).
353
+ solver : str or None, default=None
354
+ CVXPY solver passed to ``TROP_TWFE_average``.
355
+ verbose : bool, default=False
356
+ Verbosity flag passed to ``TROP_TWFE_average``.
357
+
358
+ Returns
359
+ -------
360
+ tuple of float
361
+ (lambda_unit, lambda_time, lambda_nn) minimizing the RMSE of placebo estimates.
362
+
363
+ Raises
364
+ ------
365
+ RuntimeError
366
+ If all parameter combinations fail (e.g., solver failures for every triple).
367
+ """
368
+ Y = np.asarray(Y_control, dtype=float)
369
+ _validate_panel(Y, treated_periods, n_treated_units)
370
+
371
+ unit_grid_list = _as_list(unit_grid)
372
+ time_grid_list = _as_list(time_grid)
373
+ nn_grid_list = _as_list(nn_grid)
374
+
375
+ base_rng = np.random.default_rng(random_seed)
376
+ seeds = base_rng.integers(0, 2**32 - 1, size=n_trials, dtype=np.uint32)
377
+
378
+ best_params: Optional[Tuple[float, float, float]] = None
379
+ best_score: float = float("inf")
380
+
381
+ for lambda_unit in unit_grid_list:
382
+ for lambda_time in time_grid_list:
383
+ for lambda_nn in nn_grid_list:
384
+ ates = Parallel(n_jobs=n_jobs, prefer=prefer)(
385
+ delayed(_simulate_ate)(
386
+ int(seed),
387
+ Y,
388
+ n_treated_units,
389
+ treated_periods,
390
+ float(lambda_unit),
391
+ float(lambda_time),
392
+ float(lambda_nn),
393
+ solver,
394
+ verbose,
395
+ )
396
+ for seed in seeds
397
+ )
398
+
399
+ ates_arr = np.asarray(ates, dtype=float)
400
+ ates_arr = ates_arr[np.isfinite(ates_arr)]
401
+ if ates_arr.size == 0:
402
+ continue # skip invalid setting
403
+
404
+ score = float(np.sqrt(np.mean(ates_arr**2)))
405
+ if score < best_score:
406
+ best_score = score
407
+ best_params = (float(lambda_unit), float(lambda_time), float(lambda_nn))
408
+
409
+ if best_params is None:
410
+ raise RuntimeError("All parameter combinations failed during joint CV. Check solver/settings.")
411
+ return best_params
@@ -22,44 +22,48 @@ def TROP_TWFE_average(
22
22
  verbose: bool = False,
23
23
  ) -> float:
24
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)
25
+ Compute the TROP treatment effect with unit/time weighting and optional low-rank
26
+ outcome model.
29
27
 
30
28
  Parameters
31
29
  ----------
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().
30
+ Y : array_like of shape (N, T)
31
+ Outcome matrix.
32
+ W : array_like of shape (N, T)
33
+ Treatment indicator (often binary). The estimator uses ``W`` as provided;
34
+ ``treated_periods`` is used only to construct weights/masks, not to infer
35
+ treatment timing.
36
+ treated_units : sequence of int
37
+ Row indices of treated units used to form the reference (average) treated
38
+ trajectory for unit-distance weighting.
39
+ lambda_unit : float
40
+ Nonnegative decay parameter for unit weights: ``exp(-lambda_unit * dist_unit)``.
41
+ lambda_time : float
42
+ Nonnegative decay parameter for time weights: ``exp(-lambda_time * dist_time)``.
43
+ lambda_nn : float
44
+ Nuclear-norm penalty weight for the low-rank component ``L``. Use
45
+ ``np.inf`` to disable the low-rank adjustment (i.e., omit ``L``).
46
+ treated_periods : int, default=10
47
+ Number of final columns treated as the "post/tail block" for constructing
48
+ (a) the pre-period mask (all but last ``treated_periods`` columns) used in
49
+ unit distances, and (b) the time-distance center.
50
+ solver : str or None, default=None
51
+ CVXPY solver name. If None, uses "SCS" when ``lambda_nn`` is finite and
52
+ "OSQP" when ``lambda_nn`` is infinite.
53
+ verbose : bool, default=False
54
+ Passed to ``cvxpy.Problem.solve``.
57
55
 
58
56
  Returns
59
57
  -------
60
58
  float
61
- Estimated average treatment effect tau.
62
-
59
+ Estimated treatment-effect parameter ``tau`` from the weighted TWFE objective.
60
+
61
+ Raises
62
+ ------
63
+ ValueError
64
+ If input shapes are inconsistent or tuning parameters are invalid.
65
+ RuntimeError
66
+ If the optimization fails to produce a finite ``tau``.
63
67
  """
64
68
  Y = np.asarray(Y, dtype=float)
65
69
  W = np.asarray(W, dtype=float)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trop
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Triply Robust Panel (TROP) estimator: weighted TWFE with optional low-rank adjustment.
5
5
  Author: Susan Athey, Guido Imbens, Zhaonan Qu, Davide Viviano
6
6
  License-Expression: MIT
@@ -24,6 +24,7 @@ Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: numpy>=1.23
26
26
  Requires-Dist: cvxpy>=1.4
27
+ Requires-Dist: joblib>=1.2
27
28
  Requires-Dist: osqp>=0.6.5
28
29
  Requires-Dist: scs>=3.2.4
29
30
  Provides-Extra: dev
@@ -34,18 +35,15 @@ Dynamic: license-file
34
35
 
35
36
  # TROP: Triply Robust Panel Estimator
36
37
 
37
- This package provides a Python implementation of the **Triply Robust Panel (TROP)** estimator introduced in:
38
+ `trop` is a Python package implementing the **Triply Robust Panel (TROP)** estimator for average treatment effects (ATEs) in panel data. The core estimator is expressed as a weighted two-way fixed effects (TWFE) objective, with an optional low-rank regression adjustment via a nuclear-norm penalty.
39
+
40
+
41
+ Reference:
38
42
 
39
43
  > Susan Athey, Guido Imbens, Zhaonan Qu, Davide Viviano (2025).
40
44
  > *Triply Robust Panel Estimators*.
41
45
  > arXiv:2508.21536.
42
46
 
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
47
  ---
50
48
 
51
49
  ## Installation
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  src/trop/__init__.py
5
+ src/trop/cv.py
5
6
  src/trop/estimator.py
6
7
  src/trop.egg-info/PKG-INFO
7
8
  src/trop.egg-info/SOURCES.txt
@@ -1,5 +1,6 @@
1
1
  numpy>=1.23
2
2
  cvxpy>=1.4
3
+ joblib>=1.2
3
4
  osqp>=0.6.5
4
5
  scs>=3.2.4
5
6
 
trop-0.1.1/README.md DELETED
@@ -1,21 +0,0 @@
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
- ```
@@ -1,3 +0,0 @@
1
- from .estimator import TROP_TWFE_average
2
-
3
- __all__ = ["TROP_TWFE_average"]
File without changes
File without changes
File without changes