Найти максимальную позицию для уникального бина (binargmax) - PullRequest
0 голосов
/ 24 августа 2018

Настройка

Предположим, у меня есть

bins = np.array([0, 0, 1, 1, 2, 2, 2, 0, 1, 2])
vals = np.array([8, 7, 3, 4, 1, 2, 6, 5, 0, 9])
k = 3

Мне нужна позиция максимальных значений для уникального бина в bins.

# Bin == 0
#  ↓ ↓           ↓
# [0 0 1 1 2 2 2 0 1 2]
# [8 7 3 4 1 2 6 5 0 9]
#  ↑ ↑           ↑
#  ⇧
# [0 1 2 3 4 5 6 7 8 9]
# Maximum is 8 and happens at position 0

(vals * (bins == 0)).argmax()

0

# Bin == 1
#      ↓ ↓         ↓
# [0 0 1 1 2 2 2 0 1 2]
# [8 7 3 4 1 2 6 5 0 9]
#      ↑ ↑         ↑
#        ⇧
# [0 1 2 3 4 5 6 7 8 9]
# Maximum is 4 and happens at position 3

(vals * (bins == 1)).argmax()

3

# Bin == 2
#          ↓ ↓ ↓     ↓
# [0 0 1 1 2 2 2 0 1 2]
# [8 7 3 4 1 2 6 5 0 9]
#          ↑ ↑ ↑     ↑
#                    ⇧
# [0 1 2 3 4 5 6 7 8 9]
# Maximum is 9 and happens at position 9

(vals * (bins == 2)).argmax()

9

Эти функции являются хакерскими и даже не могут быть обобщены для отрицательных значений.

Вопрос

Как получить все такие значения наиболее эффективным способомиспользуя Numpy?

Что я пробовал.

def binargmax(bins, vals, k):
  out = -np.ones(k, np.int64)
  trk = np.empty(k, vals.dtype)
  trk.fill(np.nanmin(vals) - 1)

  for i in range(len(bins)):
    v = vals[i]
    b = bins[i]
    if v > trk[b]:
      trk[b] = v
      out[b] = i

  return out

binargmax(bins, vals, k)

array([0, 3, 9])

ССЫЛКА НА ИСПЫТАНИЯ И ВАЛИДАЦИЮ

Ответы [ 7 ]

0 голосов
/ 24 августа 2018

Хорошо, вот моя запись с линейным временем, использующая только индексирование и np.(max|min)inum.at.Предполагается, что бункеры увеличиваются с 0 до максимума (бинов).

def via_at(bins, vals):
    max_vals = np.full(bins.max()+1, -np.inf)
    np.maximum.at(max_vals, bins, vals)
    expanded = max_vals[bins]
    max_idx = np.full_like(max_vals, np.inf)
    np.minimum.at(max_idx, bins, np.where(vals == expanded, np.arange(len(bins)), np.inf))
    return max_vals, max_idx
0 голосов
/ 25 августа 2018

Я знаю, что вы сказали использовать Numpy, но если Панды приемлемы:

import numpy as np; import pandas as pd;
(pd.DataFrame(
    {'bins':np.array([0, 0, 1, 1, 2, 2, 2, 0, 1, 2]),
     'values':np.array([8, 7, 3, 4, 1, 2, 6, 5, 0, 9])}) 
.groupby('bins')
.idxmax())

      values
bins        
0          0
1          3
2          9
0 голосов
/ 24 августа 2018

Если вы хотите читабельности, это может быть не лучшим решением, но я думаю, что оно работает

def binargsort(bins,vals):
    s = np.lexsort((vals,bins))
    s2 = np.sort(bins)
    msk = np.roll(s2,-1) != s2
    # or use this for msk, but not noticeably better for performance:
    # msk = np.append(np.diff(np.sort(bins)),1).astype(bool)
    return s[msk]

array([0, 3, 9])

Объяснение

lexsort сортирует индексы vals в порядке сортировки bins, затем по порядку vals:

>>> np.lexsort((vals,bins))
array([7, 1, 0, 8, 2, 3, 4, 5, 6, 9])

Итак, вы можете замаскировать то, в каком порядке bins отличается от одного индекса к следующему:

>>> np.sort(bins)
array([0, 0, 0, 1, 1, 1, 2, 2, 2, 2])

# Find where sorted bins end, use that as your mask on the `lexsort`
>>> np.append(np.diff(np.sort(bins)),1)
array([0, 0, 1, 0, 0, 1, 0, 0, 0, 1])

>>> np.lexsort((vals,bins))[np.append(np.diff(np.sort(bins)),1).astype(bool)]
array([0, 3, 9])
0 голосов
/ 24 августа 2018

