diff-stream 0.1.0.dev0__py3-none-any.whl

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 (56) hide show
  1. diff_stream/__init__.py +52 -0
  2. diff_stream/_bootstrap_worker.py +48 -0
  3. diff_stream/_core/__init__.py +5 -0
  4. diff_stream/_core/contrasts.py +199 -0
  5. diff_stream/_core/covariate_kernels.py +420 -0
  6. diff_stream/_core/cs_kernels.py +536 -0
  7. diff_stream/_core/cs_row_kernels.py +495 -0
  8. diff_stream/_core/design.py +105 -0
  9. diff_stream/_core/etwfe_kernels.py +288 -0
  10. diff_stream/_core/functionals.py +30 -0
  11. diff_stream/_core/multiplier.py +90 -0
  12. diff_stream/_core/nuisance.py +255 -0
  13. diff_stream/_core/partition.py +158 -0
  14. diff_stream/_core/process.py +61 -0
  15. diff_stream/_core/resample.py +171 -0
  16. diff_stream/_core/staging_kernels.py +90 -0
  17. diff_stream/_core/twostage_kernels.py +342 -0
  18. diff_stream/_core/weights.py +26 -0
  19. diff_stream/_core/within.py +81 -0
  20. diff_stream/_docs.py +100 -0
  21. diff_stream/_options.py +62 -0
  22. diff_stream/_present.py +202 -0
  23. diff_stream/absorbed.py +621 -0
  24. diff_stream/aggregation.py +225 -0
  25. diff_stream/api.py +1112 -0
  26. diff_stream/bacon.py +460 -0
  27. diff_stream/bacon_controls.py +188 -0
  28. diff_stream/balance.py +149 -0
  29. diff_stream/bjs.py +101 -0
  30. diff_stream/bjs_placebo.py +167 -0
  31. diff_stream/bootstrap.py +704 -0
  32. diff_stream/cohort_scores.py +68 -0
  33. diff_stream/cs.py +197 -0
  34. diff_stream/cs_unbalanced.py +336 -0
  35. diff_stream/effective_sample.py +73 -0
  36. diff_stream/estimators.py +95 -0
  37. diff_stream/etwfe.py +589 -0
  38. diff_stream/gardner.py +500 -0
  39. diff_stream/linear.py +339 -0
  40. diff_stream/periods.py +72 -0
  41. diff_stream/pretrend.py +97 -0
  42. diff_stream/py.typed +0 -0
  43. diff_stream/reference.py +125 -0
  44. diff_stream/results.py +521 -0
  45. diff_stream/simulate.py +80 -0
  46. diff_stream/stacked.py +405 -0
  47. diff_stream/store.py +706 -0
  48. diff_stream/twfe_store.py +181 -0
  49. diff_stream/twfe_weights.py +552 -0
  50. diff_stream/twostage_absorbed.py +190 -0
  51. diff_stream/unbalanced.py +297 -0
  52. diff_stream-0.1.0.dev0.dist-info/METADATA +192 -0
  53. diff_stream-0.1.0.dev0.dist-info/RECORD +56 -0
  54. diff_stream-0.1.0.dev0.dist-info/WHEEL +5 -0
  55. diff_stream-0.1.0.dev0.dist-info/licenses/LICENSE.md +21 -0
  56. diff_stream-0.1.0.dev0.dist-info/top_level.txt +1 -0
