Numpy: รับดัชนีองค์ประกอบของอาร์เรย์ 1d เป็นอาร์เรย์ 2d


10

ฉันมีอาร์เรย์ numpy เช่นนี้: [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 (สูงสุด (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- based 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))]

หรือคุณสามารถใช้ตัวดำเนินการ walrus (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

จบลงด้วยความรวดเร็วกว่าคำตอบของ @ Randy
Frederico Schardong

np.splitหนึ่งวงที่ใช้ควรจะดีกว่า
Divakar

@Divakar จุดดีขอบคุณ!
Paul Panzer

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, ...]}แล้วคุณจะจบลงด้วยพจนานุกรมของ การปรับสเกลเวลานั้นค่อนข้างใกล้เคียงกับ linear ด้วยขนาดของ array ดังนั้น 10,000,000 ใช้เวลา ~ 2.7s ในเครื่องของฉันซึ่งดูเหมือนสมเหตุสมผล


7

แม้ว่าการร้องขอสำหรับการnumpyแก้ปัญหาฉันตัดสินใจที่จะดูว่ามีการnumbaแก้ปัญหาตามที่น่าสนใจ และแน่นอนมี! ต่อไปนี้เป็นวิธีการที่แสดงรายการพาร์ติชันเป็นอาเรย์ ragged ที่เก็บไว้ในบัฟเฟอร์ที่จัดสรรล่วงหน้าเดียว นี้จะแรงบันดาลใจบางอย่างจาก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)]

รายการนี้ประมวลผลรายการสิบล้านรายการใน 75ms ซึ่งเกือบจะเร็วขึ้น 50 เท่าจากรายการตามรุ่นที่เขียนด้วย Python บริสุทธิ์

สำหรับรุ่นที่ช้ากว่า แต่ค่อนข้างอ่านได้มากขึ้นนี่คือสิ่งที่ฉันเคยมีมาก่อนหน้านี้ตามการสนับสนุนการทดลองที่เพิ่มขึ้นล่าสุดสำหรับ "รายการที่พิมพ์" ซึ่งมีขนาดไดนามิกซึ่งช่วยให้เราเติมถังแต่ละรายการได้อย่างรวดเร็ว

การต่อสู้แบบนี้กับเอ็นจิ้นnumbaอนุมานชนิด 's และฉันแน่ใจว่ามีวิธีที่ดีกว่าในการจัดการส่วนนั้น สิ่งนี้กลายเป็นช้าลงเกือบ 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()

ฉันยังทดสอบพวกเขาเทียบกับเวอร์ชัน cython ที่คอมไพล์แล้วคล้ายกับenum_bins_numba_buffer(อธิบายในรายละเอียดด้านล่าง)

ในรายการสิบ ints สุ่ม ( 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()]
)

และโมดูล cython 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จากนั้นคุณสามารถนำเข้าฟังก์ชั่นการใช้


ฉันสงสัยว่าคุณทราบ pythran ซึ่งในวงกว้างมากคล้ายกับ numba ฉันเพิ่มโซลูชัน pythran ในโพสต์ของฉัน ในโอกาสนี้ดูเหมือนว่างูเหลือมจะมีมือที่สูงกว่าส่งมอบวิธีการแก้ปัญหาที่รวดเร็วและมากขึ้น
Paul Panzer

@PaulPanzer น่าสนใจ! ฉันไม่เคยได้ยินเรื่องนี้ ฉันรวบรวมว่า dev ของ numba จะเพิ่มน้ำตาล syntactic ที่คาดไว้เมื่อรหัสรายการเสถียร นอกจากนี้ยังมีความสะดวกสบาย / การแลกเปลี่ยนความเร็วที่นี่ - มัณฑนากร jit นั้นง่ายมากที่จะรวมเข้ากับฐานรหัส Python ธรรมดาเมื่อเทียบกับวิธีการที่ต้องใช้โมดูลที่เตรียมไว้แยกต่างหาก แต่การเร่งความเร็วแบบ 3 มิติเหนือวิธี scipy นั้นน่าประทับใจและน่าประหลาดใจจริงๆ!
senderle

เพียงแค่จำได้ว่าผมเคยทำแบบนี้มาก่อนโดยทั่วไป: stackoverflow.com/q/55226662/7207392 คุณคิดจะเพิ่มเวอร์ชันของ numba และ cython ลงใน Q&A นั้นหรือไม่? ข้อแตกต่างคือ: เราไม่ได้ทำดัชนี 0,1,2, ... แต่แทนอาเรย์อื่น และเราไม่ได้สนใจที่จะสับอาร์เรย์ผลลัพธ์
Paul Panzer

@PaulPanzer อาเจ๋งมาก ฉันจะลองเพิ่มในบางวันวันนี้หรือพรุ่งนี้ คุณกำลังแนะนำคำตอบแยกต่างหากหรือเพียงแก้ไขคำตอบของคุณ? มีความสุขทั้งทาง!
senderle

ที่ดี! ฉันคิดว่าการโพสต์แยกจะดีกว่า แต่ไม่มีการตั้งค่าที่ดี
Paul Panzer

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]]

แก้ไข: นี่เป็นวิธีที่ดีที่สุดที่ฉันสามารถหาได้ในเส้นทางนี้ มันยังช้ากว่าโซลูชันของ @PaulPanzer 10 เท่า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

pseudocode:

  1. รับ "จำนวน 1 อาร์เรย์ในอาร์เรย์ 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 ที่สาม ...

pseudocode นี้จะใช้เวลาเชิงเส้นในการรันเนื่องจากมันขึ้นอยู่กับความยาวของอาเรย์ numpy ของคุณ


1

สิ่งนี้ให้สิ่งที่คุณต้องการอย่างแน่นอนและจะใช้เวลาประมาณ 2.5 วินาทีสำหรับ 10,000,000 บนเครื่องของฉัน:

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) ฉันไม่สามารถนึกถึงวิธีแก้ปัญหาที่เร็วขึ้น ณ ตอนนี้ แต่จะอัปเดตที่นี่ถ้าฉันทำ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.