I wrote some code in Python which works fine but is very slow; I think due to the for loops. I hope one can speed up the following operations using numpy commands. Let me define the goal.
Let's assume I have a 2D numpy array all_CMs
of dimensions row
x col
. For instance consider a 6
x11
array (see drawing below).
I want to calculate the mean for all rows, i.e. sumⱼ aᵢⱼ resulting in an array. This, of course can be easily done. (I call this value CM_tilde
)
Now, for each row I want to calculate the mean of some selected values, namely all values below a certain threshold by computing their sum and dividing it by the number of all columns (N
). If the value is above this defined threshold, the CM_tilde
value (mean of the entire row) is added. This value is called CM
Afterwards, the CM
value is subtracted from each element in the row
In addition to this I want to have a numpy array or list where all those CM
values are listed.
The figure:
The following code is working but very slow (especially if the arrays getting large)
CM_tilde = np.mean(data, axis=1)
N = data.shape[1]
data_cm = np.zeros(( data.shape[0], data.shape[1], data.shape[2] ))
all_CMs = np.zeros(( data.shape[0], data.shape[2]))
for frame in range(data.shape[2]):
for row in range(data.shape[0]):
CM=0
for col in range(data.shape[1]):
if data[row, col, frame] < (CM_tilde[row, frame]+threshold):
CM += data[row, col, frame]
else:
CM += CM_tilde[row, frame]
CM = CM/N
all_CMs[row, frame] = CM
# calculate CM corrected value
for col in range(data.shape[1]):
data_cm[row, col, frame] = data[row, col, frame] - CM
print "frame: ", frame
return data_cm, all_CMs
Any ideas?
It's quite easy to vectorize what you're doing:
import numpy as np
#generate dummy data
nrows=6
ncols=11
nframes=3
threshold=0.3
data=np.random.rand(nrows,ncols,nframes)
CM_tilde = np.mean(data, axis=1)
N = data.shape[1]
all_CMs2 = np.mean(np.where(data < (CM_tilde[:,None,:]+threshold),data,CM_tilde[:,None,:]),axis=1)
data_cm2 = data - all_CMs2[:,None,:]
Comparing this with your originals:
In [684]: (data_cm==data_cm2).all()
Out[684]: True
In [685]: (all_CMs==all_CMs2).all()
Out[685]: True
The logic is that we work with arrays of size [nrows,ncols,nframes]
simultaneously. The main trick is to make use of python's broadcasting, by turning CM_tilde
of size [nrows,nframes]
into CM_tilde[:,None,:]
of size [nrows,1,nframes]
. Python will then use the same values for each column, since that is a singleton dimension of this modified CM_tilde
.
By using np.where
we choose (based on the threshold
) whether we want to get the corresponding value of data
, or, again, the broadcast value of CM_tilde
. A new use of np.mean
allows us to compute all_CMs2
.
In the final step we made use of broadcasting by directly subtracting this new all_CMs2
from the corresponding elements of data
.
It might help in vectorizing code this way by looking at the implicit indices of your temporary variables. What I mean is that your temporary variable CM
lives inside a loop over [nrows,nframes]
, and its value is reset with each iteration. This means that CM
is in effect a quantity CM[row,frame]
(later explicitly assigned to the 2d array all_CMs
), and from here it's easy to see that you can construct it by summing up an appropriate CMtmp[row,col,frames]
quantity along its column dimension. If it helps, you can name the np.where(...)
part as CMtmp
for this purpose, and then compute np.mean(CMtmp,axis=1)
from that. Same result, obviously, but probably more transparent.
Here's my vectorization of your function. I worked from the inside out, and commented out earlier versions as I went along. So the first loop that I vectorized has ###
comment marks.
It isn't as clean and well reasoned as @Andras's
answer, but hopefully it is instructional, giving an idea of how you can address this issue incrementally.
def foo2(data, threshold):
CM_tilde = np.mean(data, axis=1)
N = data.shape[1]
#data_cm = np.zeros(( data.shape[0], data.shape[1], data.shape[2] ))
##all_CMs = np.zeros(( data.shape[0], data.shape[2]))
bmask = data < (CM_tilde[:,None,:] + threshold)
CM = np.zeros_like(data)
CM[:] = CM_tilde[:,None,:]
CM[bmask] = data[bmask]
CM = CM.sum(axis=1)
CM = CM/N
all_CMs = CM.copy()
"""
for frame in range(data.shape[2]):
for row in range(data.shape[0]):
###print(frame, row)
###mask = data[row, :, frame] < (CM_tilde[row, frame]+threshold)
###print(mask)
##mask = bmask[row,:,frame]
##CM = data[row, mask, frame].sum()
##CM += (CM_tilde[row, frame]*(~mask)).sum()
##CM = CM/N
##all_CMs[row, frame] = CM
## calculate CM corrected value
#for col in range(data.shape[1]):
# data_cm[row, col, frame] = data[row, col, frame] - CM[row,frame]
print "frame: ", frame
"""
data_cm = data - CM[:,None,:]
return data_cm, all_CMs
Output matches for this small test case, which more than anything helped me get the dimensions right.
threshold = .1
data = np.arange(4*3*2,dtype=float).reshape(4,3,2)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With