PyMetaAnalysis 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meta_analyze/__init__.py +62 -0
- meta_analyze/_version.py +3 -0
- meta_analyze/api.py +330 -0
- meta_analyze/binary_api.py +539 -0
- meta_analyze/config.py +34 -0
- meta_analyze/continuous_api.py +353 -0
- meta_analyze/data.py +181 -0
- meta_analyze/effect_sizes/__init__.py +17 -0
- meta_analyze/effect_sizes/binary.py +412 -0
- meta_analyze/effect_sizes/continuous.py +271 -0
- meta_analyze/estimators/__init__.py +14 -0
- meta_analyze/estimators/inverse_variance.py +169 -0
- meta_analyze/estimators/mantel_haenszel.py +101 -0
- meta_analyze/estimators/tau2.py +182 -0
- meta_analyze/exceptions.py +21 -0
- meta_analyze/heterogeneity.py +99 -0
- meta_analyze/plotting/__init__.py +7 -0
- meta_analyze/plotting/_utils.py +67 -0
- meta_analyze/plotting/forest.py +193 -0
- meta_analyze/plotting/funnel.py +168 -0
- meta_analyze/plotting/subgroup_forest.py +284 -0
- meta_analyze/provenance.py +185 -0
- meta_analyze/py.typed +1 -0
- meta_analyze/reporting.py +433 -0
- meta_analyze/results.py +519 -0
- meta_analyze/sensitivity.py +546 -0
- meta_analyze/subgroups.py +165 -0
- pymetaanalysis-0.1.0.dist-info/METADATA +218 -0
- pymetaanalysis-0.1.0.dist-info/RECORD +31 -0
- pymetaanalysis-0.1.0.dist-info/WHEEL +4 -0
- pymetaanalysis-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
"""High-level API for two-group binary outcome meta-analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
from typing import overload
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
from .api import (
|
|
12
|
+
_normalize_ci_method,
|
|
13
|
+
_normalize_model,
|
|
14
|
+
_validate_analysis_controls,
|
|
15
|
+
)
|
|
16
|
+
from .config import MethodConfig
|
|
17
|
+
from .data import ColumnOrArray, MissingPolicy
|
|
18
|
+
from .effect_sizes.binary import (
|
|
19
|
+
adjusted_tables,
|
|
20
|
+
calculate_binary_effects,
|
|
21
|
+
normalize_binary_studies,
|
|
22
|
+
normalize_correction_scope,
|
|
23
|
+
normalize_rd_zero_variance,
|
|
24
|
+
validate_correction,
|
|
25
|
+
)
|
|
26
|
+
from .estimators import fit_inverse_variance, fit_mantel_haenszel
|
|
27
|
+
from .exceptions import UnsupportedMethodError
|
|
28
|
+
from .heterogeneity import (
|
|
29
|
+
classical_heterogeneity,
|
|
30
|
+
heterogeneity_at_estimate,
|
|
31
|
+
tau2_inconsistency,
|
|
32
|
+
)
|
|
33
|
+
from .provenance import (
|
|
34
|
+
TransformationRecord,
|
|
35
|
+
add_input_field,
|
|
36
|
+
build_analysis_provenance,
|
|
37
|
+
)
|
|
38
|
+
from .results import (
|
|
39
|
+
FitDiagnostics,
|
|
40
|
+
HeterogeneityResult,
|
|
41
|
+
MetaAnalysisResult,
|
|
42
|
+
SubgroupMetaAnalysisResult,
|
|
43
|
+
)
|
|
44
|
+
from .subgroups import fit_subgroup_analysis
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _normalize_pooling_method(method: str) -> str:
|
|
48
|
+
normalized = method.lower().replace("-", "_")
|
|
49
|
+
if normalized in {"iv", "inverse", "inverse_variance"}:
|
|
50
|
+
return "inverse_variance"
|
|
51
|
+
if normalized in {"mh", "mantel_haenszel"}:
|
|
52
|
+
return "mantel_haenszel"
|
|
53
|
+
raise UnsupportedMethodError(
|
|
54
|
+
"method must be 'MH'/'mantel_haenszel' or 'IV'/'inverse_variance'."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _fit_meta_binary_single(
|
|
59
|
+
data: pd.DataFrame | None = None,
|
|
60
|
+
*,
|
|
61
|
+
event_treat: ColumnOrArray,
|
|
62
|
+
n_treat: ColumnOrArray,
|
|
63
|
+
event_control: ColumnOrArray,
|
|
64
|
+
n_control: ColumnOrArray,
|
|
65
|
+
study: ColumnOrArray | None = None,
|
|
66
|
+
measure: str = "RR",
|
|
67
|
+
method: str = "MH",
|
|
68
|
+
model: str = "common",
|
|
69
|
+
tau2_method: str = "REML",
|
|
70
|
+
ci_method: str = "normal",
|
|
71
|
+
confidence_level: float = 0.95,
|
|
72
|
+
continuity_correction: float = 0.5,
|
|
73
|
+
correction_scope: str = "only_zero_studies",
|
|
74
|
+
rd_zero_variance: str = "correct",
|
|
75
|
+
mh_continuity_correction: float | None = None,
|
|
76
|
+
mh_correction_scope: str = "only_zero_studies",
|
|
77
|
+
missing: MissingPolicy = "raise",
|
|
78
|
+
atol: float = 1e-10,
|
|
79
|
+
max_iter: int = 1000,
|
|
80
|
+
) -> MetaAnalysisResult:
|
|
81
|
+
"""Pool OR, RR, or RD from two-group binary study counts.
|
|
82
|
+
|
|
83
|
+
Mantel-Haenszel currently supports common-effect OR and RR. It uses raw
|
|
84
|
+
tables by default; set ``mh_continuity_correction`` explicitly when the
|
|
85
|
+
exact pooled estimator is undefined. Study-level effects use the separate
|
|
86
|
+
``continuity_correction`` setting for display and heterogeneity statistics.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
confidence_level, atol, max_iter = _validate_analysis_controls(
|
|
90
|
+
confidence_level=confidence_level,
|
|
91
|
+
atol=atol,
|
|
92
|
+
max_iter=max_iter,
|
|
93
|
+
)
|
|
94
|
+
normalized_model = _normalize_model(model)
|
|
95
|
+
normalized_method = _normalize_pooling_method(method)
|
|
96
|
+
normalized_ci = _normalize_ci_method(ci_method)
|
|
97
|
+
normalized_tau2 = tau2_method.upper().replace("-", "_")
|
|
98
|
+
normalized_measure = measure.upper()
|
|
99
|
+
correction = validate_correction(
|
|
100
|
+
continuity_correction, name="continuity_correction"
|
|
101
|
+
)
|
|
102
|
+
mh_correction = validate_correction(
|
|
103
|
+
mh_continuity_correction, name="mh_continuity_correction"
|
|
104
|
+
)
|
|
105
|
+
scope = normalize_correction_scope(correction_scope)
|
|
106
|
+
rd_policy = normalize_rd_zero_variance(rd_zero_variance)
|
|
107
|
+
mh_scope = normalize_correction_scope(mh_correction_scope)
|
|
108
|
+
|
|
109
|
+
if normalized_measure != "RD" and rd_policy != "correct":
|
|
110
|
+
raise UnsupportedMethodError(
|
|
111
|
+
"rd_zero_variance is only configurable when measure='RD'."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if normalized_method == "mantel_haenszel":
|
|
115
|
+
if normalized_model != "common":
|
|
116
|
+
raise UnsupportedMethodError(
|
|
117
|
+
"Mantel-Haenszel is currently implemented only for model='common'; "
|
|
118
|
+
"use method='IV' for random-effects models."
|
|
119
|
+
)
|
|
120
|
+
if normalized_measure not in {"OR", "RR"}:
|
|
121
|
+
raise UnsupportedMethodError(
|
|
122
|
+
"Mantel-Haenszel currently supports measure='OR' or measure='RR'; "
|
|
123
|
+
"use method='IV' for risk differences."
|
|
124
|
+
)
|
|
125
|
+
if normalized_ci != "normal":
|
|
126
|
+
raise UnsupportedMethodError(
|
|
127
|
+
"Mantel-Haenszel currently supports only ci_method='normal'."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
studies = normalize_binary_studies(
|
|
131
|
+
data=data,
|
|
132
|
+
event_treat=event_treat,
|
|
133
|
+
n_treat=n_treat,
|
|
134
|
+
event_control=event_control,
|
|
135
|
+
n_control=n_control,
|
|
136
|
+
study=study,
|
|
137
|
+
missing=missing,
|
|
138
|
+
)
|
|
139
|
+
effects = calculate_binary_effects(
|
|
140
|
+
studies,
|
|
141
|
+
measure=normalized_measure,
|
|
142
|
+
continuity_correction=correction,
|
|
143
|
+
correction_scope=scope,
|
|
144
|
+
rd_zero_variance=rd_policy,
|
|
145
|
+
)
|
|
146
|
+
included = effects.studies.included
|
|
147
|
+
included_effect = effects.included_effect
|
|
148
|
+
included_variance = effects.included_variance
|
|
149
|
+
|
|
150
|
+
warnings: list[str] = []
|
|
151
|
+
if normalized_method == "inverse_variance":
|
|
152
|
+
fit = fit_inverse_variance(
|
|
153
|
+
included_effect,
|
|
154
|
+
included_variance,
|
|
155
|
+
model=normalized_model,
|
|
156
|
+
tau2_method=normalized_tau2,
|
|
157
|
+
ci_method=normalized_ci,
|
|
158
|
+
confidence_level=confidence_level,
|
|
159
|
+
atol=atol,
|
|
160
|
+
max_iter=max_iter,
|
|
161
|
+
)
|
|
162
|
+
estimate = fit.estimate
|
|
163
|
+
standard_error = fit.standard_error
|
|
164
|
+
ci_low = fit.ci_low
|
|
165
|
+
ci_high = fit.ci_high
|
|
166
|
+
prediction_interval = fit.prediction_interval
|
|
167
|
+
weights = fit.weights
|
|
168
|
+
normalized_weights = fit.normalized_weights
|
|
169
|
+
tau2 = 0.0 if fit.tau2 is None else fit.tau2.value
|
|
170
|
+
diagnostics = FitDiagnostics(
|
|
171
|
+
converged=True if fit.tau2 is None else fit.tau2.converged,
|
|
172
|
+
iterations=0 if fit.tau2 is None else fit.tau2.iterations,
|
|
173
|
+
tau2_at_boundary=None if fit.tau2 is None else fit.tau2.boundary,
|
|
174
|
+
)
|
|
175
|
+
q_values = classical_heterogeneity(included_effect, included_variance)
|
|
176
|
+
mh_corrected = np.zeros(len(included), dtype=bool)
|
|
177
|
+
warnings.extend(fit.warnings)
|
|
178
|
+
else:
|
|
179
|
+
a, b, c, d, mh_corrected = adjusted_tables(
|
|
180
|
+
effects.studies,
|
|
181
|
+
correction=mh_correction,
|
|
182
|
+
scope=mh_scope,
|
|
183
|
+
)
|
|
184
|
+
mh_fit = fit_mantel_haenszel(
|
|
185
|
+
a[included],
|
|
186
|
+
b[included],
|
|
187
|
+
c[included],
|
|
188
|
+
d[included],
|
|
189
|
+
measure=normalized_measure,
|
|
190
|
+
confidence_level=confidence_level,
|
|
191
|
+
)
|
|
192
|
+
estimate = mh_fit.estimate
|
|
193
|
+
standard_error = mh_fit.standard_error
|
|
194
|
+
ci_low = mh_fit.ci_low
|
|
195
|
+
ci_high = mh_fit.ci_high
|
|
196
|
+
prediction_interval = None
|
|
197
|
+
weights = mh_fit.weights
|
|
198
|
+
normalized_weights = mh_fit.normalized_weights
|
|
199
|
+
tau2 = 0.0
|
|
200
|
+
diagnostics = FitDiagnostics(True, 0, None)
|
|
201
|
+
q_values = heterogeneity_at_estimate(
|
|
202
|
+
included_effect, included_variance, estimate
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
q, q_df, q_pvalue, i2, h2 = q_values
|
|
206
|
+
i2_method = "q_based"
|
|
207
|
+
if normalized_model == "random":
|
|
208
|
+
i2, h2 = tau2_inconsistency(included_variance, tau2)
|
|
209
|
+
i2_method = "tau2_typical_variance"
|
|
210
|
+
heterogeneity = HeterogeneityResult(q, q_df, q_pvalue, i2, h2, i2_method)
|
|
211
|
+
row_count = len(included)
|
|
212
|
+
raw_weights = np.full(row_count, np.nan, dtype=np.float64)
|
|
213
|
+
result_weights = np.full(row_count, np.nan, dtype=np.float64)
|
|
214
|
+
raw_weights[included] = weights
|
|
215
|
+
result_weights[included] = normalized_weights
|
|
216
|
+
effect_display = effects.effect.copy()
|
|
217
|
+
if effects.display_scale == "exp":
|
|
218
|
+
effect_display[included] = np.exp(effect_display[included])
|
|
219
|
+
|
|
220
|
+
study_results = pd.DataFrame(
|
|
221
|
+
{
|
|
222
|
+
"row_id": effects.studies.row_id,
|
|
223
|
+
"study": effects.studies.study,
|
|
224
|
+
"event_treat": effects.studies.event_treat,
|
|
225
|
+
"n_treat": effects.studies.n_treat,
|
|
226
|
+
"event_control": effects.studies.event_control,
|
|
227
|
+
"n_control": effects.studies.n_control,
|
|
228
|
+
"effect": effects.effect,
|
|
229
|
+
"effect_display": effect_display,
|
|
230
|
+
"variance": effects.variance,
|
|
231
|
+
"standard_error": np.sqrt(effects.variance),
|
|
232
|
+
"included": included,
|
|
233
|
+
"exclusion_reason": pd.Series(
|
|
234
|
+
effects.studies.exclusion_reason, dtype=object, copy=True
|
|
235
|
+
),
|
|
236
|
+
"continuity_corrected": effects.corrected,
|
|
237
|
+
"rd_zero_variance": effects.rd_zero_variance,
|
|
238
|
+
"mh_continuity_corrected": mh_corrected,
|
|
239
|
+
"weight": raw_weights,
|
|
240
|
+
"normalized_weight": result_weights,
|
|
241
|
+
}
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
excluded_count = int(np.count_nonzero(~included))
|
|
245
|
+
corrected_count = int(np.count_nonzero(effects.corrected))
|
|
246
|
+
mh_corrected_count = int(np.count_nonzero(mh_corrected))
|
|
247
|
+
if excluded_count:
|
|
248
|
+
warnings.append(
|
|
249
|
+
f"Excluded {excluded_count} non-informative or missing study row(s)."
|
|
250
|
+
)
|
|
251
|
+
if corrected_count:
|
|
252
|
+
warnings.append(
|
|
253
|
+
f"Applied continuity_correction={correction:g} to "
|
|
254
|
+
f"{corrected_count} study table(s) for individual effects."
|
|
255
|
+
)
|
|
256
|
+
if mh_corrected_count:
|
|
257
|
+
warnings.append(
|
|
258
|
+
f"Applied mh_continuity_correction={mh_correction:g} to "
|
|
259
|
+
f"{mh_corrected_count} study table(s) for MH pooling."
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
method_config = MethodConfig(
|
|
263
|
+
model=normalized_model,
|
|
264
|
+
pooling_method=normalized_method,
|
|
265
|
+
tau2_method=(
|
|
266
|
+
normalized_tau2
|
|
267
|
+
if normalized_model == "random" and normalized_method == "inverse_variance"
|
|
268
|
+
else None
|
|
269
|
+
),
|
|
270
|
+
ci_method=normalized_ci,
|
|
271
|
+
confidence_level=confidence_level,
|
|
272
|
+
prediction_interval_method=(
|
|
273
|
+
"HTS"
|
|
274
|
+
if normalized_model == "random" and normalized_method == "inverse_variance"
|
|
275
|
+
else None
|
|
276
|
+
),
|
|
277
|
+
missing=missing,
|
|
278
|
+
atol=atol,
|
|
279
|
+
max_iter=max_iter,
|
|
280
|
+
options=(
|
|
281
|
+
("continuity_correction", correction),
|
|
282
|
+
("correction_scope", scope),
|
|
283
|
+
*((("rd_zero_variance", rd_policy),) if normalized_measure == "RD" else ()),
|
|
284
|
+
("mh_continuity_correction", mh_correction),
|
|
285
|
+
("mh_correction_scope", mh_scope),
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
transformations = [
|
|
289
|
+
TransformationRecord(
|
|
290
|
+
name="binary_effect_size",
|
|
291
|
+
parameters=(
|
|
292
|
+
("measure", normalized_measure),
|
|
293
|
+
("model_scale", effects.effect_scale),
|
|
294
|
+
("display_scale", effects.display_scale),
|
|
295
|
+
),
|
|
296
|
+
affected_rows=tuple(int(row) for row in np.flatnonzero(included)),
|
|
297
|
+
),
|
|
298
|
+
TransformationRecord(
|
|
299
|
+
name="continuity_correction",
|
|
300
|
+
parameters=(
|
|
301
|
+
("value", correction),
|
|
302
|
+
("scope", scope),
|
|
303
|
+
("target", "individual_effects"),
|
|
304
|
+
),
|
|
305
|
+
affected_rows=tuple(int(row) for row in np.flatnonzero(effects.corrected)),
|
|
306
|
+
),
|
|
307
|
+
]
|
|
308
|
+
if normalized_measure == "RD":
|
|
309
|
+
transformations.append(
|
|
310
|
+
TransformationRecord(
|
|
311
|
+
name="rd_zero_variance_policy",
|
|
312
|
+
parameters=(
|
|
313
|
+
("policy", rd_policy),
|
|
314
|
+
("variance_correction", correction),
|
|
315
|
+
),
|
|
316
|
+
affected_rows=tuple(
|
|
317
|
+
int(row) for row in np.flatnonzero(effects.rd_zero_variance)
|
|
318
|
+
),
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
relative_exclusions = np.flatnonzero(
|
|
322
|
+
(~included)
|
|
323
|
+
& np.isin(
|
|
324
|
+
effects.studies.exclusion_reason,
|
|
325
|
+
[
|
|
326
|
+
"no events in either group",
|
|
327
|
+
"all participants have events in both groups",
|
|
328
|
+
],
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
if len(relative_exclusions):
|
|
332
|
+
transformations.append(
|
|
333
|
+
TransformationRecord(
|
|
334
|
+
name="relative_effect_exclusion",
|
|
335
|
+
parameters=(("measure", normalized_measure),),
|
|
336
|
+
affected_rows=tuple(int(row) for row in relative_exclusions),
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
if normalized_method == "mantel_haenszel":
|
|
340
|
+
transformations.append(
|
|
341
|
+
TransformationRecord(
|
|
342
|
+
name="mantel_haenszel_continuity_correction",
|
|
343
|
+
parameters=(
|
|
344
|
+
("value", mh_correction),
|
|
345
|
+
("scope", mh_scope),
|
|
346
|
+
("target", "pooling"),
|
|
347
|
+
),
|
|
348
|
+
affected_rows=tuple(int(row) for row in np.flatnonzero(mh_corrected)),
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
provenance = build_analysis_provenance(
|
|
352
|
+
analysis_type="binary",
|
|
353
|
+
data=data,
|
|
354
|
+
inputs=(
|
|
355
|
+
("event_treat", event_treat),
|
|
356
|
+
("n_treat", n_treat),
|
|
357
|
+
("event_control", event_control),
|
|
358
|
+
("n_control", n_control),
|
|
359
|
+
),
|
|
360
|
+
study=study,
|
|
361
|
+
included=included,
|
|
362
|
+
transformations=tuple(transformations),
|
|
363
|
+
)
|
|
364
|
+
return MetaAnalysisResult(
|
|
365
|
+
estimate=estimate,
|
|
366
|
+
standard_error=standard_error,
|
|
367
|
+
ci_low=ci_low,
|
|
368
|
+
ci_high=ci_high,
|
|
369
|
+
prediction_interval=prediction_interval,
|
|
370
|
+
tau2=tau2,
|
|
371
|
+
heterogeneity=heterogeneity,
|
|
372
|
+
k=len(included_effect),
|
|
373
|
+
model=normalized_model,
|
|
374
|
+
measure=normalized_measure,
|
|
375
|
+
effect_scale=effects.effect_scale,
|
|
376
|
+
display_scale=effects.display_scale,
|
|
377
|
+
method=method_config,
|
|
378
|
+
diagnostics=diagnostics,
|
|
379
|
+
provenance=provenance,
|
|
380
|
+
warnings=tuple(warnings),
|
|
381
|
+
_study_results=study_results,
|
|
382
|
+
_source_data=data,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@overload
|
|
387
|
+
def meta_binary(
|
|
388
|
+
data: pd.DataFrame | None = None,
|
|
389
|
+
*,
|
|
390
|
+
event_treat: ColumnOrArray,
|
|
391
|
+
n_treat: ColumnOrArray,
|
|
392
|
+
event_control: ColumnOrArray,
|
|
393
|
+
n_control: ColumnOrArray,
|
|
394
|
+
study: ColumnOrArray | None = None,
|
|
395
|
+
subgroup: None = None,
|
|
396
|
+
measure: str = "RR",
|
|
397
|
+
method: str = "MH",
|
|
398
|
+
model: str = "common",
|
|
399
|
+
tau2_method: str = "REML",
|
|
400
|
+
ci_method: str = "normal",
|
|
401
|
+
confidence_level: float = 0.95,
|
|
402
|
+
continuity_correction: float = 0.5,
|
|
403
|
+
correction_scope: str = "only_zero_studies",
|
|
404
|
+
rd_zero_variance: str = "correct",
|
|
405
|
+
mh_continuity_correction: float | None = None,
|
|
406
|
+
mh_correction_scope: str = "only_zero_studies",
|
|
407
|
+
missing: MissingPolicy = "raise",
|
|
408
|
+
atol: float = 1e-10,
|
|
409
|
+
max_iter: int = 1000,
|
|
410
|
+
) -> MetaAnalysisResult: ...
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@overload
|
|
414
|
+
def meta_binary(
|
|
415
|
+
data: pd.DataFrame | None = None,
|
|
416
|
+
*,
|
|
417
|
+
event_treat: ColumnOrArray,
|
|
418
|
+
n_treat: ColumnOrArray,
|
|
419
|
+
event_control: ColumnOrArray,
|
|
420
|
+
n_control: ColumnOrArray,
|
|
421
|
+
study: ColumnOrArray | None = None,
|
|
422
|
+
subgroup: ColumnOrArray,
|
|
423
|
+
measure: str = "RR",
|
|
424
|
+
method: str = "MH",
|
|
425
|
+
model: str = "common",
|
|
426
|
+
tau2_method: str = "REML",
|
|
427
|
+
ci_method: str = "normal",
|
|
428
|
+
confidence_level: float = 0.95,
|
|
429
|
+
continuity_correction: float = 0.5,
|
|
430
|
+
correction_scope: str = "only_zero_studies",
|
|
431
|
+
rd_zero_variance: str = "correct",
|
|
432
|
+
mh_continuity_correction: float | None = None,
|
|
433
|
+
mh_correction_scope: str = "only_zero_studies",
|
|
434
|
+
missing: MissingPolicy = "raise",
|
|
435
|
+
atol: float = 1e-10,
|
|
436
|
+
max_iter: int = 1000,
|
|
437
|
+
) -> SubgroupMetaAnalysisResult: ...
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def meta_binary(
|
|
441
|
+
data: pd.DataFrame | None = None,
|
|
442
|
+
*,
|
|
443
|
+
event_treat: ColumnOrArray,
|
|
444
|
+
n_treat: ColumnOrArray,
|
|
445
|
+
event_control: ColumnOrArray,
|
|
446
|
+
n_control: ColumnOrArray,
|
|
447
|
+
study: ColumnOrArray | None = None,
|
|
448
|
+
subgroup: ColumnOrArray | None = None,
|
|
449
|
+
measure: str = "RR",
|
|
450
|
+
method: str = "MH",
|
|
451
|
+
model: str = "common",
|
|
452
|
+
tau2_method: str = "REML",
|
|
453
|
+
ci_method: str = "normal",
|
|
454
|
+
confidence_level: float = 0.95,
|
|
455
|
+
continuity_correction: float = 0.5,
|
|
456
|
+
correction_scope: str = "only_zero_studies",
|
|
457
|
+
rd_zero_variance: str = "correct",
|
|
458
|
+
mh_continuity_correction: float | None = None,
|
|
459
|
+
mh_correction_scope: str = "only_zero_studies",
|
|
460
|
+
missing: MissingPolicy = "raise",
|
|
461
|
+
atol: float = 1e-10,
|
|
462
|
+
max_iter: int = 1000,
|
|
463
|
+
) -> MetaAnalysisResult | SubgroupMetaAnalysisResult:
|
|
464
|
+
"""Pool binary outcomes, optionally fitting independent study subgroups.
|
|
465
|
+
|
|
466
|
+
Event and total arguments accept DataFrame column names or one-dimensional
|
|
467
|
+
array-like values. The default is common-effect Mantel-Haenszel risk-ratio
|
|
468
|
+
pooling. Use inverse-variance pooling for random effects or risk differences.
|
|
469
|
+
For risk differences, ``rd_zero_variance="correct"`` retains boundary
|
|
470
|
+
studies with their raw effect and corrected sampling variance. Use
|
|
471
|
+
``rd_zero_variance="exclude"`` to remove them before all synthesis
|
|
472
|
+
calculations.
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
overall = _fit_meta_binary_single(
|
|
476
|
+
data,
|
|
477
|
+
event_treat=event_treat,
|
|
478
|
+
n_treat=n_treat,
|
|
479
|
+
event_control=event_control,
|
|
480
|
+
n_control=n_control,
|
|
481
|
+
study=study,
|
|
482
|
+
measure=measure,
|
|
483
|
+
method=method,
|
|
484
|
+
model=model,
|
|
485
|
+
tau2_method=tau2_method,
|
|
486
|
+
ci_method=ci_method,
|
|
487
|
+
confidence_level=confidence_level,
|
|
488
|
+
continuity_correction=continuity_correction,
|
|
489
|
+
correction_scope=correction_scope,
|
|
490
|
+
rd_zero_variance=rd_zero_variance,
|
|
491
|
+
mh_continuity_correction=mh_continuity_correction,
|
|
492
|
+
mh_correction_scope=mh_correction_scope,
|
|
493
|
+
missing=missing,
|
|
494
|
+
atol=atol,
|
|
495
|
+
max_iter=max_iter,
|
|
496
|
+
)
|
|
497
|
+
if subgroup is None:
|
|
498
|
+
return overall
|
|
499
|
+
|
|
500
|
+
overall = replace(
|
|
501
|
+
overall,
|
|
502
|
+
provenance=add_input_field(
|
|
503
|
+
overall.provenance,
|
|
504
|
+
role="subgroup",
|
|
505
|
+
value=subgroup,
|
|
506
|
+
data=data,
|
|
507
|
+
),
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
def fit_group(positions: np.ndarray) -> MetaAnalysisResult:
|
|
511
|
+
rows = overall.study_results.iloc[positions]
|
|
512
|
+
return _fit_meta_binary_single(
|
|
513
|
+
event_treat=rows["event_treat"].to_numpy(dtype=np.float64, copy=True),
|
|
514
|
+
n_treat=rows["n_treat"].to_numpy(dtype=np.float64, copy=True),
|
|
515
|
+
event_control=rows["event_control"].to_numpy(dtype=np.float64, copy=True),
|
|
516
|
+
n_control=rows["n_control"].to_numpy(dtype=np.float64, copy=True),
|
|
517
|
+
study=rows["study"].to_numpy(dtype=object, copy=True),
|
|
518
|
+
measure=measure,
|
|
519
|
+
method=method,
|
|
520
|
+
model=model,
|
|
521
|
+
tau2_method=tau2_method,
|
|
522
|
+
ci_method=ci_method,
|
|
523
|
+
confidence_level=confidence_level,
|
|
524
|
+
continuity_correction=continuity_correction,
|
|
525
|
+
correction_scope=correction_scope,
|
|
526
|
+
rd_zero_variance=rd_zero_variance,
|
|
527
|
+
mh_continuity_correction=mh_continuity_correction,
|
|
528
|
+
mh_correction_scope=mh_correction_scope,
|
|
529
|
+
missing=missing,
|
|
530
|
+
atol=atol,
|
|
531
|
+
max_iter=max_iter,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
return fit_subgroup_analysis(
|
|
535
|
+
data=data,
|
|
536
|
+
subgroup=subgroup,
|
|
537
|
+
overall=overall,
|
|
538
|
+
fit_group=fit_group,
|
|
539
|
+
)
|
meta_analyze/config.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Configuration objects recorded in fitted results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TypeAlias
|
|
7
|
+
|
|
8
|
+
MethodOptionValue: TypeAlias = str | float | int | bool | None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class MethodConfig:
|
|
13
|
+
"""The fully resolved methods used to fit a meta-analysis."""
|
|
14
|
+
|
|
15
|
+
model: str
|
|
16
|
+
pooling_method: str
|
|
17
|
+
tau2_method: str | None
|
|
18
|
+
ci_method: str
|
|
19
|
+
confidence_level: float
|
|
20
|
+
prediction_interval_method: str | None
|
|
21
|
+
missing: str
|
|
22
|
+
atol: float
|
|
23
|
+
max_iter: int
|
|
24
|
+
options: tuple[tuple[str, MethodOptionValue], ...]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class SubgroupMethodConfig:
|
|
29
|
+
"""The fully resolved assumptions used for a subgroup analysis."""
|
|
30
|
+
|
|
31
|
+
model: str
|
|
32
|
+
tau2_strategy: str
|
|
33
|
+
test_method: str
|
|
34
|
+
subgroup_missing: str
|