firthmodels 0.7.2__tar.gz → 0.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {firthmodels-0.7.2 → firthmodels-0.8.1}/CITATION.cff +1 -1
- {firthmodels-0.7.2 → firthmodels-0.8.1}/PKG-INFO +5 -1
- {firthmodels-0.7.2 → firthmodels-0.8.1}/README.md +4 -0
- firthmodels-0.8.1/benchmarks/README.md +189 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/benchmarks/benchmark_cox.py +114 -43
- {firthmodels-0.7.2 → firthmodels-0.8.1}/benchmarks/benchmark_logistic.py +113 -49
- firthmodels-0.8.1/benchmarks/cox_results.json +908 -0
- firthmodels-0.8.1/benchmarks/cox_results.png +0 -0
- firthmodels-0.8.1/benchmarks/generate_report.py +911 -0
- firthmodels-0.8.1/benchmarks/logistic_results.json +2890 -0
- firthmodels-0.8.1/benchmarks/logistic_results.png +0 -0
- firthmodels-0.8.1/benchmarks/run_benchmarks.sh +26 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/pyproject.toml +1 -1
- {firthmodels-0.7.2 → firthmodels-0.8.1}/src/firthmodels/_lrt.py +84 -17
- {firthmodels-0.7.2 → firthmodels-0.8.1}/src/firthmodels/_numba/cox.py +66 -61
- {firthmodels-0.7.2 → firthmodels-0.8.1}/src/firthmodels/_numba/logistic.py +30 -6
- {firthmodels-0.7.2 → firthmodels-0.8.1}/src/firthmodels/_profile_ci.py +4 -1
- {firthmodels-0.7.2 → firthmodels-0.8.1}/src/firthmodels/_solvers.py +20 -8
- {firthmodels-0.7.2 → firthmodels-0.8.1}/src/firthmodels/_utils.py +2 -1
- {firthmodels-0.7.2 → firthmodels-0.8.1}/src/firthmodels/adapters/statsmodels.py +4 -2
- {firthmodels-0.7.2 → firthmodels-0.8.1}/src/firthmodels/cox.py +179 -109
- {firthmodels-0.7.2 → firthmodels-0.8.1}/src/firthmodels/logistic.py +185 -47
- {firthmodels-0.7.2 → firthmodels-0.8.1}/src/firthmodels/separation.py +25 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/tests/test_cox.py +198 -3
- {firthmodels-0.7.2 → firthmodels-0.8.1}/tests/test_logistic.py +236 -3
- {firthmodels-0.7.2 → firthmodels-0.8.1}/tests/test_numba_cox.py +83 -2
- {firthmodels-0.7.2 → firthmodels-0.8.1}/tests/test_numba_logistic.py +120 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/tests/test_separation.py +20 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/uv.lock +4 -4
- firthmodels-0.7.2/benchmarks/README.md +0 -161
- firthmodels-0.7.2/benchmarks/cox_results.csv +0 -7
- firthmodels-0.7.2/benchmarks/cox_results.png +0 -0
- firthmodels-0.7.2/benchmarks/generate_report.py +0 -699
- firthmodels-0.7.2/benchmarks/logistic_results.csv +0 -11
- firthmodels-0.7.2/benchmarks/logistic_results.png +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/.github/workflows/ci.yml +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/.github/workflows/publish.yml +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/.gitignore +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/.pre-commit-config.yaml +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/.python-version +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/LICENSE +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/benchmarks/brglm2_bench.R +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/benchmarks/coxphf_bench.R +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/benchmarks/logistf_bench.R +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/src/firthmodels/__init__.py +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/src/firthmodels/_numba/__init__.py +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/src/firthmodels/_numba/blas_abi.py +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/src/firthmodels/_numba/linalg.py +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/src/firthmodels/adapters/__init__.py +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/src/firthmodels/py.typed +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/tests/conftest.py +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/tests/test_adapters_statsmodels.py +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/tests/test_blas_abi.py +0 -0
- {firthmodels-0.7.2 → firthmodels-0.8.1}/tests/test_numba_linalg.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: firthmodels
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: Firth-penalized models in Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/jzluo/firthmodels
|
|
6
6
|
Project-URL: Repository, https://github.com/jzluo/firthmodels
|
|
@@ -228,6 +228,10 @@ model.lrt_bse_ # Back-corrected standard errors (separate from Wald bse_
|
|
|
228
228
|
Each feature requires a separate constrained model fit, so you can test selectively to
|
|
229
229
|
avoid unnecessary computation. By default, LRT uses a warm start based on the full-model
|
|
230
230
|
covariance to reduce Newton-Raphson iterations; pass `warm_start=False` to disable it.
|
|
231
|
+
If the full-model fit does not converge, its likelihood is non-finite, or a constrained
|
|
232
|
+
fit does not converge, a warning is emitted and the affected p-values and
|
|
233
|
+
back-corrected standard errors remain `NaN`. A later explicit `lrt()` call retries
|
|
234
|
+
failed constrained fits.
|
|
231
235
|
|
|
232
236
|
```python
|
|
233
237
|
model.lrt(0) # Single feature by index
|
|
@@ -185,6 +185,10 @@ model.lrt_bse_ # Back-corrected standard errors (separate from Wald bse_
|
|
|
185
185
|
Each feature requires a separate constrained model fit, so you can test selectively to
|
|
186
186
|
avoid unnecessary computation. By default, LRT uses a warm start based on the full-model
|
|
187
187
|
covariance to reduce Newton-Raphson iterations; pass `warm_start=False` to disable it.
|
|
188
|
+
If the full-model fit does not converge, its likelihood is non-finite, or a constrained
|
|
189
|
+
fit does not converge, a warning is emitted and the affected p-values and
|
|
190
|
+
back-corrected standard errors remain `NaN`. A later explicit `lrt()` call retries
|
|
191
|
+
failed constrained fits.
|
|
188
192
|
|
|
189
193
|
```python
|
|
190
194
|
model.lrt(0) # Single feature by index
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Benchmarks
|
|
2
|
+
|
|
3
|
+
Benchmarking of [firthmodels](https://github.com/jzluo/firthmodels) against implementations
|
|
4
|
+
of Firth-penalized logistic regression (R [logistf](https://cran.r-project.org/package=logistf),
|
|
5
|
+
[brglm2](https://cran.r-project.org/package=brglm2)) and Cox regression
|
|
6
|
+
(R [coxphf](https://cran.r-project.org/package=coxphf)).
|
|
7
|
+
|
|
8
|
+
## Summary
|
|
9
|
+
|
|
10
|
+
For the full workflow, firthmodels' Numba backend is **10.8x
|
|
11
|
+
faster than logistf** (k=50) and **159x faster than
|
|
12
|
+
coxphf** (k=30). Without Numba, the pure NumPy backend is 7.5x
|
|
13
|
+
faster than logistf and 23.7x faster than coxphf.
|
|
14
|
+
|
|
15
|
+
| Workload | Baseline | Speedup at k=5 | Speedup at largest k |
|
|
16
|
+
|---|---|--:|--:|
|
|
17
|
+
| Logistic: fit + Wald | next-fastest package | 3.8x | 5.3x |
|
|
18
|
+
| Logistic: fit + LRT + profile CI | logistf | 2.7x | 10.8x |
|
|
19
|
+
| Cox: fit + Wald | coxphf | 11.4x | 132x |
|
|
20
|
+
| Cox: fit + LRT + profile CI | coxphf | 8.4x | 159x |
|
|
21
|
+
|
|
22
|
+
The largest observed deviation across coefficients, profile CI bounds, and p-values is 8.0e-07 (see [Correctness](#correctness)).
|
|
23
|
+
|
|
24
|
+
Python is timed with time.perf_counter() around each call, after JIT-warming the Numba
|
|
25
|
+
backend. R packages are timed in-process with `microbenchmark` inside a single
|
|
26
|
+
R session, so R startup and data transfer are excluded, while formula parsing and
|
|
27
|
+
model-frame construction are included because logistf and coxphf only
|
|
28
|
+
offer formula interfaces. brglm2 is run with `check_aliasing=FALSE` to avoid
|
|
29
|
+
extra overhead from its default aliasing check. The reported value is the fastest of 10 runs.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Firth logistic regression
|
|
34
|
+
|
|
35
|
+
Compared against R [logistf](https://cran.r-project.org/package=logistf) and
|
|
36
|
+
[brglm2](https://cran.r-project.org/package=brglm2) on simulated data with
|
|
37
|
+
n = 1,000 observations, a 20% target event rate, and
|
|
38
|
+
k = 5 to 50 features.
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
The time axis is log-scale, so a constant vertical gap means a constant speedup ratio.
|
|
43
|
+
All values are the minimum observed wall-clock time across repeated runs, in milliseconds.
|
|
44
|
+
|
|
45
|
+
### Fit + Wald inference
|
|
46
|
+
|
|
47
|
+
| k | firthmodels<br>(numba) | firthmodels<br>(numpy) | logistf | brglm2<br>(AS-mean) | brglm2<br>(MPL-Jeffreys) | numba speedup<br>vs next fastest | numpy speedup<br>vs next fastest |
|
|
48
|
+
|--:|--:|--:|--:|--:|--:|--:|--:|
|
|
49
|
+
| 5 | **0.50** | 0.87 | <ins>1.9</ins> | 4.0 | 4.0 | 3.8x | 2.2x |
|
|
50
|
+
| 10 | **0.72** | 1.1 | <ins>3.4</ins> | 4.6 | 4.6 | 4.7x | 3.0x |
|
|
51
|
+
| 15 | **0.75** | 1.2 | <ins>5.3</ins> | 5.4 | 5.4 | 7.1x | 4.4x |
|
|
52
|
+
| 20 | **1.0** | 1.5 | 7.8 | 6.3 | <ins>6.2</ins> | 6.1x | 4.1x |
|
|
53
|
+
| 25 | **1.5** | 2.2 | 11.4 | 7.8 | <ins>7.8</ins> | 5.4x | 3.6x |
|
|
54
|
+
| 30 | **1.6** | 2.3 | 14.7 | <ins>8.5</ins> | 8.7 | 5.2x | 3.7x |
|
|
55
|
+
| 35 | **1.7** | 2.6 | 19.9 | <ins>10.2</ins> | 10.2 | 5.8x | 4.0x |
|
|
56
|
+
| 40 | **2.2** | 3.1 | 24.9 | 11.6 | <ins>11.5</ins> | 5.2x | 3.8x |
|
|
57
|
+
| 45 | **2.7** | 3.7 | 33.2 | 13.1 | <ins>13.0</ins> | 4.8x | 3.5x |
|
|
58
|
+
| 50 | **3.2** | 4.3 | 40.8 | 17.0 | <ins>16.7</ins> | 5.3x | 3.9x |
|
|
59
|
+
|
|
60
|
+
The fastest time at each k is **bolded**. The next-fastest package (not including the firthmodels
|
|
61
|
+
NumPy backend) is <ins>underlined</ins> and is the baseline for the speedup columns. The two brglm2
|
|
62
|
+
fitting methods have nearly identical timings (within 1.9% at every k).
|
|
63
|
+
|
|
64
|
+
### Full workflow: fit + LRT + profile likelihood CI
|
|
65
|
+
|
|
66
|
+
brglm2 is not included here because it does not provide penalized LRT
|
|
67
|
+
p-values or profile likelihood CIs.
|
|
68
|
+
|
|
69
|
+
| k | firthmodels<br>(numba) | firthmodels<br>(numpy) | logistf | numba speedup<br>vs logistf | numpy speedup<br>vs logistf |
|
|
70
|
+
|--:|--:|--:|--:|--:|--:|
|
|
71
|
+
| 5 | **4.5** | 10.6 | <ins>11.9</ins> | 2.7x | 1.1x |
|
|
72
|
+
| 10 | **11.5** | 23.7 | <ins>41.9</ins> | 3.6x | 1.8x |
|
|
73
|
+
| 15 | **18.2** | 38.1 | <ins>101.0</ins> | 5.6x | 2.7x |
|
|
74
|
+
| 20 | **34.9** | 61.7 | <ins>206.2</ins> | 5.9x | 3.3x |
|
|
75
|
+
| 25 | **66.4** | 117.1 | <ins>418.7</ins> | 6.3x | 3.6x |
|
|
76
|
+
| 30 | **92.9** | 150.3 | <ins>624.0</ins> | 6.7x | 4.2x |
|
|
77
|
+
| 35 | **111.8** | 190.2 | <ins>1,059</ins> | 9.5x | 5.6x |
|
|
78
|
+
| 40 | **166.4** | 261.9 | <ins>1,533</ins> | 9.2x | 5.9x |
|
|
79
|
+
| 45 | **213.9** | 326.7 | <ins>2,230</ins> | 10.4x | 6.8x |
|
|
80
|
+
| 50 | **290.9** | 420.3 | <ins>3,135</ins> | 10.8x | 7.5x |
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Firth Cox proportional hazards
|
|
85
|
+
|
|
86
|
+
Compared against R [coxphf](https://cran.r-project.org/package=coxphf) on
|
|
87
|
+
simulated survival data with n = 500 observations, a 20% event rate,
|
|
88
|
+
and k = 5 to 30 features.
|
|
89
|
+
|
|
90
|
+

|
|
91
|
+
|
|
92
|
+
The time axis is log-scale, so a constant vertical gap means a constant speedup ratio.
|
|
93
|
+
All values are the minimum observed wall-clock time across repeated runs, in milliseconds.
|
|
94
|
+
|
|
95
|
+
### Fit + Wald inference
|
|
96
|
+
|
|
97
|
+
| k | firthmodels<br>(numba) | firthmodels<br>(numpy) | coxphf | numba speedup<br>vs coxphf | numpy speedup<br>vs coxphf |
|
|
98
|
+
|--:|--:|--:|--:|--:|--:|
|
|
99
|
+
| 5 | **0.29** | 1.4 | <ins>3.3</ins> | 11.4x | 2.3x |
|
|
100
|
+
| 10 | **0.40** | 2.2 | <ins>8.7</ins> | 21.9x | 4.0x |
|
|
101
|
+
| 15 | **0.57** | 3.2 | <ins>24.1</ins> | 42.5x | 7.5x |
|
|
102
|
+
| 20 | **0.85** | 5.6 | <ins>54.8</ins> | 64.1x | 9.8x |
|
|
103
|
+
| 25 | **1.2** | 7.7 | <ins>119.6</ins> | 99.8x | 15.5x |
|
|
104
|
+
| 30 | **1.6** | 10.3 | <ins>210.3</ins> | 132x | 20.5x |
|
|
105
|
+
|
|
106
|
+
The fastest time at each k is **bolded**. The next-fastest package (not including the firthmodels
|
|
107
|
+
NumPy backend) is <ins>underlined</ins> and is the baseline for the speedup columns.
|
|
108
|
+
|
|
109
|
+
### Full workflow: fit + LRT + profile likelihood CI
|
|
110
|
+
|
|
111
|
+
| k | firthmodels<br>(numba) | firthmodels<br>(numpy) | coxphf | numba speedup<br>vs coxphf | numpy speedup<br>vs coxphf |
|
|
112
|
+
|--:|--:|--:|--:|--:|--:|
|
|
113
|
+
| 5 | **2.1** | 11.9 | <ins>17.4</ins> | 8.4x | 1.5x |
|
|
114
|
+
| 10 | **6.8** | 43.6 | <ins>160.0</ins> | 23.4x | 3.7x |
|
|
115
|
+
| 15 | **17.2** | 111.3 | <ins>912.8</ins> | 53.0x | 8.2x |
|
|
116
|
+
| 20 | **31.4** | 221.8 | <ins>2,516</ins> | 80.2x | 11.3x |
|
|
117
|
+
| 25 | **58.5** | 398.4 | <ins>7,281</ins> | 124x | 18.3x |
|
|
118
|
+
| 30 | **97.1** | 651.4 | <ins>15,425</ins> | 159x | 23.7x |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Correctness
|
|
123
|
+
|
|
124
|
+
The benchmark scripts abort if firthmodels disagrees with the R reference by more than
|
|
125
|
+
1e-06 on coefficients, profile CI bounds, or p-values (and also cross-check
|
|
126
|
+
the numba backend against the numpy backend).
|
|
127
|
+
|
|
128
|
+
Maximum over all coefficients and all k during the benchmark run (recorded in the results file's run metadata):
|
|
129
|
+
|
|
130
|
+
| Comparison | Quantity | Max abs. deviation |
|
|
131
|
+
|---|---|--:|
|
|
132
|
+
| Logistic fit vs logistf | Coefficients | 2.2e-16 |
|
|
133
|
+
| Logistic fit vs brglm2 AS_mean | Coefficients | 7.3e-07 |
|
|
134
|
+
| Logistic fit vs brglm2 MPL_Jeffreys | Coefficients | 7.3e-07 |
|
|
135
|
+
| Logistic full vs logistf | Coefficients | 2.2e-16 |
|
|
136
|
+
| Logistic full vs logistf | Profile CI bounds | 5.3e-13 |
|
|
137
|
+
| Logistic full vs logistf | LRT p-values | 2.7e-10 |
|
|
138
|
+
| Logistic numba vs numpy | Coefficients | 2.2e-16 |
|
|
139
|
+
| Cox fit vs coxphf | Coefficients | 8.0e-07 |
|
|
140
|
+
| Cox full vs coxphf | Coefficients | 8.0e-07 |
|
|
141
|
+
| Cox full vs coxphf | Profile CI bounds | 4.3e-08 |
|
|
142
|
+
| Cox full vs coxphf | LRT p-values | 2.5e-08 |
|
|
143
|
+
| Cox numba vs numpy | Coefficients | 3.9e-16 |
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Environment
|
|
148
|
+
|
|
149
|
+
Collected at report-generation time on the benchmark machine.
|
|
150
|
+
|
|
151
|
+
| Component | Version |
|
|
152
|
+
|---|---|
|
|
153
|
+
| OS | CachyOS |
|
|
154
|
+
| CPU | AMD Ryzen 5 5600X 6-Core Processor |
|
|
155
|
+
| Python | 3.12.13 |
|
|
156
|
+
| firthmodels | 0.8.1 |
|
|
157
|
+
| NumPy / SciPy / Numba | 2.3.5 / 1.16.3 / 0.64.0 |
|
|
158
|
+
| NumPy BLAS | /usr/lib/libopenblas.so.0.3 (openblas 0.3.34, runtime) |
|
|
159
|
+
| R | 4.6.1 |
|
|
160
|
+
| logistf / brglm2 / coxphf | 1.26.1 / 1.1.0 / 1.13.4 |
|
|
161
|
+
| R BLAS | /usr/lib/libopenblas.so.0.3 |
|
|
162
|
+
|
|
163
|
+
NumPy and R link to the same BLAS library (`/usr/lib/libopenblas.so.0.3`). BLAS threading is left at library defaults for both stacks.
|
|
164
|
+
R packages are compiled from source with `-march=x86-64-v3 -mtune=haswell -O3 -flto=auto`,
|
|
165
|
+
so the R timings are not slowed by a conservative build.
|
|
166
|
+
|
|
167
|
+
<details>
|
|
168
|
+
<summary>Full R package compile flags (current configuration)</summary>
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
CFLAGS: -march=x86-64-v3 -mtune=haswell -O3 -pipe -fno-plt -fexceptions -Wp,-D_FORTIFY_SOURCE=3 -Wformat -Werror=format-security -fstack-clash-protection -fcf-protection -mpclmul -g1 -ffile-prefix-map=/startdir/src=/usr/src/debug/r -flto=auto -ffat-lto-objects
|
|
172
|
+
FCFLAGS: -O3 -march=x86-64-v3 -mtune=haswell
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
</details>
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Reproducing these results
|
|
180
|
+
|
|
181
|
+
Requires R with the logistf, brglm2, coxphf, survival, microbenchmark, and
|
|
182
|
+
jsonlite packages installed.
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# Run both benchmarks (writes results JSON files holding the timings, R
|
|
186
|
+
# reference values, and run metadata), then regenerate the plots and this
|
|
187
|
+
# README
|
|
188
|
+
benchmarks/run_benchmarks.sh
|
|
189
|
+
```
|
|
@@ -15,6 +15,7 @@ import subprocess
|
|
|
15
15
|
import sys
|
|
16
16
|
import tempfile
|
|
17
17
|
import time
|
|
18
|
+
from datetime import datetime, timezone
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
from typing import Literal
|
|
20
21
|
|
|
@@ -29,7 +30,7 @@ from firthmodels import FirthCoxPH
|
|
|
29
30
|
N_SAMPLES = 500
|
|
30
31
|
EVENT_RATE = 0.20
|
|
31
32
|
K_VALUES = list(range(5, 35, 5))
|
|
32
|
-
N_RUNS =
|
|
33
|
+
N_RUNS = 10
|
|
33
34
|
BASE_SEED = 0
|
|
34
35
|
|
|
35
36
|
# Solver parameters
|
|
@@ -43,8 +44,8 @@ COEF_TOL = 1e-6
|
|
|
43
44
|
CI_TOL = 1e-6
|
|
44
45
|
PVAL_TOL = 1e-6
|
|
45
46
|
|
|
46
|
-
# Reduce coxphf benchmark runs for large k
|
|
47
|
-
COXPHF_REDUCE_AFTER =
|
|
47
|
+
# Reduce coxphf benchmark runs for large k (0 disables reduction)
|
|
48
|
+
COXPHF_REDUCE_AFTER = 0
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
# -----------------------------------------------------------------------------
|
|
@@ -92,7 +93,7 @@ def make_benchmark_data(
|
|
|
92
93
|
T = -np.log(U) / (baseline_hazard * np.exp(eta))
|
|
93
94
|
|
|
94
95
|
# Censoring to achieve target event rate
|
|
95
|
-
censor_quantile =
|
|
96
|
+
censor_quantile = event_rate
|
|
96
97
|
censor_time = np.quantile(T, censor_quantile)
|
|
97
98
|
|
|
98
99
|
time = np.minimum(T, censor_time)
|
|
@@ -279,26 +280,41 @@ def check_agreement(
|
|
|
279
280
|
r_ci: np.ndarray | None = None,
|
|
280
281
|
py_pval: np.ndarray | None = None,
|
|
281
282
|
r_pval: np.ndarray | None = None,
|
|
283
|
+
deviations: dict[str, dict[str, float]] | None = None,
|
|
282
284
|
) -> None:
|
|
283
|
-
"""Check that Python and R results agree within tolerance.
|
|
284
|
-
|
|
285
|
+
"""Check that Python and R results agree within tolerance.
|
|
286
|
+
|
|
287
|
+
If a deviations dict is provided, the observed max absolute differences
|
|
288
|
+
are merged into it under label, keyed by quantity, taking the max over
|
|
289
|
+
successive calls (i.e. over k). Recorded before any tolerance failure is
|
|
290
|
+
raised.
|
|
291
|
+
"""
|
|
292
|
+
observed: dict[str, float] = {}
|
|
293
|
+
|
|
285
294
|
coef_diff = np.max(np.abs(py_coef - r_coef))
|
|
286
|
-
|
|
287
|
-
raise AssertionError(f"k={k} ({label}): coef diff {coef_diff:.2e} > {COEF_TOL}")
|
|
295
|
+
observed["coef"] = float(coef_diff)
|
|
288
296
|
|
|
289
|
-
# Check CIs if provided
|
|
290
297
|
if py_ci is not None and r_ci is not None:
|
|
291
|
-
|
|
292
|
-
if ci_diff > CI_TOL:
|
|
293
|
-
raise AssertionError(f"k={k} ({label}): CI diff {ci_diff:.2e} > {CI_TOL}")
|
|
298
|
+
observed["ci"] = float(np.max(np.abs(py_ci - r_ci)))
|
|
294
299
|
|
|
295
|
-
# Check p-values if provided
|
|
296
300
|
if py_pval is not None and r_pval is not None:
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
301
|
+
observed["pval"] = float(np.max(np.abs(py_pval - r_pval)))
|
|
302
|
+
|
|
303
|
+
if deviations is not None:
|
|
304
|
+
bucket = deviations.setdefault(label, {})
|
|
305
|
+
for quantity, value in observed.items():
|
|
306
|
+
bucket[quantity] = max(bucket.get(quantity, 0.0), value)
|
|
307
|
+
|
|
308
|
+
if coef_diff > COEF_TOL:
|
|
309
|
+
raise AssertionError(f"k={k} ({label}): coef diff {coef_diff:.2e} > {COEF_TOL}")
|
|
310
|
+
if observed.get("ci", 0.0) > CI_TOL:
|
|
311
|
+
raise AssertionError(
|
|
312
|
+
f"k={k} ({label}): CI diff {observed['ci']:.2e} > {CI_TOL}"
|
|
313
|
+
)
|
|
314
|
+
if observed.get("pval", 0.0) > PVAL_TOL:
|
|
315
|
+
raise AssertionError(
|
|
316
|
+
f"k={k} ({label}): p-value diff {observed['pval']:.2e} > {PVAL_TOL}"
|
|
317
|
+
)
|
|
302
318
|
|
|
303
319
|
|
|
304
320
|
# -----------------------------------------------------------------------------
|
|
@@ -426,17 +442,17 @@ def compute_min(times: np.ndarray) -> float:
|
|
|
426
442
|
# Saved results loading
|
|
427
443
|
# -----------------------------------------------------------------------------
|
|
428
444
|
def load_saved_results(
|
|
429
|
-
|
|
445
|
+
saved_path: str,
|
|
430
446
|
k_values: list[int],
|
|
431
447
|
load_firthmodels: bool = False,
|
|
432
448
|
load_coxphf: bool = True,
|
|
433
449
|
) -> tuple[pd.DataFrame, dict[int, dict] | None]:
|
|
434
|
-
"""Load results from a saved
|
|
450
|
+
"""Load results from a saved results JSON file.
|
|
435
451
|
|
|
436
452
|
Parameters
|
|
437
453
|
----------
|
|
438
|
-
|
|
439
|
-
Path to
|
|
454
|
+
saved_path : str
|
|
455
|
+
Path to results JSON with previous results.
|
|
440
456
|
k_values : list of int
|
|
441
457
|
Which k values to load.
|
|
442
458
|
load_firthmodels : bool
|
|
@@ -451,7 +467,8 @@ def load_saved_results(
|
|
|
451
467
|
coxphf_results : dict or None
|
|
452
468
|
coxphf results if load_coxphf=True, else None.
|
|
453
469
|
"""
|
|
454
|
-
|
|
470
|
+
with open(saved_path) as f:
|
|
471
|
+
saved_df = pd.DataFrame(json.load(f)["results"])
|
|
455
472
|
|
|
456
473
|
# Determine required columns based on what we're loading
|
|
457
474
|
required_cols = {"k", "n"}
|
|
@@ -473,20 +490,21 @@ def load_saved_results(
|
|
|
473
490
|
|
|
474
491
|
missing_cols = required_cols - set(saved_df.columns)
|
|
475
492
|
if missing_cols:
|
|
476
|
-
raise ValueError(f"Saved
|
|
493
|
+
raise ValueError(f"Saved results missing columns: {missing_cols}")
|
|
477
494
|
|
|
478
495
|
saved_n = int(saved_df["n"].iloc[0])
|
|
479
496
|
if saved_n != N_SAMPLES:
|
|
480
497
|
raise ValueError(
|
|
481
|
-
f"Saved
|
|
498
|
+
f"Saved results were created with n={saved_n}, "
|
|
499
|
+
f"but current N_SAMPLES={N_SAMPLES}."
|
|
482
500
|
)
|
|
483
501
|
|
|
484
502
|
saved_k = set(saved_df["k"].tolist())
|
|
485
503
|
missing = set(k_values) - saved_k
|
|
486
504
|
if missing:
|
|
487
|
-
raise ValueError(f"k values {missing} not in saved
|
|
505
|
+
raise ValueError(f"k values {missing} not in saved results")
|
|
488
506
|
|
|
489
|
-
#
|
|
507
|
+
# Reference values are copied verbatim from saved_df rows, so no parsing
|
|
490
508
|
return saved_df, None
|
|
491
509
|
|
|
492
510
|
|
|
@@ -501,8 +519,13 @@ def run_benchmarks(
|
|
|
501
519
|
saved: str | None = None,
|
|
502
520
|
run_firthmodels: bool = True,
|
|
503
521
|
run_coxphf: bool = True,
|
|
504
|
-
) -> tuple[pd.DataFrame, dict[str, str]]:
|
|
505
|
-
"""Run all benchmarks and return results as DataFrame.
|
|
522
|
+
) -> tuple[pd.DataFrame, dict[str, str], dict]:
|
|
523
|
+
"""Run all benchmarks and return results as DataFrame.
|
|
524
|
+
|
|
525
|
+
Also returns version_info and run_meta (run counts, verification status
|
|
526
|
+
and observed deviations, versions; saved into the results JSON by
|
|
527
|
+
main()).
|
|
528
|
+
"""
|
|
506
529
|
version_info = get_python_version_info()
|
|
507
530
|
|
|
508
531
|
# Load saved results for libraries we're not running
|
|
@@ -580,7 +603,7 @@ def run_benchmarks(
|
|
|
580
603
|
if coxphf_results is None:
|
|
581
604
|
coxphf_results = {}
|
|
582
605
|
if run_coxphf:
|
|
583
|
-
if coxphf_reduce_after
|
|
606
|
+
if coxphf_reduce_after:
|
|
584
607
|
k_low = [k for k in k_values if k <= coxphf_reduce_after]
|
|
585
608
|
k_high = [k for k in k_values if k > coxphf_reduce_after]
|
|
586
609
|
|
|
@@ -612,6 +635,7 @@ def run_benchmarks(
|
|
|
612
635
|
"Verifying results and computing statistics...", file=sys.stderr, flush=True
|
|
613
636
|
)
|
|
614
637
|
results = []
|
|
638
|
+
deviations: dict[str, dict[str, float]] = {}
|
|
615
639
|
|
|
616
640
|
for k in k_values:
|
|
617
641
|
X, surv_time, event = make_benchmark_data(
|
|
@@ -622,21 +646,32 @@ def run_benchmarks(
|
|
|
622
646
|
if run_firthmodels and run_coxphf and not skip_verification:
|
|
623
647
|
py = py_results["numba"][k]
|
|
624
648
|
r = coxphf_results[k]
|
|
625
|
-
check_agreement(
|
|
649
|
+
check_agreement(
|
|
650
|
+
k,
|
|
651
|
+
py["fit_coef"],
|
|
652
|
+
r["fit_coef"],
|
|
653
|
+
"fit vs coxphf",
|
|
654
|
+
deviations=deviations,
|
|
655
|
+
)
|
|
626
656
|
check_agreement(
|
|
627
657
|
k,
|
|
628
658
|
py["full_coef"],
|
|
629
659
|
r["full_coef"],
|
|
630
|
-
"full",
|
|
660
|
+
"full vs coxphf",
|
|
631
661
|
py["full_ci"],
|
|
632
662
|
r["full_ci"],
|
|
633
663
|
py["full_pval"],
|
|
634
664
|
r["full_pval"],
|
|
665
|
+
deviations=deviations,
|
|
635
666
|
)
|
|
636
667
|
# Also check numba vs numpy
|
|
637
668
|
py_numpy = py_results["numpy"][k]
|
|
638
669
|
check_agreement(
|
|
639
|
-
k,
|
|
670
|
+
k,
|
|
671
|
+
py["fit_coef"],
|
|
672
|
+
py_numpy["fit_coef"],
|
|
673
|
+
"numba vs numpy",
|
|
674
|
+
deviations=deviations,
|
|
640
675
|
)
|
|
641
676
|
|
|
642
677
|
# Collect timing results
|
|
@@ -673,10 +708,10 @@ def run_benchmarks(
|
|
|
673
708
|
row["coxphf_fit_ms"] = compute_min(r["fit_times"]) * 1000
|
|
674
709
|
row["coxphf_full_ms"] = compute_min(r["full_times"]) * 1000
|
|
675
710
|
# Store for reproducibility
|
|
676
|
-
row["coxphf_fit_coef"] =
|
|
677
|
-
row["coxphf_full_coef"] =
|
|
678
|
-
row["coxphf_full_ci"] =
|
|
679
|
-
row["coxphf_full_pval"] =
|
|
711
|
+
row["coxphf_fit_coef"] = np.asarray(r["fit_coef"]).tolist()
|
|
712
|
+
row["coxphf_full_coef"] = np.asarray(r["full_coef"]).tolist()
|
|
713
|
+
row["coxphf_full_ci"] = np.asarray(r["full_ci"]).tolist()
|
|
714
|
+
row["coxphf_full_pval"] = np.asarray(r["full_pval"]).tolist()
|
|
680
715
|
else:
|
|
681
716
|
assert saved_row is not None
|
|
682
717
|
row["coxphf_fit_ms"] = saved_row["coxphf_fit_ms"]
|
|
@@ -688,7 +723,39 @@ def run_benchmarks(
|
|
|
688
723
|
|
|
689
724
|
results.append(row)
|
|
690
725
|
|
|
691
|
-
|
|
726
|
+
reduced = n_runs if n_runs <= 3 else max(3, n_runs // 3)
|
|
727
|
+
run_meta = {
|
|
728
|
+
"schema_version": 1,
|
|
729
|
+
"model": "cox",
|
|
730
|
+
"created_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
731
|
+
"n_runs": n_runs,
|
|
732
|
+
"r_reduced_runs": (
|
|
733
|
+
{
|
|
734
|
+
"libraries": ["coxphf"],
|
|
735
|
+
"for_k_above": coxphf_reduce_after,
|
|
736
|
+
"n_runs": reduced,
|
|
737
|
+
}
|
|
738
|
+
if coxphf_reduce_after
|
|
739
|
+
else None
|
|
740
|
+
),
|
|
741
|
+
"k_values": list(k_values),
|
|
742
|
+
"libraries_run": [
|
|
743
|
+
name
|
|
744
|
+
for name, flag in [
|
|
745
|
+
("firthmodels", run_firthmodels),
|
|
746
|
+
("coxphf", run_coxphf),
|
|
747
|
+
]
|
|
748
|
+
if flag
|
|
749
|
+
],
|
|
750
|
+
"loaded_from_saved": saved,
|
|
751
|
+
"verification": {
|
|
752
|
+
"performed": bool(deviations),
|
|
753
|
+
"tolerances": {"coef": COEF_TOL, "ci": CI_TOL, "pval": PVAL_TOL},
|
|
754
|
+
"max_abs_deviations": deviations,
|
|
755
|
+
},
|
|
756
|
+
"versions": version_info,
|
|
757
|
+
}
|
|
758
|
+
return pd.DataFrame(results), version_info, run_meta
|
|
692
759
|
|
|
693
760
|
|
|
694
761
|
def print_table(df: pd.DataFrame) -> None:
|
|
@@ -743,14 +810,14 @@ def main():
|
|
|
743
810
|
"--out",
|
|
744
811
|
type=str,
|
|
745
812
|
default=None,
|
|
746
|
-
help="Save results to
|
|
813
|
+
help="Save results (timings, R references, run metadata) to JSON file",
|
|
747
814
|
)
|
|
748
815
|
parser.add_argument(
|
|
749
816
|
"--saved",
|
|
750
817
|
type=str,
|
|
751
818
|
default=None,
|
|
752
|
-
metavar="
|
|
753
|
-
help="Load non-selected libraries from this
|
|
819
|
+
metavar="JSON",
|
|
820
|
+
help="Load non-selected libraries from this results JSON.",
|
|
754
821
|
)
|
|
755
822
|
parser.add_argument(
|
|
756
823
|
"--firthmodels",
|
|
@@ -807,7 +874,7 @@ def main():
|
|
|
807
874
|
)
|
|
808
875
|
|
|
809
876
|
# Run benchmarks
|
|
810
|
-
df, version_info = run_benchmarks(
|
|
877
|
+
df, version_info, run_meta = run_benchmarks(
|
|
811
878
|
k_values=k_values,
|
|
812
879
|
n_runs=args.n_runs,
|
|
813
880
|
skip_verification=args.skip_verification,
|
|
@@ -821,7 +888,11 @@ def main():
|
|
|
821
888
|
print_table(df)
|
|
822
889
|
|
|
823
890
|
if args.out:
|
|
824
|
-
|
|
891
|
+
with open(args.out, "w") as f:
|
|
892
|
+
json.dump(
|
|
893
|
+
{"meta": run_meta, "results": df.to_dict(orient="records")}, f, indent=2
|
|
894
|
+
)
|
|
895
|
+
f.write("\n")
|
|
825
896
|
print(f"\nResults saved to {args.out}", file=sys.stderr)
|
|
826
897
|
|
|
827
898
|
|