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.
- pyfector-0.1.0/.github/workflows/publish.yml +103 -0
- pyfector-0.1.0/.github/workflows/test.yml +57 -0
- pyfector-0.1.0/.gitignore +11 -0
- pyfector-0.1.0/LICENSE +21 -0
- pyfector-0.1.0/PKG-INFO +262 -0
- pyfector-0.1.0/README.md +226 -0
- pyfector-0.1.0/benchmarks/gpu_vs_cpu.py +246 -0
- pyfector-0.1.0/benchmarks/large_scale.py +18 -0
- pyfector-0.1.0/benchmarks/realistic_benchmark.py +121 -0
- pyfector-0.1.0/pyproject.toml +69 -0
- pyfector-0.1.0/src/pyfector/__init__.py +41 -0
- pyfector-0.1.0/src/pyfector/backend.py +88 -0
- pyfector-0.1.0/src/pyfector/cv.py +327 -0
- pyfector-0.1.0/src/pyfector/diagnostics.py +213 -0
- pyfector-0.1.0/src/pyfector/estimators.py +827 -0
- pyfector-0.1.0/src/pyfector/fect.py +487 -0
- pyfector-0.1.0/src/pyfector/inference.py +325 -0
- pyfector-0.1.0/src/pyfector/linalg.py +376 -0
- pyfector-0.1.0/src/pyfector/panel.py +412 -0
- pyfector-0.1.0/src/pyfector/plotting.py +258 -0
- pyfector-0.1.0/tests/benchmark_vs_r.py +288 -0
- pyfector-0.1.0/tests/comparison/README.md +33 -0
- pyfector-0.1.0/tests/comparison/synthetic_comparison.html +3642 -0
- pyfector-0.1.0/tests/comparison/synthetic_comparison.qmd +466 -0
- pyfector-0.1.0/tests/conftest.py +163 -0
- pyfector-0.1.0/tests/test_bootstrap_se.py +625 -0
- pyfector-0.1.0/tests/test_linalg.py +188 -0
- pyfector-0.1.0/tests/test_pressure.py +277 -0
- pyfector-0.1.0/tests/test_smoke.py +418 -0
- pyfector-0.1.0/tests/test_vs_r_fect.py +336 -0
- pyfector-0.1.0/uv.lock +3192 -0
|
@@ -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
|
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.
|
pyfector-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
pyfector-0.1.0/README.md
ADDED
|
@@ -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
|