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/results.py
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
"""Immutable, inspectable result objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Hashable, Mapping
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from .config import MethodConfig, SubgroupMethodConfig
|
|
14
|
+
from .data import ColumnOrArray
|
|
15
|
+
from .provenance import AnalysisProvenance
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from matplotlib.axes import Axes
|
|
19
|
+
|
|
20
|
+
from .reporting import ResultReport
|
|
21
|
+
from .sensitivity import (
|
|
22
|
+
CumulativeMetaAnalysisResult,
|
|
23
|
+
LeaveOneOutResult,
|
|
24
|
+
SubgroupCumulativeMetaAnalysisResult,
|
|
25
|
+
SubgroupLeaveOneOutResult,
|
|
26
|
+
)
|
|
27
|
+
else:
|
|
28
|
+
Axes = Any
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class HeterogeneityResult:
|
|
33
|
+
"""Heterogeneity statistics and the definition used for I-squared/H-squared."""
|
|
34
|
+
|
|
35
|
+
q: float
|
|
36
|
+
df: int
|
|
37
|
+
pvalue: float
|
|
38
|
+
i2: float
|
|
39
|
+
h2: float
|
|
40
|
+
i2_method: str = "q_based"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, slots=True)
|
|
44
|
+
class FitDiagnostics:
|
|
45
|
+
"""Convergence details for the fitted model."""
|
|
46
|
+
|
|
47
|
+
converged: bool
|
|
48
|
+
iterations: int
|
|
49
|
+
tau2_at_boundary: bool | None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class MetaAnalysisSummary:
|
|
54
|
+
"""A printable summary with a machine-readable representation."""
|
|
55
|
+
|
|
56
|
+
_result: MetaAnalysisResult = field(repr=False)
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> dict[str, Any]:
|
|
59
|
+
result = self._result
|
|
60
|
+
return {
|
|
61
|
+
"model": result.model,
|
|
62
|
+
"pooling_method": result.method.pooling_method,
|
|
63
|
+
"measure": result.measure,
|
|
64
|
+
"effect_scale": result.effect_scale,
|
|
65
|
+
"display_scale": result.display_scale,
|
|
66
|
+
"studies": result.k,
|
|
67
|
+
"estimate": result.estimate,
|
|
68
|
+
"display_estimate": result.display_estimate,
|
|
69
|
+
"standard_error": result.standard_error,
|
|
70
|
+
"confidence_level": result.method.confidence_level,
|
|
71
|
+
"ci_low": result.ci_low,
|
|
72
|
+
"ci_high": result.ci_high,
|
|
73
|
+
"display_ci": result.display_ci,
|
|
74
|
+
"prediction_interval": result.prediction_interval,
|
|
75
|
+
"display_prediction_interval": result.display_prediction_interval,
|
|
76
|
+
"tau2": result.tau2,
|
|
77
|
+
"tau2_method": result.method.tau2_method,
|
|
78
|
+
"atol": result.method.atol,
|
|
79
|
+
"max_iter": result.method.max_iter,
|
|
80
|
+
"q": result.q,
|
|
81
|
+
"q_df": result.q_df,
|
|
82
|
+
"q_pvalue": result.q_pvalue,
|
|
83
|
+
"i2": result.i2,
|
|
84
|
+
"h2": result.h2,
|
|
85
|
+
"i2_method": result.i2_method,
|
|
86
|
+
"warnings": result.warnings,
|
|
87
|
+
"method_options": dict(result.method.options),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
def __str__(self) -> str:
|
|
91
|
+
result = self._result
|
|
92
|
+
level = 100.0 * result.method.confidence_level
|
|
93
|
+
display_low, display_high = result.display_ci
|
|
94
|
+
title = f"Meta-analysis ({result.model}-effect, {result.measure})"
|
|
95
|
+
lines = [
|
|
96
|
+
title,
|
|
97
|
+
f"Studies: {result.k}",
|
|
98
|
+
(
|
|
99
|
+
f"Estimate: {result.display_estimate:.6g} "
|
|
100
|
+
f"({level:g}% CI {display_low:.6g} to {display_high:.6g})"
|
|
101
|
+
),
|
|
102
|
+
]
|
|
103
|
+
if result.model == "random":
|
|
104
|
+
lines.append(f"tau^2: {result.tau2:.6g} ({result.method.tau2_method})")
|
|
105
|
+
if result.prediction_interval is not None:
|
|
106
|
+
display_prediction = result.display_prediction_interval
|
|
107
|
+
assert display_prediction is not None
|
|
108
|
+
low, high = display_prediction
|
|
109
|
+
lines.append(f"Prediction interval: {low:.6g} to {high:.6g}")
|
|
110
|
+
|
|
111
|
+
if result.q_df > 0:
|
|
112
|
+
inconsistency_label = (
|
|
113
|
+
"tau^2/typical-variance"
|
|
114
|
+
if result.i2_method == "tau2_typical_variance"
|
|
115
|
+
else "Q-based"
|
|
116
|
+
)
|
|
117
|
+
lines.extend(
|
|
118
|
+
[
|
|
119
|
+
(f"Q({result.q_df}): {result.q:.6g}, p={result.q_pvalue:.6g}"),
|
|
120
|
+
f"I^2 ({inconsistency_label}): {100.0 * result.i2:.2f}%; "
|
|
121
|
+
f"H^2: {result.h2:.6g}",
|
|
122
|
+
]
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
lines.append("Heterogeneity: not estimable with one study")
|
|
126
|
+
|
|
127
|
+
lines.append(f"Confidence interval method: {result.method.ci_method}")
|
|
128
|
+
if result.model == "random" and result.method.ci_method == "normal":
|
|
129
|
+
if result.k <= 3:
|
|
130
|
+
lines.append(
|
|
131
|
+
"Method note: with three or fewer studies, compare normal and "
|
|
132
|
+
"Hartung-Knapp intervals; neither method fully resolves the "
|
|
133
|
+
"small-sample uncertainty."
|
|
134
|
+
)
|
|
135
|
+
elif result.tau2 > 0.0:
|
|
136
|
+
lines.append(
|
|
137
|
+
"Method note: consider ci_method='hartung_knapp' as a "
|
|
138
|
+
"t-based random-effects sensitivity analysis."
|
|
139
|
+
)
|
|
140
|
+
if result.warnings:
|
|
141
|
+
lines.append("Notes:")
|
|
142
|
+
lines.extend(f"- {warning}" for warning in result.warnings)
|
|
143
|
+
return "\n".join(lines)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass(frozen=True, slots=True)
|
|
147
|
+
class SubgroupMetaAnalysisSummary:
|
|
148
|
+
"""A printable and machine-readable subgroup analysis summary."""
|
|
149
|
+
|
|
150
|
+
_result: SubgroupMetaAnalysisResult = field(repr=False)
|
|
151
|
+
|
|
152
|
+
def to_dict(self) -> dict[str, Any]:
|
|
153
|
+
"""Return nested subgroup, overall, and interaction-test results."""
|
|
154
|
+
|
|
155
|
+
result = self._result
|
|
156
|
+
return {
|
|
157
|
+
"groups": {
|
|
158
|
+
label: group.summary().to_dict()
|
|
159
|
+
for label, group in result.groups.items()
|
|
160
|
+
},
|
|
161
|
+
"overall": result.overall.summary().to_dict(),
|
|
162
|
+
"q_between": result.q_between,
|
|
163
|
+
"q_between_df": result.q_between_df,
|
|
164
|
+
"q_between_pvalue": result.q_between_pvalue,
|
|
165
|
+
"i2_between": result.i2_between,
|
|
166
|
+
"method": {
|
|
167
|
+
"model": result.method.model,
|
|
168
|
+
"tau2_strategy": result.method.tau2_strategy,
|
|
169
|
+
"test_method": result.method.test_method,
|
|
170
|
+
"subgroup_missing": result.method.subgroup_missing,
|
|
171
|
+
},
|
|
172
|
+
"warnings": result.warnings,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
def __str__(self) -> str:
|
|
176
|
+
result = self._result
|
|
177
|
+
level = 100.0 * result.overall.method.confidence_level
|
|
178
|
+
lines = [
|
|
179
|
+
f"Subgroup meta-analysis ({result.overall.model}-effect, "
|
|
180
|
+
f"{result.overall.measure})",
|
|
181
|
+
f"Groups: {len(result.groups)}; studies: {result.overall.k}",
|
|
182
|
+
]
|
|
183
|
+
for label, group in result.groups.items():
|
|
184
|
+
low, high = group.display_ci
|
|
185
|
+
lines.append(
|
|
186
|
+
f"- {label}: {group.display_estimate:.6g} "
|
|
187
|
+
f"({level:g}% CI {low:.6g} to {high:.6g}; k={group.k})"
|
|
188
|
+
)
|
|
189
|
+
lines.append(
|
|
190
|
+
"Test for subgroup differences: "
|
|
191
|
+
f"Q({result.q_between_df})={result.q_between:.6g}, "
|
|
192
|
+
f"p={result.q_between_pvalue:.6g}, "
|
|
193
|
+
f"I^2={100.0 * result.i2_between:.2f}%"
|
|
194
|
+
)
|
|
195
|
+
lines.append(
|
|
196
|
+
f"Subgroup tau^2 strategy: {result.method.tau2_strategy}; "
|
|
197
|
+
f"test method: {result.method.test_method}"
|
|
198
|
+
)
|
|
199
|
+
if result.warnings:
|
|
200
|
+
lines.append("Notes:")
|
|
201
|
+
lines.extend(f"- {warning}" for warning in result.warnings)
|
|
202
|
+
return "\n".join(lines)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass(frozen=True, slots=True)
|
|
206
|
+
class MetaAnalysisResult:
|
|
207
|
+
"""Results of a generic inverse-variance meta-analysis."""
|
|
208
|
+
|
|
209
|
+
estimate: float
|
|
210
|
+
standard_error: float
|
|
211
|
+
ci_low: float
|
|
212
|
+
ci_high: float
|
|
213
|
+
prediction_interval: tuple[float, float] | None
|
|
214
|
+
tau2: float
|
|
215
|
+
heterogeneity: HeterogeneityResult
|
|
216
|
+
k: int
|
|
217
|
+
model: str
|
|
218
|
+
measure: str
|
|
219
|
+
effect_scale: str
|
|
220
|
+
display_scale: str
|
|
221
|
+
method: MethodConfig
|
|
222
|
+
diagnostics: FitDiagnostics
|
|
223
|
+
provenance: AnalysisProvenance
|
|
224
|
+
warnings: tuple[str, ...]
|
|
225
|
+
_study_results: pd.DataFrame = field(repr=False, compare=False)
|
|
226
|
+
_source_data: pd.DataFrame | None = field(default=None, repr=False, compare=False)
|
|
227
|
+
|
|
228
|
+
def __post_init__(self) -> None:
|
|
229
|
+
object.__setattr__(self, "_study_results", self._study_results.copy(deep=True))
|
|
230
|
+
if self._source_data is not None:
|
|
231
|
+
object.__setattr__(self, "_source_data", self._source_data.copy(deep=True))
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def ci(self) -> tuple[float, float]:
|
|
235
|
+
return self.ci_low, self.ci_high
|
|
236
|
+
|
|
237
|
+
def _to_display_scale(self, value: float) -> float:
|
|
238
|
+
if self.display_scale == "identity":
|
|
239
|
+
return value
|
|
240
|
+
if self.display_scale == "exp":
|
|
241
|
+
return math.exp(value)
|
|
242
|
+
raise ValueError(f"Unknown display scale {self.display_scale!r}.")
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def display_estimate(self) -> float:
|
|
246
|
+
return self._to_display_scale(self.estimate)
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def display_ci(self) -> tuple[float, float]:
|
|
250
|
+
return (
|
|
251
|
+
self._to_display_scale(self.ci_low),
|
|
252
|
+
self._to_display_scale(self.ci_high),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def display_prediction_interval(self) -> tuple[float, float] | None:
|
|
257
|
+
if self.prediction_interval is None:
|
|
258
|
+
return None
|
|
259
|
+
low, high = self.prediction_interval
|
|
260
|
+
return self._to_display_scale(low), self._to_display_scale(high)
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def q(self) -> float:
|
|
264
|
+
return self.heterogeneity.q
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def q_df(self) -> int:
|
|
268
|
+
return self.heterogeneity.df
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def q_pvalue(self) -> float:
|
|
272
|
+
return self.heterogeneity.pvalue
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def i2(self) -> float:
|
|
276
|
+
"""I-squared as a proportion from 0 to 1."""
|
|
277
|
+
|
|
278
|
+
return self.heterogeneity.i2
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def h2(self) -> float:
|
|
282
|
+
return self.heterogeneity.h2
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def i2_method(self) -> str:
|
|
286
|
+
return self.heterogeneity.i2_method
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def study_results(self) -> pd.DataFrame:
|
|
290
|
+
"""Return a defensive copy of row-level inputs and fitted weights."""
|
|
291
|
+
|
|
292
|
+
return self._study_results.copy(deep=True)
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def excluded_studies(self) -> pd.DataFrame:
|
|
296
|
+
studies = self._study_results
|
|
297
|
+
return studies.loc[~studies["included"]].copy(deep=True)
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def source_data(self) -> pd.DataFrame | None:
|
|
301
|
+
"""Return a defensive copy of the source DataFrame, when one was supplied."""
|
|
302
|
+
|
|
303
|
+
if self._source_data is None:
|
|
304
|
+
return None
|
|
305
|
+
return self._source_data.copy(deep=True)
|
|
306
|
+
|
|
307
|
+
def summary(self) -> MetaAnalysisSummary:
|
|
308
|
+
return MetaAnalysisSummary(self)
|
|
309
|
+
|
|
310
|
+
def method_details(self) -> str:
|
|
311
|
+
"""Return a concise Methods-style description of the fitted analysis."""
|
|
312
|
+
|
|
313
|
+
from .reporting import method_details
|
|
314
|
+
|
|
315
|
+
return method_details(self)
|
|
316
|
+
|
|
317
|
+
def report(self, *, include_studies: bool = True) -> ResultReport:
|
|
318
|
+
"""Build a structured report with JSON and Markdown representations."""
|
|
319
|
+
|
|
320
|
+
from .reporting import build_report
|
|
321
|
+
|
|
322
|
+
return build_report(self, include_studies=include_studies)
|
|
323
|
+
|
|
324
|
+
def to_dataframe(self) -> pd.DataFrame:
|
|
325
|
+
"""Return the same row-level table as :attr:`study_results`."""
|
|
326
|
+
|
|
327
|
+
return self.study_results
|
|
328
|
+
|
|
329
|
+
def leave_one_out(self) -> LeaveOneOutResult:
|
|
330
|
+
"""Repeatedly refit the model while omitting one included study."""
|
|
331
|
+
|
|
332
|
+
from .sensitivity import leave_one_out
|
|
333
|
+
|
|
334
|
+
return leave_one_out(self)
|
|
335
|
+
|
|
336
|
+
def cumulative(
|
|
337
|
+
self,
|
|
338
|
+
*,
|
|
339
|
+
order: ColumnOrArray | None = None,
|
|
340
|
+
ascending: bool = True,
|
|
341
|
+
collapse: bool = False,
|
|
342
|
+
) -> CumulativeMetaAnalysisResult:
|
|
343
|
+
"""Repeatedly refit the model while accumulating included studies."""
|
|
344
|
+
|
|
345
|
+
from .sensitivity import cumulative
|
|
346
|
+
|
|
347
|
+
return cumulative(
|
|
348
|
+
self,
|
|
349
|
+
order=order,
|
|
350
|
+
ascending=ascending,
|
|
351
|
+
collapse=collapse,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def forest(
|
|
355
|
+
self,
|
|
356
|
+
*,
|
|
357
|
+
ax: Axes | None = None,
|
|
358
|
+
effect_label: str | None = None,
|
|
359
|
+
pooled_label: str | None = None,
|
|
360
|
+
show_prediction_interval: bool = True,
|
|
361
|
+
show_weights: bool = True,
|
|
362
|
+
null_value: float | None = None,
|
|
363
|
+
log_scale: bool | None = None,
|
|
364
|
+
) -> Axes:
|
|
365
|
+
"""Draw a Matplotlib forest plot without calling ``show()``.
|
|
366
|
+
|
|
367
|
+
Matplotlib is an optional dependency. Install ``PyMetaAnalysis[plot]``
|
|
368
|
+
before calling this method.
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
from .plotting import forest_plot
|
|
372
|
+
|
|
373
|
+
return forest_plot(
|
|
374
|
+
self,
|
|
375
|
+
ax=ax,
|
|
376
|
+
effect_label=effect_label,
|
|
377
|
+
pooled_label=pooled_label,
|
|
378
|
+
show_prediction_interval=show_prediction_interval,
|
|
379
|
+
show_weights=show_weights,
|
|
380
|
+
null_value=null_value,
|
|
381
|
+
log_scale=log_scale,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def funnel(
|
|
385
|
+
self,
|
|
386
|
+
*,
|
|
387
|
+
ax: Axes | None = None,
|
|
388
|
+
effect_label: str | None = None,
|
|
389
|
+
confidence_level: float | None = None,
|
|
390
|
+
show_pseudo_confidence_interval: bool = True,
|
|
391
|
+
warn_on_few_studies: bool = True,
|
|
392
|
+
log_scale: bool | None = None,
|
|
393
|
+
) -> Axes:
|
|
394
|
+
"""Draw a standard-error funnel plot without calling ``show()``.
|
|
395
|
+
|
|
396
|
+
Matplotlib is an optional dependency. Install ``PyMetaAnalysis[plot]``
|
|
397
|
+
before calling this method. Funnel asymmetry is a diagnostic for
|
|
398
|
+
possible small-study effects and does not by itself establish
|
|
399
|
+
publication bias.
|
|
400
|
+
"""
|
|
401
|
+
|
|
402
|
+
from .plotting import funnel_plot
|
|
403
|
+
|
|
404
|
+
return funnel_plot(
|
|
405
|
+
self,
|
|
406
|
+
ax=ax,
|
|
407
|
+
effect_label=effect_label,
|
|
408
|
+
confidence_level=confidence_level,
|
|
409
|
+
show_pseudo_confidence_interval=show_pseudo_confidence_interval,
|
|
410
|
+
warn_on_few_studies=warn_on_few_studies,
|
|
411
|
+
log_scale=log_scale,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
@dataclass(frozen=True, slots=True)
|
|
416
|
+
class SubgroupMetaAnalysisResult:
|
|
417
|
+
"""Results for independent study subgroups and their formal comparison."""
|
|
418
|
+
|
|
419
|
+
groups: Mapping[Hashable, MetaAnalysisResult]
|
|
420
|
+
overall: MetaAnalysisResult
|
|
421
|
+
q_between: float
|
|
422
|
+
q_between_df: int
|
|
423
|
+
q_between_pvalue: float
|
|
424
|
+
i2_between: float
|
|
425
|
+
method: SubgroupMethodConfig
|
|
426
|
+
warnings: tuple[str, ...]
|
|
427
|
+
_study_results: pd.DataFrame = field(repr=False, compare=False)
|
|
428
|
+
|
|
429
|
+
def __post_init__(self) -> None:
|
|
430
|
+
"""Defensively freeze the group mapping and combined study table."""
|
|
431
|
+
|
|
432
|
+
object.__setattr__(self, "groups", MappingProxyType(dict(self.groups)))
|
|
433
|
+
object.__setattr__(self, "_study_results", self._study_results.copy(deep=True))
|
|
434
|
+
|
|
435
|
+
@property
|
|
436
|
+
def study_results(self) -> pd.DataFrame:
|
|
437
|
+
"""Return all rows with their subgroup labels and overall fit metadata."""
|
|
438
|
+
|
|
439
|
+
return self._study_results.copy(deep=True)
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def excluded_studies(self) -> pd.DataFrame:
|
|
443
|
+
"""Return rows excluded from the overall and subgroup fits."""
|
|
444
|
+
|
|
445
|
+
studies = self._study_results
|
|
446
|
+
return studies.loc[~studies["included"]].copy(deep=True)
|
|
447
|
+
|
|
448
|
+
def summary(self) -> SubgroupMetaAnalysisSummary:
|
|
449
|
+
"""Return a printable and machine-readable subgroup summary."""
|
|
450
|
+
|
|
451
|
+
return SubgroupMetaAnalysisSummary(self)
|
|
452
|
+
|
|
453
|
+
def method_details(self) -> str:
|
|
454
|
+
"""Return a Methods-style description including subgroup assumptions."""
|
|
455
|
+
|
|
456
|
+
from .reporting import subgroup_method_details
|
|
457
|
+
|
|
458
|
+
return subgroup_method_details(self)
|
|
459
|
+
|
|
460
|
+
def report(self, *, include_studies: bool = True) -> ResultReport:
|
|
461
|
+
"""Build a structured subgroup report with JSON and Markdown forms."""
|
|
462
|
+
|
|
463
|
+
from .reporting import build_subgroup_report
|
|
464
|
+
|
|
465
|
+
return build_subgroup_report(self, include_studies=include_studies)
|
|
466
|
+
|
|
467
|
+
def to_dataframe(self) -> pd.DataFrame:
|
|
468
|
+
"""Return the combined row-level subgroup table."""
|
|
469
|
+
|
|
470
|
+
return self.study_results
|
|
471
|
+
|
|
472
|
+
def leave_one_out(self) -> SubgroupLeaveOneOutResult:
|
|
473
|
+
"""Run leave-one-out analyses for each subgroup and the overall model."""
|
|
474
|
+
|
|
475
|
+
from .sensitivity import subgroup_leave_one_out
|
|
476
|
+
|
|
477
|
+
return subgroup_leave_one_out(self)
|
|
478
|
+
|
|
479
|
+
def cumulative(
|
|
480
|
+
self,
|
|
481
|
+
*,
|
|
482
|
+
order: ColumnOrArray | None = None,
|
|
483
|
+
ascending: bool = True,
|
|
484
|
+
collapse: bool = False,
|
|
485
|
+
) -> SubgroupCumulativeMetaAnalysisResult:
|
|
486
|
+
"""Run cumulative analyses for each subgroup and the overall model."""
|
|
487
|
+
|
|
488
|
+
from .sensitivity import subgroup_cumulative
|
|
489
|
+
|
|
490
|
+
return subgroup_cumulative(
|
|
491
|
+
self,
|
|
492
|
+
order=order,
|
|
493
|
+
ascending=ascending,
|
|
494
|
+
collapse=collapse,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
def forest(
|
|
498
|
+
self,
|
|
499
|
+
*,
|
|
500
|
+
ax: Axes | None = None,
|
|
501
|
+
effect_label: str | None = None,
|
|
502
|
+
show_prediction_interval: bool = True,
|
|
503
|
+
show_weights: bool = True,
|
|
504
|
+
null_value: float | None = None,
|
|
505
|
+
log_scale: bool | None = None,
|
|
506
|
+
) -> Axes:
|
|
507
|
+
"""Draw a subgroup forest plot without calling ``show()``."""
|
|
508
|
+
|
|
509
|
+
from .plotting import subgroup_forest_plot
|
|
510
|
+
|
|
511
|
+
return subgroup_forest_plot(
|
|
512
|
+
self,
|
|
513
|
+
ax=ax,
|
|
514
|
+
effect_label=effect_label,
|
|
515
|
+
show_prediction_interval=show_prediction_interval,
|
|
516
|
+
show_weights=show_weights,
|
|
517
|
+
null_value=null_value,
|
|
518
|
+
log_scale=log_scale,
|
|
519
|
+
)
|