|
| 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