firthmodels 0.8.0__tar.gz → 0.8.2__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.8.0 → firthmodels-0.8.2}/.pre-commit-config.yaml +1 -1
- {firthmodels-0.8.0 → firthmodels-0.8.2}/CITATION.cff +1 -1
- {firthmodels-0.8.0 → firthmodels-0.8.2}/PKG-INFO +2 -2
- firthmodels-0.8.2/benchmarks/README.md +191 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/benchmarks/benchmark_cox.py +113 -43
- {firthmodels-0.8.0 → firthmodels-0.8.2}/benchmarks/benchmark_logistic.py +113 -49
- firthmodels-0.8.2/benchmarks/cox_results.json +908 -0
- firthmodels-0.8.2/benchmarks/cox_results.png +0 -0
- firthmodels-0.8.2/benchmarks/generate_report.py +908 -0
- firthmodels-0.8.2/benchmarks/logistic_results.json +2890 -0
- firthmodels-0.8.2/benchmarks/logistic_results.png +0 -0
- firthmodels-0.8.2/benchmarks/run_benchmarks.sh +26 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/pyproject.toml +2 -2
- {firthmodels-0.8.0 → firthmodels-0.8.2}/src/firthmodels/__init__.py +1 -1
- {firthmodels-0.8.0 → firthmodels-0.8.2}/src/firthmodels/_numba/cox.py +75 -66
- {firthmodels-0.8.0 → firthmodels-0.8.2}/src/firthmodels/_numba/linalg.py +23 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/src/firthmodels/_numba/logistic.py +37 -13
- {firthmodels-0.8.0 → firthmodels-0.8.2}/src/firthmodels/_solvers.py +12 -1
- {firthmodels-0.8.0 → firthmodels-0.8.2}/src/firthmodels/_utils.py +6 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/src/firthmodels/adapters/statsmodels.py +1 -2
- {firthmodels-0.8.0 → firthmodels-0.8.2}/src/firthmodels/cox.py +150 -88
- {firthmodels-0.8.0 → firthmodels-0.8.2}/src/firthmodels/logistic.py +5 -3
- {firthmodels-0.8.0 → firthmodels-0.8.2}/tests/test_adapters_statsmodels.py +2 -2
- {firthmodels-0.8.0 → firthmodels-0.8.2}/tests/test_cox.py +79 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/tests/test_logistic.py +23 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/tests/test_numba_cox.py +42 -9
- {firthmodels-0.8.0 → firthmodels-0.8.2}/tests/test_numba_linalg.py +1 -3
- {firthmodels-0.8.0 → firthmodels-0.8.2}/tests/test_numba_logistic.py +23 -6
- {firthmodels-0.8.0 → firthmodels-0.8.2}/uv.lock +4 -4
- firthmodels-0.8.0/benchmarks/README.md +0 -161
- firthmodels-0.8.0/benchmarks/cox_results.csv +0 -7
- firthmodels-0.8.0/benchmarks/cox_results.png +0 -0
- firthmodels-0.8.0/benchmarks/generate_report.py +0 -699
- firthmodels-0.8.0/benchmarks/logistic_results.csv +0 -11
- firthmodels-0.8.0/benchmarks/logistic_results.png +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/.github/workflows/ci.yml +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/.github/workflows/publish.yml +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/.gitignore +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/.python-version +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/LICENSE +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/README.md +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/benchmarks/brglm2_bench.R +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/benchmarks/coxphf_bench.R +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/benchmarks/logistf_bench.R +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/src/firthmodels/_lrt.py +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/src/firthmodels/_numba/__init__.py +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/src/firthmodels/_numba/blas_abi.py +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/src/firthmodels/_profile_ci.py +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/src/firthmodels/adapters/__init__.py +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/src/firthmodels/py.typed +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/src/firthmodels/separation.py +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/tests/conftest.py +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/tests/test_blas_abi.py +0 -0
- {firthmodels-0.8.0 → firthmodels-0.8.2}/tests/test_separation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: firthmodels
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
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
|
|
@@ -0,0 +1,191 @@
|
|
|
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.5x
|
|
11
|
+
faster than logistf** (k=50) and **158x faster than
|
|
12
|
+
coxphf** (k=30). Without Numba, the pure NumPy backend is 7.3x
|
|
13
|
+
faster than logistf and 23.4x faster than coxphf.
|
|
14
|
+
|
|
15
|
+
| Workload | Baseline | Speedup at k=5 | Speedup at largest k |
|
|
16
|
+
|---|---|--:|--:|
|
|
17
|
+
| Logistic: fit + Wald | next-fastest package | 3.7x | 5.2x |
|
|
18
|
+
| Logistic: fit + LRT + profile CI | logistf | 2.5x | 10.5x |
|
|
19
|
+
| Cox: fit + Wald | coxphf | 11.2x | 130x |
|
|
20
|
+
| Cox: fit + LRT + profile CI | coxphf | 8.1x | 158x |
|
|
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.53** | 0.92 | <ins>2.0</ins> | 4.1 | 4.1 | 3.7x | 2.1x |
|
|
50
|
+
| 10 | **0.74** | 1.2 | <ins>3.5</ins> | 4.8 | 4.8 | 4.7x | 2.8x |
|
|
51
|
+
| 15 | **0.77** | 1.3 | <ins>5.4</ins> | 5.6 | 5.5 | 7.0x | 4.2x |
|
|
52
|
+
| 20 | **1.1** | 1.6 | 8.0 | 6.4 | <ins>6.4</ins> | 6.1x | 4.0x |
|
|
53
|
+
| 25 | **1.5** | 2.3 | 11.7 | 8.1 | <ins>8.0</ins> | 5.4x | 3.6x |
|
|
54
|
+
| 30 | **1.7** | 2.5 | 15.1 | <ins>8.8</ins> | 8.9 | 5.3x | 3.5x |
|
|
55
|
+
| 35 | **1.8** | 2.8 | 20.3 | 10.5 | <ins>10.5</ins> | 5.9x | 3.7x |
|
|
56
|
+
| 40 | **2.1** | 3.2 | 25.3 | 11.9 | <ins>11.8</ins> | 5.5x | 3.7x |
|
|
57
|
+
| 45 | **2.8** | 3.8 | 33.4 | 13.3 | <ins>13.2</ins> | 4.8x | 3.5x |
|
|
58
|
+
| 50 | **3.3** | 4.4 | 41.5 | 17.3 | <ins>17.1</ins> | 5.2x | 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.1% 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.8** | 11.3 | <ins>12.0</ins> | 2.5x | 1.1x |
|
|
72
|
+
| 10 | **11.9** | 25.3 | <ins>42.4</ins> | 3.5x | 1.7x |
|
|
73
|
+
| 15 | **19.1** | 40.3 | <ins>102.8</ins> | 5.4x | 2.6x |
|
|
74
|
+
| 20 | **36.4** | 64.6 | <ins>209.1</ins> | 5.7x | 3.2x |
|
|
75
|
+
| 25 | **68.5** | 120.3 | <ins>418.9</ins> | 6.1x | 3.5x |
|
|
76
|
+
| 30 | **95.4** | 165.7 | <ins>629.5</ins> | 6.6x | 3.8x |
|
|
77
|
+
| 35 | **115.4** | 208.8 | <ins>1,054</ins> | 9.1x | 5.0x |
|
|
78
|
+
| 40 | **162.8** | 269.2 | <ins>1,531</ins> | 9.4x | 5.7x |
|
|
79
|
+
| 45 | **222.1** | 334.8 | <ins>2,226</ins> | 10.0x | 6.6x |
|
|
80
|
+
| 50 | **299.2** | 429.5 | <ins>3,128</ins> | 10.5x | 7.3x |
|
|
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.30** | 1.5 | <ins>3.4</ins> | 11.2x | 2.2x |
|
|
100
|
+
| 10 | **0.41** | 2.3 | <ins>9.0</ins> | 22.0x | 4.0x |
|
|
101
|
+
| 15 | **0.57** | 3.3 | <ins>24.9</ins> | 43.4x | 7.5x |
|
|
102
|
+
| 20 | **0.86** | 5.7 | <ins>56.1</ins> | 65.4x | 9.8x |
|
|
103
|
+
| 25 | **1.2** | 7.9 | <ins>122.2</ins> | 98.7x | 15.6x |
|
|
104
|
+
| 30 | **1.6** | 10.5 | <ins>211.6</ins> | 130x | 20.2x |
|
|
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.2** | 12.8 | <ins>17.9</ins> | 8.1x | 1.4x |
|
|
114
|
+
| 10 | **7.1** | 45.6 | <ins>163.0</ins> | 22.9x | 3.6x |
|
|
115
|
+
| 15 | **17.4** | 114.1 | <ins>928.5</ins> | 53.4x | 8.1x |
|
|
116
|
+
| 20 | **30.8** | 227.2 | <ins>2,547</ins> | 82.6x | 11.2x |
|
|
117
|
+
| 25 | **59.4** | 404.1 | <ins>7,418</ins> | 125x | 18.4x |
|
|
118
|
+
| 30 | **98.1** | 660.6 | <ins>15,467</ins> | 158x | 23.4x |
|
|
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.4e-13 |
|
|
137
|
+
| Logistic full vs logistf | LRT p-values | 4.9e-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.6e-08 |
|
|
143
|
+
| Cox numba vs numpy | Coefficients | 2.8e-16 |
|
|
144
|
+
|
|
145
|
+
Note: the benchmark run(s) recorded firthmodels 0.8.1, while the currently installed version is 0.8.2. Timings describe the recorded version.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Environment
|
|
150
|
+
|
|
151
|
+
Collected at report-generation time on the benchmark machine.
|
|
152
|
+
|
|
153
|
+
| Component | Version |
|
|
154
|
+
|---|---|
|
|
155
|
+
| OS | CachyOS |
|
|
156
|
+
| CPU | AMD Ryzen 5 5600X 6-Core Processor |
|
|
157
|
+
| Python | 3.12.13 |
|
|
158
|
+
| firthmodels | 0.8.2 |
|
|
159
|
+
| NumPy / SciPy / Numba | 2.3.5 / 1.16.3 / 0.64.0 |
|
|
160
|
+
| NumPy BLAS | /usr/lib/libopenblas.so.0.3 (openblas 0.3.34, runtime) |
|
|
161
|
+
| R | 4.6.1 |
|
|
162
|
+
| logistf / brglm2 / coxphf | 1.26.1 / 1.1.0 / 1.13.4 |
|
|
163
|
+
| R BLAS | /usr/lib/libopenblas.so.0.3 |
|
|
164
|
+
|
|
165
|
+
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.
|
|
166
|
+
R packages are compiled from source with `-march=x86-64-v3 -mtune=haswell -O3 -flto=auto`,
|
|
167
|
+
so the R timings are not slowed by a conservative build.
|
|
168
|
+
|
|
169
|
+
<details>
|
|
170
|
+
<summary>Full R package compile flags (current configuration)</summary>
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
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
|
|
174
|
+
FCFLAGS: -O3 -march=x86-64-v3 -mtune=haswell
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
</details>
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Reproducing these results
|
|
182
|
+
|
|
183
|
+
Requires R with the logistf, brglm2, coxphf, survival, microbenchmark, and
|
|
184
|
+
jsonlite packages installed.
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Run both benchmarks (writes results JSON files holding the timings, R
|
|
188
|
+
# reference values, and run metadata), then regenerate the plots and this
|
|
189
|
+
# README
|
|
190
|
+
benchmarks/run_benchmarks.sh
|
|
191
|
+
```
|
|
@@ -14,7 +14,7 @@ import platform as plat
|
|
|
14
14
|
import subprocess
|
|
15
15
|
import sys
|
|
16
16
|
import tempfile
|
|
17
|
-
import
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from typing import Literal
|
|
20
20
|
|
|
@@ -29,7 +29,7 @@ from firthmodels import FirthCoxPH
|
|
|
29
29
|
N_SAMPLES = 500
|
|
30
30
|
EVENT_RATE = 0.20
|
|
31
31
|
K_VALUES = list(range(5, 35, 5))
|
|
32
|
-
N_RUNS =
|
|
32
|
+
N_RUNS = 10
|
|
33
33
|
BASE_SEED = 0
|
|
34
34
|
|
|
35
35
|
# Solver parameters
|
|
@@ -43,8 +43,8 @@ COEF_TOL = 1e-6
|
|
|
43
43
|
CI_TOL = 1e-6
|
|
44
44
|
PVAL_TOL = 1e-6
|
|
45
45
|
|
|
46
|
-
# Reduce coxphf benchmark runs for large k
|
|
47
|
-
COXPHF_REDUCE_AFTER =
|
|
46
|
+
# Reduce coxphf benchmark runs for large k (0 disables reduction)
|
|
47
|
+
COXPHF_REDUCE_AFTER = 0
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
# -----------------------------------------------------------------------------
|
|
@@ -279,26 +279,41 @@ def check_agreement(
|
|
|
279
279
|
r_ci: np.ndarray | None = None,
|
|
280
280
|
py_pval: np.ndarray | None = None,
|
|
281
281
|
r_pval: np.ndarray | None = None,
|
|
282
|
+
deviations: dict[str, dict[str, float]] | None = None,
|
|
282
283
|
) -> None:
|
|
283
|
-
"""Check that Python and R results agree within tolerance.
|
|
284
|
-
|
|
284
|
+
"""Check that Python and R results agree within tolerance.
|
|
285
|
+
|
|
286
|
+
If a deviations dict is provided, the observed max absolute differences
|
|
287
|
+
are merged into it under label, keyed by quantity, taking the max over
|
|
288
|
+
successive calls (i.e. over k). Recorded before any tolerance failure is
|
|
289
|
+
raised.
|
|
290
|
+
"""
|
|
291
|
+
observed: dict[str, float] = {}
|
|
292
|
+
|
|
285
293
|
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}")
|
|
294
|
+
observed["coef"] = float(coef_diff)
|
|
288
295
|
|
|
289
|
-
# Check CIs if provided
|
|
290
296
|
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}")
|
|
297
|
+
observed["ci"] = float(np.max(np.abs(py_ci - r_ci)))
|
|
294
298
|
|
|
295
|
-
# Check p-values if provided
|
|
296
299
|
if py_pval is not None and r_pval is not None:
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
observed["pval"] = float(np.max(np.abs(py_pval - r_pval)))
|
|
301
|
+
|
|
302
|
+
if deviations is not None:
|
|
303
|
+
bucket = deviations.setdefault(label, {})
|
|
304
|
+
for quantity, value in observed.items():
|
|
305
|
+
bucket[quantity] = max(bucket.get(quantity, 0.0), value)
|
|
306
|
+
|
|
307
|
+
if coef_diff > COEF_TOL:
|
|
308
|
+
raise AssertionError(f"k={k} ({label}): coef diff {coef_diff:.2e} > {COEF_TOL}")
|
|
309
|
+
if observed.get("ci", 0.0) > CI_TOL:
|
|
310
|
+
raise AssertionError(
|
|
311
|
+
f"k={k} ({label}): CI diff {observed['ci']:.2e} > {CI_TOL}"
|
|
312
|
+
)
|
|
313
|
+
if observed.get("pval", 0.0) > PVAL_TOL:
|
|
314
|
+
raise AssertionError(
|
|
315
|
+
f"k={k} ({label}): p-value diff {observed['pval']:.2e} > {PVAL_TOL}"
|
|
316
|
+
)
|
|
302
317
|
|
|
303
318
|
|
|
304
319
|
# -----------------------------------------------------------------------------
|
|
@@ -426,17 +441,17 @@ def compute_min(times: np.ndarray) -> float:
|
|
|
426
441
|
# Saved results loading
|
|
427
442
|
# -----------------------------------------------------------------------------
|
|
428
443
|
def load_saved_results(
|
|
429
|
-
|
|
444
|
+
saved_path: str,
|
|
430
445
|
k_values: list[int],
|
|
431
446
|
load_firthmodels: bool = False,
|
|
432
447
|
load_coxphf: bool = True,
|
|
433
448
|
) -> tuple[pd.DataFrame, dict[int, dict] | None]:
|
|
434
|
-
"""Load results from a saved
|
|
449
|
+
"""Load results from a saved results JSON file.
|
|
435
450
|
|
|
436
451
|
Parameters
|
|
437
452
|
----------
|
|
438
|
-
|
|
439
|
-
Path to
|
|
453
|
+
saved_path : str
|
|
454
|
+
Path to results JSON with previous results.
|
|
440
455
|
k_values : list of int
|
|
441
456
|
Which k values to load.
|
|
442
457
|
load_firthmodels : bool
|
|
@@ -451,7 +466,8 @@ def load_saved_results(
|
|
|
451
466
|
coxphf_results : dict or None
|
|
452
467
|
coxphf results if load_coxphf=True, else None.
|
|
453
468
|
"""
|
|
454
|
-
|
|
469
|
+
with open(saved_path) as f:
|
|
470
|
+
saved_df = pd.DataFrame(json.load(f)["results"])
|
|
455
471
|
|
|
456
472
|
# Determine required columns based on what we're loading
|
|
457
473
|
required_cols = {"k", "n"}
|
|
@@ -473,20 +489,21 @@ def load_saved_results(
|
|
|
473
489
|
|
|
474
490
|
missing_cols = required_cols - set(saved_df.columns)
|
|
475
491
|
if missing_cols:
|
|
476
|
-
raise ValueError(f"Saved
|
|
492
|
+
raise ValueError(f"Saved results missing columns: {missing_cols}")
|
|
477
493
|
|
|
478
494
|
saved_n = int(saved_df["n"].iloc[0])
|
|
479
495
|
if saved_n != N_SAMPLES:
|
|
480
496
|
raise ValueError(
|
|
481
|
-
f"Saved
|
|
497
|
+
f"Saved results were created with n={saved_n}, "
|
|
498
|
+
f"but current N_SAMPLES={N_SAMPLES}."
|
|
482
499
|
)
|
|
483
500
|
|
|
484
501
|
saved_k = set(saved_df["k"].tolist())
|
|
485
502
|
missing = set(k_values) - saved_k
|
|
486
503
|
if missing:
|
|
487
|
-
raise ValueError(f"k values {missing} not in saved
|
|
504
|
+
raise ValueError(f"k values {missing} not in saved results")
|
|
488
505
|
|
|
489
|
-
#
|
|
506
|
+
# Reference values are copied verbatim from saved_df rows, so no parsing
|
|
490
507
|
return saved_df, None
|
|
491
508
|
|
|
492
509
|
|
|
@@ -501,8 +518,13 @@ def run_benchmarks(
|
|
|
501
518
|
saved: str | None = None,
|
|
502
519
|
run_firthmodels: bool = True,
|
|
503
520
|
run_coxphf: bool = True,
|
|
504
|
-
) -> tuple[pd.DataFrame, dict[str, str]]:
|
|
505
|
-
"""Run all benchmarks and return results as DataFrame.
|
|
521
|
+
) -> tuple[pd.DataFrame, dict[str, str], dict]:
|
|
522
|
+
"""Run all benchmarks and return results as DataFrame.
|
|
523
|
+
|
|
524
|
+
Also returns version_info and run_meta (run counts, verification status
|
|
525
|
+
and observed deviations, versions; saved into the results JSON by
|
|
526
|
+
main()).
|
|
527
|
+
"""
|
|
506
528
|
version_info = get_python_version_info()
|
|
507
529
|
|
|
508
530
|
# Load saved results for libraries we're not running
|
|
@@ -580,7 +602,7 @@ def run_benchmarks(
|
|
|
580
602
|
if coxphf_results is None:
|
|
581
603
|
coxphf_results = {}
|
|
582
604
|
if run_coxphf:
|
|
583
|
-
if coxphf_reduce_after
|
|
605
|
+
if coxphf_reduce_after:
|
|
584
606
|
k_low = [k for k in k_values if k <= coxphf_reduce_after]
|
|
585
607
|
k_high = [k for k in k_values if k > coxphf_reduce_after]
|
|
586
608
|
|
|
@@ -612,6 +634,7 @@ def run_benchmarks(
|
|
|
612
634
|
"Verifying results and computing statistics...", file=sys.stderr, flush=True
|
|
613
635
|
)
|
|
614
636
|
results = []
|
|
637
|
+
deviations: dict[str, dict[str, float]] = {}
|
|
615
638
|
|
|
616
639
|
for k in k_values:
|
|
617
640
|
X, surv_time, event = make_benchmark_data(
|
|
@@ -622,21 +645,32 @@ def run_benchmarks(
|
|
|
622
645
|
if run_firthmodels and run_coxphf and not skip_verification:
|
|
623
646
|
py = py_results["numba"][k]
|
|
624
647
|
r = coxphf_results[k]
|
|
625
|
-
check_agreement(
|
|
648
|
+
check_agreement(
|
|
649
|
+
k,
|
|
650
|
+
py["fit_coef"],
|
|
651
|
+
r["fit_coef"],
|
|
652
|
+
"fit vs coxphf",
|
|
653
|
+
deviations=deviations,
|
|
654
|
+
)
|
|
626
655
|
check_agreement(
|
|
627
656
|
k,
|
|
628
657
|
py["full_coef"],
|
|
629
658
|
r["full_coef"],
|
|
630
|
-
"full",
|
|
659
|
+
"full vs coxphf",
|
|
631
660
|
py["full_ci"],
|
|
632
661
|
r["full_ci"],
|
|
633
662
|
py["full_pval"],
|
|
634
663
|
r["full_pval"],
|
|
664
|
+
deviations=deviations,
|
|
635
665
|
)
|
|
636
666
|
# Also check numba vs numpy
|
|
637
667
|
py_numpy = py_results["numpy"][k]
|
|
638
668
|
check_agreement(
|
|
639
|
-
k,
|
|
669
|
+
k,
|
|
670
|
+
py["fit_coef"],
|
|
671
|
+
py_numpy["fit_coef"],
|
|
672
|
+
"numba vs numpy",
|
|
673
|
+
deviations=deviations,
|
|
640
674
|
)
|
|
641
675
|
|
|
642
676
|
# Collect timing results
|
|
@@ -673,10 +707,10 @@ def run_benchmarks(
|
|
|
673
707
|
row["coxphf_fit_ms"] = compute_min(r["fit_times"]) * 1000
|
|
674
708
|
row["coxphf_full_ms"] = compute_min(r["full_times"]) * 1000
|
|
675
709
|
# Store for reproducibility
|
|
676
|
-
row["coxphf_fit_coef"] =
|
|
677
|
-
row["coxphf_full_coef"] =
|
|
678
|
-
row["coxphf_full_ci"] =
|
|
679
|
-
row["coxphf_full_pval"] =
|
|
710
|
+
row["coxphf_fit_coef"] = np.asarray(r["fit_coef"]).tolist()
|
|
711
|
+
row["coxphf_full_coef"] = np.asarray(r["full_coef"]).tolist()
|
|
712
|
+
row["coxphf_full_ci"] = np.asarray(r["full_ci"]).tolist()
|
|
713
|
+
row["coxphf_full_pval"] = np.asarray(r["full_pval"]).tolist()
|
|
680
714
|
else:
|
|
681
715
|
assert saved_row is not None
|
|
682
716
|
row["coxphf_fit_ms"] = saved_row["coxphf_fit_ms"]
|
|
@@ -688,7 +722,39 @@ def run_benchmarks(
|
|
|
688
722
|
|
|
689
723
|
results.append(row)
|
|
690
724
|
|
|
691
|
-
|
|
725
|
+
reduced = n_runs if n_runs <= 3 else max(3, n_runs // 3)
|
|
726
|
+
run_meta = {
|
|
727
|
+
"schema_version": 1,
|
|
728
|
+
"model": "cox",
|
|
729
|
+
"created_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
730
|
+
"n_runs": n_runs,
|
|
731
|
+
"r_reduced_runs": (
|
|
732
|
+
{
|
|
733
|
+
"libraries": ["coxphf"],
|
|
734
|
+
"for_k_above": coxphf_reduce_after,
|
|
735
|
+
"n_runs": reduced,
|
|
736
|
+
}
|
|
737
|
+
if coxphf_reduce_after
|
|
738
|
+
else None
|
|
739
|
+
),
|
|
740
|
+
"k_values": list(k_values),
|
|
741
|
+
"libraries_run": [
|
|
742
|
+
name
|
|
743
|
+
for name, flag in [
|
|
744
|
+
("firthmodels", run_firthmodels),
|
|
745
|
+
("coxphf", run_coxphf),
|
|
746
|
+
]
|
|
747
|
+
if flag
|
|
748
|
+
],
|
|
749
|
+
"loaded_from_saved": saved,
|
|
750
|
+
"verification": {
|
|
751
|
+
"performed": bool(deviations),
|
|
752
|
+
"tolerances": {"coef": COEF_TOL, "ci": CI_TOL, "pval": PVAL_TOL},
|
|
753
|
+
"max_abs_deviations": deviations,
|
|
754
|
+
},
|
|
755
|
+
"versions": version_info,
|
|
756
|
+
}
|
|
757
|
+
return pd.DataFrame(results), version_info, run_meta
|
|
692
758
|
|
|
693
759
|
|
|
694
760
|
def print_table(df: pd.DataFrame) -> None:
|
|
@@ -743,14 +809,14 @@ def main():
|
|
|
743
809
|
"--out",
|
|
744
810
|
type=str,
|
|
745
811
|
default=None,
|
|
746
|
-
help="Save results to
|
|
812
|
+
help="Save results (timings, R references, run metadata) to JSON file",
|
|
747
813
|
)
|
|
748
814
|
parser.add_argument(
|
|
749
815
|
"--saved",
|
|
750
816
|
type=str,
|
|
751
817
|
default=None,
|
|
752
|
-
metavar="
|
|
753
|
-
help="Load non-selected libraries from this
|
|
818
|
+
metavar="JSON",
|
|
819
|
+
help="Load non-selected libraries from this results JSON.",
|
|
754
820
|
)
|
|
755
821
|
parser.add_argument(
|
|
756
822
|
"--firthmodels",
|
|
@@ -807,7 +873,7 @@ def main():
|
|
|
807
873
|
)
|
|
808
874
|
|
|
809
875
|
# Run benchmarks
|
|
810
|
-
df, version_info = run_benchmarks(
|
|
876
|
+
df, version_info, run_meta = run_benchmarks(
|
|
811
877
|
k_values=k_values,
|
|
812
878
|
n_runs=args.n_runs,
|
|
813
879
|
skip_verification=args.skip_verification,
|
|
@@ -821,7 +887,11 @@ def main():
|
|
|
821
887
|
print_table(df)
|
|
822
888
|
|
|
823
889
|
if args.out:
|
|
824
|
-
|
|
890
|
+
with open(args.out, "w") as f:
|
|
891
|
+
json.dump(
|
|
892
|
+
{"meta": run_meta, "results": df.to_dict(orient="records")}, f, indent=2
|
|
893
|
+
)
|
|
894
|
+
f.write("\n")
|
|
825
895
|
print(f"\nResults saved to {args.out}", file=sys.stderr)
|
|
826
896
|
|
|
827
897
|
|