pyfector 0.1.0__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.
@@ -0,0 +1,103 @@
1
+ name: Build & Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Set up Python
15
+ uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+
19
+ - name: Install build tools
20
+ run: pip install build twine
21
+
22
+ - name: Build wheel and sdist
23
+ run: python -m build
24
+
25
+ - name: Check distributions
26
+ run: twine check dist/*
27
+
28
+ - name: Upload artifacts
29
+ uses: actions/upload-artifact@v4
30
+ with:
31
+ name: dist
32
+ path: dist/
33
+
34
+ # pyfector is pure Python — one wheel works on all platforms.
35
+ # No need for cibuildwheel or platform-specific builds.
36
+ # The py3-none-any wheel is universal.
37
+ verify:
38
+ needs: build
39
+ strategy:
40
+ matrix:
41
+ os: [ubuntu-latest, macos-latest, windows-latest]
42
+ python-version: ["3.10", "3.12"]
43
+ runs-on: ${{ matrix.os }}
44
+ steps:
45
+ - uses: actions/setup-python@v5
46
+ with:
47
+ python-version: ${{ matrix.python-version }}
48
+
49
+ - name: Download artifacts
50
+ uses: actions/download-artifact@v4
51
+ with:
52
+ name: dist
53
+ path: dist/
54
+
55
+ - name: Install wheel
56
+ shell: bash
57
+ run: pip install dist/*.whl
58
+
59
+ - name: Smoke test
60
+ shell: bash
61
+ run: |
62
+ python -c "
63
+ import pyfector
64
+ print(f'pyfector {pyfector.__version__} loaded successfully')
65
+ print(f'Device: {pyfector.get_device()}')
66
+ "
67
+
68
+ upload-release:
69
+ needs: [build, verify]
70
+ runs-on: ubuntu-latest
71
+ if: github.event_name == 'release'
72
+ permissions:
73
+ contents: write
74
+ steps:
75
+ - name: Download artifacts
76
+ uses: actions/download-artifact@v4
77
+ with:
78
+ name: dist
79
+ path: dist/
80
+
81
+ - name: Upload to GitHub Release
82
+ env:
83
+ GH_TOKEN: ${{ github.token }}
84
+ run: gh release upload "${{ github.event.release.tag_name }}" dist/* --clobber --repo "${{ github.repository }}"
85
+
86
+ publish-pypi:
87
+ needs: [build, verify]
88
+ runs-on: ubuntu-latest
89
+ if: github.event_name == 'release'
90
+ permissions:
91
+ id-token: write
92
+ environment:
93
+ name: pypi
94
+ url: https://pypi.org/p/pyfector
95
+ steps:
96
+ - name: Download artifacts
97
+ uses: actions/download-artifact@v4
98
+ with:
99
+ name: dist
100
+ path: dist/
101
+
102
+ - name: Publish to PyPI
103
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,57 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install dependencies
25
+ run: |
26
+ python -m pip install --upgrade pip
27
+ pip install -e ".[dev]"
28
+
29
+ - name: Run tests
30
+ run: |
31
+ python -m pytest tests/test_linalg.py tests/test_smoke.py tests/test_pressure.py tests/test_bootstrap_se.py -v -k "not very_large and not slow"
32
+
33
+ test-vs-r:
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+
38
+ - name: Set up Python
39
+ uses: actions/setup-python@v5
40
+ with:
41
+ python-version: "3.12"
42
+
43
+ - name: Set up R
44
+ uses: r-lib/actions/setup-r@v2
45
+
46
+ - name: Install R packages
47
+ run: |
48
+ Rscript -e 'install.packages(c("fect", "jsonlite"), repos="https://cloud.r-project.org")'
49
+
50
+ - name: Install Python dependencies
51
+ run: |
52
+ python -m pip install --upgrade pip
53
+ RPY2_CFFI_MODE=ABI pip install -e ".[dev,r-comparison]"
54
+
55
+ - name: Run R comparison tests
56
+ run: |
57
+ python -m pytest tests/test_vs_r_fect.py -v
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ .pytest_cache/
9
+ *.egg
10
+ .eggs/
11
+ ref/
pyfector-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pyfector contributors
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.
@@ -0,0 +1,262 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyfector
3
+ Version: 0.1.0
4
+ Summary: Fast counterfactual estimators for panel data — Python reimplementation of R fect
5
+ Author: Alan
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: causal-inference,counterfactual,difference-in-differences,fixed-effects,interactive-fixed-effects,matrix-completion,panel-data,treatment-effects
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: joblib>=1.3
19
+ Requires-Dist: matplotlib>=3.7
20
+ Requires-Dist: numpy>=1.24
21
+ Requires-Dist: polars>=0.20
22
+ Requires-Dist: pyarrow>=12.0
23
+ Requires-Dist: scipy>=1.10
24
+ Provides-Extra: dev
25
+ Requires-Dist: pandas>=2.0; extra == 'dev'
26
+ Requires-Dist: pytest-xdist; extra == 'dev'
27
+ Requires-Dist: pytest>=7.0; extra == 'dev'
28
+ Provides-Extra: gpu
29
+ Requires-Dist: cupy-cuda12x>=13.0; extra == 'gpu'
30
+ Provides-Extra: pandas
31
+ Requires-Dist: pandas>=2.0; extra == 'pandas'
32
+ Provides-Extra: r-comparison
33
+ Requires-Dist: pandas>=2.0; extra == 'r-comparison'
34
+ Requires-Dist: rpy2>=3.5; extra == 'r-comparison'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # pyfector
38
+
39
+ > **Alpha (v0.1.x)** -- This package is in early development. Results have been validated against R fect on synthetic data, but edge cases may remain. **APIs are not stable and may change without notice.** Please verify critical results independently. Bug reports, feature requests, and contributions are welcome via [GitHub Issues](https://github.com/AlanHuang99/pyfector/issues).
40
+
41
+ Fast counterfactual estimators for panel data in Python.
42
+
43
+ A high-performance reimplementation of the R [fect](https://github.com/xuyiqing/fect) package (Liu, Wang & Xu, 2024, *AJPS*), featuring GPU acceleration, parallel computing, and Polars data handling.
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install pyfector
49
+ ```
50
+
51
+ For GPU support:
52
+ ```bash
53
+ pip install pyfector[gpu]
54
+ ```
55
+
56
+ ## Quick Start
57
+
58
+ ```python
59
+ import pyfector
60
+
61
+ result = pyfector.fect(
62
+ data=df, # Polars or Pandas DataFrame
63
+ Y="outcome", # outcome column
64
+ D="treatment", # binary treatment indicator
65
+ index=("unit_id", "year"), # (unit, time) columns
66
+ X=["gdp", "population"], # covariates (optional)
67
+ method="ife", # "fe", "ife", "mc", "cfe"
68
+ r=(0, 5), # cross-validate over 0..5 factors
69
+ se=True, # bootstrap standard errors
70
+ nboots=500, # bootstrap replications
71
+ device="cpu", # "cpu" or "gpu"
72
+ n_jobs=4, # parallel workers
73
+ seed=42, # full reproducibility
74
+ )
75
+
76
+ # Results
77
+ print(result.summary())
78
+
79
+ # Plotting (gap plot, status heatmap, etc.)
80
+ result.plot(kind="gap")
81
+ result.plot(kind="status")
82
+
83
+ # Diagnostic tests
84
+ diag = result.diagnose(f_threshold=0.5, tost_threshold=0.36, loo=True)
85
+ print(diag.summary())
86
+ ```
87
+
88
+ ## Methods
89
+
90
+ | Method | Description | Key Parameter | When to Use |
91
+ |--------|-------------|---------------|-------------|
92
+ | `"fe"` | Fixed effects counterfactual | `force` | Parallel trends holds |
93
+ | `"ife"` | Interactive fixed effects | `r` (number of factors) | Unobserved time-varying confounders |
94
+ | `"mc"` | Matrix completion | `lam` (nuclear norm penalty) | Alternative to IFE |
95
+ | `"cfe"` | Complex fixed effects | `Z`, `Q` | Unit/time-varying interactions |
96
+
97
+ ## API Reference
98
+
99
+ ### `pyfector.fect()`
100
+
101
+ Main estimation function. Returns a `FectResult` object.
102
+
103
+ ```python
104
+ pyfector.fect(
105
+ data, # DataFrame (Polars or Pandas)
106
+ Y: str, # outcome column name
107
+ D: str, # treatment column name (0/1)
108
+ index: tuple[str, str], # (unit_id, time) column names
109
+ X: list[str] = None, # covariate column names
110
+ W: str = None, # weight column name
111
+ method: str = "ife", # "fe", "ife", "mc", "cfe", "both"
112
+ force: str = "two-way", # "none", "unit", "time", "two-way"
113
+ r: int | tuple = 0, # factors; tuple (min, max) for CV
114
+ lam: float = None, # lambda for MC; None = auto CV
115
+ nlambda: int = 10, # lambda grid size for MC CV
116
+ CV: bool = True, # cross-validate r or lambda
117
+ k: int = 10, # CV folds
118
+ cv_prop: float = 0.1, # fraction of control obs to mask
119
+ criterion: str = "mspe", # CV criterion: "mspe", "gmspe", "mad"
120
+ se: bool = False, # compute standard errors
121
+ vartype: str = "bootstrap", # "bootstrap" or "jackknife"
122
+ nboots: int = 200, # bootstrap replications
123
+ alpha: float = 0.05, # significance level
124
+ tol: float = 1e-7, # convergence tolerance
125
+ max_iter: int = 5000, # max EM iterations
126
+ min_T0: int = 1, # min pre-treatment periods per unit
127
+ normalize: bool = False, # normalize outcome by SD
128
+ device: str = "cpu", # "cpu" or "gpu"
129
+ n_jobs: int = 1, # parallel workers
130
+ seed: int = None, # random seed
131
+ )
132
+ ```
133
+
134
+ ### `FectResult` object
135
+
136
+ | Attribute | Type | Description |
137
+ |-----------|------|-------------|
138
+ | `att_avg` | `float` | Overall average treatment effect on the treated |
139
+ | `att_avg_unit` | `float` | Unit-averaged ATT |
140
+ | `att_on` | `ndarray` | Dynamic ATT by relative time to treatment |
141
+ | `time_on` | `ndarray` | Relative time indices |
142
+ | `count_on` | `ndarray` | Observation counts per relative time |
143
+ | `beta` | `ndarray` | Covariate coefficients |
144
+ | `Y_ct` | `ndarray` | T x N counterfactual outcome matrix |
145
+ | `eff` | `ndarray` | T x N treatment effect matrix |
146
+ | `factors` | `ndarray` | T x r estimated factors (IFE only) |
147
+ | `loadings` | `ndarray` | N x r estimated loadings (IFE only) |
148
+ | `sigma2` | `float` | Error variance estimate |
149
+ | `r_cv` | `int` | CV-selected number of factors |
150
+ | `lambda_cv` | `float` | CV-selected lambda |
151
+ | `inference` | `InferenceResult` | Bootstrap/jackknife results (if `se=True`) |
152
+
153
+ Methods:
154
+ - `result.summary()` — formatted summary table
155
+ - `result.plot(kind="gap")` — dynamic effects plot
156
+ - `result.plot(kind="status")` — treatment status heatmap
157
+ - `result.plot(kind="factors")` — latent factors
158
+ - `result.plot(kind="counterfactual", units=[1, 5])` — actual vs counterfactual
159
+ - `result.diagnose(...)` — run diagnostic tests
160
+
161
+ ### `InferenceResult` (when `se=True`)
162
+
163
+ | Attribute | Description |
164
+ |-----------|-------------|
165
+ | `att_avg_se` | Standard error of overall ATT |
166
+ | `att_avg_ci` | 95% confidence interval (tuple) |
167
+ | `att_avg_pval` | p-value for H0: ATT=0 |
168
+ | `att_on_se` | Per-period standard errors |
169
+ | `att_on_ci_lower` | Per-period CI lower bounds |
170
+ | `att_on_ci_upper` | Per-period CI upper bounds |
171
+ | `att_on_pval` | Per-period p-values |
172
+ | `att_avg_boot` | Bootstrap distribution of overall ATT |
173
+ | `att_on_boot` | Bootstrap distribution of dynamic ATTs |
174
+
175
+ ### Diagnostic Tests
176
+
177
+ ```python
178
+ diag = result.diagnose(
179
+ f_threshold=0.5, # equivalence F-test threshold
180
+ tost_threshold=0.36, # TOST equivalence bound
181
+ placebo_period=(-5, -1), # placebo test window
182
+ loo=True, # leave-one-period-out
183
+ )
184
+ print(diag.summary())
185
+ ```
186
+
187
+ | Test | What it Tests | Key Output |
188
+ |------|---------------|------------|
189
+ | **Pre-trend F-test** | Joint significance of pre-treatment ATTs | `f_stat`, `f_pval` |
190
+ | **Equivalence F-test** | Pre-trends within equivalence bounds | `equiv_f_pval` |
191
+ | **TOST** | Per-period equivalence | `tost_pvals` |
192
+ | **Placebo test** | No effect in specified pre-period window | `placebo_att`, `placebo_pval` |
193
+ | **Carryover test** | No lingering effect after treatment ends | `carryover_att`, `carryover_pval` |
194
+ | **Leave-one-out** | Sensitivity of ATT to dropping periods | `loo_max_change` |
195
+
196
+ ## Validation Against R fect
197
+
198
+ pyfector is validated against the original R fect package on identical simulated data. The table below shows exact comparison results (N=200, T=50):
199
+
200
+ ### Point Estimates
201
+
202
+ | Scenario | pyfector ATT | R fect ATT | Difference | True ATT |
203
+ |----------|------------|------------|------------|----------|
204
+ | FE (no factors) | 4.995583 | 4.995640 | -0.000057 | 5.0 |
205
+ | FE + covariates | 4.975683 | 4.975809 | -0.000126 | 5.0 |
206
+ | IFE r=2 | 3.010223 | 3.013046 | -0.002822 | 3.0 |
207
+ | IFE r=2 + covariates | 2.993155 | 2.996099 | -0.002944 | 3.0 |
208
+ | MC lambda=0.01 | 3.176671 | 3.176721 | -0.000050 | 3.0 |
209
+
210
+ **Point estimates agree to 4-6 decimal places** for FE and MC. IFE differences (~0.003) are due to different SVD implementations converging to slightly different factor rotations — both are equally valid and equally close to the true ATT.
211
+
212
+ ### Standard Errors (500 bootstrap replications)
213
+
214
+ | Scenario | pyfector SE | R fect SE | Ratio |
215
+ |----------|-----------|-----------|-------|
216
+ | FE (500 boots) | 0.020011 | 0.021291 | 0.94 |
217
+ | IFE r=2 (500 boots) | 0.017128 | 0.018382 | 0.93 |
218
+
219
+ **SEs agree within 7%** despite using different random draws and different SVD implementations. Per-period SE ratios range from 0.7 to 1.25.
220
+
221
+ ### Covariate Coefficients
222
+
223
+ | Covariate | pyfector | R fect | Difference |
224
+ |-----------|--------|--------|------------|
225
+ | X1 | 0.817041 | 0.817042 | -0.000001 |
226
+ | X2 | 1.173748 | 1.173747 | +0.000000 |
227
+ | X3 | 1.857722 | 1.857723 | -0.000001 |
228
+
229
+ **Coefficients agree to 6 decimal places.**
230
+
231
+ ## Performance
232
+
233
+ The key bottleneck in R fect is full SVD inside the EM loop: O(NT min(N,T)) per iteration. pyfector uses randomized truncated SVD for large matrices: O(NTr) where r << min(N,T).
234
+
235
+ | Scenario | pyfector | R fect | Speedup |
236
+ |----------|--------|--------|---------|
237
+ | FE, N=200 T=50 | 0.01s | 0.05s | 5x |
238
+ | IFE, N=200 T=50 | 0.08s | 0.06s | 0.7x |
239
+ | FE + 500 boots, N=200 T=50 | 0.54s | 5.52s | **10x** |
240
+ | IFE + 500 boots, N=200 T=50 | 3.61s | 22.83s | **6x** |
241
+ | IFE, N=1000 T=50 | 0.17s | 0.17s | 1x |
242
+
243
+ The main speedup comes from **bootstrap/CV parallelization** (`n_jobs`) and will increase further with GPU acceleration on large panels.
244
+
245
+ ## Design Principles
246
+
247
+ - **Reproducible**: every random operation is seeded; same seed = identical results
248
+ - **Idempotent**: calling `fect()` twice with the same inputs produces the same output
249
+ - **GPU-optional**: toggle `device="gpu"` for CuPy acceleration (no code changes needed)
250
+ - **Parallel**: CV folds and bootstrap replications run in parallel via `n_jobs`
251
+ - **Polars-first**: native Polars support; Pandas also accepted
252
+
253
+ ## References
254
+
255
+ - Liu, L., Wang, Y., & Xu, Y. (2024). A Practical Guide to Counterfactual Estimators for Causal Inference with Time-Series Cross-Sectional Data. *American Journal of Political Science*, 68(1), 160-176.
256
+ - Xu, Y. (2017). Generalized Synthetic Control Method. *Political Analysis*, 25(1), 57-76.
257
+ - Athey, S., et al. (2021). Matrix Completion Methods for Causal Panel Data Models. *JASA*, 116(536), 1716-1730.
258
+ - Bai, J. (2009). Panel Data Models with Interactive Fixed Effects. *Econometrica*, 77(4), 1229-1279.
259
+
260
+ ## License
261
+
262
+ MIT
@@ -0,0 +1,226 @@
1
+ # pyfector
2
+
3
+ > **Alpha (v0.1.x)** -- This package is in early development. Results have been validated against R fect on synthetic data, but edge cases may remain. **APIs are not stable and may change without notice.** Please verify critical results independently. Bug reports, feature requests, and contributions are welcome via [GitHub Issues](https://github.com/AlanHuang99/pyfector/issues).
4
+
5
+ Fast counterfactual estimators for panel data in Python.
6
+
7
+ A high-performance reimplementation of the R [fect](https://github.com/xuyiqing/fect) package (Liu, Wang & Xu, 2024, *AJPS*), featuring GPU acceleration, parallel computing, and Polars data handling.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install pyfector
13
+ ```
14
+
15
+ For GPU support:
16
+ ```bash
17
+ pip install pyfector[gpu]
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```python
23
+ import pyfector
24
+
25
+ result = pyfector.fect(
26
+ data=df, # Polars or Pandas DataFrame
27
+ Y="outcome", # outcome column
28
+ D="treatment", # binary treatment indicator
29
+ index=("unit_id", "year"), # (unit, time) columns
30
+ X=["gdp", "population"], # covariates (optional)
31
+ method="ife", # "fe", "ife", "mc", "cfe"
32
+ r=(0, 5), # cross-validate over 0..5 factors
33
+ se=True, # bootstrap standard errors
34
+ nboots=500, # bootstrap replications
35
+ device="cpu", # "cpu" or "gpu"
36
+ n_jobs=4, # parallel workers
37
+ seed=42, # full reproducibility
38
+ )
39
+
40
+ # Results
41
+ print(result.summary())
42
+
43
+ # Plotting (gap plot, status heatmap, etc.)
44
+ result.plot(kind="gap")
45
+ result.plot(kind="status")
46
+
47
+ # Diagnostic tests
48
+ diag = result.diagnose(f_threshold=0.5, tost_threshold=0.36, loo=True)
49
+ print(diag.summary())
50
+ ```
51
+
52
+ ## Methods
53
+
54
+ | Method | Description | Key Parameter | When to Use |
55
+ |--------|-------------|---------------|-------------|
56
+ | `"fe"` | Fixed effects counterfactual | `force` | Parallel trends holds |
57
+ | `"ife"` | Interactive fixed effects | `r` (number of factors) | Unobserved time-varying confounders |
58
+ | `"mc"` | Matrix completion | `lam` (nuclear norm penalty) | Alternative to IFE |
59
+ | `"cfe"` | Complex fixed effects | `Z`, `Q` | Unit/time-varying interactions |
60
+
61
+ ## API Reference
62
+
63
+ ### `pyfector.fect()`
64
+
65
+ Main estimation function. Returns a `FectResult` object.
66
+
67
+ ```python
68
+ pyfector.fect(
69
+ data, # DataFrame (Polars or Pandas)
70
+ Y: str, # outcome column name
71
+ D: str, # treatment column name (0/1)
72
+ index: tuple[str, str], # (unit_id, time) column names
73
+ X: list[str] = None, # covariate column names
74
+ W: str = None, # weight column name
75
+ method: str = "ife", # "fe", "ife", "mc", "cfe", "both"
76
+ force: str = "two-way", # "none", "unit", "time", "two-way"
77
+ r: int | tuple = 0, # factors; tuple (min, max) for CV
78
+ lam: float = None, # lambda for MC; None = auto CV
79
+ nlambda: int = 10, # lambda grid size for MC CV
80
+ CV: bool = True, # cross-validate r or lambda
81
+ k: int = 10, # CV folds
82
+ cv_prop: float = 0.1, # fraction of control obs to mask
83
+ criterion: str = "mspe", # CV criterion: "mspe", "gmspe", "mad"
84
+ se: bool = False, # compute standard errors
85
+ vartype: str = "bootstrap", # "bootstrap" or "jackknife"
86
+ nboots: int = 200, # bootstrap replications
87
+ alpha: float = 0.05, # significance level
88
+ tol: float = 1e-7, # convergence tolerance
89
+ max_iter: int = 5000, # max EM iterations
90
+ min_T0: int = 1, # min pre-treatment periods per unit
91
+ normalize: bool = False, # normalize outcome by SD
92
+ device: str = "cpu", # "cpu" or "gpu"
93
+ n_jobs: int = 1, # parallel workers
94
+ seed: int = None, # random seed
95
+ )
96
+ ```
97
+
98
+ ### `FectResult` object
99
+
100
+ | Attribute | Type | Description |
101
+ |-----------|------|-------------|
102
+ | `att_avg` | `float` | Overall average treatment effect on the treated |
103
+ | `att_avg_unit` | `float` | Unit-averaged ATT |
104
+ | `att_on` | `ndarray` | Dynamic ATT by relative time to treatment |
105
+ | `time_on` | `ndarray` | Relative time indices |
106
+ | `count_on` | `ndarray` | Observation counts per relative time |
107
+ | `beta` | `ndarray` | Covariate coefficients |
108
+ | `Y_ct` | `ndarray` | T x N counterfactual outcome matrix |
109
+ | `eff` | `ndarray` | T x N treatment effect matrix |
110
+ | `factors` | `ndarray` | T x r estimated factors (IFE only) |
111
+ | `loadings` | `ndarray` | N x r estimated loadings (IFE only) |
112
+ | `sigma2` | `float` | Error variance estimate |
113
+ | `r_cv` | `int` | CV-selected number of factors |
114
+ | `lambda_cv` | `float` | CV-selected lambda |
115
+ | `inference` | `InferenceResult` | Bootstrap/jackknife results (if `se=True`) |
116
+
117
+ Methods:
118
+ - `result.summary()` — formatted summary table
119
+ - `result.plot(kind="gap")` — dynamic effects plot
120
+ - `result.plot(kind="status")` — treatment status heatmap
121
+ - `result.plot(kind="factors")` — latent factors
122
+ - `result.plot(kind="counterfactual", units=[1, 5])` — actual vs counterfactual
123
+ - `result.diagnose(...)` — run diagnostic tests
124
+
125
+ ### `InferenceResult` (when `se=True`)
126
+
127
+ | Attribute | Description |
128
+ |-----------|-------------|
129
+ | `att_avg_se` | Standard error of overall ATT |
130
+ | `att_avg_ci` | 95% confidence interval (tuple) |
131
+ | `att_avg_pval` | p-value for H0: ATT=0 |
132
+ | `att_on_se` | Per-period standard errors |
133
+ | `att_on_ci_lower` | Per-period CI lower bounds |
134
+ | `att_on_ci_upper` | Per-period CI upper bounds |
135
+ | `att_on_pval` | Per-period p-values |
136
+ | `att_avg_boot` | Bootstrap distribution of overall ATT |
137
+ | `att_on_boot` | Bootstrap distribution of dynamic ATTs |
138
+
139
+ ### Diagnostic Tests
140
+
141
+ ```python
142
+ diag = result.diagnose(
143
+ f_threshold=0.5, # equivalence F-test threshold
144
+ tost_threshold=0.36, # TOST equivalence bound
145
+ placebo_period=(-5, -1), # placebo test window
146
+ loo=True, # leave-one-period-out
147
+ )
148
+ print(diag.summary())
149
+ ```
150
+
151
+ | Test | What it Tests | Key Output |
152
+ |------|---------------|------------|
153
+ | **Pre-trend F-test** | Joint significance of pre-treatment ATTs | `f_stat`, `f_pval` |
154
+ | **Equivalence F-test** | Pre-trends within equivalence bounds | `equiv_f_pval` |
155
+ | **TOST** | Per-period equivalence | `tost_pvals` |
156
+ | **Placebo test** | No effect in specified pre-period window | `placebo_att`, `placebo_pval` |
157
+ | **Carryover test** | No lingering effect after treatment ends | `carryover_att`, `carryover_pval` |
158
+ | **Leave-one-out** | Sensitivity of ATT to dropping periods | `loo_max_change` |
159
+
160
+ ## Validation Against R fect
161
+
162
+ pyfector is validated against the original R fect package on identical simulated data. The table below shows exact comparison results (N=200, T=50):
163
+
164
+ ### Point Estimates
165
+
166
+ | Scenario | pyfector ATT | R fect ATT | Difference | True ATT |
167
+ |----------|------------|------------|------------|----------|
168
+ | FE (no factors) | 4.995583 | 4.995640 | -0.000057 | 5.0 |
169
+ | FE + covariates | 4.975683 | 4.975809 | -0.000126 | 5.0 |
170
+ | IFE r=2 | 3.010223 | 3.013046 | -0.002822 | 3.0 |
171
+ | IFE r=2 + covariates | 2.993155 | 2.996099 | -0.002944 | 3.0 |
172
+ | MC lambda=0.01 | 3.176671 | 3.176721 | -0.000050 | 3.0 |
173
+
174
+ **Point estimates agree to 4-6 decimal places** for FE and MC. IFE differences (~0.003) are due to different SVD implementations converging to slightly different factor rotations — both are equally valid and equally close to the true ATT.
175
+
176
+ ### Standard Errors (500 bootstrap replications)
177
+
178
+ | Scenario | pyfector SE | R fect SE | Ratio |
179
+ |----------|-----------|-----------|-------|
180
+ | FE (500 boots) | 0.020011 | 0.021291 | 0.94 |
181
+ | IFE r=2 (500 boots) | 0.017128 | 0.018382 | 0.93 |
182
+
183
+ **SEs agree within 7%** despite using different random draws and different SVD implementations. Per-period SE ratios range from 0.7 to 1.25.
184
+
185
+ ### Covariate Coefficients
186
+
187
+ | Covariate | pyfector | R fect | Difference |
188
+ |-----------|--------|--------|------------|
189
+ | X1 | 0.817041 | 0.817042 | -0.000001 |
190
+ | X2 | 1.173748 | 1.173747 | +0.000000 |
191
+ | X3 | 1.857722 | 1.857723 | -0.000001 |
192
+
193
+ **Coefficients agree to 6 decimal places.**
194
+
195
+ ## Performance
196
+
197
+ The key bottleneck in R fect is full SVD inside the EM loop: O(NT min(N,T)) per iteration. pyfector uses randomized truncated SVD for large matrices: O(NTr) where r << min(N,T).
198
+
199
+ | Scenario | pyfector | R fect | Speedup |
200
+ |----------|--------|--------|---------|
201
+ | FE, N=200 T=50 | 0.01s | 0.05s | 5x |
202
+ | IFE, N=200 T=50 | 0.08s | 0.06s | 0.7x |
203
+ | FE + 500 boots, N=200 T=50 | 0.54s | 5.52s | **10x** |
204
+ | IFE + 500 boots, N=200 T=50 | 3.61s | 22.83s | **6x** |
205
+ | IFE, N=1000 T=50 | 0.17s | 0.17s | 1x |
206
+
207
+ The main speedup comes from **bootstrap/CV parallelization** (`n_jobs`) and will increase further with GPU acceleration on large panels.
208
+
209
+ ## Design Principles
210
+
211
+ - **Reproducible**: every random operation is seeded; same seed = identical results
212
+ - **Idempotent**: calling `fect()` twice with the same inputs produces the same output
213
+ - **GPU-optional**: toggle `device="gpu"` for CuPy acceleration (no code changes needed)
214
+ - **Parallel**: CV folds and bootstrap replications run in parallel via `n_jobs`
215
+ - **Polars-first**: native Polars support; Pandas also accepted
216
+
217
+ ## References
218
+
219
+ - Liu, L., Wang, Y., & Xu, Y. (2024). A Practical Guide to Counterfactual Estimators for Causal Inference with Time-Series Cross-Sectional Data. *American Journal of Political Science*, 68(1), 160-176.
220
+ - Xu, Y. (2017). Generalized Synthetic Control Method. *Political Analysis*, 25(1), 57-76.
221
+ - Athey, S., et al. (2021). Matrix Completion Methods for Causal Panel Data Models. *JASA*, 116(536), 1716-1730.
222
+ - Bai, J. (2009). Panel Data Models with Interactive Fixed Effects. *Econometrica*, 77(4), 1229-1279.
223
+
224
+ ## License
225
+
226
+ MIT