Numpy: отримайте індекс елементів 1d масиву у вигляді 2d масиву


10

У мене такий масивний масив: [1 2 2 0 0 1 3 5]

Чи можливо отримати індекс елементів у вигляді 2d масиву? Наприклад, відповідь на вищенаведене введення буде[[3 4], [0 5], [1 2], [6], [], [7]]

Наразі мені доводиться циклічно визначати різні значення та викликати numpy.where(input == i)кожне значення, яке має жахливу ефективність із достатньо великим входом.


np.argsort([1, 2, 2, 0, 0, 1, 3, 5])дає array([3, 4, 0, 5, 1, 2, 6, 7], dtype=int64). то можна просто порівняти наступні елементи.
vb_rises

Відповіді:


11

Ось підхід O (max (x) + len (x)), використовуючи scipy.sparse:

import numpy as np
from scipy import sparse

x = np.array("1 2 2 0 0 1 3 5".split(),int)
x
# array([1, 2, 2, 0, 0, 1, 3, 5])


M,N = x.max()+1,x.size
sparse.csc_matrix((x,x,np.arange(N+1)),(M,N)).tolil().rows.tolist()
# [[3, 4], [0, 5], [1, 2], [6], [], [7]]

Це працює, створюючи розріджену матрицю із записами у позиціях (x [0], 0), (x [1], 1), ... Використовуючи CSCформат (стислий розріджений стовпець), це досить просто. Потім матриця перетворюється у LILформат (пов'язаний список). Цей формат зберігає індекси стовпців для кожного рядка як список у своєму rowsатрибуті, тому все, що нам потрібно зробити, це взяти це та перетворити його у список.

Зауважте, що argsortрішення на основі малих масивів , ймовірно, швидше, але у деяких не шалено великих розмірів це перетнеться.

Редагувати:

argsortзасноване numpy-тільки рішення:

np.split(x.argsort(kind="stable"),np.bincount(x)[:-1].cumsum())
# [array([3, 4]), array([0, 5]), array([1, 2]), array([6]), array([], dtype=int64), array([7])]

Якщо порядок індексів у групах не має значення, ви можете також спробувати argpartition(у цьому невеликому прикладі це не має значення, але це взагалі не гарантується):

bb = np.bincount(x)[:-1].cumsum()
np.split(x.argpartition(bb),bb)
# [array([3, 4]), array([0, 5]), array([1, 2]), array([6]), array([], dtype=int64), array([7])]

Редагувати:

@Divakar рекомендує не використовувати np.split. Натомість цикл, ймовірно, швидший:

A = x.argsort(kind="stable")
B = np.bincount(x+1).cumsum()
[A[B[i-1]:B[i]] for i in range(1,len(B))]

Або ви можете використовувати абсолютно новий (Python3.8 +) оператор моржів:

A = x.argsort(kind="stable")
B = np.bincount(x)
L = 0
[A[L:(L:=L+b)] for b in B.tolist()]

РЕДАКТУВАННЯ (редаговано)

(Не чистий numpy): як альтернатива numba (див. Пост @ senderle), ми також можемо використовувати pythran.

Компілювати з pythran -O3 <filename.py>

import numpy as np

#pythran export sort_to_bins(int[:],int)

def sort_to_bins(idx, mx):
    if mx==-1: 
        mx = idx.max() + 1
    cnts = np.zeros(mx + 2, int)
    for i in range(idx.size):
        cnts[idx[i] + 2] += 1
    for i in range(3, cnts.size):
        cnts[i] += cnts[i-1]
    res = np.empty_like(idx)
    for i in range(idx.size):
        res[cnts[idx[i]+1]] = i
        cnts[idx[i]+1] += 1
    return [res[cnts[i]:cnts[i+1]] for i in range(mx)]

Тут numbaвиграє виграш на основі виступу:

repeat(lambda:enum_bins_numba_buffer(x),number=10)
# [0.6235917090671137, 0.6071486569708213, 0.6096088469494134]
repeat(lambda:sort_to_bins(x,-1),number=10)
# [0.6235359431011602, 0.6264424560358748, 0.6217901279451326]

Старіші речі:

import numpy as np

#pythran export bincollect(int[:])

def bincollect(a):
    o = [[] for _ in range(a.max()+1)]
    for i,j in enumerate(a):
        o[j].append(i)
    return o

Таймінги проти numba (стара)

timeit(lambda:bincollect(x),number=10)
# 3.5732191529823467
timeit(lambda:enumerate_bins(x),number=10)
# 6.7462647299980745

Це закінчилося трохи швидше, ніж відповідь @ Ренді
Фредеріко Шардонг

На основі циклу має бути краще, ніж np.split.
Дівакар

@Divakar гарний момент, дякую!
Пол Панцер

8

Один з можливих варіантів залежно від розміру ваших даних - це просто вийти з numpyта використовувати collections.defaultdict:

In [248]: from collections import defaultdict

In [249]: d = defaultdict(list)

In [250]: l = np.random.randint(0, 100, 100000)

In [251]: %%timeit
     ...: for k, v in enumerate(l):
     ...:     d[v].append(k)
     ...:
10 loops, best of 3: 22.8 ms per loop

Тоді ви закінчуєте словник {value1: [index1, index2, ...], value2: [index3, index4, ...]}. Масштабування часу досить близьке до лінійного за розміром масиву, тому 100000000 займає ~ 2,7 секунди на моїй машині, що здається досить розумним.


7

Хоча запит numpyвирішується, я вирішив зрозуміти, чи є цікаве numbaрішення. І справді є! Ось підхід, який представляє список розділених сторін у вигляді нерівного масиву, що зберігається в одному попередньо виділеному буфері. Це потребує певного натхнення в argsortпідході, запропонованому Полом Панцером . (Для старішої версії, яка не працювала, але була простішою, див. Нижче.)

@numba.jit(numba.void(numba.int64[:], 
                      numba.int64[:], 
                      numba.int64[:]), 
           nopython=True)
def enum_bins_numba_buffer_inner(ints, bins, starts):
    for x in range(len(ints)):
        i = ints[x]
        bins[starts[i]] = x
        starts[i] += 1

@numba.jit(nopython=False)  # Not 100% sure this does anything...
def enum_bins_numba_buffer(ints):
    ends = np.bincount(ints).cumsum()
    starts = np.empty(ends.shape, dtype=np.int64)
    starts[1:] = ends[:-1]
    starts[0] = 0

    bins = np.empty(ints.shape, dtype=np.int64)
    enum_bins_numba_buffer_inner(ints, bins, starts)

    starts[1:] = ends[:-1]
    starts[0] = 0
    return [bins[s:e] for s, e in zip(starts, ends)]

Це обробляє 10-мільйонний список елементів за 75 мс, що майже 50-кратне прискорення у списку на основі версії, написаної на чистому Python.

Що стосується повільнішої, але дещо читабельнішої версії, ось що у мене було раніше, грунтуючись на нещодавно доданій експериментальній підтримці "набраних списків" з динамічним розміром, які дозволяють нам набагато швидше заповнювати кожну скриньку поза порядком.

Це боротьба з numbaдвигуном виводу типу, і я впевнений, що є кращий спосіб впоратися з цією частиною. Це також виявляється майже в 10 разів повільніше, ніж вище.

@numba.jit(nopython=True)
def enum_bins_numba(ints):
    bins = numba.typed.List()
    for i in range(ints.max() + 1):
        inner = numba.typed.List()
        inner.append(0)  # An awkward way of forcing type inference.
        inner.pop()
        bins.append(inner)

    for x, i in enumerate(ints):
        bins[i].append(x)

    return bins

Я перевірив їх на наступне:

def enum_bins_dict(ints):
    enum_bins = defaultdict(list)
    for k, v in enumerate(ints):
        enum_bins[v].append(k)
    return enum_bins

def enum_bins_list(ints):
    enum_bins = [[] for i in range(ints.max() + 1)]
    for x, i in enumerate(ints):
        enum_bins[i].append(x)
    return enum_bins

def enum_bins_sparse(ints):
    M, N = ints.max() + 1, ints.size
    return sparse.csc_matrix((ints, ints, np.arange(N + 1)),
                             (M, N)).tolil().rows.tolist()

Я також протестував їх на попередньо складеній версії цитону, подібній до enum_bins_numba_buffer(детально описаної нижче).

У списку з десяти мільйонів випадкових інт ( ints = np.random.randint(0, 100, 10000000)) я отримую такі результати:

enum_bins_dict(ints)
3.71 s ± 80.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

enum_bins_list(ints)
3.28 s ± 52.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

enum_bins_sparse(ints)
1.02 s ± 34.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

enum_bins_numba(ints)
693 ms ± 5.81 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

enum_bins_cython(ints)
82.3 ms ± 1.77 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

enum_bins_numba_buffer(ints)
77.4 ms ± 2.06 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Вражаючий, такий спосіб роботи з numbaвипереджає cythonверсію тієї самої функції, навіть із вимкненням меж. У мене ще недостатньо знайоме pythranтестування цього підходу, використовуючи його, але мені було б цікаво подивитися порівняння. Мабуть, ймовірно, виходячи з цього прискоренняpythran мабуть, версія при цьому підході також може бути дещо швидшою.

Ось cythonверсія для довідки з деякими інструкціями щодо побудови. Після cythonвстановлення вам знадобиться простий setup.pyфайл на зразок цього:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
import numpy

ext_modules = [
    Extension(
        'enum_bins_cython',
        ['enum_bins_cython.pyx'],
    )
]

setup(
    ext_modules=cythonize(ext_modules),
    include_dirs=[numpy.get_include()]
)

І модуль цитону enum_bins_cython.pyx:

# cython: language_level=3

import cython
import numpy
cimport numpy

@cython.boundscheck(False)
@cython.cdivision(True)
@cython.wraparound(False)
cdef void enum_bins_inner(long[:] ints, long[:] bins, long[:] starts) nogil:
    cdef long i, x
    for x in range(len(ints)):
        i = ints[x]
        bins[starts[i]] = x
        starts[i] = starts[i] + 1

def enum_bins_cython(ints):
    assert (ints >= 0).all()
    # There might be a way to avoid storing two offset arrays and
    # save memory, but `enum_bins_inner` modifies the input, and
    # having separate lists of starts and ends is convenient for
    # the final partition stage.
    ends = numpy.bincount(ints).cumsum()
    starts = numpy.empty(ends.shape, dtype=numpy.int64)
    starts[1:] = ends[:-1]
    starts[0] = 0

    bins = numpy.empty(ints.shape, dtype=numpy.int64)
    enum_bins_inner(ints, bins, starts)

    starts[1:] = ends[:-1]
    starts[0] = 0
    return [bins[s:e] for s, e in zip(starts, ends)]

За допомогою цих двох файлів у вашому робочому каталозі запустіть цю команду:

python setup.py build_ext --inplace

Потім можна імпортувати функцію, використовуючи from enum_bins_cython import enum_bins_cython.


Цікаво, чи знаєте ви пітрану, який в дуже широкому розумінні схожий на нумба. Я додав розчин пітрану до своєї посади. З цього приводу, схоже, пітран мав би перевагу, забезпечуючи швидше і набагато більше пітонічне рішення.
Пол Панцер

@PaulPanzer цікаво! Я не чув про це. Я вважаю, що numba devs додасть очікуваний синтаксичний цукор, коли код списку буде стабільним. Тут також здається, що тут є зручність / швидкість компромісу - jit decorator дуже легко інтегрувати в звичайну базу коду Python порівняно з підходом, що вимагає окремих попередньо складених модулів. Але 3-кратне прискорення над підходом scipy насправді вражає, навіть дивує!
senderle

Просто згадав, що я в основному робив це раніше: stackoverflow.com/q/55226662/7207392 . Ви не хотіли б додати свої версії numba та cython до цих запитань? Різниця лише в тому, що ми не біньємо індекси 0,1,2, ... а замість іншого масиву. І ми не турбуємось насправді рубати отриманий масив.
Пол Панцер

@PaulPanzer ах дуже круто. Я спробую додати його в якийсь момент сьогодні чи завтра. Ви пропонуєте окрему відповідь чи просто редагування своєї відповіді? Щасливі в будь-якому випадку!
відправник

Чудово! Я думаю, що окрема посада була б кращою, але немає сильних переваг.
Пол Панцер

6

Ось справді дивний спосіб зробити це жахливе, але мені здалося, що це занадто смішно, щоб не ділитися - і все numpy!

out = np.array([''] * (x.max() + 1), dtype = object)
np.add.at(out, x, ["{} ".format(i) for i in range(x.size)])
[[int(i) for i in o.split()] for o in out]

Out[]:
[[3, 4], [0, 5], [1, 2], [6], [], [7]]

EDIT: це найкращий метод, який я міг знайти на цьому шляху. Це все ще в 10 разів повільніше, ніж рішення @PaulPanzer argsort:

out = np.empty((x.max() + 1), dtype = object)
out[:] = [[]] * (x.max() + 1)
coords = np.empty(x.size, dtype = object)
coords[:] = [[i] for i in range(x.size)]
np.add.at(out, x, coords)
list(out)

2

Ви можете зробити це, склавши словник чисел, клавішами будуть цифри, а значення повинні бути індексами цього числа, це один з найшвидших способів зробити це, ви можете побачити код нижче:

>>> import numpy as np
>>> a = np.array([1 ,2 ,2 ,0 ,0 ,1 ,3, 5])
>>> b = {}
# Creating an empty list for the numbers that exist in array a
>>> for i in range(np.min(a),np.max(a)+1):
    b[str(i)] = []

# Adding indices to the corresponding key
>>> for i in range(len(a)):
    b[str(a[i])].append(i)

# Resulting Dictionary
>>> b
{'0': [3, 4], '1': [0, 5], '2': [1, 2], '3': [6], '4': [], '5': [7]}

# Printing the result in the way you wanted.
>>> for i in sorted (b.keys()) :
     print(b[i], end = " ")

[3, 4] [0, 5] [1, 2] [6] [] [7] 

1

Псевдокод:

  1. отримайте "число масивів 1d у масиві 2d", віднявши мінімальне значення вашого numpy масиву від максимального значення, а потім плюс один. У вашому випадку це буде 5-0 + 1 = 6

  2. ініціалізувати 2d масив з кількістю 1d масивів всередині нього. У вашому випадку ініціалізуйте 2d масив з 6 1d масивом. Кожен масив 1d відповідає унікальному елементу у вашому масиві numpy, наприклад, перший масив 1d буде відповідати "0", другий масив 1d відповідатиме "1", ...

  3. Проведіть цикл через ваш numpy масив, покладіть індекс елемента в правий відповідний 1d масив. У вашому випадку індекс першого елемента у вашому масиві numpy буде розміщений на другому масиві 1d, індекс другого елемента у вашому масиві numpy буде покладений на третій масив 1d, ....

Цей псевдокод запустить лінійний час, оскільки це залежить від довжини вашої нумерованої масиви.


1

Це дає вам саме те, що ви хочете, і на моїй машині знадобиться приблизно 2,5 секунди за 1000000:

import numpy as np
import timeit

# x = np.array("1 2 2 0 0 1 3 5".split(),int)
x = np.random.randint(0, 100, 100000)

def create_index_list(x):
    d = {}
    max_value = -1
    for i,v in enumerate(x):
        if v > max_value:
            max_value = v
        try:
            d[v].append(i)
        except:
            d[v] = [i]
    result_list = []
    for i in range(max_value+1):
        if i in d:
            result_list.append(d[i])
        else:
            result_list.append([])
    return result_list

# print(create_index_list(x))
print(timeit.timeit(stmt='create_index_list(x)', number=1, globals=globals()))

0

Отже, задавши список елементів, ви хочете скласти (елемент, індекс) пар. У лінійний час це можна зробити так:

hashtable = dict()
for idx, val in enumerate(mylist):
    if val not in hashtable.keys():
         hashtable[val] = list()
    hashtable[val].append(idx)
newlist = sorted(hashtable.values())

Це має зайняти O (n) час. Я не можу придумати швидшого рішення, як зараз, але оновлюсь тут, якщо мені це зробити.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.