PyMetaAnalysis 0.1.0__tar.gz → 0.2.0__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.
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/.github/workflows/release.yml +1 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/CHANGELOG.md +16 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/CITATION.cff +5 -3
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/PKG-INFO +7 -3
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/README.md +5 -1
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/getting-started.md +4 -3
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/generic-effects.md +17 -10
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/input-data.md +3 -1
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/provenance-reporting.md +4 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/r-interoperability.md +13 -1
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/index.md +4 -3
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/limitations.md +2 -3
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/reference/api.md +8 -2
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/releasing.md +4 -4
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/pyproject.toml +1 -1
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/_version.py +1 -1
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/api.py +85 -12
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/data.py +66 -24
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/reporting.py +7 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_api.py +84 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_properties.py +32 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_release_readiness.py +17 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_reporting.py +26 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_subgroups.py +26 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tools/check_release.py +1 -1
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tools/inspect_distribution.py +1 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/.github/workflows/ci.yml +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/.github/workflows/pages.yml +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/.gitignore +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/CONTRIBUTING.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/LICENSE +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/SECURITY.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/benchmarks/README.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/benchmarks/benchmark_core.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/adr/0001-optional-matplotlib.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/adr/0002-statistical-policy.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/citation.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/development.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/binary-outcomes.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/continuous-outcomes.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/method-selection.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/plotting.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/sensitivity-analysis.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/zero-events.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/installation.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/methods/statistical-methods.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/reference/report-schema.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/reference/results.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/stylesheets/extra.css +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/validation.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/examples/README.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/examples/quickstart.ipynb +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/mkdocs.yml +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/__init__.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/binary_api.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/config.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/continuous_api.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/effect_sizes/__init__.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/effect_sizes/binary.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/effect_sizes/continuous.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/estimators/__init__.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/estimators/inverse_variance.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/estimators/mantel_haenszel.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/estimators/tau2.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/exceptions.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/heterogeneity.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/plotting/__init__.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/plotting/_utils.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/plotting/forest.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/plotting/funnel.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/plotting/subgroup_forest.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/provenance.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/py.typed +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/results.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/sensitivity.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/subgroups.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/README.md +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/binary_input.csv +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/binary_metafor.json +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/binary_sparse_input.csv +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/continuous_input.csv +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/continuous_metafor.json +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/generate_binary_metafor.R +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/generate_continuous_metafor.R +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/generate_generic_metafor.R +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/generate_workflow_metafor.R +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/generic_input.csv +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/generic_metafor.json +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/workflow_input.csv +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/workflow_metafor.json +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_binary.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_continuous.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_documentation.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_estimators.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_funnel_plot.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_numerical_stability.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_plotting.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_r_references.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_reference_results.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_sensitivity.py +0 -0
- {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tools/execute_notebooks.py +0 -0
|
@@ -6,6 +6,22 @@ Changes planned for the next release accumulate under `Unreleased`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.2.0 - 2026-07-16
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- generic `meta_analysis()` accepts either sampling variances or standard
|
|
14
|
+
errors, with explicit validation and auditable conversion provenance.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- package author metadata identifies the project maintainer directly.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- GitHub Release creation receives explicit repository context in tag-driven
|
|
23
|
+
release jobs.
|
|
24
|
+
|
|
9
25
|
## 0.1.0 - 2026-07-15
|
|
10
26
|
|
|
11
27
|
### Added
|
|
@@ -5,9 +5,11 @@ message: >-
|
|
|
5
5
|
title: PyMetaAnalysis
|
|
6
6
|
type: software
|
|
7
7
|
authors:
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
- family-names: Ding
|
|
9
|
+
given-names: Zhaobo
|
|
10
|
+
email: ding.zb@yahoo.com
|
|
11
|
+
version: 0.2.0
|
|
12
|
+
date-released: 2026-07-16
|
|
11
13
|
repository-code: https://github.com/ZhaoboDing/PyMetaAnalysis
|
|
12
14
|
url: https://zhaoboding.github.io/PyMetaAnalysis/
|
|
13
15
|
license: MIT
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyMetaAnalysis
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A pandas-first, auditable meta-analysis library for Python
|
|
5
5
|
Project-URL: Documentation, https://zhaoboding.github.io/PyMetaAnalysis/
|
|
6
6
|
Project-URL: Source, https://github.com/ZhaoboDing/PyMetaAnalysis
|
|
7
7
|
Project-URL: Issues, https://github.com/ZhaoboDing/PyMetaAnalysis/issues
|
|
8
8
|
Project-URL: Changelog, https://github.com/ZhaoboDing/PyMetaAnalysis/blob/main/CHANGELOG.md
|
|
9
|
-
Author:
|
|
9
|
+
Author-email: Zhaobo Ding <ding.zb@yahoo.com>
|
|
10
10
|
Maintainer-email: Zhaobo Ding <ding.zb@yahoo.com>
|
|
11
11
|
License-Expression: MIT
|
|
12
12
|
License-File: LICENSE
|
|
@@ -115,7 +115,7 @@ subgroup differences.
|
|
|
115
115
|
|
|
116
116
|
| Input | Effects | Pooling/models |
|
|
117
117
|
| --- | --- | --- |
|
|
118
|
-
| Effect + sampling variance | Generic | Common/random inverse variance |
|
|
118
|
+
| Effect + sampling variance or standard error | Generic | Common/random inverse variance |
|
|
119
119
|
| Two-group events + totals | OR, RR, RD | Common MH OR/RR; common/random IV |
|
|
120
120
|
| Two-group means + SDs + sizes | MD, Hedges' g | Common/random inverse variance |
|
|
121
121
|
|
|
@@ -124,6 +124,10 @@ and DerSimonian-Laird tau-squared estimators. Mean confidence intervals support
|
|
|
124
124
|
the normal default plus unmodified and safeguarded Hartung-Knapp variants.
|
|
125
125
|
Eligible random-effects fits include an HTS prediction interval.
|
|
126
126
|
|
|
127
|
+
Generic analyses accept exactly one of `variance=` or `standard_error=`.
|
|
128
|
+
Standard errors are squared internally and the conversion is recorded in the
|
|
129
|
+
result provenance.
|
|
130
|
+
|
|
127
131
|
Sparse binary behavior is explicit: study-level and Mantel-Haenszel continuity
|
|
128
132
|
corrections are separate, relative-effect double-zero/double-all rows remain
|
|
129
133
|
visible as exclusions, and RD exposes
|
|
@@ -66,7 +66,7 @@ subgroup differences.
|
|
|
66
66
|
|
|
67
67
|
| Input | Effects | Pooling/models |
|
|
68
68
|
| --- | --- | --- |
|
|
69
|
-
| Effect + sampling variance | Generic | Common/random inverse variance |
|
|
69
|
+
| Effect + sampling variance or standard error | Generic | Common/random inverse variance |
|
|
70
70
|
| Two-group events + totals | OR, RR, RD | Common MH OR/RR; common/random IV |
|
|
71
71
|
| Two-group means + SDs + sizes | MD, Hedges' g | Common/random inverse variance |
|
|
72
72
|
|
|
@@ -75,6 +75,10 @@ and DerSimonian-Laird tau-squared estimators. Mean confidence intervals support
|
|
|
75
75
|
the normal default plus unmodified and safeguarded Hartung-Knapp variants.
|
|
76
76
|
Eligible random-effects fits include an HTS prediction interval.
|
|
77
77
|
|
|
78
|
+
Generic analyses accept exactly one of `variance=` or `standard_error=`.
|
|
79
|
+
Standard errors are squared internally and the conversion is recorded in the
|
|
80
|
+
result provenance.
|
|
81
|
+
|
|
78
82
|
Sparse binary behavior is explicit: study-level and Mantel-Haenszel continuity
|
|
79
83
|
corrections are separate, relative-effect double-zero/double-all rows remain
|
|
80
84
|
visible as exclusions, and RD exposes
|
|
@@ -20,9 +20,10 @@ See [installation](installation.md) for all extras and development checks.
|
|
|
20
20
|
|
|
21
21
|
## 2. Prepare a DataFrame
|
|
22
22
|
|
|
23
|
-
The generic API expects one effect estimate and
|
|
24
|
-
variance
|
|
25
|
-
|
|
23
|
+
The generic API expects one effect estimate and either a strictly positive
|
|
24
|
+
sampling variance or standard error per study. Supply `variance=` or
|
|
25
|
+
`standard_error=`; standard errors are squared internally and recorded in
|
|
26
|
+
provenance.
|
|
26
27
|
|
|
27
28
|
```python
|
|
28
29
|
import pandas as pd
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Generic effects
|
|
2
2
|
|
|
3
3
|
Use `meta_analysis()` when each study already has an effect estimate and its
|
|
4
|
-
sampling variance. This is the generic inverse-variance
|
|
5
|
-
calculate an outcome-specific effect size.
|
|
4
|
+
sampling variance or standard error. This is the generic inverse-variance
|
|
5
|
+
workflow; it does not calculate an outcome-specific effect size.
|
|
6
6
|
|
|
7
7
|
The pooling, tau-squared, confidence-interval, prediction-interval, and
|
|
8
8
|
heterogeneity equations are specified under
|
|
@@ -49,22 +49,29 @@ result = ma.meta_analysis(
|
|
|
49
49
|
All array-like arguments must be one-dimensional and have equal lengths.
|
|
50
50
|
Generated row labels start at zero when no study labels are supplied.
|
|
51
51
|
|
|
52
|
-
## Variance
|
|
52
|
+
## Variance or standard error
|
|
53
53
|
|
|
54
|
-
`variance=`
|
|
55
|
-
reported standard
|
|
54
|
+
Supply exactly one of `variance=` or `standard_error=`. Both must contain
|
|
55
|
+
finite, strictly positive values. A reported standard-error column can be used
|
|
56
|
+
directly:
|
|
56
57
|
|
|
57
58
|
```python
|
|
58
|
-
|
|
59
|
+
result = ma.meta_analysis(
|
|
60
|
+
data,
|
|
61
|
+
effect="yi",
|
|
62
|
+
standard_error="standard_error",
|
|
63
|
+
)
|
|
59
64
|
```
|
|
60
65
|
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
PyMetaAnalysis squares standard errors internally, retains both uncertainty
|
|
67
|
+
columns in the study table, and records the conversion in provenance. Do not
|
|
68
|
+
pass confidence-interval widths or study sample standard deviations as
|
|
69
|
+
standard errors; they describe different quantities.
|
|
63
70
|
|
|
64
71
|
## Missing values
|
|
65
72
|
|
|
66
|
-
The default `missing="raise"` rejects missing effects or
|
|
67
|
-
retain incomplete rows as structured exclusions:
|
|
73
|
+
The default `missing="raise"` rejects missing effects or values in the selected
|
|
74
|
+
uncertainty input. To retain incomplete rows as structured exclusions:
|
|
68
75
|
|
|
69
76
|
```python
|
|
70
77
|
result = ma.meta_analysis(
|
|
@@ -30,6 +30,8 @@ result = ma.meta_analysis(
|
|
|
30
30
|
|
|
31
31
|
Column names must exist. Array-like inputs may be mixed with DataFrame columns,
|
|
32
32
|
but every array must contain exactly one value for every DataFrame row.
|
|
33
|
+
Generic analyses require exactly one uncertainty input: `variance=` or
|
|
34
|
+
`standard_error=`.
|
|
33
35
|
|
|
34
36
|
## Array-like inputs
|
|
35
37
|
|
|
@@ -38,7 +40,7 @@ Lists, tuples, pandas Series, and one-dimensional NumPy arrays are accepted:
|
|
|
38
40
|
```python
|
|
39
41
|
result = ma.meta_analysis(
|
|
40
42
|
effect=[0.1, 0.3, -0.2],
|
|
41
|
-
|
|
43
|
+
standard_error=[0.14, 0.20, 0.17],
|
|
42
44
|
study=["A", "B", "C"],
|
|
43
45
|
)
|
|
44
46
|
```
|
|
@@ -38,6 +38,10 @@ document.
|
|
|
38
38
|
Each `TransformationRecord` has a stable name, resolved parameters, and the
|
|
39
39
|
original row IDs it affected.
|
|
40
40
|
|
|
41
|
+
Generic analyses supplied through `standard_error=` record a
|
|
42
|
+
`standard_error_to_variance` transformation for every non-missing standard
|
|
43
|
+
error that was squared before fitting.
|
|
44
|
+
|
|
41
45
|
Binary analyses record:
|
|
42
46
|
|
|
43
47
|
- the OR, RR, or RD effect-size transformation and its scales;
|
|
@@ -139,7 +139,19 @@ rma.uni(
|
|
|
139
139
|
```
|
|
140
140
|
|
|
141
141
|
The corresponding R `meta` configuration starts from standard errors rather
|
|
142
|
-
than variances
|
|
142
|
+
than variances. PyMetaAnalysis can accept that uncertainty column directly as
|
|
143
|
+
`standard_error=`; no manual squaring step is required:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
result = ma.meta_analysis(
|
|
147
|
+
studies,
|
|
148
|
+
effect="yi",
|
|
149
|
+
standard_error="sei",
|
|
150
|
+
model="random",
|
|
151
|
+
tau2_method="REML",
|
|
152
|
+
ci_method="hartung_knapp_adhoc",
|
|
153
|
+
)
|
|
154
|
+
```
|
|
143
155
|
|
|
144
156
|
```r
|
|
145
157
|
metagen(
|
|
@@ -82,9 +82,10 @@ lists unsupported methods explicitly.
|
|
|
82
82
|
|
|
83
83
|
## Project status
|
|
84
84
|
|
|
85
|
-
PyMetaAnalysis 0.
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
PyMetaAnalysis 0.2.0 adds direct generic standard-error input to the initial
|
|
86
|
+
public analysis scope. It has not undergone a formal external statistical
|
|
87
|
+
audit. Pin the package version for consequential work and independently check
|
|
88
|
+
important analyses. See the repository
|
|
88
89
|
[changelog](https://github.com/ZhaoboDing/PyMetaAnalysis/blob/main/CHANGELOG.md)
|
|
89
90
|
and [contribution guide](development.md). For manuscripts and archived
|
|
90
91
|
analyses, see [citing PyMetaAnalysis](citation.md).
|
|
@@ -27,8 +27,7 @@ deferred functionality explicit.
|
|
|
27
27
|
- multilevel, multivariate, network, dose-response, diagnostic-accuracy, or
|
|
28
28
|
individual-participant-data meta-analysis;
|
|
29
29
|
- robust variance estimation or dependent-effect clustering;
|
|
30
|
-
- single proportions, incidence rates, correlations, survival outcomes
|
|
31
|
-
generic standard-error input helpers;
|
|
30
|
+
- single proportions, incidence rates, correlations, or survival outcomes;
|
|
32
31
|
- Knapp-Hartung variants beyond the two documented choices;
|
|
33
32
|
- alternative prediction-interval methods;
|
|
34
33
|
- formal funnel-asymmetry, trim-and-fill, selection-model, or publication-bias
|
|
@@ -70,7 +69,7 @@ workflow must version those artifacts separately.
|
|
|
70
69
|
|
|
71
70
|
## Stability and review status
|
|
72
71
|
|
|
73
|
-
The package version is currently `0.
|
|
72
|
+
The package version is currently `0.2.0`. Public APIs and serialized schemas
|
|
74
73
|
may change during the 0.x series. Pin versions in analysis environments and
|
|
75
74
|
inspect changelog/schema updates before upgrading.
|
|
76
75
|
|
|
@@ -47,7 +47,8 @@ ma.meta_analysis(
|
|
|
47
47
|
data=None,
|
|
48
48
|
*,
|
|
49
49
|
effect,
|
|
50
|
-
variance,
|
|
50
|
+
variance=None,
|
|
51
|
+
standard_error=None,
|
|
51
52
|
study=None,
|
|
52
53
|
subgroup=None,
|
|
53
54
|
model="random",
|
|
@@ -66,7 +67,8 @@ Fits generic study effects using inverse-variance pooling.
|
|
|
66
67
|
| --- | --- |
|
|
67
68
|
| `data` | Optional DataFrame used by string-valued input selectors |
|
|
68
69
|
| `effect` | Study effect column or array on a consistent model scale |
|
|
69
|
-
| `variance` | Finite, strictly positive sampling variances
|
|
70
|
+
| `variance` | Finite, strictly positive sampling variances |
|
|
71
|
+
| `standard_error` | Finite, strictly positive standard errors, squared internally |
|
|
70
72
|
| `study` | Optional label column/array; defaults to index or row number |
|
|
71
73
|
| `subgroup` | Optional subgroup column/array |
|
|
72
74
|
| `model` | `"common"` or `"random"` |
|
|
@@ -77,6 +79,10 @@ Fits generic study effects using inverse-variance pooling.
|
|
|
77
79
|
| `atol` | Strictly positive iterative-estimator tolerance |
|
|
78
80
|
| `max_iter` | Positive iterative-estimator iteration limit |
|
|
79
81
|
|
|
82
|
+
Supply exactly one of `variance` or `standard_error`. The selected argument
|
|
83
|
+
supports the same DataFrame-column and array-like conventions as `effect`.
|
|
84
|
+
Standard-error conversion is recorded in result provenance.
|
|
85
|
+
|
|
80
86
|
## `meta_binary()`
|
|
81
87
|
|
|
82
88
|
```text
|
|
@@ -44,7 +44,7 @@ and its
|
|
|
44
44
|
`CITATION.cff`. Hatchling reads the package version dynamically from
|
|
45
45
|
`_version.py`.
|
|
46
46
|
4. Move the relevant `CHANGELOG.md` entries from `Unreleased` to a dated
|
|
47
|
-
heading such as `##
|
|
47
|
+
heading such as `## X.Y.Z - YYYY-MM-DD`.
|
|
48
48
|
5. Run the complete validation suite.
|
|
49
49
|
|
|
50
50
|
The local consistency check is:
|
|
@@ -56,7 +56,7 @@ python tools/check_release.py
|
|
|
56
56
|
For a proposed tag, add:
|
|
57
57
|
|
|
58
58
|
```console
|
|
59
|
-
python tools/check_release.py --tag
|
|
59
|
+
python tools/check_release.py --tag vX.Y.Z
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
The tag check rejects development versions, version mismatches, and releases
|
|
@@ -90,8 +90,8 @@ commit:
|
|
|
90
90
|
```console
|
|
91
91
|
git switch main
|
|
92
92
|
git pull --ff-only
|
|
93
|
-
git tag -a
|
|
94
|
-
git push origin
|
|
93
|
+
git tag -a vX.Y.Z -m "PyMetaAnalysis X.Y.Z"
|
|
94
|
+
git push origin vX.Y.Z
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
The release workflow then:
|
|
@@ -11,7 +11,7 @@ requires-python = ">=3.10"
|
|
|
11
11
|
license = "MIT"
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
authors = [
|
|
14
|
-
{ name = "
|
|
14
|
+
{ name = "Zhaobo Ding", email = "ding.zb@yahoo.com" },
|
|
15
15
|
]
|
|
16
16
|
maintainers = [
|
|
17
17
|
{ name = "Zhaobo Ding", email = "ding.zb@yahoo.com" },
|
|
@@ -13,7 +13,11 @@ from .data import ColumnOrArray, MissingPolicy, normalize_studies
|
|
|
13
13
|
from .estimators import fit_inverse_variance
|
|
14
14
|
from .exceptions import InvalidStudyDataError, UnsupportedMethodError
|
|
15
15
|
from .heterogeneity import classical_heterogeneity, tau2_inconsistency
|
|
16
|
-
from .provenance import
|
|
16
|
+
from .provenance import (
|
|
17
|
+
TransformationRecord,
|
|
18
|
+
add_input_field,
|
|
19
|
+
build_analysis_provenance,
|
|
20
|
+
)
|
|
17
21
|
from .results import (
|
|
18
22
|
FitDiagnostics,
|
|
19
23
|
HeterogeneityResult,
|
|
@@ -65,7 +69,8 @@ def _fit_meta_analysis_single(
|
|
|
65
69
|
data: pd.DataFrame | None = None,
|
|
66
70
|
*,
|
|
67
71
|
effect: ColumnOrArray,
|
|
68
|
-
variance: ColumnOrArray,
|
|
72
|
+
variance: ColumnOrArray | None = None,
|
|
73
|
+
standard_error: ColumnOrArray | None = None,
|
|
69
74
|
study: ColumnOrArray | None = None,
|
|
70
75
|
model: str = "random",
|
|
71
76
|
tau2_method: str = "REML",
|
|
@@ -82,9 +87,14 @@ def _fit_meta_analysis_single(
|
|
|
82
87
|
data:
|
|
83
88
|
Optional pandas DataFrame. String-valued input arguments select columns
|
|
84
89
|
from this frame.
|
|
85
|
-
effect
|
|
90
|
+
effect:
|
|
86
91
|
A DataFrame column name or one-dimensional array-like containing study
|
|
87
|
-
effects
|
|
92
|
+
effects.
|
|
93
|
+
variance, standard_error:
|
|
94
|
+
Exactly one must be provided as a DataFrame column name or
|
|
95
|
+
one-dimensional array-like. Values must be finite and strictly
|
|
96
|
+
positive. Standard errors are squared internally to obtain sampling
|
|
97
|
+
variances.
|
|
88
98
|
study:
|
|
89
99
|
Optional study label column/array. DataFrame input defaults to its index;
|
|
90
100
|
array-only input defaults to integer row labels.
|
|
@@ -118,6 +128,7 @@ def _fit_meta_analysis_single(
|
|
|
118
128
|
data=data,
|
|
119
129
|
effect=effect,
|
|
120
130
|
variance=variance,
|
|
131
|
+
standard_error=standard_error,
|
|
121
132
|
study=study,
|
|
122
133
|
missing=missing,
|
|
123
134
|
)
|
|
@@ -190,12 +201,30 @@ def _fit_meta_analysis_single(
|
|
|
190
201
|
max_iter=max_iter,
|
|
191
202
|
options=(),
|
|
192
203
|
)
|
|
204
|
+
transformations: tuple[TransformationRecord, ...] = ()
|
|
205
|
+
if standard_error is not None:
|
|
206
|
+
uncertainty_input = ("standard_error", standard_error)
|
|
207
|
+
transformed_rows = tuple(
|
|
208
|
+
int(row) for row in np.flatnonzero(~pd.isna(studies.variance))
|
|
209
|
+
)
|
|
210
|
+
transformations = (
|
|
211
|
+
TransformationRecord(
|
|
212
|
+
name="standard_error_to_variance",
|
|
213
|
+
affected_rows=transformed_rows,
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
if variance is None: # pragma: no cover - validated by normalize_studies
|
|
218
|
+
raise RuntimeError("variance input unexpectedly missing")
|
|
219
|
+
uncertainty_input = ("variance", variance)
|
|
220
|
+
|
|
193
221
|
provenance = build_analysis_provenance(
|
|
194
222
|
analysis_type="generic",
|
|
195
223
|
data=data,
|
|
196
|
-
inputs=(("effect", effect),
|
|
224
|
+
inputs=(("effect", effect), uncertainty_input),
|
|
197
225
|
study=study,
|
|
198
226
|
included=studies.included,
|
|
227
|
+
transformations=transformations,
|
|
199
228
|
)
|
|
200
229
|
|
|
201
230
|
return MetaAnalysisResult(
|
|
@@ -226,6 +255,26 @@ def meta_analysis(
|
|
|
226
255
|
*,
|
|
227
256
|
effect: ColumnOrArray,
|
|
228
257
|
variance: ColumnOrArray,
|
|
258
|
+
standard_error: None = None,
|
|
259
|
+
study: ColumnOrArray | None = None,
|
|
260
|
+
subgroup: None = None,
|
|
261
|
+
model: str = "random",
|
|
262
|
+
tau2_method: str = "REML",
|
|
263
|
+
ci_method: str = "normal",
|
|
264
|
+
confidence_level: float = 0.95,
|
|
265
|
+
missing: MissingPolicy = "raise",
|
|
266
|
+
atol: float = 1e-10,
|
|
267
|
+
max_iter: int = 1000,
|
|
268
|
+
) -> MetaAnalysisResult: ...
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@overload
|
|
272
|
+
def meta_analysis(
|
|
273
|
+
data: pd.DataFrame | None = None,
|
|
274
|
+
*,
|
|
275
|
+
effect: ColumnOrArray,
|
|
276
|
+
variance: None = None,
|
|
277
|
+
standard_error: ColumnOrArray,
|
|
229
278
|
study: ColumnOrArray | None = None,
|
|
230
279
|
subgroup: None = None,
|
|
231
280
|
model: str = "random",
|
|
@@ -244,6 +293,7 @@ def meta_analysis(
|
|
|
244
293
|
*,
|
|
245
294
|
effect: ColumnOrArray,
|
|
246
295
|
variance: ColumnOrArray,
|
|
296
|
+
standard_error: None = None,
|
|
247
297
|
study: ColumnOrArray | None = None,
|
|
248
298
|
subgroup: ColumnOrArray,
|
|
249
299
|
model: str = "random",
|
|
@@ -256,11 +306,31 @@ def meta_analysis(
|
|
|
256
306
|
) -> SubgroupMetaAnalysisResult: ...
|
|
257
307
|
|
|
258
308
|
|
|
309
|
+
@overload
|
|
259
310
|
def meta_analysis(
|
|
260
311
|
data: pd.DataFrame | None = None,
|
|
261
312
|
*,
|
|
262
313
|
effect: ColumnOrArray,
|
|
263
|
-
variance:
|
|
314
|
+
variance: None = None,
|
|
315
|
+
standard_error: ColumnOrArray,
|
|
316
|
+
study: ColumnOrArray | None = None,
|
|
317
|
+
subgroup: ColumnOrArray,
|
|
318
|
+
model: str = "random",
|
|
319
|
+
tau2_method: str = "REML",
|
|
320
|
+
ci_method: str = "normal",
|
|
321
|
+
confidence_level: float = 0.95,
|
|
322
|
+
missing: MissingPolicy = "raise",
|
|
323
|
+
atol: float = 1e-10,
|
|
324
|
+
max_iter: int = 1000,
|
|
325
|
+
) -> SubgroupMetaAnalysisResult: ...
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def meta_analysis(
|
|
329
|
+
data: pd.DataFrame | None = None,
|
|
330
|
+
*,
|
|
331
|
+
effect: ColumnOrArray,
|
|
332
|
+
variance: ColumnOrArray | None = None,
|
|
333
|
+
standard_error: ColumnOrArray | None = None,
|
|
264
334
|
study: ColumnOrArray | None = None,
|
|
265
335
|
subgroup: ColumnOrArray | None = None,
|
|
266
336
|
model: str = "random",
|
|
@@ -273,18 +343,21 @@ def meta_analysis(
|
|
|
273
343
|
) -> MetaAnalysisResult | SubgroupMetaAnalysisResult:
|
|
274
344
|
"""Fit a generic inverse-variance meta-analysis, optionally by subgroup.
|
|
275
345
|
|
|
276
|
-
``effect`` and
|
|
277
|
-
array-like values.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
346
|
+
``effect`` and the selected uncertainty input accept DataFrame column names
|
|
347
|
+
or one-dimensional array-like values. Supply exactly one of ``variance`` or
|
|
348
|
+
``standard_error``; standard errors are squared internally. Uncertainty
|
|
349
|
+
values must be finite and strictly positive. The default is a REML
|
|
350
|
+
random-effects model with a normal confidence interval. ``subgroup``
|
|
351
|
+
returns :class:`SubgroupMetaAnalysisResult` when supplied; otherwise the
|
|
352
|
+
return value is :class:`MetaAnalysisResult`. Missing subgroup labels are
|
|
353
|
+
rejected explicitly.
|
|
282
354
|
"""
|
|
283
355
|
|
|
284
356
|
overall = _fit_meta_analysis_single(
|
|
285
357
|
data,
|
|
286
358
|
effect=effect,
|
|
287
359
|
variance=variance,
|
|
360
|
+
standard_error=standard_error,
|
|
288
361
|
study=study,
|
|
289
362
|
model=model,
|
|
290
363
|
tau2_method=tau2_method,
|
|
@@ -70,6 +70,7 @@ def _study_labels(
|
|
|
70
70
|
*,
|
|
71
71
|
data: pd.DataFrame | None,
|
|
72
72
|
length: int,
|
|
73
|
+
uncertainty_label: str = "variance",
|
|
73
74
|
) -> NDArray[np.object_]:
|
|
74
75
|
if study is None:
|
|
75
76
|
labels: NDArray[Any]
|
|
@@ -82,17 +83,33 @@ def _study_labels(
|
|
|
82
83
|
|
|
83
84
|
if len(labels) != length:
|
|
84
85
|
raise InvalidStudyDataError(
|
|
85
|
-
f"study has length {len(labels)}, but effect and
|
|
86
|
-
f"length {length}."
|
|
86
|
+
f"study has length {len(labels)}, but effect and {uncertainty_label} "
|
|
87
|
+
f"have length {length}."
|
|
87
88
|
)
|
|
88
89
|
return np.asarray(labels, dtype=object)
|
|
89
90
|
|
|
90
91
|
|
|
92
|
+
def _select_uncertainty_input(
|
|
93
|
+
variance: ColumnOrArray | None,
|
|
94
|
+
standard_error: ColumnOrArray | None,
|
|
95
|
+
) -> tuple[ColumnOrArray, str, str]:
|
|
96
|
+
if (variance is None) == (standard_error is None):
|
|
97
|
+
raise InvalidStudyDataError(
|
|
98
|
+
"Exactly one of variance or standard_error must be provided."
|
|
99
|
+
)
|
|
100
|
+
if standard_error is not None:
|
|
101
|
+
return standard_error, "standard_error", "standard error"
|
|
102
|
+
if variance is None: # pragma: no cover - guarded by the exclusive check
|
|
103
|
+
raise RuntimeError("variance input unexpectedly missing")
|
|
104
|
+
return variance, "variance", "variance"
|
|
105
|
+
|
|
106
|
+
|
|
91
107
|
def normalize_studies(
|
|
92
108
|
*,
|
|
93
109
|
data: pd.DataFrame | None,
|
|
94
110
|
effect: ColumnOrArray,
|
|
95
|
-
variance: ColumnOrArray,
|
|
111
|
+
variance: ColumnOrArray | None,
|
|
112
|
+
standard_error: ColumnOrArray | None = None,
|
|
96
113
|
study: ColumnOrArray | None,
|
|
97
114
|
missing: MissingPolicy,
|
|
98
115
|
) -> NormalizedStudies:
|
|
@@ -103,12 +120,15 @@ def normalize_studies(
|
|
|
103
120
|
if missing not in {"raise", "drop"}:
|
|
104
121
|
raise InvalidStudyDataError("missing must be either 'raise' or 'drop'.")
|
|
105
122
|
|
|
123
|
+
uncertainty, uncertainty_name, uncertainty_label = _select_uncertainty_input(
|
|
124
|
+
variance, standard_error
|
|
125
|
+
)
|
|
106
126
|
raw_effect = _resolve_vector(effect, data=data, name="effect")
|
|
107
|
-
|
|
108
|
-
if len(raw_effect) != len(
|
|
127
|
+
raw_uncertainty = _resolve_vector(uncertainty, data=data, name=uncertainty_name)
|
|
128
|
+
if len(raw_effect) != len(raw_uncertainty):
|
|
109
129
|
raise InvalidStudyDataError(
|
|
110
|
-
"effect and
|
|
111
|
-
f"got {len(raw_effect)} and {len(
|
|
130
|
+
f"effect and {uncertainty_label} must have the same length; "
|
|
131
|
+
f"got {len(raw_effect)} and {len(raw_uncertainty)}."
|
|
112
132
|
)
|
|
113
133
|
if data is not None and len(data) != len(raw_effect):
|
|
114
134
|
raise InvalidStudyDataError(
|
|
@@ -116,55 +136,77 @@ def normalize_studies(
|
|
|
116
136
|
"DataFrame row."
|
|
117
137
|
)
|
|
118
138
|
|
|
119
|
-
labels = _study_labels(
|
|
139
|
+
labels = _study_labels(
|
|
140
|
+
study,
|
|
141
|
+
data=data,
|
|
142
|
+
length=len(raw_effect),
|
|
143
|
+
uncertainty_label=uncertainty_label,
|
|
144
|
+
)
|
|
120
145
|
|
|
121
146
|
try:
|
|
122
147
|
effect_values = np.asarray(raw_effect, dtype=np.float64)
|
|
123
|
-
|
|
148
|
+
uncertainty_values = np.asarray(raw_uncertainty, dtype=np.float64)
|
|
124
149
|
except (TypeError, ValueError) as error:
|
|
125
150
|
raise InvalidStudyDataError(
|
|
126
|
-
"effect and
|
|
151
|
+
f"effect and {uncertainty_label} must contain numeric values."
|
|
127
152
|
) from error
|
|
128
153
|
|
|
129
154
|
effect_missing = pd.isna(effect_values)
|
|
130
|
-
|
|
131
|
-
any_missing = effect_missing |
|
|
155
|
+
uncertainty_missing = pd.isna(uncertainty_values)
|
|
156
|
+
any_missing = effect_missing | uncertainty_missing
|
|
132
157
|
if np.any(any_missing) and missing == "raise":
|
|
133
158
|
rows = np.flatnonzero(any_missing).tolist()
|
|
134
159
|
raise InvalidStudyDataError(
|
|
135
|
-
f"Missing effect or
|
|
160
|
+
f"Missing effect or {uncertainty_label} values at row positions {rows}; "
|
|
136
161
|
"use missing='drop' to exclude them explicitly."
|
|
137
162
|
)
|
|
138
163
|
|
|
139
164
|
finite_effect = np.isfinite(effect_values) | effect_missing
|
|
140
|
-
|
|
165
|
+
finite_uncertainty = np.isfinite(uncertainty_values) | uncertainty_missing
|
|
141
166
|
if not np.all(finite_effect):
|
|
142
167
|
rows = np.flatnonzero(~finite_effect).tolist()
|
|
143
168
|
raise InvalidStudyDataError(
|
|
144
169
|
f"Effect values must be finite; invalid rows: {rows}."
|
|
145
170
|
)
|
|
146
|
-
if not np.all(
|
|
147
|
-
rows = np.flatnonzero(~
|
|
171
|
+
if not np.all(finite_uncertainty):
|
|
172
|
+
rows = np.flatnonzero(~finite_uncertainty).tolist()
|
|
148
173
|
raise InvalidStudyDataError(
|
|
149
|
-
f"
|
|
174
|
+
f"{uncertainty_label.capitalize()} values must be finite; "
|
|
175
|
+
f"invalid rows: {rows}."
|
|
150
176
|
)
|
|
151
177
|
|
|
152
|
-
|
|
153
|
-
if np.any(
|
|
154
|
-
rows = np.flatnonzero(
|
|
178
|
+
nonpositive_uncertainty = (~uncertainty_missing) & (uncertainty_values <= 0.0)
|
|
179
|
+
if np.any(nonpositive_uncertainty):
|
|
180
|
+
rows = np.flatnonzero(nonpositive_uncertainty).tolist()
|
|
155
181
|
raise InvalidStudyDataError(
|
|
156
|
-
f"Sampling
|
|
182
|
+
f"Sampling {uncertainty_label}s must be strictly positive; "
|
|
183
|
+
f"invalid rows: {rows}."
|
|
157
184
|
)
|
|
158
185
|
|
|
186
|
+
if uncertainty_name == "standard_error":
|
|
187
|
+
with np.errstate(over="ignore", under="ignore", invalid="ignore"):
|
|
188
|
+
variance_values = np.square(uncertainty_values)
|
|
189
|
+
invalid_variance = (~uncertainty_missing) & (
|
|
190
|
+
(~np.isfinite(variance_values)) | (variance_values <= 0.0)
|
|
191
|
+
)
|
|
192
|
+
if np.any(invalid_variance):
|
|
193
|
+
rows = np.flatnonzero(invalid_variance).tolist()
|
|
194
|
+
raise InvalidStudyDataError(
|
|
195
|
+
"Standard errors must produce finite, strictly positive sampling "
|
|
196
|
+
f"variances after squaring; invalid rows: {rows}."
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
variance_values = uncertainty_values
|
|
200
|
+
|
|
159
201
|
included = ~any_missing
|
|
160
202
|
reasons = np.full(len(effect_values), None, dtype=object)
|
|
161
203
|
for index in np.flatnonzero(any_missing):
|
|
162
|
-
if effect_missing[index] and
|
|
163
|
-
reasons[index] = "missing effect and
|
|
204
|
+
if effect_missing[index] and uncertainty_missing[index]:
|
|
205
|
+
reasons[index] = f"missing effect and {uncertainty_label}"
|
|
164
206
|
elif effect_missing[index]:
|
|
165
207
|
reasons[index] = "missing effect"
|
|
166
208
|
else:
|
|
167
|
-
reasons[index] = "missing
|
|
209
|
+
reasons[index] = f"missing {uncertainty_label}"
|
|
168
210
|
|
|
169
211
|
if not np.any(included):
|
|
170
212
|
raise InvalidStudyDataError(
|
|
@@ -105,6 +105,13 @@ def method_details(result: MetaAnalysisResult) -> str:
|
|
|
105
105
|
f"using {_measure_description(result.measure)}, pooled with {pooling}."
|
|
106
106
|
]
|
|
107
107
|
|
|
108
|
+
standard_error_rows = _transformation_rows(result, "standard_error_to_variance")
|
|
109
|
+
if standard_error_rows:
|
|
110
|
+
sentences.append(
|
|
111
|
+
"Supplied standard errors were squared to obtain sampling "
|
|
112
|
+
f"variances for {len(standard_error_rows)} row(s)."
|
|
113
|
+
)
|
|
114
|
+
|
|
108
115
|
if result.model == "random":
|
|
109
116
|
sentences.append(
|
|
110
117
|
"Between-study variance was estimated with "
|
|
@@ -42,6 +42,34 @@ def test_dataframe_columns_and_default_index_labels() -> None:
|
|
|
42
42
|
assert result.study_results["row_id"].tolist() == [0, 1, 2]
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def test_standard_error_input_matches_sampling_variance_input() -> None:
|
|
46
|
+
effect = np.asarray([0.1, 0.4, -0.2])
|
|
47
|
+
standard_error = np.asarray([0.2, 0.3, 0.4])
|
|
48
|
+
|
|
49
|
+
from_standard_error = ma.meta_analysis(
|
|
50
|
+
effect=effect,
|
|
51
|
+
standard_error=standard_error,
|
|
52
|
+
model="common",
|
|
53
|
+
)
|
|
54
|
+
from_variance = ma.meta_analysis(
|
|
55
|
+
effect=effect,
|
|
56
|
+
variance=standard_error**2,
|
|
57
|
+
model="common",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
assert from_standard_error.estimate == pytest.approx(from_variance.estimate)
|
|
61
|
+
assert from_standard_error.standard_error == pytest.approx(
|
|
62
|
+
from_variance.standard_error
|
|
63
|
+
)
|
|
64
|
+
assert from_standard_error.q == pytest.approx(from_variance.q)
|
|
65
|
+
np.testing.assert_allclose(
|
|
66
|
+
from_standard_error.study_results["variance"], standard_error**2
|
|
67
|
+
)
|
|
68
|
+
np.testing.assert_allclose(
|
|
69
|
+
from_standard_error.study_results["standard_error"], standard_error
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
45
73
|
def test_explicit_study_column_overrides_dataframe_index() -> None:
|
|
46
74
|
data = pd.DataFrame(
|
|
47
75
|
{
|
|
@@ -162,6 +190,62 @@ def test_invalid_study_data_raises_domain_error(
|
|
|
162
190
|
ma.meta_analysis(**kwargs, model="common") # type: ignore[arg-type]
|
|
163
191
|
|
|
164
192
|
|
|
193
|
+
@pytest.mark.parametrize(
|
|
194
|
+
("kwargs", "match"),
|
|
195
|
+
[
|
|
196
|
+
({"effect": [0.1]}, "Exactly one"),
|
|
197
|
+
(
|
|
198
|
+
{
|
|
199
|
+
"effect": [0.1],
|
|
200
|
+
"variance": [0.01],
|
|
201
|
+
"standard_error": [0.1],
|
|
202
|
+
},
|
|
203
|
+
"Exactly one",
|
|
204
|
+
),
|
|
205
|
+
(
|
|
206
|
+
{"effect": [0.1], "standard_error": [0.0]},
|
|
207
|
+
"standard errors must be strictly positive",
|
|
208
|
+
),
|
|
209
|
+
(
|
|
210
|
+
{"effect": [0.1], "standard_error": [np.inf]},
|
|
211
|
+
"Standard error values must be finite",
|
|
212
|
+
),
|
|
213
|
+
(
|
|
214
|
+
{"effect": [0.1], "standard_error": ["invalid"]},
|
|
215
|
+
"standard error must contain numeric values",
|
|
216
|
+
),
|
|
217
|
+
(
|
|
218
|
+
{"effect": [0.1], "standard_error": [1e308]},
|
|
219
|
+
"finite, strictly positive sampling variances after squaring",
|
|
220
|
+
),
|
|
221
|
+
(
|
|
222
|
+
{"effect": [0.1], "standard_error": [1e-300]},
|
|
223
|
+
"finite, strictly positive sampling variances after squaring",
|
|
224
|
+
),
|
|
225
|
+
],
|
|
226
|
+
)
|
|
227
|
+
def test_standard_error_input_errors_are_explicit(
|
|
228
|
+
kwargs: dict[str, object], match: str
|
|
229
|
+
) -> None:
|
|
230
|
+
with pytest.raises(ma.InvalidStudyDataError, match=match):
|
|
231
|
+
ma.meta_analysis(**kwargs, model="common") # type: ignore[arg-type]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_missing_standard_error_drop_records_specific_reasons() -> None:
|
|
235
|
+
result = ma.meta_analysis(
|
|
236
|
+
effect=[np.nan, 0.2, 0.3],
|
|
237
|
+
standard_error=[np.nan, np.nan, 0.2],
|
|
238
|
+
missing="drop",
|
|
239
|
+
model="common",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
assert result.study_results["exclusion_reason"].tolist() == [
|
|
243
|
+
"missing effect and standard error",
|
|
244
|
+
"missing standard error",
|
|
245
|
+
None,
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
|
|
165
249
|
def test_random_effects_requires_two_included_studies() -> None:
|
|
166
250
|
with pytest.raises(ma.InsufficientStudiesError, match="at least two"):
|
|
167
251
|
ma.meta_analysis(effect=[0.1], variance=[0.01], model="random")
|
|
@@ -82,3 +82,35 @@ def test_normalized_weights_are_nonnegative_and_sum_to_one(
|
|
|
82
82
|
|
|
83
83
|
assert np.all(weights >= 0.0)
|
|
84
84
|
assert np.sum(weights) == pytest.approx(1.0, abs=1e-12)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@given(study_vectors())
|
|
88
|
+
@settings(max_examples=75, deadline=None)
|
|
89
|
+
def test_standard_error_and_variance_inputs_are_equivalent(
|
|
90
|
+
vectors: tuple[np.ndarray, np.ndarray],
|
|
91
|
+
) -> None:
|
|
92
|
+
effect, variance = vectors
|
|
93
|
+
from_variance = ma.meta_analysis(
|
|
94
|
+
effect=effect,
|
|
95
|
+
variance=variance,
|
|
96
|
+
model="common",
|
|
97
|
+
)
|
|
98
|
+
from_standard_error = ma.meta_analysis(
|
|
99
|
+
effect=effect,
|
|
100
|
+
standard_error=np.sqrt(variance),
|
|
101
|
+
model="common",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
assert from_standard_error.estimate == pytest.approx(
|
|
105
|
+
from_variance.estimate, rel=1e-12, abs=1e-12
|
|
106
|
+
)
|
|
107
|
+
assert from_standard_error.standard_error == pytest.approx(
|
|
108
|
+
from_variance.standard_error, rel=1e-12, abs=1e-12
|
|
109
|
+
)
|
|
110
|
+
assert from_standard_error.q == pytest.approx(from_variance.q, rel=1e-11, abs=1e-11)
|
|
111
|
+
np.testing.assert_allclose(
|
|
112
|
+
from_standard_error.study_results["normalized_weight"],
|
|
113
|
+
from_variance.study_results["normalized_weight"],
|
|
114
|
+
rtol=1e-12,
|
|
115
|
+
atol=1e-12,
|
|
116
|
+
)
|
|
@@ -26,6 +26,10 @@ def test_release_metadata_sources_match() -> None:
|
|
|
26
26
|
assert 'license = "MIT"' in pyproject
|
|
27
27
|
assert 'license-files = ["LICENSE"]' in pyproject
|
|
28
28
|
assert '"License ::' not in pyproject
|
|
29
|
+
assert pyproject.count('{ name = "Zhaobo Ding", email = "ding.zb@yahoo.com" }') == 2
|
|
30
|
+
assert "family-names: Ding" in citation
|
|
31
|
+
assert "given-names: Zhaobo" in citation
|
|
32
|
+
assert "email: ding.zb@yahoo.com" in citation
|
|
29
33
|
|
|
30
34
|
|
|
31
35
|
def test_release_metadata_checker_runs() -> None:
|
|
@@ -39,6 +43,19 @@ def test_release_metadata_checker_runs() -> None:
|
|
|
39
43
|
assert ma.__version__ in completed.stdout
|
|
40
44
|
|
|
41
45
|
|
|
46
|
+
def test_github_release_command_has_repository_context() -> None:
|
|
47
|
+
workflow = (ROOT / ".github/workflows/release.yml").read_text(encoding="utf-8")
|
|
48
|
+
|
|
49
|
+
assert (
|
|
50
|
+
""" - name: Create release
|
|
51
|
+
env:
|
|
52
|
+
GH_TOKEN: ${{ github.token }}
|
|
53
|
+
GH_REPO: ${{ github.repository }}
|
|
54
|
+
"""
|
|
55
|
+
in workflow
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
42
59
|
def test_quickstart_notebook_is_valid_unexecuted_json() -> None:
|
|
43
60
|
path = ROOT / "examples" / "quickstart.ipynb"
|
|
44
61
|
notebook = json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -82,6 +82,32 @@ def test_array_inputs_and_generated_study_labels_are_explicit() -> None:
|
|
|
82
82
|
assert dict(result.provenance.column_mapping) == {}
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def test_standard_error_provenance_records_column_and_conversion() -> None:
|
|
86
|
+
data = pd.DataFrame(
|
|
87
|
+
{
|
|
88
|
+
"yi": [0.1, 0.2, 0.3],
|
|
89
|
+
"sei": [0.1, np.nan, 0.2],
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
result = ma.meta_analysis(
|
|
93
|
+
data,
|
|
94
|
+
effect="yi",
|
|
95
|
+
standard_error="sei",
|
|
96
|
+
missing="drop",
|
|
97
|
+
model="common",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert dict(result.provenance.column_mapping) == {
|
|
101
|
+
"effect": "yi",
|
|
102
|
+
"standard_error": "sei",
|
|
103
|
+
}
|
|
104
|
+
[transformation] = result.provenance.transformations
|
|
105
|
+
assert transformation.name == "standard_error_to_variance"
|
|
106
|
+
assert transformation.affected_rows == (0, 2)
|
|
107
|
+
assert "standard errors were squared" in result.method_details()
|
|
108
|
+
assert "for 2 row(s)" in result.method_details()
|
|
109
|
+
|
|
110
|
+
|
|
85
111
|
def test_binary_provenance_records_corrections_and_uninformative_rows() -> None:
|
|
86
112
|
result = _sparse_binary()
|
|
87
113
|
transformations = {
|
|
@@ -80,6 +80,32 @@ def test_dataframe_column_preserves_group_order_labels_and_global_row_ids() -> N
|
|
|
80
80
|
assert result.groups["A"].study_results["row_id"].tolist() == [2, 3]
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def test_generic_subgroups_accept_standard_error_column() -> None:
|
|
84
|
+
data = pd.DataFrame(
|
|
85
|
+
{
|
|
86
|
+
"yi": [0.0, 0.2, 0.8, 1.0],
|
|
87
|
+
"sei": [0.2, 0.2, 0.2, 0.2],
|
|
88
|
+
"region": ["A", "A", "B", "B"],
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
result = ma.meta_analysis(
|
|
92
|
+
data,
|
|
93
|
+
effect="yi",
|
|
94
|
+
standard_error="sei",
|
|
95
|
+
subgroup="region",
|
|
96
|
+
model="common",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
assert isinstance(result, ma.SubgroupMetaAnalysisResult)
|
|
100
|
+
assert result.overall.estimate == pytest.approx(0.5)
|
|
101
|
+
assert result.groups["A"].estimate == pytest.approx(0.1)
|
|
102
|
+
assert dict(result.overall.provenance.column_mapping) == {
|
|
103
|
+
"effect": "yi",
|
|
104
|
+
"standard_error": "sei",
|
|
105
|
+
"subgroup": "region",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
83
109
|
def test_binary_mantel_haenszel_supports_subgroups() -> None:
|
|
84
110
|
result = ma.meta_binary(
|
|
85
111
|
event_treat=[12, 5, 20, 7],
|
|
@@ -54,7 +54,7 @@ def _validate_tag(version: str, tag: str) -> None:
|
|
|
54
54
|
|
|
55
55
|
def main() -> None:
|
|
56
56
|
parser = argparse.ArgumentParser()
|
|
57
|
-
parser.add_argument("--tag", help="Optional release tag
|
|
57
|
+
parser.add_argument("--tag", help="Optional release tag in vX.Y.Z form")
|
|
58
58
|
arguments = parser.parse_args()
|
|
59
59
|
|
|
60
60
|
_validate_pyproject_version_source()
|
|
@@ -57,6 +57,7 @@ def _inspect(path: Path) -> None:
|
|
|
57
57
|
with zipfile.ZipFile(path) as archive:
|
|
58
58
|
metadata = archive.read(metadata_name).decode("utf-8")
|
|
59
59
|
required_metadata = {
|
|
60
|
+
"Author-email: Zhaobo Ding <ding.zb@yahoo.com>",
|
|
60
61
|
"License-Expression: MIT",
|
|
61
62
|
"License-File: LICENSE",
|
|
62
63
|
"Maintainer-email: Zhaobo Ding <ding.zb@yahoo.com>",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/estimators/inverse_variance.py
RENAMED
|
File without changes
|
{pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/estimators/mantel_haenszel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|