Функция быстрого применения к группам для двумерного массива - PullRequest
1 голос
/ 06 ноября 2019

Допустим, у меня есть двумерная матрица данных, и я хочу применить функцию к группам в этой матрице.

Например:

data. ,,,Index

Для каждого уникального индекса я хочу применить некоторую функцию f.

Например, для группы с index = 1 функция fприменяется к значениям 0.556, 0.492, 0.148 (см. первый столбец) и для группы index = 2 функция применяется к значению 0.043.

Дополнительно:

  1. Функция должна транслироватьрезультат с исходным размером входных данных.
  2. Группы уникальны для каждого столбца. Вы можете увидеть это в примере выше, где каждая группа содержит только значения, которые находятся в одном и том же столбце.

Тогда каков абсолютный самый быстрый способ выполнить эту операцию в Python?

В настоящее время я делаю следующее (со случайными данными [2000x500] и 5 случайными группами на столбец):

import numpy as np

rows = 2000
cols = 500
ngroup = 5

data = np.random.rand(rows,cols)
groups = np.random.randint(ngroup, size=(rows,cols)) + 10*np.tile(np.arange(cols),(rows,1))

result = np.zeros(data.shape)                       # Pre-allocating the result
f = lambda x: (x-np.average(x))/np.std(x)           # The function I want to apply
for group in np.unique(groups):                     # Loop over every unique group
    location = np.where(groups == group)            # Find the location of the data
    group_data = data[location[0],location[1]]      # Get the data
    result[location[0],location[1]] = f(group_data) # Apply the function

На моем оборудовании этот расчет занимает около 10 секунд. Есть ли более быстрый способ сделать это?

1 Ответ

2 голосов
/ 06 ноября 2019

Не уверен, что самый быстрый из возможных, но это векторизованное решение намного быстрее:

import numpy as np
import time

np.random.seed(0)
rows = 2000
cols = 500
ngroup = 5

data = np.random.rand(rows,cols)
groups = np.random.randint(ngroup, size=(rows,cols)) + 10*np.tile(np.arange(cols),(rows,1))

t = time.perf_counter()
# Flatten the data
dataf = data.ravel()
groupsf = groups.ravel()
# Sort by group
idx_sort = groupsf.argsort()
datafs = dataf[idx_sort]
groupsfs = groupsf[idx_sort]
# Find group bounds
idx = np.nonzero(groupsfs[1:] > groupsfs[:-1])[0]
idx = np.concatenate([[0], idx + 1, [len(datafs)]])
# Sum by groups
a = np.add.reduceat(datafs, idx[:-1])
# Count group elements
c = np.diff(idx)
# Compute group means
m = a / c
# Repeat means and counts to match data shape
means = np.repeat(m, c)
counts = np.repeat(c, c)
# Compute variance and std
v = np.add.reduceat(np.square(datafs - means), idx[:-1]) / c
s = np.sqrt(v)
# Repeat stds
stds = np.repeat(s, c)
# Compute result values
resultfs = (datafs - means) / stds
# Undo sorting
idx_unsort = np.empty_like(idx_sort)
idx_unsort[idx_sort] = np.arange(len(idx_sort))
resultf = resultfs[idx_unsort]
# Reshape back
result = np.reshape(resultf, data.shape)
print(time.perf_counter() - t)
# 0.09932469999999999

# Previous method to check result
t = time.perf_counter()
result_orig= np.zeros(data.shape)
f = lambda x: (x-np.average(x))/np.std(x)
for group in np.unique(groups):
    location = np.where(groups == group)
    group_data = data[location[0],location[1]]
    result_orig[location[0],location[1]] = f(group_data)
print(time.perf_counter() - t)
# 6.0592527

print(np.allclose(result, result_orig))
# True

РЕДАКТИРОВАТЬ: Чтобы вычислить медианы, вы можете сделать что-то следующим образом:

# Flatten the data
dataf = data.ravel()
groupsf = groups.ravel()
# Sort by group and value
idx_sort = np.lexsort((dataf, groupsf))
datafs = dataf[idx_sort]
groupsfs = groupsf[idx_sort]
# Find group bounds
idx = np.nonzero(groupsfs[1:] > groupsfs[:-1])[0]
idx = np.concatenate([[0], idx + 1, [len(datafs)]])
# Count group elements
c = np.diff(idx)
# Meadian index
idx_median1 = c // 2
idx_median2 = idx_median1 + (c % 2) - 1
idx_median1 += idx[:-1]
idx_median2 += idx[:-1]
# Get medians
meds = 0.5 * (datafs[idx_median1] + datafs[idx_median2])

Хитрость здесь в том, чтобы использовать np.lexsort вместо просто np.argsort для сортировки по группам и значениям. meds будет массивом с медианой каждой группы, затем вы можете использовать np.repeat для него, как для средств, так и для чего угодно еще.

...