Самый эффективный способ найти режим в массиве numpy


89

У меня есть 2D-массив, содержащий целые числа (как положительные, так и отрицательные). Каждая строка представляет значения во времени для конкретного пространственного сайта, тогда как каждый столбец представляет значения для различных пространственных сайтов для данного времени.

Итак, если массив такой:

1 3 4 2 2 7
5 2 2 1 4 1
3 3 2 2 1 1

Результат должен быть

1 3 2 2 2 1

Обратите внимание, что при наличии нескольких значений для режима любое из них (выбранное случайным образом) может быть установлено в качестве режима.

Я могу перебирать режим поиска столбцов по одному, но я надеялся, что у numpy может быть какая-то встроенная функция для этого. Или есть трюк, чтобы найти это эффективно без зацикливания.



1
@ tom10: Вы имеете в виду scipy.stats.mode () , верно? Другой, кажется, выводит замаскированный массив.
fgb 02

@fgb: верно, спасибо за исправление (и +1 за ваш ответ).
tom10

Ответы:


121

Проверьте scipy.stats.mode()(вдохновленный комментарием @ tom10):

import numpy as np
from scipy import stats

a = np.array([[1, 3, 4, 2, 2, 7],
              [5, 2, 2, 1, 4, 1],
              [3, 3, 2, 2, 1, 1]])

m = stats.mode(a)
print(m)

Вывод:

ModeResult(mode=array([[1, 3, 2, 2, 1, 1]]), count=array([[1, 2, 2, 2, 1, 2]]))

Как видите, он возвращает как режим, так и количество. Вы можете выбрать режимы напрямую через m[0]:

print(m[0])

Вывод:

[[1 3 2 2 1 1]]

4
Итак, numpy сам по себе не поддерживает такую ​​функцию?
Ник

1
По-видимому, нет, но реализация scipy полагается только на numpy , поэтому вы можете просто скопировать этот код в свою собственную функцию.
fgb 02

12
Просто примечание для людей, которые смотрят на это в будущем: вам нужно import scipy.statsявно, это не включается, когда вы просто делаете import scipy.
Ffledgling

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

2
@Rahul: вы должны учитывать второй аргумент по умолчанию axis=0. Приведенный выше код сообщает режим для каждого столбца ввода. Счетчик сообщает нам, сколько раз он видел отчетный режим в каждом из столбцов. Если вам нужен общий режим, вам нужно указать axis=None. Для получения дополнительной информации, пожалуйста, обратитесь к docs.scipy.org/doc/scipy/reference/generated/…
fgb

22

Обновить

В scipy.stats.mode этого поста функция была значительно оптимизирована и будет рекомендованным методом.

Старый ответ

Это сложная проблема, так как не так много возможностей для расчета режима вдоль оси. Решение прямо вперед для 1-D массивов, где numpy.bincountэто удобно, наряду с numpy.uniqueс return_countsарг как True. Самая распространенная n-мерная функция, которую я вижу, - это scipy.stats.mode, хотя она чрезмерно медленная, особенно для больших массивов со многими уникальными значениями. В качестве решения я разработал эту функцию и активно ее использую:

import numpy

def mode(ndarray, axis=0):
    # Check inputs
    ndarray = numpy.asarray(ndarray)
    ndim = ndarray.ndim
    if ndarray.size == 1:
        return (ndarray[0], 1)
    elif ndarray.size == 0:
        raise Exception('Cannot compute mode on empty array')
    try:
        axis = range(ndarray.ndim)[axis]
    except:
        raise Exception('Axis "{}" incompatible with the {}-dimension array'.format(axis, ndim))

    # If array is 1-D and numpy version is > 1.9 numpy.unique will suffice
    if all([ndim == 1,
            int(numpy.__version__.split('.')[0]) >= 1,
            int(numpy.__version__.split('.')[1]) >= 9]):
        modals, counts = numpy.unique(ndarray, return_counts=True)
        index = numpy.argmax(counts)
        return modals[index], counts[index]

    # Sort array
    sort = numpy.sort(ndarray, axis=axis)
    # Create array to transpose along the axis and get padding shape
    transpose = numpy.roll(numpy.arange(ndim)[::-1], axis)
    shape = list(sort.shape)
    shape[axis] = 1
    # Create a boolean array along strides of unique values
    strides = numpy.concatenate([numpy.zeros(shape=shape, dtype='bool'),
                                 numpy.diff(sort, axis=axis) == 0,
                                 numpy.zeros(shape=shape, dtype='bool')],
                                axis=axis).transpose(transpose).ravel()
    # Count the stride lengths
    counts = numpy.cumsum(strides)
    counts[~strides] = numpy.concatenate([[0], numpy.diff(counts[~strides])])
    counts[strides] = 0
    # Get shape of padded counts and slice to return to the original shape
    shape = numpy.array(sort.shape)
    shape[axis] += 1
    shape = shape[transpose]
    slices = [slice(None)] * ndim
    slices[axis] = slice(1, None)
    # Reshape and compute final counts
    counts = counts.reshape(shape).transpose(transpose)[slices] + 1

    # Find maximum counts and return modals/counts
    slices = [slice(None, i) for i in sort.shape]
    del slices[axis]
    index = numpy.ogrid[slices]
    index.insert(axis, numpy.argmax(counts, axis=axis))
    return sort[index], counts[index]

