PyMetaAnalysis 0.2.1__tar.gz → 0.3.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.
Files changed (115) hide show
  1. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/CHANGELOG.md +22 -0
  2. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/CITATION.cff +2 -2
  3. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/PKG-INFO +22 -1
  4. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/README.md +21 -0
  5. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/benchmarks/README.md +5 -4
  6. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/benchmarks/benchmark_core.py +14 -2
  7. pymetaanalysis-0.3.0/docs/guides/meta-regression.md +220 -0
  8. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/guides/plotting.md +32 -1
  9. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/guides/provenance-reporting.md +1 -1
  10. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/index.md +3 -1
  11. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/limitations.md +13 -4
  12. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/methods/statistical-methods.md +121 -0
  13. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/reference/api.md +63 -3
  14. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/reference/report-schema.md +35 -4
  15. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/reference/results.md +69 -0
  16. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/releasing.md +18 -0
  17. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/validation.md +20 -0
  18. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/examples/README.md +4 -1
  19. pymetaanalysis-0.3.0/examples/meta_regression.ipynb +290 -0
  20. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/mkdocs.yml +1 -0
  21. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/__init__.py +17 -1
  22. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/_version.py +1 -1
  23. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/config.py +18 -0
  24. pymetaanalysis-0.3.0/src/meta_analyze/design_matrix.py +477 -0
  25. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/estimators/__init__.py +12 -0
  26. pymetaanalysis-0.3.0/src/meta_analyze/estimators/meta_regression.py +462 -0
  27. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/plotting/__init__.py +2 -1
  28. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/plotting/_utils.py +2 -2
  29. pymetaanalysis-0.3.0/src/meta_analyze/plotting/regression.py +128 -0
  30. pymetaanalysis-0.3.0/src/meta_analyze/regression_api.py +396 -0
  31. pymetaanalysis-0.3.0/src/meta_analyze/regression_results.py +388 -0
  32. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/reporting.py +169 -1
  33. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/reference/README.md +11 -0
  34. pymetaanalysis-0.3.0/tests/reference/generate_meta_regression_metafor.R +339 -0
  35. pymetaanalysis-0.3.0/tests/reference/meta_regression_boundary_input.csv +9 -0
  36. pymetaanalysis-0.3.0/tests/reference/meta_regression_input.csv +16 -0
  37. pymetaanalysis-0.3.0/tests/reference/meta_regression_metafor.json +1600 -0
  38. pymetaanalysis-0.3.0/tests/test_meta_regression.py +634 -0
  39. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/test_numerical_stability.py +85 -0
  40. pymetaanalysis-0.3.0/tests/test_properties.py +252 -0
  41. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/test_r_references.py +331 -1
  42. pymetaanalysis-0.3.0/tests/test_regression_plotting.py +200 -0
  43. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/test_release_readiness.py +23 -17
  44. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/test_reporting.py +1 -1
  45. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tools/inspect_distribution.py +1 -0
  46. pymetaanalysis-0.2.1/tests/test_properties.py +0 -116
  47. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/.github/workflows/ci.yml +0 -0
  48. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/.github/workflows/pages.yml +0 -0
  49. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/.github/workflows/release.yml +0 -0
  50. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/.gitignore +0 -0
  51. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/CONTRIBUTING.md +0 -0
  52. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/LICENSE +0 -0
  53. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/SECURITY.md +0 -0
  54. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/adr/0001-optional-matplotlib.md +0 -0
  55. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/adr/0002-statistical-policy.md +0 -0
  56. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/citation.md +0 -0
  57. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/development.md +0 -0
  58. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/getting-started.md +0 -0
  59. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/guides/binary-outcomes.md +0 -0
  60. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/guides/continuous-outcomes.md +0 -0
  61. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/guides/generic-effects.md +0 -0
  62. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/guides/input-data.md +0 -0
  63. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/guides/method-selection.md +0 -0
  64. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/guides/r-interoperability.md +0 -0
  65. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/guides/sensitivity-analysis.md +0 -0
  66. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/guides/zero-events.md +0 -0
  67. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/installation.md +0 -0
  68. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/docs/stylesheets/extra.css +0 -0
  69. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/examples/quickstart.ipynb +0 -0
  70. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/pyproject.toml +0 -0
  71. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/api.py +0 -0
  72. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/binary_api.py +0 -0
  73. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/continuous_api.py +0 -0
  74. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/data.py +0 -0
  75. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/effect_sizes/__init__.py +0 -0
  76. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/effect_sizes/binary.py +0 -0
  77. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/effect_sizes/continuous.py +0 -0
  78. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/estimators/inverse_variance.py +0 -0
  79. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/estimators/mantel_haenszel.py +0 -0
  80. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/estimators/tau2.py +0 -0
  81. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/exceptions.py +0 -0
  82. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/heterogeneity.py +0 -0
  83. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/plotting/forest.py +0 -0
  84. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/plotting/funnel.py +0 -0
  85. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/plotting/subgroup_forest.py +0 -0
  86. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/provenance.py +0 -0
  87. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/py.typed +0 -0
  88. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/results.py +0 -0
  89. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/sensitivity.py +0 -0
  90. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/src/meta_analyze/subgroups.py +0 -0
  91. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/reference/binary_input.csv +0 -0
  92. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/reference/binary_metafor.json +0 -0
  93. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/reference/binary_sparse_input.csv +0 -0
  94. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/reference/continuous_input.csv +0 -0
  95. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/reference/continuous_metafor.json +0 -0
  96. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/reference/generate_binary_metafor.R +0 -0
  97. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/reference/generate_continuous_metafor.R +0 -0
  98. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/reference/generate_generic_metafor.R +0 -0
  99. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/reference/generate_workflow_metafor.R +0 -0
  100. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/reference/generic_input.csv +0 -0
  101. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/reference/generic_metafor.json +0 -0
  102. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/reference/workflow_input.csv +0 -0
  103. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/reference/workflow_metafor.json +0 -0
  104. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/test_api.py +0 -0
  105. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/test_binary.py +0 -0
  106. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/test_continuous.py +0 -0
  107. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/test_documentation.py +0 -0
  108. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/test_estimators.py +0 -0
  109. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/test_funnel_plot.py +0 -0
  110. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/test_plotting.py +0 -0
  111. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/test_reference_results.py +0 -0
  112. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/test_sensitivity.py +0 -0
  113. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tests/test_subgroups.py +0 -0
  114. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tools/check_release.py +0 -0
  115. {pymetaanalysis-0.2.1 → pymetaanalysis-0.3.0}/tools/execute_notebooks.py +0 -0