Вот один способ, смещая данные каждой группы, чтобы мы могли использовать argsort для всех данных за один раз -

def binargmax_scale_sort(bins, vals):
    w = np.bincount(bins)
    valid_mask = w!=0
    last_idx = w[valid_mask].cumsum()-1
    scaled_vals = bins*(vals.max()+1) + vals
    #unique_bins = np.flatnonzero(valid_mask) # if needed
    return len(bins) -1 -np.argsort(scaled_vals[::-1], kind='mergesort')[last_idx]
0 голосов
/ 24 августа 2018

Как насчет этого:

>>> import numpy as np
>>> bins = np.array([0, 0, 1, 1, 2, 2, 2, 0, 1, 2])
>>> vals = np.array([8, 7, 3, 4, 1, 2, 6, 5, 0, 9])
>>> k = 3
>>> np.argmax(vals*(bins == np.arange(k)[:,np.newaxis]),axis=-1)
array([0, 3, 9])
0 голосов
/ 24 августа 2018

Библиотека numpy_indexed:

Я знаю, что это технически не numpy, но библиотека numpy_indexed имеет векторизованную функцию group_by, которая идеально подходит для этого, просто хотела поделиться какя часто использую альтернативу:

>>> import numpy_indexed as npi
>>> npi.group_by(bins).argmax(vals)
(array([0, 1, 2]), array([0, 3, 9], dtype=int64))

Использование простых pandas groupby и idxmax:

df = pd.DataFrame({'bins': bins, 'vals': vals})
df.groupby('bins').vals.idxmax()

Использование sparse.csr_matrix

Эта опция очень быстрая на очень больших входах.

sparse.csr_matrix(
    (vals, bins, np.arange(vals.shape[0]+1)), (vals.shape[0], k)
).argmax(0)

# matrix([[0, 3, 9]])

Производительность

Функции

def chris(bins, vals, k):
    return npi.group_by(bins).argmax(vals)

def chris2(df):
    return df.groupby('bins').vals.idxmax()

def chris3(bins, vals, k):
    sparse.csr_matrix((vals, bins, np.arange(vals.shape[0] + 1)), (vals.shape[0], k)).argmax(0)

def divakar(bins, vals, k):
    mx = vals.max()+1

    sidx = bins.argsort()
    sb = bins[sidx]
    sm = np.r_[sb[:-1] != sb[1:],True]

    argmax_out = np.argsort(bins*mx + vals)[sm]
    max_out = vals[argmax_out]
    return max_out, argmax_out

def divakar2(bins, vals, k):
    last_idx = np.bincount(bins).cumsum()-1
    scaled_vals = bins*(vals.max()+1) + vals
    argmax_out = np.argsort(scaled_vals)[last_idx]
    max_out = vals[argmax_out]
    return max_out, argmax_out


def user545424(bins, vals, k):
    return np.argmax(vals*(bins == np.arange(bins.max()+1)[:,np.newaxis]),axis=-1)

def user2699(bins, vals, k):
    res = []
    for v in np.unique(bins):
        idx = (bins==v)
        r = np.where(idx)[0][np.argmax(vals[idx])]
        res.append(r)
    return np.array(res)

def sacul(bins, vals, k):
    return np.lexsort((vals, bins))[np.append(np.diff(np.sort(bins)), 1).astype(bool)]

@njit
def piRSquared(bins, vals, k):
    out = -np.ones(k, np.int64)
    trk = np.empty(k, vals.dtype)
    trk.fill(np.nanmin(vals))

    for i in range(len(bins)):
        v = vals[i]
        b = bins[i]
        if v > trk[b]:
            trk[b] = v
            out[b] = i

    return out

Настройка

import numpy_indexed as npi
import numpy as np
import pandas as pd
from timeit import timeit
import matplotlib.pyplot as plt
from numba import njit
from scipy import sparse

res = pd.DataFrame(
       index=['chris', 'chris2', 'chris3', 'divakar', 'divakar2', 'user545424', 'user2699', 'sacul', 'piRSquared'],
       columns=[10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000, 500000],
       dtype=float
)

k = 5

for f in res.index:
    for c in res.columns:
        bins = np.random.randint(0, k, c)
        k = 5
        vals = np.random.rand(c)
        df = pd.DataFrame({'bins': bins, 'vals': vals})
        stmt = '{}(df)'.format(f) if f in {'chris2'} else '{}(bins, vals, k)'.format(f)
        setp = 'from __main__ import bins, vals, k, df, {}'.format(f)
        res.at[f, c] = timeit(stmt, setp, number=50)

ax = res.div(res.min()).T.plot(loglog=True)
ax.set_xlabel("N");
ax.set_ylabel("time (relative)");

plt.show()

Результаты

enter image description here