Результат:

In [2]: a = numpy.array([[1, 3, 4, 2, 2, 7],
                         [5, 2, 2, 1, 4, 1],
                         [3, 3, 2, 2, 1, 1]])

In [3]: mode(a)
Out[3]: (array([1, 3, 2, 2, 1, 1]), array([1, 2, 2, 2, 1, 2]))

Некоторые тесты:

In [4]: import scipy.stats

In [5]: a = numpy.random.randint(1,10,(1000,1000))

In [6]: %timeit scipy.stats.mode(a)
10 loops, best of 3: 41.6 ms per loop

In [7]: %timeit mode(a)
10 loops, best of 3: 46.7 ms per loop

In [8]: a = numpy.random.randint(1,500,(1000,1000))

In [9]: %timeit scipy.stats.mode(a)
1 loops, best of 3: 1.01 s per loop

In [10]: %timeit mode(a)
10 loops, best of 3: 80 ms per loop

In [11]: a = numpy.random.random((200,200))

In [12]: %timeit scipy.stats.mode(a)
1 loops, best of 3: 3.26 s per loop

In [13]: %timeit mode(a)
1000 loops, best of 3: 1.75 ms per loop

РЕДАКТИРОВАТЬ: предоставил больше информации и изменил подход, чтобы он был более эффективным с точки зрения памяти


1
Пожалуйста, внесите его в модуль статистики scipy, чтобы другие также могли извлечь из него пользу.
ARF

Для задач более высокой размерности с большими int ndarrays ваше решение, кажется, все еще намного быстрее, чем scipy.stats.mode. Мне пришлось вычислить режим вдоль первой оси ndarray 4x250x250x500, и ваша функция заняла 10 секунд, а scipy.stats.mode - почти 600 секунд.
CheshireCat

11

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

(_, idx, counts) = np.unique(a, return_index=True, return_counts=True)
index = idx[np.argmax(counts)]
mode = a[index]

Не забудьте отказаться от режима, когда len (np.argmax (counts))> 1, а также для проверки того, действительно ли он является репрезентативным для центрального распределения ваших данных, вы можете проверить, попадает ли он в ваш интервал стандартного отклонения.


Когда np.argmax когда-либо возвращает что-то с длиной больше 1, если вы не укажете ось?
loganjones16

10

Изящное решение, которое использует толькоnumpy (но scipyне Counterкласс):

A = np.array([[1,3,4,2,2,7], [5,2,2,1,4,1], [3,3,2,2,1,1]])

np.apply_along_axis(lambda x: np.bincount(x).argmax(), axis=0, arr=A)

массив ([1, 3, 2, 2, 1, 1])


1
Красиво и кратко, но следует использовать с осторожностью, если исходные массивы содержат очень большое число, потому что bincount создаст массивы bin с len (max (A [i])) для каждого исходного массива A [i].
scottlittle,

Это отличное решение. На самом деле есть недостаток scipy.stats.mode. Когда есть несколько значений, имеющих наибольшее количество случаев (несколько режимов), будет выдано ожидание. Но этот метод автоматически перейдет в «первый режим».
Christopher

5

Если вы хотите использовать только numpy:

x = [-1, 2, 1, 3, 3]
vals,counts = np.unique(x, return_counts=True)

дает

(array([-1,  1,  2,  3]), array([1, 1, 1, 2]))

И извлеките его:

index = np.argmax(counts)
return vals[index]

Нравится этот метод, потому что он поддерживает не только целые числа, но также числа с плавающей запятой и даже строки!
Кристофер

3

Я думаю, что очень простой способ - использовать класс Counter. Затем вы можете использовать функцию most_common () экземпляра Counter, как указано здесь .

Для одномерных массивов:

import numpy as np
from collections import Counter

nparr = np.arange(10) 
nparr[2] = 6 
nparr[3] = 6 #6 is now the mode
mode = Counter(nparr).most_common(1)
# mode will be [(6,3)] to give the count of the most occurring value, so ->
print(mode[0][0])    

Для многомерных массивов (небольшая разница):

import numpy as np
from collections import Counter

nparr = np.arange(10) 
nparr[2] = 6 
nparr[3] = 6 
nparr = nparr.reshape((10,2,5))     #same thing but we add this to reshape into ndarray
mode = Counter(nparr.flatten()).most_common(1)  # just use .flatten() method

# mode will be [(6,3)] to give the count of the most occurring value, so ->
print(mode[0][0])

Это может быть или не быть эффективной реализацией, но она удобна.


2
from collections import Counter

n = int(input())
data = sorted([int(i) for i in input().split()])

sorted(sorted(Counter(data).items()), key = lambda x: x[1], reverse = True)[0][0]

print(Mean)

Counter(data)Подсчитывает частоту и возвращает defaultdict. sorted(Counter(data).items())сортирует с помощью клавиш, а не по частоте. Наконец, необходимо отсортировать частоту, используя другую сортировку key = lambda x: x[1]. Обратное указывает Python, что нужно отсортировать частоту от наибольшей к наименьшей.


Поскольку вопрос был задан 6 лет назад, это нормально, что он не получил большой репутации.
Зелиха Бектас

1

самый простой способ в Python получить режим списка или массива

   import statistics
   print("mode = "+str(statistics.(mode(a)))

это оно

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.