@@ -6,6 +6,28 @@ Changes planned for the next release accumulate under `Unreleased`.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.3.0 - 2026-07-22
10
+
11
+ ### Added
12
+
13
+ - pandas-first `meta_regression()` for numeric, explicitly encoded categorical,
14
+ and multiple study-level moderators;
15
+ - common- and mixed-effects weighted regression with generalized DL, PM, and
16
+ REML residual tau-squared estimators;
17
+ - normal, Hartung-Knapp, and safeguarded Hartung-Knapp coefficient inference,
18
+ distribution-explicit moderator tests, residual heterogeneity, pseudo-R²,
19
+ prediction, provenance, and structured reports;
20
+ - optional weighted bubble plots for intercept-containing Meta-regression fits
21
+ with exactly one numeric moderator;
22
+ - independent R `metafor` fixtures covering numeric, categorical,
23
+ multivariable, zero-tau-squared, missing-row, and small-sample cases;
24
+ - an executable Meta-regression notebook plus a multivariable performance
25
+ baseline and expanded property, numerical-stability, and warning tests.
26
+
27
+ ### Changed
28
+
29
+ - report schema 1.2 adds the `meta_regression` report type.
30
+
9
31
  ## 0.2.1 - 2026-07-17
10
32
 
11
33
  ### Fixed
@@ -8,8 +8,8 @@ authors:
8
8
  - family-names: Ding
9
9
  given-names: Zhaobo
10
10
  email: ding.zb@yahoo.com
11
- version: 0.2.1
12
- date-released: 2026-07-17
11
+ version: 0.3.0
12
+ date-released: 2026-07-22
13
13
  repository-code: https://github.com/ZhaoboDing/PyMetaAnalysis
14
14
  url: https://zhaoboding.github.io/PyMetaAnalysis/
15
15
  license: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyMetaAnalysis
3
- Version: 0.2.1
3
+ Version: 0.3.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
@@ -111,6 +111,22 @@ Omit `study=` to use the DataFrame index. Supplying `subgroup=` returns a
111
111
  dedicated result containing group fits, the overall fit, and a formal test for
112
112
  subgroup differences.
113
113
 
114
+ Study-level moderators use the dedicated Meta-regression entry point:
115
+
116
+ ```python
117
+ regression = ma.meta_regression(
118
+ studies,
119
+ effect="effect",
120
+ standard_error="se",
121
+ moderators=["mean_age", "region"],
122
+ categorical={"region": ["Europe", "Asia", "North America"]},
123
+ )
124
+ ```
125
+
126
+ Categorical levels are explicit and ordered; the first value is the treatment-
127
+ coding reference. Moderator coefficients describe study-level associations,
128
+ not individual-level or causal effects.
129
+
114
130
  ## Supported analyses
115
131
 
116
132
  | Input | Effects | Pooling/models |
