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.
Files changed (101) hide show
  1. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/.github/workflows/release.yml +1 -0
  2. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/CHANGELOG.md +16 -0
  3. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/CITATION.cff +5 -3
  4. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/PKG-INFO +7 -3
  5. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/README.md +5 -1
  6. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/getting-started.md +4 -3
  7. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/generic-effects.md +17 -10
  8. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/input-data.md +3 -1
  9. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/provenance-reporting.md +4 -0
  10. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/r-interoperability.md +13 -1
  11. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/index.md +4 -3
  12. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/limitations.md +2 -3
  13. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/reference/api.md +8 -2
  14. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/releasing.md +4 -4
  15. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/pyproject.toml +1 -1
  16. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/_version.py +1 -1
  17. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/api.py +85 -12
  18. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/data.py +66 -24
  19. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/reporting.py +7 -0
  20. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_api.py +84 -0
  21. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_properties.py +32 -0
  22. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_release_readiness.py +17 -0
  23. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_reporting.py +26 -0
  24. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_subgroups.py +26 -0
  25. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tools/check_release.py +1 -1
  26. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tools/inspect_distribution.py +1 -0
  27. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/.github/workflows/ci.yml +0 -0
  28. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/.github/workflows/pages.yml +0 -0
  29. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/.gitignore +0 -0
  30. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/CONTRIBUTING.md +0 -0
  31. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/LICENSE +0 -0
  32. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/SECURITY.md +0 -0
  33. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/benchmarks/README.md +0 -0
  34. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/benchmarks/benchmark_core.py +0 -0
  35. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/adr/0001-optional-matplotlib.md +0 -0
  36. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/adr/0002-statistical-policy.md +0 -0
  37. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/citation.md +0 -0
  38. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/development.md +0 -0
  39. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/binary-outcomes.md +0 -0
  40. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/continuous-outcomes.md +0 -0
  41. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/method-selection.md +0 -0
  42. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/plotting.md +0 -0
  43. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/sensitivity-analysis.md +0 -0
  44. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/guides/zero-events.md +0 -0
  45. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/installation.md +0 -0
  46. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/methods/statistical-methods.md +0 -0
  47. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/reference/report-schema.md +0 -0
  48. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/reference/results.md +0 -0
  49. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/stylesheets/extra.css +0 -0
  50. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/docs/validation.md +0 -0
  51. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/examples/README.md +0 -0
  52. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/examples/quickstart.ipynb +0 -0
  53. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/mkdocs.yml +0 -0
  54. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/__init__.py +0 -0
  55. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/binary_api.py +0 -0
  56. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/config.py +0 -0
  57. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/continuous_api.py +0 -0
  58. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/effect_sizes/__init__.py +0 -0
  59. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/effect_sizes/binary.py +0 -0
  60. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/effect_sizes/continuous.py +0 -0
  61. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/estimators/__init__.py +0 -0
  62. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/estimators/inverse_variance.py +0 -0
  63. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/estimators/mantel_haenszel.py +0 -0
  64. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/estimators/tau2.py +0 -0
  65. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/exceptions.py +0 -0
  66. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/heterogeneity.py +0 -0
  67. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/plotting/__init__.py +0 -0
  68. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/plotting/_utils.py +0 -0
  69. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/plotting/forest.py +0 -0
  70. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/plotting/funnel.py +0 -0
  71. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/plotting/subgroup_forest.py +0 -0
  72. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/provenance.py +0 -0
  73. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/py.typed +0 -0
  74. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/results.py +0 -0
  75. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/sensitivity.py +0 -0
  76. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/src/meta_analyze/subgroups.py +0 -0
  77. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/README.md +0 -0
  78. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/binary_input.csv +0 -0
  79. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/binary_metafor.json +0 -0
  80. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/binary_sparse_input.csv +0 -0
  81. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/continuous_input.csv +0 -0
  82. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/continuous_metafor.json +0 -0
  83. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/generate_binary_metafor.R +0 -0
  84. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/generate_continuous_metafor.R +0 -0
  85. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/generate_generic_metafor.R +0 -0
  86. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/generate_workflow_metafor.R +0 -0
  87. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/generic_input.csv +0 -0
  88. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/generic_metafor.json +0 -0
  89. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/workflow_input.csv +0 -0
  90. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/reference/workflow_metafor.json +0 -0
  91. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_binary.py +0 -0
  92. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_continuous.py +0 -0
  93. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_documentation.py +0 -0
  94. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_estimators.py +0 -0
  95. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_funnel_plot.py +0 -0
  96. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_numerical_stability.py +0 -0
  97. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_plotting.py +0 -0
  98. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_r_references.py +0 -0
  99. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_reference_results.py +0 -0
  100. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tests/test_sensitivity.py +0 -0
  101. {pymetaanalysis-0.1.0 → pymetaanalysis-0.2.0}/tools/execute_notebooks.py +0 -0
@@ -116,6 +116,7 @@ jobs:
116
116
  - name: Create release
117
117
  env:
118
118
  GH_TOKEN: ${{ github.token }}
119
+ GH_REPO: ${{ github.repository }}
119
120
  run: |
120
121
  args=("$GITHUB_REF_NAME" dist/* release-benchmark.json --verify-tag --generate-notes)
121
122
  version="${GITHUB_REF_NAME#v}"
@@ -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
- - name: PyMetaAnalysis contributors
9
- version: 0.1.0
10
- date-released: 2026-07-15
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.1.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: PyMetaAnalysis contributors
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 one strictly positive sampling
24
- variance per study. A standard error is not a variance; square it before using
25
- it as `variance=`.
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 workflow; it does not
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, not standard error
52
+ ## Variance or standard error
53
53
 
54
- `variance=` must contain finite, strictly positive sampling variances. Convert
55
- reported standard errors explicitly:
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
- data["vi"] = data["standard_error"] ** 2
59
+ result = ma.meta_analysis(
60
+ data,
61
+ effect="yi",
62
+ standard_error="standard_error",
63
+ )
59
64
  ```
60
65
 
61
- Do not pass confidence-interval widths or study sample variances unless they
62
- have first been converted to the sampling variance of the effect estimate.
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 variances. To
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
- variance=[0.02, 0.04, 0.03],
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.1.0 is the initial public release and has not undergone a
86
- formal external statistical audit. Pin the package version for consequential
87
- work and independently check important analyses. See the repository
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, or
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.1.0`. Public APIs and serialized schemas
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—not standard errors |
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 `## 0.1.0 - 2026-07-15`.
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 v0.1.0
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 v0.1.0 -m "PyMetaAnalysis 0.1.0"
94
- git push origin v0.1.0
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 = "PyMetaAnalysis contributors" },
14
+ { name = "Zhaobo Ding", email = "ding.zb@yahoo.com" },
15
15
  ]
16
16
  maintainers = [
17
17
  { name = "Zhaobo Ding", email = "ding.zb@yahoo.com" },
@@ -1,3 +1,3 @@
1
1
  """Single source of truth for the package version."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
@@ -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 add_input_field, build_analysis_provenance
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, variance:
90
+ effect:
86
91
  A DataFrame column name or one-dimensional array-like containing study
87
- effects and strictly positive sampling variances.
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), ("variance", variance)),
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: ColumnOrArray,
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 ``variance`` accept DataFrame column names or one-dimensional
277
- array-like values. Sampling variances must be finite and strictly positive.
278
- The default is a REML random-effects model with a normal confidence
279
- interval. ``subgroup`` returns :class:`SubgroupMetaAnalysisResult` when
280
- supplied; otherwise the return value is :class:`MetaAnalysisResult`.
281
- Missing subgroup labels are rejected explicitly.
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 variance have "
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
- raw_variance = _resolve_vector(variance, data=data, name="variance")
108
- if len(raw_effect) != len(raw_variance):
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 variance must have the same length; "
111
- f"got {len(raw_effect)} and {len(raw_variance)}."
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(study, data=data, length=len(raw_effect))
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
- variance_values = np.asarray(raw_variance, dtype=np.float64)
148
+ uncertainty_values = np.asarray(raw_uncertainty, dtype=np.float64)
124
149
  except (TypeError, ValueError) as error:
125
150
  raise InvalidStudyDataError(
126
- "effect and variance must contain numeric values."
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
- variance_missing = pd.isna(variance_values)
131
- any_missing = effect_missing | variance_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 variance values at row positions {rows}; "
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
- finite_variance = np.isfinite(variance_values) | variance_missing
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(finite_variance):
147
- rows = np.flatnonzero(~finite_variance).tolist()
171
+ if not np.all(finite_uncertainty):
172
+ rows = np.flatnonzero(~finite_uncertainty).tolist()
148
173
  raise InvalidStudyDataError(
149
- f"Variance values must be finite; invalid rows: {rows}."
174
+ f"{uncertainty_label.capitalize()} values must be finite; "
175
+ f"invalid rows: {rows}."
150
176
  )
151
177
 
152
- nonpositive_variance = (~variance_missing) & (variance_values <= 0.0)
153
- if np.any(nonpositive_variance):
154
- rows = np.flatnonzero(nonpositive_variance).tolist()
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 variances must be strictly positive; invalid rows: {rows}."
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 variance_missing[index]:
163
- reasons[index] = "missing effect and variance"
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 variance"
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, for example v0.1.0")
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