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
meta_analyze/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Pandas-first tools for auditable meta-analysis workflows."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .api import meta_analysis
|
|
5
|
+
from .binary_api import meta_binary
|
|
6
|
+
from .config import MethodConfig, SubgroupMethodConfig
|
|
7
|
+
from .continuous_api import meta_continuous
|
|
8
|
+
from .exceptions import (
|
|
9
|
+
ConvergenceError,
|
|
10
|
+
InsufficientStudiesError,
|
|
11
|
+
InvalidStudyDataError,
|
|
12
|
+
MetaAnalysisError,
|
|
13
|
+
UnsupportedMethodError,
|
|
14
|
+
)
|
|
15
|
+
from .provenance import (
|
|
16
|
+
AnalysisProvenance,
|
|
17
|
+
InputFieldProvenance,
|
|
18
|
+
TransformationRecord,
|
|
19
|
+
)
|
|
20
|
+
from .reporting import ResultReport
|
|
21
|
+
from .results import (
|
|
22
|
+
FitDiagnostics,
|
|
23
|
+
HeterogeneityResult,
|
|
24
|
+
MetaAnalysisResult,
|
|
25
|
+
MetaAnalysisSummary,
|
|
26
|
+
SubgroupMetaAnalysisResult,
|
|
27
|
+
SubgroupMetaAnalysisSummary,
|
|
28
|
+
)
|
|
29
|
+
from .sensitivity import (
|
|
30
|
+
CumulativeMetaAnalysisResult,
|
|
31
|
+
LeaveOneOutResult,
|
|
32
|
+
SubgroupCumulativeMetaAnalysisResult,
|
|
33
|
+
SubgroupLeaveOneOutResult,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"AnalysisProvenance",
|
|
38
|
+
"ConvergenceError",
|
|
39
|
+
"CumulativeMetaAnalysisResult",
|
|
40
|
+
"FitDiagnostics",
|
|
41
|
+
"HeterogeneityResult",
|
|
42
|
+
"InsufficientStudiesError",
|
|
43
|
+
"InputFieldProvenance",
|
|
44
|
+
"InvalidStudyDataError",
|
|
45
|
+
"LeaveOneOutResult",
|
|
46
|
+
"MetaAnalysisError",
|
|
47
|
+
"MetaAnalysisResult",
|
|
48
|
+
"MetaAnalysisSummary",
|
|
49
|
+
"MethodConfig",
|
|
50
|
+
"ResultReport",
|
|
51
|
+
"SubgroupMetaAnalysisResult",
|
|
52
|
+
"SubgroupMetaAnalysisSummary",
|
|
53
|
+
"SubgroupCumulativeMetaAnalysisResult",
|
|
54
|
+
"SubgroupLeaveOneOutResult",
|
|
55
|
+
"SubgroupMethodConfig",
|
|
56
|
+
"TransformationRecord",
|
|
57
|
+
"UnsupportedMethodError",
|
|
58
|
+
"meta_analysis",
|
|
59
|
+
"meta_binary",
|
|
60
|
+
"meta_continuous",
|
|
61
|
+
"__version__",
|
|
62
|
+
]
|
meta_analyze/_version.py
ADDED
meta_analyze/api.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""High-level public analysis functions."""
|
|
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 .config import MethodConfig
|
|
12
|
+
from .data import ColumnOrArray, MissingPolicy, normalize_studies
|
|
13
|
+
from .estimators import fit_inverse_variance
|
|
14
|
+
from .exceptions import InvalidStudyDataError, UnsupportedMethodError
|
|
15
|
+
from .heterogeneity import classical_heterogeneity, tau2_inconsistency
|
|
16
|
+
from .provenance import add_input_field, build_analysis_provenance
|
|
17
|
+
from .results import (
|
|
18
|
+
FitDiagnostics,
|
|
19
|
+
HeterogeneityResult,
|
|
20
|
+
MetaAnalysisResult,
|
|
21
|
+
SubgroupMetaAnalysisResult,
|
|
22
|
+
)
|
|
23
|
+
from .subgroups import fit_subgroup_analysis
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _normalize_model(model: str) -> str:
|
|
27
|
+
normalized = model.lower().replace("-", "_")
|
|
28
|
+
if normalized in {"common", "common_effect", "fixed", "fixed_effect"}:
|
|
29
|
+
return "common"
|
|
30
|
+
if normalized in {"random", "random_effects", "random_effect"}:
|
|
31
|
+
return "random"
|
|
32
|
+
raise UnsupportedMethodError(
|
|
33
|
+
f"Unsupported model={model!r}; expected 'common' or 'random'."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _normalize_ci_method(ci_method: str) -> str:
|
|
38
|
+
normalized = ci_method.lower().replace("-", "_")
|
|
39
|
+
aliases = {
|
|
40
|
+
"classic": "normal",
|
|
41
|
+
"z": "normal",
|
|
42
|
+
"hksj": "hartung_knapp",
|
|
43
|
+
"hk": "hartung_knapp",
|
|
44
|
+
"adhoc": "hartung_knapp_adhoc",
|
|
45
|
+
"hksj_adhoc": "hartung_knapp_adhoc",
|
|
46
|
+
}
|
|
47
|
+
return aliases.get(normalized, normalized)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _validate_analysis_controls(
|
|
51
|
+
*, confidence_level: float, atol: float, max_iter: int
|
|
52
|
+
) -> tuple[float, float, int]:
|
|
53
|
+
if not isinstance(confidence_level, (int, float)) or not (
|
|
54
|
+
0.0 < float(confidence_level) < 1.0
|
|
55
|
+
):
|
|
56
|
+
raise InvalidStudyDataError("confidence_level must be between 0 and 1.")
|
|
57
|
+
if not isinstance(max_iter, int) or isinstance(max_iter, bool) or max_iter < 1:
|
|
58
|
+
raise InvalidStudyDataError("max_iter must be a positive integer.")
|
|
59
|
+
if not isinstance(atol, (int, float)) or not np.isfinite(atol) or atol <= 0.0:
|
|
60
|
+
raise InvalidStudyDataError("atol must be finite and strictly positive.")
|
|
61
|
+
return float(confidence_level), float(atol), max_iter
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _fit_meta_analysis_single(
|
|
65
|
+
data: pd.DataFrame | None = None,
|
|
66
|
+
*,
|
|
67
|
+
effect: ColumnOrArray,
|
|
68
|
+
variance: ColumnOrArray,
|
|
69
|
+
study: ColumnOrArray | None = None,
|
|
70
|
+
model: str = "random",
|
|
71
|
+
tau2_method: str = "REML",
|
|
72
|
+
ci_method: str = "normal",
|
|
73
|
+
confidence_level: float = 0.95,
|
|
74
|
+
missing: MissingPolicy = "raise",
|
|
75
|
+
atol: float = 1e-10,
|
|
76
|
+
max_iter: int = 1000,
|
|
77
|
+
) -> MetaAnalysisResult:
|
|
78
|
+
"""Fit a generic inverse-variance meta-analysis.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
data:
|
|
83
|
+
Optional pandas DataFrame. String-valued input arguments select columns
|
|
84
|
+
from this frame.
|
|
85
|
+
effect, variance:
|
|
86
|
+
A DataFrame column name or one-dimensional array-like containing study
|
|
87
|
+
effects and strictly positive sampling variances.
|
|
88
|
+
study:
|
|
89
|
+
Optional study label column/array. DataFrame input defaults to its index;
|
|
90
|
+
array-only input defaults to integer row labels.
|
|
91
|
+
model:
|
|
92
|
+
``"common"`` (``"fixed"`` is an alias) or ``"random"``.
|
|
93
|
+
tau2_method:
|
|
94
|
+
``"REML"`` (default), ``"PM"``, or ``"DL"`` for random-effects models.
|
|
95
|
+
ci_method:
|
|
96
|
+
``"normal"``, ``"hartung_knapp"``, or
|
|
97
|
+
``"hartung_knapp_adhoc"``.
|
|
98
|
+
confidence_level:
|
|
99
|
+
Confidence level strictly between zero and one.
|
|
100
|
+
missing:
|
|
101
|
+
``"raise"`` (default) or ``"drop"``. Dropped studies remain visible in
|
|
102
|
+
the result with a structured exclusion reason.
|
|
103
|
+
atol, max_iter:
|
|
104
|
+
Numerical controls for iterative tau-squared estimators.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
confidence_level, atol, max_iter = _validate_analysis_controls(
|
|
108
|
+
confidence_level=confidence_level,
|
|
109
|
+
atol=atol,
|
|
110
|
+
max_iter=max_iter,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
normalized_model = _normalize_model(model)
|
|
114
|
+
normalized_ci = _normalize_ci_method(ci_method)
|
|
115
|
+
normalized_tau2 = tau2_method.upper().replace("-", "_")
|
|
116
|
+
|
|
117
|
+
studies = normalize_studies(
|
|
118
|
+
data=data,
|
|
119
|
+
effect=effect,
|
|
120
|
+
variance=variance,
|
|
121
|
+
study=study,
|
|
122
|
+
missing=missing,
|
|
123
|
+
)
|
|
124
|
+
included_effect = studies.included_effect
|
|
125
|
+
included_variance = studies.included_variance
|
|
126
|
+
|
|
127
|
+
fit = fit_inverse_variance(
|
|
128
|
+
included_effect,
|
|
129
|
+
included_variance,
|
|
130
|
+
model=normalized_model,
|
|
131
|
+
tau2_method=normalized_tau2,
|
|
132
|
+
ci_method=normalized_ci,
|
|
133
|
+
confidence_level=confidence_level,
|
|
134
|
+
atol=atol,
|
|
135
|
+
max_iter=max_iter,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
q, q_df, q_pvalue, i2, h2 = classical_heterogeneity(
|
|
139
|
+
included_effect, included_variance
|
|
140
|
+
)
|
|
141
|
+
tau2_value = 0.0 if fit.tau2 is None else fit.tau2.value
|
|
142
|
+
i2_method = "q_based"
|
|
143
|
+
if normalized_model == "random":
|
|
144
|
+
i2, h2 = tau2_inconsistency(included_variance, tau2_value)
|
|
145
|
+
i2_method = "tau2_typical_variance"
|
|
146
|
+
heterogeneity = HeterogeneityResult(q, q_df, q_pvalue, i2, h2, i2_method)
|
|
147
|
+
|
|
148
|
+
row_count = len(studies.row_id)
|
|
149
|
+
raw_weights = np.full(row_count, np.nan, dtype=np.float64)
|
|
150
|
+
normalized_weights = np.full(row_count, np.nan, dtype=np.float64)
|
|
151
|
+
raw_weights[studies.included] = fit.weights
|
|
152
|
+
normalized_weights[studies.included] = fit.normalized_weights
|
|
153
|
+
study_results = pd.DataFrame(
|
|
154
|
+
{
|
|
155
|
+
"row_id": studies.row_id,
|
|
156
|
+
"study": studies.study,
|
|
157
|
+
"effect": studies.effect,
|
|
158
|
+
"variance": studies.variance,
|
|
159
|
+
"standard_error": np.sqrt(studies.variance),
|
|
160
|
+
"included": studies.included,
|
|
161
|
+
"exclusion_reason": pd.Series(
|
|
162
|
+
studies.exclusion_reason, dtype=object, copy=True
|
|
163
|
+
),
|
|
164
|
+
"weight": raw_weights,
|
|
165
|
+
"normalized_weight": normalized_weights,
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
warnings = list(fit.warnings)
|
|
170
|
+
excluded_count = int(np.count_nonzero(~studies.included))
|
|
171
|
+
if excluded_count:
|
|
172
|
+
warnings.append(
|
|
173
|
+
f"Excluded {excluded_count} study row(s) under missing={missing!r}."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
diagnostics = FitDiagnostics(
|
|
177
|
+
converged=True if fit.tau2 is None else fit.tau2.converged,
|
|
178
|
+
iterations=0 if fit.tau2 is None else fit.tau2.iterations,
|
|
179
|
+
tau2_at_boundary=None if fit.tau2 is None else fit.tau2.boundary,
|
|
180
|
+
)
|
|
181
|
+
method = MethodConfig(
|
|
182
|
+
model=normalized_model,
|
|
183
|
+
pooling_method="inverse_variance",
|
|
184
|
+
tau2_method=None if normalized_model == "common" else normalized_tau2,
|
|
185
|
+
ci_method=normalized_ci,
|
|
186
|
+
confidence_level=confidence_level,
|
|
187
|
+
prediction_interval_method="HTS" if normalized_model == "random" else None,
|
|
188
|
+
missing=missing,
|
|
189
|
+
atol=atol,
|
|
190
|
+
max_iter=max_iter,
|
|
191
|
+
options=(),
|
|
192
|
+
)
|
|
193
|
+
provenance = build_analysis_provenance(
|
|
194
|
+
analysis_type="generic",
|
|
195
|
+
data=data,
|
|
196
|
+
inputs=(("effect", effect), ("variance", variance)),
|
|
197
|
+
study=study,
|
|
198
|
+
included=studies.included,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return MetaAnalysisResult(
|
|
202
|
+
estimate=fit.estimate,
|
|
203
|
+
standard_error=fit.standard_error,
|
|
204
|
+
ci_low=fit.ci_low,
|
|
205
|
+
ci_high=fit.ci_high,
|
|
206
|
+
prediction_interval=fit.prediction_interval,
|
|
207
|
+
tau2=tau2_value,
|
|
208
|
+
heterogeneity=heterogeneity,
|
|
209
|
+
k=len(included_effect),
|
|
210
|
+
model=normalized_model,
|
|
211
|
+
measure="GENERIC",
|
|
212
|
+
effect_scale="identity",
|
|
213
|
+
display_scale="identity",
|
|
214
|
+
method=method,
|
|
215
|
+
diagnostics=diagnostics,
|
|
216
|
+
provenance=provenance,
|
|
217
|
+
warnings=tuple(warnings),
|
|
218
|
+
_study_results=study_results,
|
|
219
|
+
_source_data=data,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@overload
|
|
224
|
+
def meta_analysis(
|
|
225
|
+
data: pd.DataFrame | None = None,
|
|
226
|
+
*,
|
|
227
|
+
effect: ColumnOrArray,
|
|
228
|
+
variance: ColumnOrArray,
|
|
229
|
+
study: ColumnOrArray | None = None,
|
|
230
|
+
subgroup: None = None,
|
|
231
|
+
model: str = "random",
|
|
232
|
+
tau2_method: str = "REML",
|
|
233
|
+
ci_method: str = "normal",
|
|
234
|
+
confidence_level: float = 0.95,
|
|
235
|
+
missing: MissingPolicy = "raise",
|
|
236
|
+
atol: float = 1e-10,
|
|
237
|
+
max_iter: int = 1000,
|
|
238
|
+
) -> MetaAnalysisResult: ...
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@overload
|
|
242
|
+
def meta_analysis(
|
|
243
|
+
data: pd.DataFrame | None = None,
|
|
244
|
+
*,
|
|
245
|
+
effect: ColumnOrArray,
|
|
246
|
+
variance: ColumnOrArray,
|
|
247
|
+
study: ColumnOrArray | None = None,
|
|
248
|
+
subgroup: ColumnOrArray,
|
|
249
|
+
model: str = "random",
|
|
250
|
+
tau2_method: str = "REML",
|
|
251
|
+
ci_method: str = "normal",
|
|
252
|
+
confidence_level: float = 0.95,
|
|
253
|
+
missing: MissingPolicy = "raise",
|
|
254
|
+
atol: float = 1e-10,
|
|
255
|
+
max_iter: int = 1000,
|
|
256
|
+
) -> SubgroupMetaAnalysisResult: ...
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def meta_analysis(
|
|
260
|
+
data: pd.DataFrame | None = None,
|
|
261
|
+
*,
|
|
262
|
+
effect: ColumnOrArray,
|
|
263
|
+
variance: ColumnOrArray,
|
|
264
|
+
study: ColumnOrArray | None = None,
|
|
265
|
+
subgroup: ColumnOrArray | None = None,
|
|
266
|
+
model: str = "random",
|
|
267
|
+
tau2_method: str = "REML",
|
|
268
|
+
ci_method: str = "normal",
|
|
269
|
+
confidence_level: float = 0.95,
|
|
270
|
+
missing: MissingPolicy = "raise",
|
|
271
|
+
atol: float = 1e-10,
|
|
272
|
+
max_iter: int = 1000,
|
|
273
|
+
) -> MetaAnalysisResult | SubgroupMetaAnalysisResult:
|
|
274
|
+
"""Fit a generic inverse-variance meta-analysis, optionally by subgroup.
|
|
275
|
+
|
|
276
|
+
``effect`` and ``variance`` accept DataFrame column names or one-dimensional
|
|
277
|
+
array-like values. Sampling variances must be finite and strictly positive.
|
|
278
|
+
The default is a REML random-effects model with a normal confidence
|
|
279
|
+
interval. ``subgroup`` returns :class:`SubgroupMetaAnalysisResult` when
|
|
280
|
+
supplied; otherwise the return value is :class:`MetaAnalysisResult`.
|
|
281
|
+
Missing subgroup labels are rejected explicitly.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
overall = _fit_meta_analysis_single(
|
|
285
|
+
data,
|
|
286
|
+
effect=effect,
|
|
287
|
+
variance=variance,
|
|
288
|
+
study=study,
|
|
289
|
+
model=model,
|
|
290
|
+
tau2_method=tau2_method,
|
|
291
|
+
ci_method=ci_method,
|
|
292
|
+
confidence_level=confidence_level,
|
|
293
|
+
missing=missing,
|
|
294
|
+
atol=atol,
|
|
295
|
+
max_iter=max_iter,
|
|
296
|
+
)
|
|
297
|
+
if subgroup is None:
|
|
298
|
+
return overall
|
|
299
|
+
|
|
300
|
+
overall = replace(
|
|
301
|
+
overall,
|
|
302
|
+
provenance=add_input_field(
|
|
303
|
+
overall.provenance,
|
|
304
|
+
role="subgroup",
|
|
305
|
+
value=subgroup,
|
|
306
|
+
data=data,
|
|
307
|
+
),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def fit_group(positions: np.ndarray) -> MetaAnalysisResult:
|
|
311
|
+
rows = overall.study_results.iloc[positions]
|
|
312
|
+
return _fit_meta_analysis_single(
|
|
313
|
+
effect=rows["effect"].to_numpy(dtype=np.float64, copy=True),
|
|
314
|
+
variance=rows["variance"].to_numpy(dtype=np.float64, copy=True),
|
|
315
|
+
study=rows["study"].to_numpy(dtype=object, copy=True),
|
|
316
|
+
model=model,
|
|
317
|
+
tau2_method=tau2_method,
|
|
318
|
+
ci_method=ci_method,
|
|
319
|
+
confidence_level=confidence_level,
|
|
320
|
+
missing=missing,
|
|
321
|
+
atol=atol,
|
|
322
|
+
max_iter=max_iter,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return fit_subgroup_analysis(
|
|
326
|
+
data=data,
|
|
327
|
+
subgroup=subgroup,
|
|
328
|
+
overall=overall,
|
|
329
|
+
fit_group=fit_group,
|
|
330
|
+
)
|