Skip to content

Commit 570d464

Browse files
authored
Merge pull request #3454 from effigies/opt/icc_global_cache
RF: Optimize ICC_rep_anova with a memoized helper function
2 parents 6cfba9d + ad2249a commit 570d464

File tree

1 file changed

+35
-17
lines changed

1 file changed

+35
-17
lines changed

nipype/algorithms/icc.py

+35-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
import os
3+
from functools import lru_cache
34
import numpy as np
45
from numpy import ones, kron, mean, eye, hstack, tile
56
from numpy.linalg import pinv
@@ -86,44 +87,61 @@ def _list_outputs(self):
8687
return outputs
8788

8889

89-
def ICC_rep_anova(Y):
90+
@lru_cache(maxsize=1)
91+
def ICC_projection_matrix(shape):
92+
nb_subjects, nb_conditions = shape
93+
94+
x = kron(eye(nb_conditions), ones((nb_subjects, 1))) # sessions
95+
x0 = tile(eye(nb_subjects), (nb_conditions, 1)) # subjects
96+
X = hstack([x, x0])
97+
return X @ pinv(X.T @ X, hermitian=True) @ X.T
98+
99+
100+
def ICC_rep_anova(Y, projection_matrix=None):
90101
"""
91102
the data Y are entered as a 'table' ie subjects are in rows and repeated
92103
measures in columns
93104
94105
One Sample Repeated measure ANOVA
95106
96107
Y = XB + E with X = [FaTor / Subjects]
97-
"""
98108
109+
``ICC_rep_anova`` involves an expensive operation to compute a projection
110+
matrix, which depends only on the shape of ``Y``, which is computed by
111+
calling ``ICC_projection_matrix(Y.shape)``. If arrays of multiple shapes are
112+
expected, it may be worth pre-computing and passing directly as an
113+
argument to ``ICC_rep_anova``.
114+
115+
If only one ``Y.shape`` will occur, you do not need to explicitly handle
116+
these, as the most recently calculated matrix is cached automatically.
117+
For example, if you are running the same computation on every voxel of
118+
an image, you will see significant speedups.
119+
120+
If a ``Y`` is passed with a new shape, a new matrix will be calculated
121+
automatically.
122+
"""
99123
[nb_subjects, nb_conditions] = Y.shape
100124
dfc = nb_conditions - 1
101-
dfe = (nb_subjects - 1) * dfc
102125
dfr = nb_subjects - 1
126+
dfe = dfr * dfc
103127

104128
# Compute the repeated measure effect
105129
# ------------------------------------
106130

107131
# Sum Square Total
108-
mean_Y = mean(Y)
109-
SST = ((Y - mean_Y) ** 2).sum()
110-
111-
# create the design matrix for the different levels
112-
x = kron(eye(nb_conditions), ones((nb_subjects, 1))) # sessions
113-
x0 = tile(eye(nb_subjects), (nb_conditions, 1)) # subjects
114-
X = hstack([x, x0])
132+
demeaned_Y = Y - mean(Y)
133+
SST = np.sum(demeaned_Y**2)
115134

116135
# Sum Square Error
117-
predicted_Y = X @ (pinv(X.T @ X, hermitian=True) @ (X.T @ Y.flatten("F")))
118-
residuals = Y.flatten("F") - predicted_Y
119-
SSE = (residuals**2).sum()
120-
121-
residuals.shape = Y.shape
136+
if projection_matrix is None:
137+
projection_matrix = ICC_projection_matrix(Y.shape)
138+
residuals = Y.flatten("F") - (projection_matrix @ Y.flatten("F"))
139+
SSE = np.sum(residuals**2)
122140

123141
MSE = SSE / dfe
124142

125-
# Sum square session effect - between colums/sessions
126-
SSC = ((mean(Y, 0) - mean_Y) ** 2).sum() * nb_subjects
143+
# Sum square session effect - between columns/sessions
144+
SSC = np.sum(mean(demeaned_Y, 0) ** 2) * nb_subjects
127145
MSC = SSC / dfc / nb_subjects
128146

129147
session_effect_F = MSC / MSE

0 commit comments

Comments
 (0)