Результаты с гораздо большим k (Это место, где радиовещание сильно пострадали):

res = pd.DataFrame(
       index=['chris', 'chris2', 'chris3', 'divakar', 'divakar2', 'user545424', 'user2699', 'sacul', 'piRSquared'],
       columns=[10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000, 500000],
       dtype=float
)

k = 500

for f in res.index:
    for c in res.columns:
        bins = np.random.randint(0, k, c)
        vals = np.random.rand(c)
        df = pd.DataFrame({'bins': bins, 'vals': vals})
        stmt = '{}(df)'.format(f) if f in {'chris2'} else '{}(bins, vals, k)'.format(f)
        setp = 'from __main__ import bins, vals, df, k, {}'.format(f)
        res.at[f, c] = timeit(stmt, setp, number=50)

ax = res.div(res.min()).T.plot(loglog=True)
ax.set_xlabel("N");
ax.set_ylabel("time (relative)");

plt.show()

enter image description here

Как видно из графиков, вещание - это хитрый трюк, когда количество групп невелико, однако сложность времени / память вещания слишком быстро увеличивается при увеличении k значений для высокой производительности.

0 голосов
/ 24 августа 2018

Это небольшая забавная проблема, которую нужно решить. Мой подход заключается в том, чтобы получить индекс в vals на основе значений в bins. Использование where для получения точек с индексом True в сочетании с argmax для этих точек в vals дает результирующее значение.

def binargmaxA(bins, vals):
    res = []
    for v in unique(bins):
        idx = (bins==v)
        r = where(idx)[0][argmax(vals[idx])]
        res.append(r)
    return array(res)

Можно удалить вызов unique, используя range(k), чтобы получить возможные значения бина. Это ускоряет процесс, но все равно оставляет его с низкой производительностью при увеличении размера k.

def binargmaxA2(bins, vals, k):
    res = []
    for v in range(k):
        idx = (bins==v)
        r = where(idx)[0][argmax(vals[idx])]
        res.append(r)
    return array(res)

Последняя попытка сравнения каждого значения существенно замедляет процесс. Эта версия вычисляет отсортированный массив значений, а не делает сравнение для каждого уникального значения. Ну, на самом деле он вычисляет отсортированные индексы и получает отсортированные значения только при необходимости, поскольку это позволяет избежать однократной загрузки значений в память. Производительность по-прежнему зависит от количества бинов, но гораздо медленнее, чем раньше.

def binargmaxB(bins, vals):
    idx = argsort(bins)   # Find sorted indices
    split = r_[0, where(diff(bins[idx]))[0]+1, len(bins)]  # Compute where values start in sorted array
    newmax = [argmax(vals[idx[i1:i2]]) for i1, i2 in zip(split, split[1:])]  # Find max for each value in sorted array
    return idx[newmax +split[:-1]] # Convert to indices in unsorted array

Тесты

Вот некоторые тесты с другими ответами.

3000 элементов

с немного большим набором данных (bins = randint(0, 30, 3000); vals = randn(3000); k = 30;)

  • 171us binargmax_scale_sort2 от Divakar
  • 209us этот ответ, версия B
  • 281us binargmax_scale_sort by Divakar
  • 329us широковещательная версия user545424
  • 399us этот ответ, версия A
  • 416us ответ от sacul, используя lexsort
  • 899us ссылочный код от piRsquared

30000 элементов

И еще больший набор данных (bins = randint(0, 30, 30000); vals = randn(30000); k = 30). Удивительно, но это не меняет относительную производительность между решениями.

  • 1,27мс этот ответ, версия B
  • 2,01 мс binargmax_scale_sort2 от Divakar
  • 2,38 мс широковещательная версия пользователя 5445424
  • 2,68 мс этот ответ, версия A
  • 5,71 мс ответ от sacul, используя lexsort
  • 9.12ms ссылочный код от piRSquared

Редактировать Я не изменил k с увеличением количества возможных значений бина, теперь, когда я установил, что тесты более ровные.

1000 значений корзины

Увеличение числа уникальных значений корзины также может повлиять на производительность. Решения Divakar и Sacul в основном не затронуты, в то время как другие имеют довольно существенное влияние. bins = randint(0, 1000, 30000); vals = randn(30000); k = 1000

  • 1,99 мс binargmax_scale_sort2 от Divakar
  • 3,48 мс этот ответ, версия B
  • 6,15 мс ответ от sacul, используя lexsort
  • 10,6 мс ссылочный код от piRsquared
  • 27,2мс этот ответ, версия A
  • 129мс широковещательная версия от пользователя545424

Редактировать Включая тесты для ссылочного кода в вопросе, он удивительно конкурентоспособен, особенно с большим количеством корзин.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...