@@ -118,6 +134,7 @@ subgroup differences.
118
134
  | Effect + sampling variance or standard error | Generic | Common/random inverse variance |
119
135
  | Two-group events + totals | OR, RR, RD | Common MH OR/RR; common/random IV |
120
136
  | Two-group means + SDs + sizes | MD, Hedges' g | Common/random inverse variance |
137
+ | Effect + variance/SE + moderators | Generic | Common/mixed Meta-regression |
121
138
 
122
139
  Random-effects inverse-variance models support REML (default), Paule-Mandel,
123
140
  and DerSimonian-Laird tau-squared estimators. Mean confidence intervals support
@@ -172,6 +189,9 @@ ax = result.funnel()
172
189
 
173
190
  Plotting methods return Matplotlib axes and never call `show()`. Funnel plots
174
191
  are descriptive small-study-effect diagnostics, not proof of publication bias.
192
+ An eligible single-numeric-moderator Meta-regression result additionally
193
+ provides `regression.bubble()` with fitted confidence and optional prediction
194
+ bands.
175
195
 
176
196
  ## Documentation
177
197
 
@@ -182,6 +202,7 @@ The complete documentation is published at
182
202
  - [Getting started](https://zhaoboding.github.io/PyMetaAnalysis/getting-started/)
183
203
  - [Input data and row decisions](https://zhaoboding.github.io/PyMetaAnalysis/guides/input-data/)
184
204
  - [Generic](https://zhaoboding.github.io/PyMetaAnalysis/guides/generic-effects/), [binary](https://zhaoboding.github.io/PyMetaAnalysis/guides/binary-outcomes/), and [continuous](https://zhaoboding.github.io/PyMetaAnalysis/guides/continuous-outcomes/) guides
205
+ - [Meta-regression](https://zhaoboding.github.io/PyMetaAnalysis/guides/meta-regression/)
185
206
  - [Choosing methods](https://zhaoboding.github.io/PyMetaAnalysis/guides/method-selection/) and [statistical formulas](https://zhaoboding.github.io/PyMetaAnalysis/methods/statistical-methods/)
186
207
  - [Sensitivity analysis](https://zhaoboding.github.io/PyMetaAnalysis/guides/sensitivity-analysis/) and [plotting](https://zhaoboding.github.io/PyMetaAnalysis/guides/plotting/)
187
208
  - [Public API](https://zhaoboding.github.io/PyMetaAnalysis/reference/api/), [result objects](https://zhaoboding.github.io/PyMetaAnalysis/reference/results/), and [report schema](https://zhaoboding.github.io/PyMetaAnalysis/reference/report-schema/)
@@ -62,6 +62,22 @@ Omit `study=` to use the DataFrame index. Supplying `subgroup=` returns a
62
62
  dedicated result containing group fits, the overall fit, and a formal test for
63
63
  subgroup differences.
64
64
 
65
+ Study-level moderators use the dedicated Meta-regression entry point:
66
+
67
+ ```python
68
+ regression = ma.meta_regression(
69
+ studies,
70
+ effect="effect",
71
+ standard_error="se",
72
+ moderators=["mean_age", "region"],
73
+ categorical={"region": ["Europe", "Asia", "North America"]},
74
+ )
75
+ ```
76
+
77
+ Categorical levels are explicit and ordered; the first value is the treatment-
78
+ coding reference. Moderator coefficients describe study-level associations,
79
+ not individual-level or causal effects.
80
+
65
81
  ## Supported analyses
66
82
 
67
83
  | Input | Effects | Pooling/models |
@@ -69,6 +85,7 @@ subgroup differences.
69
85
  | Effect + sampling variance or standard error | Generic | Common/random inverse variance |
70
86
  | Two-group events + totals | OR, RR, RD | Common MH OR/RR; common/random IV |
71
87
  | Two-group means + SDs + sizes | MD, Hedges' g | Common/random inverse variance |
88
+ | Effect + variance/SE + moderators | Generic | Common/mixed Meta-regression |
72
89
 
73
90
  Random-effects inverse-variance models support REML (default), Paule-Mandel,
74
91
  and DerSimonian-Laird tau-squared estimators. Mean confidence intervals support
@@ -123,6 +140,9 @@ ax = result.funnel()
123
140
 
124
141
  Plotting methods return Matplotlib axes and never call `show()`. Funnel plots
125
142
  are descriptive small-study-effect diagnostics, not proof of publication bias.
143
+ An eligible single-numeric-moderator Meta-regression result additionally
144
+ provides `regression.bubble()` with fitted confidence and optional prediction
145
+ bands.
126
146
 
127
147
  ## Documentation
128
148
 
@@ -133,6 +153,7 @@ The complete documentation is published at
133
153
  - [Getting started](https://zhaoboding.github.io/PyMetaAnalysis/getting-started/)
134
154
  - [Input data and row decisions](https://zhaoboding.github.io/PyMetaAnalysis/guides/input-data/)
135
155
  - [Generic](https://zhaoboding.github.io/PyMetaAnalysis/guides/generic-effects/), [binary](https://zhaoboding.github.io/PyMetaAnalysis/guides/binary-outcomes/), and [continuous](https://zhaoboding.github.io/PyMetaAnalysis/guides/continuous-outcomes/) guides
156
+ - [Meta-regression](https://zhaoboding.github.io/PyMetaAnalysis/guides/meta-regression/)
136
157
  - [Choosing methods](https://zhaoboding.github.io/PyMetaAnalysis/guides/method-selection/) and [statistical formulas](https://zhaoboding.github.io/PyMetaAnalysis/methods/statistical-methods/)
137
158
  - [Sensitivity analysis](https://zhaoboding.github.io/PyMetaAnalysis/guides/sensitivity-analysis/) and [plotting](https://zhaoboding.github.io/PyMetaAnalysis/guides/plotting/)
138
159
  - [Public API](https://zhaoboding.github.io/PyMetaAnalysis/reference/api/), [result objects](https://zhaoboding.github.io/PyMetaAnalysis/reference/results/), and [report schema](https://zhaoboding.github.io/PyMetaAnalysis/reference/report-schema/)
@@ -1,9 +1,10 @@
1
1
  # Core performance baseline
2
2
 
3
- `benchmark_core.py` measures representative generic, binary, and continuous
4
- random-effects fits using deterministic synthetic data. Correctness remains the
5
- primary performance requirement; this benchmark is intended to reveal large
6
- regressions, not to enforce a fragile wall-clock threshold.
3
+ `benchmark_core.py` measures representative generic, binary, continuous, and
4
+ multivariable Meta-regression random-effects fits using deterministic synthetic
5
+ data. Correctness remains the primary performance requirement; this benchmark
6
+ is intended to reveal large regressions, not to enforce a fragile wall-clock
7
+ threshold.
7
8
 
8
9
  Run the default benchmark with:
9
10
 
@@ -34,6 +34,8 @@ def _cases(studies: int) -> dict[str, Callable[[], object]]:
34
34
 
35
35
  generic_effect = rng.normal(0.1, 0.25, size=studies)
36
36
  generic_variance = rng.uniform(0.01, 0.09, size=studies)
37
+ moderator = np.linspace(-1.0, 1.0, studies)
38
+ region = np.resize(np.array(["North", "South", "East"], dtype=object), studies)
37
39
 
38
40
  n_treat = rng.integers(60, 240, size=studies)
39
41
  n_control = rng.integers(60, 240, size=studies)
@@ -75,6 +77,14 @@ def _cases(studies: int) -> dict[str, Callable[[], object]]:
75
77
  model="random",
76
78
  tau2_method="REML",
77
79
  ),
80
+ "meta_regression_multivariable_reml": lambda: ma.meta_regression(
81
+ effect=generic_effect,
82
+ variance=generic_variance,
83
+ moderators={"moderator": moderator, "region": region},
84
+ categorical={"region": ["North", "South", "East"]},
85
+ model="mixed",
86
+ tau2_method="REML",
87
+ ),
78
88
  }
79
89
 
80
90
 
@@ -102,8 +112,10 @@ def main() -> None:
102
112
  parser.add_argument("--number", type=int, default=5)
103
113
  parser.add_argument("--output", type=Path)
104
114
  arguments = parser.parse_args()
105
- if min(arguments.studies, arguments.repeat, arguments.number) < 1:
106
- parser.error("studies, repeat, and number must all be positive")
115
+ if arguments.studies < 5:
116
+ parser.error("studies must be at least 5 for the multivariable benchmark")
117
+ if min(arguments.repeat, arguments.number) < 1:
118
+ parser.error("repeat and number must both be positive")
107
119
 
108
120
  payload = _metadata(arguments)
109
121
  payload["cases"] = {
@@ -0,0 +1,220 @@
1
+ # Meta-regression
2
+
3
+ Meta-regression relates study effect estimates to one or more study-level
4
+ moderators. It is a natural extension of subgroup analysis, but it does not
5
+ turn aggregate study data into individual-level evidence.
6
+
7
+ !!! warning "Interpretation boundary"
8
+
9
+ A Meta-regression coefficient is a study-level association. It may reflect
10
+ ecological bias, confounding, measurement differences, or post-hoc model
11
+ selection and must not be interpreted as an individual-level causal effect.
12
+ Fits with fewer than ten studies carry an explicit warning.
13
+
14
+ ## Fit a numeric moderator
15
+
16
+ Provide a generic effect plus exactly one of its sampling variance or standard
17
+ error:
18
+
19
+ ```python
20
+ import pandas as pd
21
+ import meta_analyze as ma
22
+
23
+ studies = pd.DataFrame(
24
+ {
25
+ "citation": ["A", "B", "C", "D", "E", "F"],
26
+ "effect": [0.12, 0.25, 0.41, 0.38, 0.62, 0.76],
27
+ "se": [0.18, 0.20, 0.17, 0.23, 0.19, 0.21],
28
+ "mean_age": [42.0, 48.0, 51.0, 55.0, 60.0, 64.0],
29
+ }
30
+ )
31
+
32
+ result = ma.meta_regression(
33
+ studies,
34
+ effect="effect",
35
+ standard_error="se",
36
+ moderators=["mean_age"],
37
+ study="citation",
38
+ model="mixed",
39
+ tau2_method="REML",
40
+ )
41
+
42
+ print(result.summary())
43
+ print(result.coefficients)
44
+ ```
45
+
46
+ Numeric moderators are used exactly as supplied. PyMetaAnalysis does not
47
+ center, scale, transform, or impute them automatically.
48
+
49
+ ## Encode categorical moderators explicitly
50
+
51
+ Every categorical moderator requires an ordered, complete list of levels. The
52
+ first level is the treatment-coding reference:
53
+
54
+ ```python
55
+ result = ma.meta_regression(
56
+ studies,
57
+ effect="effect",
58
+ standard_error="se",
59
+ moderators=["mean_age", "region"],
60
+ categorical={
61
+ "region": ["Europe", "Asia", "North America"],
62
+ },
63
+ )
64
+ ```
65
+
66
+ This produces terms such as `region[Asia]` and `region[North America]`, each
67
+ relative to `Europe`. The reference never depends on row order. Undeclared
68
+ levels, levels absent after exclusions, and string moderators omitted from
69
+ `categorical=` are errors rather than implicit recoding decisions.
70
+
71
+ Formula parsing, automatic interactions, splines, and polynomial terms are not
72
+ implemented. Construct those columns explicitly before fitting when they are
73
+ scientifically prespecified.
74
+
75
+ ## Array-like input
76
+
77
+ Use a mapping when moderators are arrays rather than DataFrame column names:
78
+
79
+ ```python
80
+ result = ma.meta_regression(
81
+ effect=[0.10, 0.32, 0.45, 0.71],
82
+ variance=[0.04, 0.05, 0.06, 0.08],
83
+ moderators={"dose": [0.0, 1.0, 2.0, 3.0]},
84
+ model="common",
85
+ )
86
+ ```
87
+
88
+ A sequence such as `moderators=["mean_age", "region"]` is only meaningful
89
+ with a DataFrame. A mapping may mix DataFrame column selectors and array-like
90
+ values, provided every input has one value per row.
91
+
92
+ ## Models and inference
93
+
94
+ `model="mixed"` is the default. It estimates residual tau-squared after the
95
+ moderators using REML, PM, or DL. `model="common"` fixes residual tau-squared
96
+ at zero and supports normal inference only.
97
+
98
+ | Setting | Coefficient tests and intervals | Joint moderator test |
99
+ | --- | --- | --- |
100
+ | `normal` | z / normal | chi-squared |
101
+ | `hartung_knapp` | t with `k-p` df | F with `k-p` denominator df |
102
+ | `hartung_knapp_adhoc` | safeguarded t with `k-p` df | safeguarded F |
103
+
104
+ The unmodified Hartung-Knapp result warns when its covariance is below the
105
+ classic covariance. The `adhoc` choice explicitly applies a lower-bound scale
106
+ of one. Common-effect models reject both Hartung-Knapp choices.
107
+
108
+ The design matrix must be full rank and leave positive residual degrees of
109
+ freedom (`k > p`). PyMetaAnalysis does not silently drop collinear terms or use
110
+ a pseudo-inverse.
111
+
112
+ ## Inspect the result
113
+
114
+ Meta-regression has no single pooled effect, so `MetaRegressionResult` does not
115
+ provide a scalar `estimate` or `ci`. Inspect its coefficient table instead:
116
+
117
+ ```python
118
+ result.coefficients
119
+ result.coefficient_covariance
120
+ result.global_test
121
+ result.test_moderator("region")
122
+ result.heterogeneity
123
+ result.tau2
124
+ result.tau2_null
125
+ result.pseudo_r2
126
+ result.diagnostics
127
+ ```
128
+
129
+ `global_test` tests all non-intercept terms. `test_moderator(name)` tests every
130
+ encoded term belonging to that original moderator, so a multi-level category
131
+ receives one joint test rather than separate interpretation through dummy-term
132
+ p-values.
133
+
134
+ `study_results` retains every input row and contains fitted values, residuals,
135
+ precision weights, normalized precision weights, and leverage. A regression
136
+ precision weight is not a universal percentage contribution to every
137
+ coefficient.
138
+
139
+ ## Predict at moderator values
140
+
141
+ Prediction replays the fitted numeric and categorical encoding:
142
+
143
+ ```python
144
+ predictions = result.predict(
145
+ pd.DataFrame(
146
+ {
147
+ "mean_age": [50.0, 65.0],
148
+ "region": ["Europe", "Asia"],
149
+ }
150
+ )
151
+ )
152
+ ```
153
+
154
+ Every model returns the fitted mean and its confidence interval. Mixed-effects
155
+ models also return `pi_low` and `pi_high` for the distribution of true effects
156
+ in a new study with those moderators. The interval does not include an
157
+ additional, unknown sampling variance for a future observed estimate. Unknown
158
+ categories and missing prediction inputs are rejected.
159
+
160
+ ## Plot a single numeric moderator
161
+
162
+ After installing the `plot` extra, an intercept-containing model with exactly
163
+ one numeric moderator can be displayed as a weighted bubble plot:
164
+
165
+ ```python
166
+ ax = result.bubble(
167
+ moderator_label="Mean age (years)",
168
+ effect_label="Treatment effect",
169
+ show_confidence_interval=True,
170
+ show_prediction_interval=True,
171
+ )
172
+ ```
173
+
174
+ Bubble area is proportional to normalized fitted precision weight. The fitted
175
+ line and interval bands reuse `result.predict()`, including the selected normal
176
+ or Hartung-Knapp covariance and critical value. A prediction band is available
177
+ only for mixed-effects models.
178
+
179
+ PyMetaAnalysis rejects bubble plots for categorical, multiple-moderator, or
180
+ no-intercept fits. Drawing a marginal or partial-effect line for those models
181
+ requires explicit choices for the other moderator values, and the library does
182
+ not silently choose them.
183
+
184
+ ## Residual heterogeneity and pseudo-R²
185
+
186
+ The result reports residual `QE`, I-squared, H-squared, and tau-squared. For an
187
+ intercept-containing mixed model, PyMetaAnalysis also refits the same included
188
+ rows without moderators and reports:
189
+
190
+ ```text
191
+ pseudo-R² = max(0, 1 - tau²_model / tau²_null)
192
+ ```
193
+
194
+ The raw value is retained as `pseudo_r2_raw`. If the null-model tau-squared is
195
+ zero, pseudo-R² is undefined; if moderators increase estimated tau-squared,
196
+ the public value is truncated to zero and the negative raw value is retained
197
+ with a warning. Pseudo-R² is not the proportion of outcome variance explained
198
+ in ordinary individual-level regression.
199
+
200
+ ## Missing values, provenance, and reports
201
+
202
+ `missing="raise"` identifies every missing effect, uncertainty, study label,
203
+ or moderator field by row. `missing="drop"` excludes the entire row from every
204
+ model calculation while retaining it in `study_results` with all applicable
205
+ reasons.
206
+
207
+ ```python
208
+ result.provenance
209
+ result.method_details()
210
+ result.report().to_json()
211
+ ```
212
+
213
+ Provenance records moderator input roles and categorical treatment coding.
214
+ Reports include coefficients, their covariance, residual heterogeneity,
215
+ moderator tests, encoding, diagnostics, row decisions, and the study-level
216
+ interpretation warning.
217
+
218
+ See [statistical methods](../methods/statistical-methods.md#meta-regression) for
219
+ the equations and [scope and limitations](../limitations.md) before using the
220
+ model for consequential work.
@@ -1,6 +1,7 @@
1
1
  # Plotting
2
2
 
3
- Forest, subgroup forest, and funnel plots use optional Matplotlib support.
3
+ Forest, subgroup forest, funnel, and Meta-regression bubble plots use optional
4
+ Matplotlib support.
4
5
  Install it with:
5
6
 
6
7
  ```console
@@ -96,6 +97,36 @@ selection, design differences, chance, or publication processes. It is not by
96
97
  itself evidence of publication bias. PyMetaAnalysis currently provides the
97
98
  plot but not formal asymmetry tests.
98
99
 
100
+ ## Meta-regression bubble plots
101
+
102
+ An intercept-containing Meta-regression with exactly one numeric moderator
103
+ provides:
104
+
105
+ ```python
106
+ ax = regression.bubble(
107
+ moderator_label="Dose",
108
+ effect_label="Effect",
109
+ show_confidence_interval=True,
110
+ show_prediction_interval=False,
111
+ )
112
+ ```
113
+
114
+ Study marker area is proportional to normalized precision weight. The line,
115
+ mean confidence band, and optional mixed-effects true-effect prediction band
116
+ are obtained from the fitted model's `predict()` method.
117
+
118
+ | Parameter | Meaning |
119
+ | --- | --- |
120
+ | `ax` | Existing axes; a new one is created when omitted |
121
+ | `moderator_label` | X-axis label; defaults to the moderator name |
122
+ | `effect_label` | Y-axis label; defaults to `"Effect"` |
123
+ | `show_confidence_interval` | Draw the fitted mean confidence band |
124
+ | `show_prediction_interval` | Draw a mixed-effects true-effect prediction band |
125
+
126
+ Categorical, multivariable, and no-intercept fits are rejected because a
127
+ marginal plot would require values or averaging rules for other terms. The
128
+ function does not infer those scientific choices.
129
+
99
130
  ## Save or display
100
131
 
101
132
  The caller controls rendering:
@@ -100,7 +100,7 @@ The payload contains:
100
100
  - provenance and warnings;
101
101
  - row-level study results.
102
102
 
103
- Report schema 1.1 records the resolved heterogeneity definition as
103
+ Report schema 1.2 records the resolved heterogeneity definition as
104
104
  `heterogeneity.i2_method`.
105
105
 
106
106
  The complete key-level contract, including the separately versioned
@@ -18,8 +18,9 @@ effects, exclusions, weights, method choices, and diagnostics.
18
18
  | Effect estimates and sampling variances | `meta_analysis()` | Generic inverse variance |
19
19
  | Events and totals for two groups | `meta_binary()` | OR, RR, RD |
20
20
  | Means, SDs, and sample sizes for two groups | `meta_continuous()` | MD, Hedges' g |
21
+ | Effect estimates, uncertainty, and study-level moderators | `meta_regression()` | Generic Meta-regression |
21
22
 
22
- All three functions accept an optional `subgroup=` column or array. Supplying
23
+ The three pooling functions accept an optional `subgroup=` column or array. Supplying
23
24
  it returns a dedicated subgroup result containing each group, the overall
24
25
  analysis, and a formal test for subgroup differences.
25
26
 
@@ -52,6 +53,7 @@ shows how to inspect its output. Continue with the guide matching your input:
52
53
  - [generic effects and variances](guides/generic-effects.md);
53
54
  - [binary outcomes](guides/binary-outcomes.md);
54
55
  - [continuous outcomes](guides/continuous-outcomes.md).
56
+ - [study-level Meta-regression](guides/meta-regression.md).
55
57
 
56
58
  Read [input data and row decisions](guides/input-data.md) before building a
57
59
  pipeline around exclusions or mixed DataFrame/array inputs.
@@ -17,13 +17,16 @@ deferred functionality explicit.
17
17
  - independent subgroup fits and a formal subgroup-differences test;
18
18
  - leave-one-out and cumulative repeated-fit workflows;
19
19
  - forest and descriptive funnel plots;
20
- - structured provenance and reports.
20
+ - structured provenance and reports;
21
+ - study-level Meta-regression with numeric and explicitly encoded categorical
22
+ moderators, generalized DL/PM/REML, and normal/Hartung-Knapp inference.
21
23
 
22
24
  ## Not currently implemented
23
25
 
24
26
  - Peto odds ratios;
25
27
  - Mantel-Haenszel risk differences or random-effects MH pooling;
26
- - meta-regression or continuous/categorical moderator models;
28
+ - formula parsing, automatic interactions/splines, stepwise moderator
29
+ selection, multiplicity correction, or arbitrary linear contrasts;
27
30
  - multilevel, multivariate, network, dose-response, diagnostic-accuracy, or
28
31
  individual-participant-data meta-analysis;
29
32
  - robust variance estimation or dependent-effect clustering;
@@ -51,8 +54,14 @@ of heterogeneity and should not be interpreted as interchangeable decision
51
54
  rules. A non-significant Q test is not evidence of homogeneity.
52
55
 
53
56
  Subgroup analyses estimate tau-squared independently within each random-
54
- effects subgroup. A common tau-squared strategy and meta-regression-based
55
- moderator tests are not yet available.
57
+ effects subgroup. This differs from fitting a Meta-regression with categorical
58
+ moderators and a single residual tau-squared.
59
+
60
+ Meta-regression uses aggregate study-level moderators. Coefficients are
61
+ susceptible to ecological bias, confounding, measurement error, low power, and
62
+ post-hoc model selection. They do not establish individual-level associations
63
+ or causal effects. The package warns below ten studies but does not prohibit a
64
+ scientifically prespecified, full-rank model when `k > p`.
56
65
 
57
66
  Funnel plots are descriptive. Their pseudo confidence limits exclude tau-
58
67
  squared, and asymmetry does not establish publication bias.
@@ -161,6 +161,127 @@ proportion internally and is formatted as a percentage in human-readable
161
161
  output. With one study, Q is zero and its p-value, I-squared, and H-squared are
162
162
  unavailable.
163
163
 
164
+ ## Meta-regression
165
+
166
+ For `k` study effects, let `X` be the full-rank `k`-by-`p` design matrix. It
167
+ contains an intercept by default plus numeric moderator columns and treatment-
168
+ coded categorical terms:
169
+
170
+ ```text
171
+ y = X beta + u + epsilon
172
+ epsilon ~ N(0, V), V = diag(v_i)
173
+ u ~ N(0, tau^2 I)
174
+ ```
175
+
176
+ Given residual tau-squared:
177
+
178
+ ```text
179
+ W = diag(1 / (v_i + tau^2))
180
+ B = (X' W X)^(-1)
181
+ beta_hat = B X' W y
182
+ P = W - W X B X' W
183
+ residual df = k - p
184
+ ```
185
+
186
+ The implementation uses stable linear solves rather than forming an explicit
187
+ matrix inverse. A rank-deficient design or `k <= p` is rejected; terms are not
188
+ silently removed and a pseudo-inverse is not used.
189
+
190
+ ### Residual tau-squared
191
+
192
+ For generalized DL, define `W0` and `P0` at tau-squared zero:
193
+
194
+ ```text
195
+ QE0 = y' P0 y
196
+ C = trace(P0)
197
+ tau^2_DL = max(0, (QE0 - (k-p)) / C)
198
+ ```
199
+
200
+ PM solves:
201
+
202
+ ```text
203
+ y' P(tau^2) y = k - p
204
+ ```
205
+
206
+ REML solves the restricted score:
207
+
208
+ ```text
209
+ y' P(tau^2)^2 y - trace(P(tau^2)) = 0
210
+ ```
211
+
212
+ All estimators are constrained to non-negative tau-squared. PM and REML use a
213
+ bracketed scalar root; a non-positive equation at zero returns the boundary.
214
+ Failure raises `ConvergenceError` without estimator fallback.
215
+
216
+ ### Coefficient and moderator inference
217
+
218
+ Normal inference uses covariance `B`, z coefficient tests, and a chi-squared
219
+ Wald test for one or more moderator terms.
220
+
221
+ For both Hartung-Knapp choices:
222
+
223
+ ```text
224
+ q = y' P(tau^2) y / (k-p)
225
+ Cov_HK(beta_hat) = q B
226
+ ```
227
+
228
+ Coefficient tests use a t distribution with `k-p` degrees of freedom. Joint
229
+ tests divide their Wald statistic by the number of tested terms and use an F
230
+ distribution with that numerator df and `k-p` denominator df.
231
+ `hartung_knapp_adhoc` replaces `q` with `max(1, q)`; unmodified
232
+ `hartung_knapp` retains `q` and warns when it is below one.
233
+
234
+ The global moderator test covers every non-intercept term.
235
+ `test_moderator(name)` covers every encoded term for one original moderator,
236
+ so a multi-level category receives a joint test.
237
+
238
+ ### Residual heterogeneity and pseudo-R-squared
239
+
240
+ Residual heterogeneity uses:
241
+
242
+ ```text
243
+ QE = y' P0 y
244
+ QE ~ chi-square_(k-p) under the null
245
+ ```
246
+
247
+ Common models derive residual I-squared and H-squared from QE. Mixed models
248
+ use:
249
+
250
+ ```text
251
+ v_typical = (k-p) / trace(P0)
252
+ I^2_residual = tau^2 / (tau^2 + v_typical)
253
+ H^2_residual = 1 + tau^2 / v_typical
254
+ ```
255
+
256
+ For mixed models with an intercept, the same rows are refitted with an
257
+ intercept-only design and the same tau-squared estimator:
258
+
259
+ ```text
260
+ R^2_raw = 1 - tau^2_model / tau^2_null
261
+ R^2 = max(0, R^2_raw)
262
+ ```
263
+
264
+ The raw value is retained. Pseudo-R-squared is unavailable when the null
265
+ tau-squared is zero and is not equivalent to ordinary regression R-squared.
266
+
267
+ ### Meta-regression prediction
268
+
269
+ For a new design vector `x0`:
270
+
271
+ ```text
272
+ fitted = x0' beta_hat
273
+ Var_mean = x0' Cov(beta_hat) x0
274
+ ```
275
+
276
+ Mean-effect intervals use the selected normal or `t_(k-p)` critical value. A
277
+ mixed model additionally reports a true-effect prediction interval:
278
+
279
+ ```text
280
+ fitted +/- critical * sqrt(tau^2 + Var_mean)
281
+ ```
282
+
283
+ It does not include a sampling variance for a future observed effect.
284
+
164
285
  ## Binary study effects
165
286
 
166
287
  For a treatment/control 2-by-2 table: