Skip to content

Commit 4ccace9

Browse files
committed
Merge branch 'master' into doc/example
2 parents 4eabd26 + 4fd19c9 commit 4ccace9

File tree

12 files changed

+351
-244
lines changed

12 files changed

+351
-244
lines changed

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,25 @@ from pymare.estimators import VarianceBasedLikelihoodEstimator
6060
dataset = Dataset(y, v, X)
6161
# Estimator class for likelihood-based methods when variances are known
6262
estimator = VarianceBasedLikelihoodEstimator(method='REML')
63-
# All estimators accept a `Dataset` instance as the first argument to `.fit()`
64-
estimator.fit(dataset)
63+
# All estimators expose a fit_dataset() method that takes a `Dataset`
64+
# instance as the first (and usually only) argument.
65+
estimator.fit_dataset(dataset)
6566
# Post-fitting we can obtain a MetaRegressionResults instance via .summary()
6667
results = estimator.summary()
6768
# Print summary of results as a pandas DataFrame
6869
print(result.to_df())
6970
```
71+
72+
And if we want to be even more explicit, we can avoid the `Dataset` abstraction
73+
entirely (though we'll lose some convenient validation checks):
74+
75+
```python
76+
estimator = VarianceBasedLikelihoodEstimator(method='REML')
77+
78+
# X must be 2-d; this is one of the things the Dataset implicitly handles.
79+
X = X[:, None]
80+
81+
estimator.fit(y, v, X)
82+
83+
results = estimator.summary()
84+
```

examples/02_meta-analysis/plot_run_meta-analysis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@
2929
# Datasets can also be created from pandas DataFrames
3030
# ---------------------------------------------------
3131
dataset = core.Dataset(v=v, X=X, y=y, n=n)
32-
est = estimators.WeightedLeastSquares().fit(dataset)
32+
est = estimators.WeightedLeastSquares().fit_dataset(dataset)
3333
results = est.summary()
3434
print(results.to_df())

pymare/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,5 +144,5 @@ def meta_regression(y=None, v=None, X=None, n=None, data=None, X_names=None,
144144

145145
# Get estimates
146146
est = est_cls(**kwargs)
147-
est.fit(data)
147+
est.fit_dataset(data)
148148
return est.summary()

pymare/estimators/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from .estimators import (WeightedLeastSquares, DerSimonianLaird,
22
VarianceBasedLikelihoodEstimator,
33
SampleSizeBasedLikelihoodEstimator,
4-
StanMetaRegression, Hedges, Stouffers, Fishers)
4+
StanMetaRegression, Hedges)
5+
from .combination import StoufferCombinationTest, FisherCombinationTest
56

67
__all__ = [
78
'WeightedLeastSquares',
@@ -10,6 +11,6 @@
1011
'SampleSizeBasedLikelihoodEstimator',
1112
'StanMetaRegression',
1213
'Hedges',
13-
'Stouffers',
14-
'Fishers'
14+
'StoufferCombinationTest',
15+
'FisherCombinationTest'
1516
]

pymare/estimators/combination.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
from abc import abstractmethod
2+
import warnings
3+
4+
import numpy as np
5+
import scipy.stats as ss
6+
7+
from .estimators import BaseEstimator
8+
from ..results import CombinationTestResults
9+
10+
11+
class CombinationTest(BaseEstimator):
12+
"""Base class for methods based on combining p/z values."""
13+
def __init__(self, mode='directed'):
14+
mode = mode.lower()
15+
if mode not in {'directed', 'undirected', 'concordant'}:
16+
raise ValueError("Invalid mode; must be one of 'directed', "
17+
"'undirected', or 'concordant'.")
18+
if mode == 'undirected':
19+
warnings.warn(
20+
"You have opted to conduct an 'undirected' test. Are you sure "
21+
"this is what you want? If you're looking for the analog of a "
22+
"conventional two-tailed test, use 'concordant'.")
23+
self.mode = mode
24+
25+
@abstractmethod
26+
def p_value(self, z, *args, **kwargs):
27+
pass
28+
29+
def _z_to_p(self, z):
30+
return ss.norm.sf(z)
31+
32+
def fit(self, z, *args, **kwargs):
33+
if self.mode == 'concordant':
34+
ose = self.__class__(mode='directed')
35+
p1 = ose.p_value(z, *args, **kwargs)
36+
p2 = ose.p_value(-z, *args, **kwargs)
37+
p = np.minimum(1, 2 * np.minimum(p1, p2))
38+
else:
39+
if self.mode == 'undirected':
40+
z = np.abs(z)
41+
p = self.p_value(z, *args, **kwargs)
42+
self.params_ = {'p': p}
43+
return self
44+
45+
def summary(self):
46+
if not hasattr(self, 'params_'):
47+
name = self.__class__.__name__
48+
raise ValueError("This {} instance hasn't been fitted yet. Please "
49+
"call fit() before summary().".format(name))
50+
return CombinationTestResults(self, self.dataset_, p=self.params_['p'])
51+
52+
53+
class StoufferCombinationTest(CombinationTest):
54+
"""Stouffer's Z-score meta-analysis method.
55+
56+
Takes a set of independent z-scores and combines them via Stouffer's method
57+
to produce a fixed-effect estimate of the combined effect.
58+
59+
Args:
60+
mode (str): The type of test to perform-- i.e., what null hypothesis to
61+
reject. See Winkler et al. (2016) for details. Valid options are:
62+
* 'directed': tests a directional hypothesis--i.e., that the
63+
observed value is consistently greater than 0 in the input
64+
studies.
65+
* 'undirected': tests an undirected hypothesis--i.e., that the
66+
observed value differs from 0 in the input studies, but
67+
allowing the direction of the deviation to vary by study.
68+
* 'concordant': equivalent to two directed tests, one for each
69+
sign, with correction for 2 tests.
70+
71+
Notes:
72+
(1) All input z-scores are assumed to correspond to one-sided p-values.
73+
Do NOT pass in z-scores that have been directly converted from
74+
two-tailed p-values, as these do not preserve directional
75+
information.
76+
(2) The 'directed' and 'undirected' modes are NOT the same as
77+
one-tailed and two-tailed tests. In general, users who want to test
78+
directed hypotheses should use the 'directed' mode, and users who
79+
want to test for consistent effects in either the positive or
80+
negative direction should use the 'concordant' mode. The
81+
'undirected' mode tests a fairly uncommon null that doesn't
82+
constrain the sign of effects to be consistent across studies
83+
(one can think of it as a test of extremity). In the vast majority
84+
of meta-analysis applications, this mode is not appropriate, and
85+
users should instead opt for 'directed' or 'concordant'.
86+
(3) This estimator does not support meta-regression; any moderators
87+
passed in to fit() as the X array will be ignored.
88+
"""
89+
90+
# Maps Dataset attributes onto fit() args; see BaseEstimator for details.
91+
_dataset_attr_map = {'z': 'y', 'w': 'v'}
92+
93+
def fit(self, z, w=None):
94+
return super().fit(z, w=w)
95+
96+
def p_value(self, z, w=None):
97+
if w is None:
98+
w = np.ones_like(z)
99+
cz = (z * w).sum(0) / np.sqrt((w**2).sum(0))
100+
return ss.norm.sf(cz)
101+
102+
103+
class FisherCombinationTest(CombinationTest):
104+
"""Fisher's method for combining p-values.
105+
106+
Takes a set of independent z-scores and combines them via Fisher's method
107+
to produce a fixed-effect estimate of the combined effect.
108+
109+
Args:
110+
mode (str): The type of test to perform-- i.e., what null hypothesis to
111+
reject. See Winkler et al. (2016) for details. Valid options are:
112+
* 'directed': tests a directional hypothesis--i.e., that the
113+
observed value is consistently greater than 0 in the input
114+
studies.
115+
* 'undirected': tests an undirected hypothesis--i.e., that the
116+
observed value differs from 0 in the input studies, but
117+
allowing the direction of the deviation to vary by study.
118+
* 'concordant': equivalent to two directed tests, one for each
119+
sign, with correction for 2 tests.
120+
121+
Notes:
122+
(1) All input z-scores are assumed to correspond to one-sided p-values.
123+
Do NOT pass in z-scores that have been directly converted from
124+
two-tailed p-values, as these do not preserve directional
125+
information.
126+
(2) The 'directed' and 'undirected' modes are NOT the same as
127+
one-tailed and two-tailed tests. In general, users who want to test
128+
directed hypotheses should use the 'directed' mode, and users who
129+
want to test for consistent effects in either the positive or
130+
negative direction should use the 'concordant' mode. The
131+
'undirected' mode tests a fairly uncommon null that doesn't
132+
constrain the sign of effects to be consistent across studies
133+
(one can think of it as a test of extremity). In the vast majority
134+
of meta-analysis applications, this mode is not appropriate, and
135+
users should instead opt for 'directed' or 'concordant'.
136+
(3) This estimator does not support meta-regression; any moderators
137+
passed in to fit() as the X array will be ignored.
138+
"""
139+
140+
# Maps Dataset attributes onto fit() args; see BaseEstimator for details.
141+
_dataset_attr_map = {'z': 'y'}
142+
143+
def p_value(self, z):
144+
p = self._z_to_p(z)
145+
chi2 = -2 * np.log(p).sum(0)
146+
return ss.chi2.sf(chi2, 2 * z.shape[0])

0 commit comments

Comments
 (0)