diff_stream/api.py ADDED
@@ -0,0 +1,1112 @@
1
+ """The panel estimators: Callaway-Sant'Anna, Sun-Abraham and BJS imputation."""
2
+
3
+ from numbers import Integral
4
+ from pathlib import Path
5
+ from tempfile import TemporaryDirectory
6
+
7
+ import numpy as np
8
+ import polars as pl
9
+ import pyarrow as pa
10
+ from hdfe_stream.core.profile import Component, Profile
11
+ from stream_limits import MemoryBudget, current_rss_gb, detect_cpus, thread_scope
12
+
13
+ from . import _docs, _options
14
+ from ._core.multiplier import MAX_DRAW_ENTRIES, StreamingMultiplier, validate_bootstrap
15
+ from ._core.partition import preprocess_budget_mb, staging_bytes
16
+ from ._present import present, public_profile
17
+ from .bjs import BJSInference
18
+ from .bjs_placebo import fit_placebos
19
+ from .cohort_scores import CohortScoreCovariance
20
+ from .cs import fit_cs
21
+ from .effective_sample import finish_result, threshold
22
+ from .estimators import change_scores, fit_means, setup
23
+ from .results import DIDResult
24
+ from .store import PanelStore, inspect
25
+
26
+
27
+ def _fit(
28
+ fml,
29
+ data,
30
+ *,
31
+ panel,
32
+ cohort=None,
33
+ treatment=None,
34
+ estimator="cs",
35
+ control_group="nevertreated",
36
+ base_period="varying",
37
+ vcov="CRV0",
38
+ batch_units=None,
39
+ threads=None,
40
+ memory_gb=None,
41
+ memory_reserve=None,
42
+ workdir=None,
43
+ profile=False,
44
+ preprocess_mb=None,
45
+ avgeffectsby="cohort_time",
46
+ leaveout=False,
47
+ est_method="dr",
48
+ trim_level=0.995,
49
+ panel_mode="balanced",
50
+ anticipation=0,
51
+ weights=None,
52
+ allow_time_varying_weights=False,
53
+ bootstrap_reps=0,
54
+ bootstrap_seed=None,
55
+ bootstrap_weights="rademacher",
56
+ pretrends=0,
57
+ ess_warn_threshold=30,
58
+ ess_suppress_threshold=None,
59
+ specification="dynamic",
60
+ reference_period=None,
61
+ fixed_effects="cohort",
62
+ inference="unconditional",
63
+ max_design_columns=2048,
64
+ reference_group="auto",
65
+ balance=False,
66
+ balance_ecdf=False,
67
+ fe_solver="auto",
68
+ ):
69
+ """Fit any panel estimator from internal option names.
70
+
71
+ The public functions (did_callaway_santanna, ..., did_extended_twfe) and
72
+ refit_bootstrap translate their options with ``_options.internal`` and
73
+ call this. The two-stage and extended-TWFE estimators are dispatched to
74
+ their own modules.
75
+ """
76
+ if reference_group not in ("auto", "nevertreated", "lasttreated"):
77
+ raise ValueError("reference_group must be auto, nevertreated or lasttreated")
78
+ if not isinstance(balance, bool) or not isinstance(balance_ecdf, bool):
79
+ raise ValueError("covariate_balance and balance_distributions must be True or False")
80
+ if (balance or balance_ecdf) and estimator != "cs":
81
+ raise ValueError("covariate balance diagnostics are available only for CS")
82
+ if balance_ecdf and not balance:
83
+ raise ValueError("balance_distributions requires covariate_balance=True")
84
+ if estimator == "etwfe":
85
+ if (
86
+ base_period != "varying"
87
+ or avgeffectsby != "cohort_time"
88
+ or leaveout
89
+ or est_method != "dr"
90
+ or trim_level != 0.995
91
+ or bootstrap_reps != 0
92
+ or bootstrap_seed is not None
93
+ or bootstrap_weights != "rademacher"
94
+ or pretrends != 0
95
+ or ess_warn_threshold not in (None, 30)
96
+ or ess_suppress_threshold is not None
97
+ or specification != "dynamic"
98
+ or reference_period is not None
99
+ ):
100
+ raise ValueError(
101
+ "ETWFE does not accept CS/BJS/Gardner-specific options or bootstrap options"
102
+ )
103
+ from .etwfe import _extended_twfe
104
+
105
+ return _extended_twfe(
106
+ fml,
107
+ data,
108
+ panel=panel,
109
+ cohort=cohort,
110
+ treatment=treatment,
111
+ control_group=control_group,
112
+ fixed_effects=fixed_effects,
113
+ inference=inference,
114
+ max_design_columns=max_design_columns,
115
+ vcov=vcov,
116
+ weights=weights,
117
+ allow_time_varying_weights=allow_time_varying_weights,
118
+ panel_mode=panel_mode,
119
+ anticipation=anticipation,
120
+ batch_units=batch_units or 4096,
121
+ threads=threads,
122
+ memory_gb=memory_gb,
123
+ memory_reserve=memory_reserve,
124
+ preprocess_mb=preprocess_mb,
125
+ workdir=workdir,
126
+ profile=profile,
127
+ reference_group=reference_group,
128
+ )
129
+ if fixed_effects != "cohort" or inference != "unconditional" or max_design_columns != 2048:
130
+ raise ValueError("fixed_effects, inference and max_design_columns are ETWFE options")
131
+ if estimator == "gardner":
132
+ if (
133
+ control_group != "nevertreated"
134
+ or base_period != "varying"
135
+ or avgeffectsby != "cohort_time"
136
+ or leaveout
137
+ or est_method != "dr"
138
+ or trim_level != 0.995
139
+ or bootstrap_reps != 0
140
+ or bootstrap_seed is not None
141
+ or bootstrap_weights != "rademacher"
142
+ or pretrends != 0
143
+ or ess_warn_threshold not in (None, 30)
144
+ or ess_suppress_threshold is not None
145
+ ):
146
+ raise ValueError("Gardner does not accept CS/BJS-specific options or bootstrap options")
147
+ from .gardner import _two_stage
148
+
149
+ return _two_stage(
150
+ fml,
151
+ data,
152
+ panel=panel,
153
+ cohort=cohort,
154
+ treatment=treatment,
155
+ specification=specification,
156
+ reference_period=reference_period,
157
+ vcov=vcov,
158
+ batch_units=batch_units or 4096,
159
+ threads=threads,
160
+ memory_gb=memory_gb,
161
+ memory_reserve=memory_reserve,
162
+ workdir=workdir,
163
+ profile=profile,
164
+ preprocess_mb=preprocess_mb,
165
+ panel_mode=panel_mode,
166
+ anticipation=anticipation,
167
+ weights=weights,
168
+ allow_time_varying_weights=allow_time_varying_weights,
169
+ reference_group=reference_group,
170
+ )
171
+ if specification != "dynamic" or reference_period is not None:
172
+ raise ValueError("specification and reference_period are Gardner options")
173
+ validate_bootstrap(bootstrap_reps, bootstrap_seed, bootstrap_weights)
174
+ if not isinstance(allow_time_varying_weights, bool):
175
+ raise ValueError("allow_time_varying_weights must be True or False")
176
+ bootstrap_reps = int(bootstrap_reps)
177
+ if estimator != "cs" and bootstrap_reps:
178
+ raise ValueError("unit multiplier bootstrap is currently available only for CS")
179
+ if (
180
+ isinstance(anticipation, (bool, np.bool_))
181
+ or not isinstance(anticipation, Integral)
182
+ or not 0 <= anticipation <= np.iinfo(np.int64).max
183
+ ):
184
+ raise ValueError("anticipation must be a nonnegative integer within the int64 range")
185
+ anticipation = int(anticipation)
186
+ if (
187
+ isinstance(pretrends, (bool, np.bool_))
188
+ or not isinstance(pretrends, Integral)
189
+ or not 0 <= pretrends <= np.iinfo(np.int64).max - anticipation
190
+ ):
191
+ raise ValueError(
192
+ "pretrend_leads must be a nonnegative integer with anticipation+pretrend_leads within int64 range"
193
+ )
194
+ pretrends = int(pretrends)
195
+ if estimator != "bjs" and pretrends:
196
+ raise ValueError("pretrends is available only for BJS")
197
+ if panel_mode not in ("balanced", "complete", "unbalanced"):
198
+ raise ValueError("panel_mode must be 'balanced', 'complete' or 'unbalanced'")
199
+ if estimator not in ("cs", "sunab", "bjs"):
200
+ raise ValueError("estimator must be 'cs', 'sunab', 'bjs', 'gardner' or 'etwfe'")
201
+ if est_method not in ("reg", "ipw", "dr"):
202
+ raise ValueError("est_method must be 'reg', 'ipw' or 'dr'")
203
+ if isinstance(trim_level, bool) or not np.isfinite(trim_level) or not 0 < trim_level <= 1:
204
+ raise ValueError("propensity_trim must be in (0, 1]")
205
+ if estimator != "cs" and (est_method != "dr" or trim_level != 0.995):
206
+ raise ValueError("est_method and trim_level are CS options")
207
+ if control_group not in ("nevertreated", "notyettreated"):
208
+ raise ValueError("unknown control_group")
209
+ if base_period not in ("varying", "universal"):
210
+ raise ValueError("unknown base_period")
211
+ if estimator != "cs" and control_group != "nevertreated":
212
+ raise ValueError("notyettreated controls are available only for CS")
213
+ if vcov not in ("CRV0", "CRV1"):
214
+ raise ValueError("vcov must be 'CRV0' or 'CRV1'")
215
+ if batch_units is not None and (
216
+ not isinstance(batch_units, int) or isinstance(batch_units, bool) or batch_units < 1
217
+ ):
218
+ raise ValueError("batch_units must be a positive integer or None")
219
+ if threads is not None and (
220
+ not isinstance(threads, int) or isinstance(threads, bool) or threads < 1
221
+ ):
222
+ raise ValueError("n_threads must be a positive integer")
223
+ if avgeffectsby not in ("cohort_time", "event_time", "cohort", "all"):
224
+ raise ValueError(
225
+ "residual_averaging must be 'cohort_time', 'event_time', 'cohort' or 'all'"
226
+ )
227
+ if not isinstance(leaveout, bool):
228
+ raise ValueError("leave_out must be True or False")
229
+ if estimator != "bjs" and (leaveout or avgeffectsby != "cohort_time"):
230
+ raise ValueError("leaveout and avgeffectsby options are available only for BJS")
231
+ ess_warn_threshold = threshold(ess_warn_threshold, "effective_sample_warning")
232
+ ess_suppress_threshold = threshold(ess_suppress_threshold, "min_effective_sample")
233
+ if estimator != "cs" and (
234
+ ess_suppress_threshold is not None or ess_warn_threshold not in (None, 30)
235
+ ):
236
+ raise ValueError("ESS warning/suppression thresholds are CS options")
237
+
238
+ balance_collector = None
239
+
240
+ def finish(result):
241
+ if balance_collector is not None:
242
+ result._balance_table = balance_collector.table()
243
+ result.diagnostics["balance_source_passes"] = balance_collector.source_passes
244
+ result.diagnostics["balance_scale"] = (
245
+ "pooled before-weighting SD, held fixed after weighting"
246
+ )
247
+ result = finish_result(result, ess_warn_threshold, ess_suppress_threshold)
248
+ return present(result, store.period_labels)
249
+
250
+ budget = MemoryBudget(memory_gb, memory_reserve)
251
+ automatic_preprocess = preprocess_mb is None
252
+ preprocess_mb = preprocess_budget_mb(preprocess_mb, budget)
253
+ baseline = current_rss_gb() or 0
254
+ with thread_scope(threads):
255
+ if fe_solver not in ("auto", "dense", "iterative"):
256
+ raise ValueError("fixed_effect_solver must be 'auto', 'dense' or 'iterative'")
257
+ lf, yname, design, fe_terms, iterative = inspect(
258
+ fml,
259
+ data,
260
+ panel,
261
+ cohort,
262
+ treatment=treatment,
263
+ allow_covariates=True,
264
+ weights=weights,
265
+ allow_fe=estimator != "cs",
266
+ fe_solver=fe_solver,
267
+ keep_fe_columns=estimator == "bjs",
268
+ )
269
+ absorbed = fe_terms if iterative else None
270
+ if automatic_preprocess:
271
+ # counting the levels of large fixed effects used memory: size the
272
+ # default from what is left now
273
+ preprocess_mb = preprocess_budget_mb(None, budget)
274
+ if not budget.fits((preprocess_mb * 1e6 + 96e6) / 1e9):
275
+ raise MemoryError(
276
+ "preprocessing buffers exceed job headroom before staging; reduce preprocess_mb or request more memory"
277
+ )
278
+ if workdir is not None:
279
+ Path(workdir).mkdir(parents=True, exist_ok=True)
280
+ with TemporaryDirectory(prefix="diff-stream-", dir=workdir) as run:
281
+ try:
282
+ store = PanelStore(
283
+ lf,
284
+ run,
285
+ batch_units or 16384,
286
+ budget,
287
+ preprocess_mb,
288
+ design,
289
+ panel_mode,
290
+ anticipation,
291
+ require_never_treated=reference_group == "nevertreated",
292
+ retain_actual_cohort=bool(pretrends),
293
+ allow_time_varying_weights=allow_time_varying_weights,
294
+ )
295
+ except ValueError as error:
296
+ if "256" in str(error) and "|" in fml and absorbed is None:
297
+ raise ValueError(
298
+ f"{error}; fixed_effect_solver='iterative' absorbs the additional "
299
+ "fixed effects instead"
300
+ ) from None
301
+ raise
302
+ from .reference import reference_store
303
+
304
+ store, reference_metadata = reference_store(store, reference_group, anticipation)
305
+ unimputed = {}
306
+ if estimator == "bjs" and fe_terms:
307
+ from .absorbed import drop_unimputable
308
+
309
+ store, unimputed = drop_unimputable(store, fe_terms, anticipation)
310
+ if balance:
311
+ from .balance import CSBalanceCollector
312
+
313
+ balance_collector = CSBalanceCollector(store, balance_ecdf)
314
+ # Preparation readers/writers are closed. Return their unused Arrow
315
+ # pool pages before reserving inference/JIT buffers (not live data).
316
+ pa.default_memory_pool().release_unused()
317
+ times, gc, N, means = store.times, store.gc, store.n_units, store.means
318
+ groups, cells, bases, controls, sizes = setup(
319
+ estimator, times, gc, control_group, base_period, anticipation
320
+ )
321
+ omitted = []
322
+ if not store.is_balanced:
323
+ supported = []
324
+ for j, (g, t) in enumerate(cells):
325
+ ti, bi = t - times[0], bases[j]
326
+ ok = store.cell_counts[g][ti] > 0
327
+ if estimator == "cs":
328
+ cc = np.sum(
329
+ [
330
+ store.cell_counts[h]
331
+ for h, take in zip(groups, controls[j], strict=True)
332
+ if take
333
+ ],
334
+ axis=0,
335
+ )
336
+ ok = ok and store.cell_counts[g][bi] > 0 and cc[ti] > 0 and cc[bi] > 0
337
+ if ok:
338
+ supported.append(j)
339
+ else:
340
+ omitted.append(
341
+ dict(
342
+ cohort=g,
343
+ time=t,
344
+ reason="no observations in a required cohort/date stratum",
345
+ )
346
+ )
347
+ cells = [cells[j] for j in supported]
348
+ bases, controls = bases[supported], controls[supported]
349
+ if not cells:
350
+ raise ValueError("no supported observed-row comparisons")
351
+ sample_diagnostics = dict(
352
+ **reference_metadata,
353
+ **unimputed,
354
+ balance_requested=balance,
355
+ balance_ecdf=balance_ecdf,
356
+ pretrends=pretrends,
357
+ placebo_cohort_policy="original known adoption dates, including reference/future cohorts"
358
+ if pretrends
359
+ else None,
360
+ bootstrap_reps=bootstrap_reps,
361
+ bootstrap_seed=int(bootstrap_seed) if bootstrap_seed is not None else None,
362
+ bootstrap_weights=bootstrap_weights if bootstrap_reps else None,
363
+ bootstrap_unit_order="staged whole-unit traversal; seed reproducible for fixed partition layout"
364
+ if bootstrap_reps
365
+ else None,
366
+ sampling_weights=weights,
367
+ weight_convention=(
368
+ "positive time-invariant unit sampling weights"
369
+ if store.weights_time_invariant
370
+ else "positive time-varying observation weights"
371
+ )
372
+ if weights is not None
373
+ else "equal unit weights",
374
+ allow_time_varying_weights=allow_time_varying_weights,
375
+ weights_time_invariant=store.weights_time_invariant,
376
+ unit_weight_anchor=store.unit_weight_anchor,
377
+ weight_scale=store.weight_scale / store.weight_normalizer,
378
+ total_unit_weight=float(N * (store.weight_scale / store.weight_normalizer)),
379
+ cohort_weight_totals={
380
+ str(g): float(v * (store.weight_scale / store.weight_normalizer))
381
+ for g, v in store.gw.items()
382
+ },
383
+ anticipation=anticipation,
384
+ reference_event_time=-anticipation - 1,
385
+ cohort_sizes={str(g): int(n) for g, n in gc.items()},
386
+ time_min=int(times[0]),
387
+ time_max=int(times[-1]),
388
+ panel_mode=panel_mode,
389
+ balanced_sample=store.is_balanced,
390
+ input_units=store.n_units_input,
391
+ input_rows=store.n_rows,
392
+ dropped_units=store.n_units_input - N,
393
+ dropped_rows=store.n_rows - store.n_rows_used,
394
+ omitted_cells=omitted,
395
+ )
396
+ M, T, G = len(cells), len(times), len(groups)
397
+ rich_regression = estimator != "cs" and (
398
+ design is not None
399
+ or not store.weights_time_invariant
400
+ or absorbed is not None
401
+ or (estimator == "bjs" and not store.is_balanced)
402
+ )
403
+
404
+ # Numerical buffers grow with the batch; an automatic batch starts
405
+ # large (fast) and halves until it fits the memory left.
406
+ def buffers(B):
407
+ numeric = 8 * (12 * M * M + 4 * B * M + 8 * G * T * T + 8 * T * T + 4 * B * T)
408
+ if pretrends:
409
+ P = T - 1 + pretrends
410
+ numeric += 8 * (8 * P * P + 4 * T * P + 8 * B * P + 8 * B * T)
411
+ numeric += 1024 * len(store.actual_gc) * min(pretrends, T) + 32e6
412
+ if bootstrap_reps:
413
+ numeric += 8 * (
414
+ 6 * bootstrap_reps * M
415
+ + 2 * max(bootstrap_reps, min(B * bootstrap_reps, MAX_DRAW_ENTRIES))
416
+ )
417
+ if rich_regression:
418
+ K = len(design.names) if design is not None else 0
419
+ P = T - 1 + K + (M if estimator == "sunab" else pretrends)
420
+ numeric += 8 * (8 * P * P + 12 * B * T * (2 * T + K + pretrends))
421
+ elif design is not None:
422
+ K = len(design.names)
423
+ # Nuisance Grams are system-by-K-by-K and row designs are
424
+ # batch-by-time-by-K. Cross-cell scores/covariance above have
425
+ # no covariate axis: neither M-by-M-by-K nor B-by-M-by-K is
426
+ # materialized. Keep generous allowances for four unbalanced
427
+ # outcome strata, Newton information and contrast derivatives.
428
+ numeric += 8 * (8 * M * K * K + 8 * M * T * K + 12 * B * T * K)
429
+ return numeric
430
+
431
+ B = min(batch_units or 16384, N)
432
+ if batch_units is None:
433
+ while B > 256 and not budget.fits(buffers(B) / 1e9 + 128e6 / 1e9):
434
+ B //= 2
435
+ numeric = buffers(B)
436
+ store.batch_units = B
437
+ p = Profile(
438
+ model=f"diff-stream {estimator}",
439
+ threads=threads or detect_cpus(),
440
+ memory_limit_gb=budget.limit_gb,
441
+ memory_reserve=budget.reserve,
442
+ polars_threads=pl.thread_pool_size(),
443
+ baseline_gb=baseline,
444
+ components=[
445
+ Component(
446
+ "numerical buffers and runtime", numeric + 192e6, numeric * 1.5 + 256e6
447
+ ),
448
+ Component(
449
+ "bounded partition preprocessing",
450
+ staging_bytes(preprocess_mb, store) + 96e6,
451
+ preprocess_mb * 1e6 + 256e6,
452
+ ),
453
+ Component(
454
+ "snapshot, hash partitions and sorted leaves",
455
+ 64 * N * T,
456
+ 128 * N * T,
457
+ where="disk",
458
+ ),
459
+ ],
460
+ notes={
461
+ **sample_diagnostics,
462
+ "n_units": N,
463
+ "periods": T,
464
+ "cells": M,
465
+ "batch_units": B,
466
+ "validation": "whole-unit disk partitions; no global unit-ID table",
467
+ "partitions": store.partition_count,
468
+ "max_partition_depth": store.max_depth,
469
+ "max_decoded_leaf_bytes": store.max_leaf_bytes,
470
+ "preprocess_mb": preprocess_mb,
471
+ "covariate_names": design.names if design is not None else [],
472
+ },
473
+ warnings=[
474
+ "Heuristic estimate: source query, readers, writers and allocator retention can exceed allowances."
475
+ ],
476
+ )
477
+ if profile:
478
+ return p
479
+ if not budget.fits(numeric / 1e9 + 128e6 / 1e9):
480
+ raise MemoryError(
481
+ "estimated numerical buffers exceed job headroom; reduce batch_units or request more memory"
482
+ )
483
+ multiplier = (
484
+ StreamingMultiplier(2 * M, bootstrap_reps, bootstrap_seed, bootstrap_weights)
485
+ if bootstrap_reps
486
+ else None
487
+ )
488
+ multiplier_fields = (
489
+ dict(
490
+ _multiplier_draws=multiplier.draws[:, :M],
491
+ _multiplier_shares=multiplier.draws[:, M:],
492
+ )
493
+ if multiplier is not None
494
+ else {}
495
+ )
496
+ placebo_fields = {}
497
+ if pretrends:
498
+ if absorbed is not None:
499
+ from .absorbed import fit_placebos_absorbed
500
+
501
+ placebo = fit_placebos_absorbed(
502
+ store,
503
+ pretrends,
504
+ anticipation,
505
+ absorbed,
506
+ fml=fml,
507
+ depvar=yname,
508
+ panel=panel,
509
+ vcov=vcov,
510
+ threads=threads,
511
+ )
512
+ else:
513
+ placebo = fit_placebos(
514
+ store,
515
+ pretrends,
516
+ anticipation,
517
+ fml=fml,
518
+ depvar=yname,
519
+ panel=panel,
520
+ vcov=vcov,
521
+ )
522
+ placebo.diagnostics.update(sampling_weights=weights, panel_mode=panel_mode)
523
+ placebo_fields = dict(_bjs_placebos=placebo)
524
+ if not store.is_balanced or (store.weighted and estimator != "cs") or rich_regression:
525
+ from .cs_unbalanced import fit_cs_unbalanced
526
+ from .unbalanced import fit_bjs, fit_sunab
527
+
528
+ context, cross, share_cov, metadata = None, None, None, {}
529
+ if estimator == "cs":
530
+ att, counts, V, cross, share_cov, metadata = fit_cs_unbalanced(
531
+ store,
532
+ groups,
533
+ cells,
534
+ bases,
535
+ controls,
536
+ est_method,
537
+ trim_level,
538
+ multiplier,
539
+ balance_collector=balance_collector,
540
+ )
541
+ elif rich_regression:
542
+ from . import linear
543
+
544
+ if absorbed is not None and estimator == "sunab":
545
+ from .absorbed import fit_sunab_absorbed
546
+
547
+ att, counts, V, info = fit_sunab_absorbed(
548
+ store, cells, absorbed, threads=threads
549
+ )
550
+ model = None
551
+ metadata.update(info)
552
+ elif estimator == "sunab":
553
+ att, counts, V, model = linear.fit_sunab(store, cells, anticipation)
554
+ else:
555
+ if absorbed is not None:
556
+ from .absorbed import fit_bjs_absorbed
557
+
558
+ att, counts, V, context, info = fit_bjs_absorbed(
559
+ store,
560
+ cells,
561
+ absorbed,
562
+ avgeffectsby,
563
+ leaveout,
564
+ workdir,
565
+ anticipation,
566
+ threads=threads,
567
+ )
568
+ model = None
569
+ metadata.update(info)
570
+ else:
571
+ att, counts, V, context, model = linear.fit_bjs(
572
+ store, cells, avgeffectsby, leaveout, workdir, anticipation
573
+ )
574
+ if model is not None:
575
+ metadata.update(
576
+ regression_backend="streamed weighted unit elimination; bounded joint design",
577
+ regression_columns=[model.names[j] for j in model.keep],
578
+ absorbed_or_redundant_columns=model.dropped,
579
+ )
580
+ metadata.update(
581
+ covariate_names=design.names if design is not None else [],
582
+ fixed_effect_solver="iterative" if absorbed is not None else "dense",
583
+ )
584
+ elif estimator == "sunab":
585
+ att, counts, V = fit_sunab(store, cells, anticipation)
586
+ else:
587
+ att, counts, V, context = fit_bjs(
588
+ store, cells, avgeffectsby, leaveout, workdir, anticipation
589
+ )
590
+ factor = N / (N - 1) if vcov == "CRV1" else 1.0
591
+ diagnostics = dict(
592
+ **sample_diagnostics,
593
+ control_group=control_group,
594
+ base_period=base_period if estimator == "cs" else f"g-{anticipation + 1}",
595
+ inference="unit clustered; observed-row analytic scores",
596
+ periods=T,
597
+ batch_units=B,
598
+ preprocess_mb=preprocess_mb,
599
+ partitions=store.partition_count,
600
+ max_decoded_leaf_bytes=store.max_leaf_bytes,
601
+ warnings=[],
602
+ )
603
+ diagnostics.update(metadata)
604
+ if omitted:
605
+ diagnostics["warnings"].append(
606
+ "Some cells have no required observed-row stratum; inspect omitted_cells."
607
+ )
608
+ if context is not None:
609
+ diagnostics.update(
610
+ avgeffectsby=avgeffectsby,
611
+ leaveout=leaveout,
612
+ aggregation_storage="temporary Parquet; call cleanup() after aggregation",
613
+ )
614
+ if np.isnan(V).any():
615
+ diagnostics["warnings"].append(
616
+ "Some cell leave-out variances are undefined; aggregate over supported groups before inference."
617
+ )
618
+ return finish(
619
+ DIDResult(
620
+ **placebo_fields,
621
+ **multiplier_fields,
622
+ fml=fml,
623
+ depvar=yname,
624
+ coefnames=[f"g::{g}:t::{t}" for g, t in cells],
625
+ beta=att,
626
+ vcov=V * factor,
627
+ vcov_type=vcov,
628
+ df_t=N - 1,
629
+ all_vcovs={vcov: (V * factor, N - 1)},
630
+ n_obs=store.n_rows_used,
631
+ n_clusters={panel[0]: N},
632
+ estimator=estimator,
633
+ cells=cells,
634
+ counts=counts,
635
+ observed_counts=np.array(
636
+ [store.cell_counts[g][t - times[0]] for g, t in cells], dtype=float
637
+ ),
638
+ cohort_counts=np.array([gc[g] for g, t in cells], dtype=float),
639
+ n_units=N,
640
+ diagnostics=diagnostics,
641
+ _share_cross=cross * factor if cross is not None else None,
642
+ _share_cov=share_cov * factor if share_cov is not None else None,
643
+ _bjs_inference=context,
644
+ )
645
+ )
646
+ if (
647
+ design is not None
648
+ or (store.weighted and estimator == "cs")
649
+ or multiplier is not None
650
+ ):
651
+ att, counts, V, cross, share_cov, metadata = fit_cs(
652
+ store,
653
+ groups,
654
+ cells,
655
+ bases,
656
+ controls,
657
+ est_method,
658
+ trim_level,
659
+ multiplier,
660
+ balance_collector=balance_collector,
661
+ )
662
+ factor = N / (N - 1) if vcov == "CRV1" else 1.0
663
+ return finish(
664
+ DIDResult(
665
+ **multiplier_fields,
666
+ fml=fml,
667
+ depvar=yname,
668
+ coefnames=[f"g::{g}:t::{t}" for g, t in cells],
669
+ beta=att,
670
+ vcov=V * factor,
671
+ vcov_type=vcov,
672
+ df_t=N - 1,
673
+ all_vcovs={vcov: (V * factor, N - 1)},
674
+ n_obs=store.n_rows_used,
675
+ n_clusters={panel[0]: N},
676
+ estimator=estimator,
677
+ cells=cells,
678
+ counts=counts,
679
+ observed_counts=np.array(
680
+ [store.cell_counts[g][t - times[0]] for g, t in cells], dtype=float
681
+ ),
682
+ cohort_counts=np.array([gc[g] for g, t in cells], dtype=float),
683
+ n_units=N,
684
+ _share_cross=cross * factor,
685
+ _share_cov=share_cov * factor,
686
+ diagnostics=dict(
687
+ **sample_diagnostics,
688
+ inference="unit clustered; nuisance-adjusted analytic scores",
689
+ control_group=control_group,
690
+ base_period=base_period,
691
+ periods=T,
692
+ batch_units=B,
693
+ partitions=store.partition_count,
694
+ preprocess_mb=preprocess_mb,
695
+ max_decoded_leaf_bytes=store.max_leaf_bytes,
696
+ **metadata,
697
+ ),
698
+ )
699
+ )
700
+ if estimator == "cs" and est_method != "reg":
701
+ ng = np.array([gc[g] for g, t in cells])
702
+ probability = np.minimum(ng / (ng + controls @ sizes), 1 - 1e-6)
703
+ if np.any(probability >= trim_level):
704
+ raise ValueError(
705
+ "no controls remain after propensity trimming in an intercept-only comparison"
706
+ )
707
+ att, counts, args = fit_means(
708
+ estimator, times, groups, cells, bases, controls, sizes, means, anticipation
709
+ )
710
+ V, cross, share_cov = np.zeros((M, M)), np.zeros((M, M)), np.zeros((M, M))
711
+ scatter = np.zeros((G, T, T)) if estimator == "bjs" else None
712
+ # Cell scores are zero outside a unit's cohort's cells; accumulate
713
+ # by cohort. Shares are uncentered here and centered below.
714
+ by_cohort = CohortScoreCovariance(args[0]) if estimator != "bjs" else None
715
+ for g, y in store.batches():
716
+ idx = np.searchsorted(groups, g)
717
+ if estimator == "bjs":
718
+ for k in np.unique(idx):
719
+ centered = y[idx == k] - means[k]
720
+ use = (
721
+ times < groups[k] - anticipation
722
+ if groups[k]
723
+ else np.ones(T, dtype=bool)
724
+ )
725
+ centered -= centered[:, use].mean(axis=1, keepdims=True)
726
+ scatter[k] += centered.T @ centered
727
+ else:
728
+ scores, _ = change_scores(idx, y, *args, sizes)
729
+ by_cohort.add(idx, scores)
730
+ if by_cohort is not None:
731
+ V, cross, share_cov = by_cohort.blocks()
732
+ if estimator != "cs":
733
+ cross, share_cov = np.zeros((M, M)), np.zeros((M, M))
734
+ bjs_inference = None
735
+ if estimator == "bjs":
736
+ _, _, Z, theta, H = args
737
+ bjs_inference = BJSInference(
738
+ np.array(groups),
739
+ sizes,
740
+ times,
741
+ cells,
742
+ means,
743
+ Z,
744
+ theta,
745
+ H,
746
+ scatter,
747
+ avgeffectsby,
748
+ leaveout,
749
+ anticipation,
750
+ )
751
+ V = bjs_inference.covariance(
752
+ np.eye(M), allow_unidentified=avgeffectsby != "cohort_time"
753
+ )
754
+ factor = N / (N - 1) if vcov == "CRV1" else 1.0
755
+ diagnostics = {
756
+ **sample_diagnostics,
757
+ "control_group": control_group,
758
+ "base_period": base_period if estimator == "cs" else f"g-{anticipation + 1}",
759
+ "est_method": est_method if estimator == "cs" else None,
760
+ "inference": "unit clustered; normal; "
761
+ + ("BJS smart residual averaging" if estimator == "bjs" else "analytic scores"),
762
+ "avgeffectsby": avgeffectsby if estimator == "bjs" else None,
763
+ "leaveout": leaveout if estimator == "bjs" else None,
764
+ "periods": T,
765
+ "batch_units": B,
766
+ "preprocess_mb": preprocess_mb,
767
+ "partitions": store.partition_count,
768
+ "max_decoded_leaf_bytes": store.max_leaf_bytes,
769
+ "warnings": (
770
+ ["Small BJS cohorts: consider leaveout=True and broader avgeffectsby groups."]
771
+ if estimator == "bjs" and not leaveout and min(gc[g] for g in gc if g) < 20
772
+ else []
773
+ ),
774
+ }
775
+ if estimator == "cs":
776
+ diagnostics["comparisons"] = [
777
+ dict(
778
+ cohort=g,
779
+ time=t,
780
+ cells=[j],
781
+ effective_treated=float(gc[g]),
782
+ effective_controls=float(controls[j] @ sizes),
783
+ )
784
+ for j, (g, t) in enumerate(cells)
785
+ ]
786
+ if np.isnan(V).any():
787
+ diagnostics["warnings"].append(
788
+ "Some cell leave-out variances are undefined; aggregate over supported groups before inference."
789
+ )
790
+ return finish(
791
+ DIDResult(
792
+ **placebo_fields,
793
+ fml=fml,
794
+ depvar=yname,
795
+ coefnames=[f"g::{g}:t::{t}" for g, t in cells],
796
+ beta=att,
797
+ vcov=V * factor,
798
+ vcov_type=vcov,
799
+ df_t=N - 1,
800
+ all_vcovs={vcov: (V * factor, N - 1)},
801
+ n_obs=store.n_rows_used,
802
+ n_clusters={panel[0]: N},
803
+ estimator=estimator,
804
+ cells=cells,
805
+ counts=counts,
806
+ observed_counts=np.array(
807
+ [store.cell_counts[g][t - times[0]] for g, t in cells], dtype=float
808
+ ),
809
+ cohort_counts=np.array([gc[g] for g, t in cells], dtype=float),
810
+ n_units=N,
811
+ diagnostics=diagnostics,
812
+ _share_cross=cross * factor if estimator == "cs" else None,
813
+ _share_cov=(share_cov - np.outer(counts, counts) / N) * factor
814
+ if estimator == "cs"
815
+ else None,
816
+ _bjs_inference=bjs_inference,
817
+ )
818
+ )
819
+
820
+
821
+ def _public(estimator, options):
822
+ """Call _fit with a public function's arguments (its locals())."""
823
+ options = dict(options)
824
+ fml, data = options.pop("fml"), options.pop("data")
825
+ return public_profile(_fit(fml, data, estimator=estimator, **_options.internal(options)))
826
+
827
+
828
+ @_docs.document
829
+ def did_callaway_santanna(
830
+ fml,
831
+ data,
832
+ *,
833
+ panel=None,
834
+ time=None,
835
+ cohort=None,
836
+ treatment=None,
837
+ control_group="never_treated",
838
+ base_period="varying",
839
+ adjustment="doubly_robust",
840
+ propensity_trim=0.995,
841
+ anticipation=0,
842
+ weights=None,
843
+ allow_time_varying_weights=False,
844
+ incomplete_units="error",
845
+ last_treated_controls="if_needed",
846
+ vcov="CRV0",
847
+ multiplier_bootstrap_reps=0,
848
+ multiplier_bootstrap_seed=None,
849
+ multiplier_bootstrap_weights="rademacher",
850
+ effective_sample_warning=30,
851
+ min_effective_sample=None,
852
+ covariate_balance=False,
853
+ balance_distributions=False,
854
+ n_threads=None,
855
+ max_memory_gb=None,
856
+ memory_reserve=None,
857
+ workdir=None,
858
+ profile=False,
859
+ batch_units=None,
860
+ preprocess_mb=None,
861
+ ):
862
+ """Callaway and Sant'Anna's group-time average treatment effects.
863
+
864
+ Estimates the average effect for each cohort (units first treated in the
865
+ same period) in each period, by comparing its outcome changes with those
866
+ of a control group, then aggregates them with ``aggregate()``. Covariates
867
+ enter by outcome regression, inverse probability weighting or the doubly
868
+ robust combination of Sant'Anna and Zhao (2020), as in R's did.
869
+
870
+ Callaway, B. and Sant'Anna, P. H. C. (2021). Difference-in-differences
871
+ with multiple time periods. *Journal of Econometrics* 225(2), 200-230.
872
+
873
+ Sant'Anna, P. H. C. and Zhao, J. (2020). Doubly robust
874
+ difference-in-differences estimators. *Journal of Econometrics* 219(1),
875
+ 101-122.
876
+
877
+ Parameters
878
+ ----------
879
+ fml : str
880
+ ``"y ~ 1"``, or ``"y ~ x + C(region)"`` with covariates (up to 256
881
+ design columns), measured in the comparison's base period.
882
+ {DATA}
883
+ control_group : {"never_treated", "not_yet_treated"}, default "never_treated"
884
+ The comparison units: never-treated units only, or also units not yet
885
+ treated by the period compared.
886
+ base_period : {"varying", "universal"}, default "varying"
887
+ The pre-treatment comparison period: the period just before each
888
+ pre-treatment cell ("varying"), or the last clean period before
889
+ treatment for every cell ("universal"). Post-treatment cells use the
890
+ latter either way.
891
+ adjustment : {"doubly_robust", "regression", "ipw"}, default "doubly_robust"
892
+ How covariates enter: outcome regression on the controls, inverse
893
+ probability weighting with a logit propensity score, or both.
894
+ propensity_trim : float, default 0.995
895
+ Controls with a propensity score above this are dropped from IPW and
896
+ doubly robust comparisons (treated units are kept). 1 disables it.
897
+ {ANTICIPATION}
898
+ {WEIGHTS}
899
+ {INCOMPLETE_UNITS}
900
+ With "keep", comparisons use each period's observed rows, as R's did
901
+ does for unbalanced panels.
902
+ {LAST_TREATED}
903
+ {VCOV}
904
+ multiplier_bootstrap_reps : int, default 0
905
+ Draws of the multiplier bootstrap, which perturbs each unit's
906
+ influence function instead of refitting (R did's default inference).
907
+ It gives simultaneous confidence bands through
908
+ ``multiplier_confint()``; the analytic standard errors are unchanged.
909
+ 0 skips it. For a bootstrap that refits the model, see
910
+ ``refit_bootstrap``.
911
+ multiplier_bootstrap_seed : int, optional
912
+ Seed for the multiplier draws.
913
+ multiplier_bootstrap_weights : {"rademacher", "mammen", "webb", "norm"}, default "rademacher"
914
+ The distribution of the multiplier draws.
915
+ effective_sample_warning : float or None, default 30
916
+ Warn (``LowEffectiveSampleWarning``) when a comparison's treated or
917
+ control group has an effective sample size (Kish) below this. None
918
+ turns the warning off.
919
+ min_effective_sample : float, optional
920
+ Report coefficients whose effective sample is below this as NaN.
921
+ ``suppress()`` does the same after fitting.
922
+ covariate_balance : bool, default False
923
+ Save covariate balance tables for each comparison, before and after
924
+ propensity weighting, for ``covariate_balance()``.
925
+ balance_distributions : bool, default False
926
+ With ``covariate_balance``, also compare the covariates' empirical
927
+ distributions.
928
+ {RESOURCES}
929
+
930
+ Returns
931
+ -------
932
+ DIDResult or Profile
933
+ ``cohort_time()`` lists the group-time effects; ``aggregate()`` gives
934
+ an event study, ``aggregate("overall")`` the overall ATT.
935
+ """
936
+ options = dict(locals())
937
+ if time is not None:
938
+ options = _cross_sections(options)
939
+ elif panel is None:
940
+ raise ValueError("give panel=(unit, time), or time= for repeated cross sections")
941
+ options.pop("time")
942
+ return _cross_section_labels(_public("cs", options), time)
943
+
944
+
945
+ def _cross_sections(options):
946
+ """Repeated cross sections: every row is its own unit, as in R did (panel=FALSE)."""
947
+ if options["panel"] is not None:
948
+ raise ValueError(
949
+ "give panel=(unit, time) for a panel or time= for cross sections, not both"
950
+ )
951
+ if options["incomplete_units"] != "error":
952
+ raise ValueError("incomplete_units does not apply to repeated cross sections")
953
+ from .store import source
954
+
955
+ frame = source(options["data"])
956
+ name = "observation"
957
+ while name in frame.collect_schema().names():
958
+ name = "_" + name
959
+ options.update(
960
+ data=frame.with_row_index(name),
961
+ panel=(name, options["time"]),
962
+ incomplete_units="keep",
963
+ )
964
+ return options
965
+
966
+
967
+ def _cross_section_labels(result, time):
968
+ if isinstance(result, DIDResult):
969
+ result.diagnostics["repeated_cross_sections"] = time is not None
970
+ if time is not None:
971
+ result.diagnostics["incomplete_units"] = None
972
+ result.n_clusters = {"observation": result.n_units}
973
+ return result
974
+
975
+
976
+ @_docs.document
977
+ def did_sun_abraham(
978
+ fml,
979
+ data,
980
+ *,
981
+ panel,
982
+ cohort=None,
983
+ treatment=None,
984
+ anticipation=0,
985
+ weights=None,
986
+ allow_time_varying_weights=False,
987
+ incomplete_units="error",
988
+ last_treated_controls="if_needed",
989
+ fixed_effect_solver="auto",
990
+ vcov="CRV0",
991
+ n_threads=None,
992
+ max_memory_gb=None,
993
+ memory_reserve=None,
994
+ workdir=None,
995
+ profile=False,
996
+ batch_units=None,
997
+ preprocess_mb=None,
998
+ ):
999
+ """Sun and Abraham's interaction-weighted event study.
1000
+
1001
+ Regresses the outcome on a full set of cohort-by-period treatment
1002
+ indicators, with unit and time fixed effects, so that each cohort's
1003
+ effects are estimated separately; ``aggregate()`` then averages them by
1004
+ event time with cohort-share weights. The period before treatment
1005
+ (before ``cohort - anticipation``) is the omitted reference.
1006
+
1007
+ Sun, L. and Abraham, S. (2021). Estimating dynamic treatment effects in
1008
+ event studies with heterogeneous treatment effects. *Journal of
1009
+ Econometrics* 225(2), 175-199.
1010
+
1011
+ Parameters
1012
+ ----------
1013
+ fml : str
1014
+ ``"y ~ 1"``; covariates ``"y ~ x"``; additional fixed effects after
1015
+ a bar, ``"y ~ x | region^time"``. Unit and time fixed effects are
1016
+ always included.
1017
+ {DATA}
1018
+ {ANTICIPATION}
1019
+ {WEIGHTS}
1020
+ {INCOMPLETE_UNITS}
1021
+ {LAST_TREATED}
1022
+ {FIXED_EFFECT_SOLVER}
1023
+ {VCOV}
1024
+ {RESOURCES}
1025
+
1026
+ Returns
1027
+ -------
1028
+ DIDResult or Profile
1029
+ ``cohort_time()`` lists the cohort-period coefficients;
1030
+ ``aggregate()`` gives the event study.
1031
+ """
1032
+ return _public("sunab", locals())
1033
+
1034
+
1035
+ @_docs.document
1036
+ def did_bjs(
1037
+ fml,
1038
+ data,
1039
+ *,
1040
+ panel,
1041
+ cohort=None,
1042
+ treatment=None,
1043
+ residual_averaging="cohort_time",
1044
+ leave_out=False,
1045
+ pretrend_leads=0,
1046
+ anticipation=0,
1047
+ weights=None,
1048
+ allow_time_varying_weights=False,
1049
+ incomplete_units="error",
1050
+ last_treated_controls="if_needed",
1051
+ fixed_effect_solver="auto",
1052
+ vcov="CRV0",
1053
+ n_threads=None,
1054
+ max_memory_gb=None,
1055
+ memory_reserve=None,
1056
+ workdir=None,
1057
+ profile=False,
1058
+ batch_units=None,
1059
+ preprocess_mb=None,
1060
+ ):
1061
+ """Borusyak, Jaravel and Spiess's imputation estimator.
1062
+
1063
+ Fits unit and time fixed effects (and any covariates) on the untreated
1064
+ observations only, imputes each treated observation's untreated outcome,
1065
+ and averages the differences. The standard errors are the authors'
1066
+ conservative ones, as in their Stata command ``did_imputation``.
1067
+
1068
+ Borusyak, K., Jaravel, X. and Spiess, J. (2024). Revisiting event-study
1069
+ designs: robust and efficient estimation. *Review of Economic Studies*
1070
+ 91(6), 3253-3285.
1071
+
1072
+ Parameters
1073
+ ----------
1074
+ fml : str
1075
+ ``"y ~ 1"``; covariates ``"y ~ x"``; additional fixed effects after
1076
+ a bar, ``"y ~ x | region^time"``. Unit and time fixed effects are
1077
+ always included. Treated observations whose fixed-effect level has no
1078
+ untreated observations cannot be imputed: they are dropped, with an
1079
+ ``UnimputedObservationsWarning``, and ``fit.diagnostics`` reports
1080
+ them (``unimputed_observations``, ``unimputed_cells``).
1081
+ {DATA}
1082
+ residual_averaging : {"cohort_time", "event_time", "cohort", "all"}, default "cohort_time"
1083
+ The groups within which the standard errors average treated
1084
+ residuals (the authors' ``avgeffectsby``). Coarser groups give more
1085
+ conservative standard errors and need fewer observations per group.
1086
+ leave_out : bool, default False
1087
+ Leave each unit out of its own group's average residual (the authors'
1088
+ ``leaveout``), which reduces the downward bias in small groups.
1089
+ pretrend_leads : int, default 0
1090
+ For a pre-trend test: fit a separate regression on the untreated
1091
+ observations with indicators for the last ``pretrend_leads`` periods
1092
+ before treatment. ``placebos()`` returns those coefficients and
1093
+ ``pretrend_test()`` tests that they are jointly zero. The treatment
1094
+ effects are unchanged. 0 skips it.
1095
+ {ANTICIPATION}
1096
+ {WEIGHTS}
1097
+ {INCOMPLETE_UNITS}
1098
+ {LAST_TREATED}
1099
+ {FIXED_EFFECT_SOLVER}
1100
+ {VCOV}
1101
+ {RESOURCES}
1102
+
1103
+ Returns
1104
+ -------
1105
+ DIDResult or Profile
1106
+ ``cohort_time()`` lists the imputed effects by cohort and period;
1107
+ ``aggregate()`` gives the event study, ``aggregate("overall")`` the
1108
+ overall ATT. Weighted, unbalanced or covariate fits, and fits with
1109
+ additional fixed effects, keep temporary files for aggregation until
1110
+ ``cleanup()``.
1111
+ """
1112
+ return _public("bjs", locals())