Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Support precomputed correlation matrix for calculating variance inflation term in Stouffers #121

Merged
merged 4 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest", "macos-latest"]
os: ["ubuntu-latest", "macos-12"]
python-version: ["3.8", "3.9", "3.10", "3.11"]

name: ${{ matrix.os }} with Python ${{ matrix.python-version }}
Expand Down
28 changes: 21 additions & 7 deletions pymare/estimators/combination.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class StoufferCombinationTest(CombinationTest):
# Maps Dataset attributes onto fit() args; see BaseEstimator for details.
_dataset_attr_map = {"z": "y", "w": "n", "g": "v"}

def _inflation_term(self, z, w, g):
def _inflation_term(self, z, w, g, corr=None):
"""Calculate the variance inflation term for each group.

This term is used to adjust the variance of the combined z-score when
Expand All @@ -127,6 +127,8 @@ def _inflation_term(self, z, w, g):
Array of weights.
g : :obj:`numpy.ndarray` of shape (n, d)
Array of group labels.
corr : :obj:`numpy.ndarray` of shape (n, n), optional
The correlation matrix of the z-values. If None, it will be calculated.

Returns
-------
Expand Down Expand Up @@ -157,26 +159,38 @@ def _inflation_term(self, z, w, g):
continue

# Calculate the within group correlation matrix and sum the non-diagonal elements
corr = np.corrcoef(group_z, rowvar=True)
if corr is None:
if z.shape[1] < 2:
raise ValueError("The number of features must be greater than 1.")
group_corr = np.corrcoef(group_z, rowvar=True)
else:
group_corr = corr[group_indices][:, group_indices]

upper_indices = np.triu_indices(n_samples, k=1)
non_diag_corr = corr[upper_indices]
non_diag_corr = group_corr[upper_indices]
w_i, w_j = weights[upper_indices[0]], weights[upper_indices[1]]

sigma += (2 * w_i * w_j * non_diag_corr).sum()

return sigma

def fit(self, z, w=None, g=None):
def fit(self, z, w=None, g=None, corr=None):
"""Fit the estimator to z-values, optionally with weights and groups."""
return super().fit(z, w=w, g=g)
return super().fit(z, w=w, g=g, corr=corr)

def p_value(self, z, w=None, g=None):
def p_value(self, z, w=None, g=None, corr=None):
"""Calculate p-values."""
if w is None:
w = np.ones_like(z)

if g is None and corr is not None:
warnings.warn("Correlation matrix provided without groups. Ignoring.")

if g is not None and corr is not None and g.shape[0] != corr.shape[0]:
raise ValueError("Group labels must have the same length as the correlation matrix.")

# Calculate the variance inflation term, sum of non-diagonal elements of sigma.
sigma = self._inflation_term(z, w, g) if g is not None else 0
sigma = self._inflation_term(z, w, g, corr=corr) if g is not None else 0

# The sum of diagonal elements of sigma is given by (w**2).sum(0).
variance = (w**2).sum(0) + sigma
Expand Down
25 changes: 25 additions & 0 deletions pymare/tests/test_combination_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,28 @@ def test_stouffer_adjusted():
sigma_l1 = n_maps_l1 * (n_maps_l1 - 1) # Expected inflation term
z_expected_l1 = n_maps_l1 * common_sample / np.sqrt(n_maps_l1 + sigma_l1)
assert np.allclose(z_l1, z_expected_l1, atol=1e-5)

# Test with correlation matrix and groups.
data_corr = data - data.mean(0)
corr = np.corrcoef(data_corr, rowvar=True)
results_corr = (
StoufferCombinationTest("directed").fit(z=data, w=weights, g=groups, corr=corr).params_
)
z_corr = ss.norm.isf(results_corr["p"])

z_corr_expected = np.array([5.00088912, 3.70356943, 4.05465924, 5.4633001, 5.18927878])
assert np.allclose(z_corr, z_corr_expected, atol=1e-5)

# Test with no correlation matrix and groups, but only one feature.
with pytest.raises(ValueError):
StoufferCombinationTest("directed").fit(z=data[:, :1], w=weights[:, :1], g=groups)

# Test with correlation matrix and groups of different shapes.
with pytest.raises(ValueError):
StoufferCombinationTest("directed").fit(z=data, w=weights, g=groups, corr=corr[:-2, :-2])

# Test with correlation matrix and no groups.
results1 = StoufferCombinationTest("directed").fit(z=_z1, corr=corr).params_
z1 = ss.norm.isf(results1["p"])

assert np.allclose(z1, [4.69574], atol=1e-5)
3 changes: 2 additions & 1 deletion pymare/tests/test_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ def test_combination_test_results_from_arrays(dataset):

# fit overwrites dataset_ attribute with None
assert fitted_estimator.dataset_ is None

# fit_dataset overwrites it with the Dataset
fitted_estimator.fit_dataset(dataset)
fitted_estimator.fit_dataset(Dataset(dataset.y))
assert isinstance(fitted_estimator.dataset_, Dataset)
# fit sets it back to None
fitted_estimator.fit(z=dataset.y)
Expand Down
Loading