Numpy: temukan indeks nilai pertama dengan cepat


105

Bagaimana saya bisa menemukan indeks kemunculan pertama angka dalam array Numpy? Kecepatan penting bagi saya. Saya tidak tertarik dengan jawaban berikut karena mereka memindai seluruh larik dan tidak berhenti ketika mereka menemukan kejadian pertama:

itemindex = numpy.where(array==item)[0][0]
nonzero(array == item)[0][0]

Catatan 1: tidak ada jawaban dari pertanyaan itu yang tampak relevan Apakah ada fungsi Numpy untuk mengembalikan indeks pertama dari sesuatu dalam sebuah array?

Catatan 2: menggunakan metode kompilasi-C lebih disukai daripada loop Python.

Jawaban:



30

Meski sudah terlambat bagi Anda, namun untuk referensi di masa mendatang: Menggunakan numba ( 1 ) adalah cara termudah sampai numpy mengimplementasikannya. Jika Anda menggunakan distribusi python anaconda, itu seharusnya sudah diinstal. Kode tersebut akan di-compile sehingga menjadi cepat.

@jit(nopython=True)
def find_first(item, vec):
    """return the index of the first occurence of item in vec"""
    for i in xrange(len(vec)):
        if item == vec[i]:
            return i
    return -1

lalu:

>>> a = array([1,7,8,32])
>>> find_first(8,a)
2

4
Untuk python3 xrangeperlu diubah untuk range.

Sedikit perbaikan kode di Python 3+: gunakan enumerate, seperti for i, v in enumerate(vec):; if v == item: return i. (Ini bukan ide yang baik dengan Python <= 2.7, di mana enumeratemembuat daftar daripada iterator dasar.)
acdr

23

Saya telah membuat patokan untuk beberapa metode:

  • argwhere
  • nonzero seperti dalam pertanyaan
  • .tostring() seperti dalam jawaban @Rob Reilink
  • lingkaran python
  • Lingkaran Fortran

Kode Python dan Fortran tersedia. Saya melewatkan yang tidak menjanjikan seperti mengonversi ke daftar.

Hasil pada skala log. Sumbu X adalah posisi jarum (diperlukan waktu lebih lama untuk mengetahui apakah jarum berada jauh di bawah larik); nilai terakhir adalah jarum yang tidak ada dalam larik. Sumbu Y adalah waktu untuk menemukannya.

hasil benchmark

Array memiliki 1 juta elemen dan pengujian dijalankan 100 kali. Hasil masih sedikit berfluktuasi, tetapi tren kualitatifnya jelas: Python dan f2py berhenti pada elemen pertama sehingga skalanya berbeda. Python menjadi terlalu lambat jika jarumnya tidak di 1% pertama, sedangkan f2pycepat (tetapi Anda perlu mengkompilasinya).

Singkatnya, f2py adalah solusi tercepat , terutama jika jarum muncul cukup awal.

Ini tidak dibangun yang mengganggu, tetapi sebenarnya hanya 2 menit kerja. Tambahkan ini ke file bernama search.f90:

subroutine find_first(needle, haystack, haystack_length, index)
    implicit none
    integer, intent(in) :: needle
    integer, intent(in) :: haystack_length
    integer, intent(in), dimension(haystack_length) :: haystack
!f2py intent(inplace) haystack
    integer, intent(out) :: index
    integer :: k
    index = -1
    do k = 1, haystack_length
        if (haystack(k)==needle) then
            index = k - 1
            exit
        endif
    enddo
end

Jika Anda mencari sesuatu selain integer, ubah saja tipenya. Kemudian kompilasi menggunakan:

f2py -c -m search search.f90

setelah itu Anda dapat melakukannya (dari Python):

import search
print(search.find_first.__doc__)
a = search.find_first(your_int_needle, your_int_array)

2
Mengapa f2pylebih lambat untuk 1 item dari 10?
Eric

2
@Eric, tebakan saya adalah bahwa pada skala tersebut (10e-6), itu hanya noise dalam data, dan kecepatan aktual per item sangat cepat sehingga tidak berkontribusi secara berarti untuk keseluruhan waktu pada n <100 atau lebih
Brendan

11

Anda bisa mengonversi array boolean menjadi string Python menggunakan array.tostring()dan kemudian menggunakan metode find ():

(array==item).tostring().find('\x01')

Ini memang melibatkan penyalinan data, karena string Python harus tetap. Keuntungannya adalah Anda juga dapat mencari, misalnya, tepi naik dengan menemukan\x00\x01


Ini menarik, tetapi hampir tidak lebih cepat, jika sama sekali, karena Anda masih perlu berurusan dengan semua data (lihat jawaban saya untuk patokan).
Tandai

10

Dalam kasus array yang diurutkan np.searchsortedbekerja.


2
Jika array tidak memiliki item ini, semua panjang array akan dikembalikan.
Boris Tsema

7

Saya pikir Anda telah mengalami masalah di mana metode yang berbeda dan beberapa pengetahuan apriori tentang array akan sangat membantu. Jenis hal di mana Anda memiliki probabilitas X untuk menemukan jawaban Anda dalam persen Y pertama dari data. Memecah masalah dengan harapan menjadi beruntung kemudian melakukan ini dengan python dengan pemahaman daftar bersarang atau semacamnya.

Menulis fungsi C untuk melakukan kekerasan ini juga tidak terlalu sulit menggunakan ctypes .

Kode C yang saya retas bersama (index.c):

long index(long val, long *data, long length){
    long ans, i;
    for(i=0;i<length;i++){
        if (data[i] == val)
            return(i);
    }
    return(-999);
}

dan python:

# to compile (mac)
# gcc -shared index.c -o index.dylib
import ctypes
lib = ctypes.CDLL('index.dylib')
lib.index.restype = ctypes.c_long
lib.index.argtypes = (ctypes.c_long, ctypes.POINTER(ctypes.c_long), ctypes.c_long)

import numpy as np
np.random.seed(8675309)
a = np.random.random_integers(0, 100, 10000)
print lib.index(57, a.ctypes.data_as(ctypes.POINTER(ctypes.c_long)), len(a))

dan saya mendapatkan 92.

Bungkus python menjadi fungsi yang tepat dan begitulah.

Versi C jauh (~ 20x) lebih cepat untuk seed ini (peringatan saya tidak baik dengan waktu)

import timeit
t = timeit.Timer('np.where(a==57)[0][0]', 'import numpy as np; np.random.seed(1); a = np.random.random_integers(0, 1000000, 10000000)')
t.timeit(100)/100
# 0.09761879920959472
t2 = timeit.Timer('lib.index(57, a.ctypes.data_as(ctypes.POINTER(ctypes.c_long)), len(a))', 'import numpy as np; np.random.seed(1); a = np.random.random_integers(0, 1000000, 10000000); import ctypes; lib = ctypes.CDLL("index.dylib"); lib.index.restype = ctypes.c_long; lib.index.argtypes = (ctypes.c_long, ctypes.POINTER(ctypes.c_long), ctypes.c_long) ')
t2.timeit(100)/100
# 0.005288000106811523

1
Jika arraynya ganda (ingat python float adalah C double secara default) maka Anda harus berpikir sedikit lebih keras karena == tidak benar-benar aman atau apa yang Anda inginkan untuk nilai floating point. Juga jangan lupa bahwa ini adalah ide yang sangat bagus saat menggunakan ctypes untuk mengetik array numpy Anda.
Brian Larsen

Terima kasih @Brian Larsen. Saya mungkin akan mencobanya. Saya pikir ini adalah permintaan fitur yang sepele untuk revisi numpy berikutnya.
cyborg

5

@tal sudah menyajikan numbafungsi untuk menemukan indeks pertama tetapi itu hanya berfungsi untuk array 1D. Dengan np.ndenumerateAnda juga dapat menemukan indeks pertama dalam array dimensi arbitar:

from numba import njit
import numpy as np

@njit
def index(array, item):
    for idx, val in np.ndenumerate(array):
        if val == item:
            return idx
    return None

Contoh kasus:

>>> arr = np.arange(9).reshape(3,3)
>>> index(arr, 3)
(1, 0)

Pengaturan waktu menunjukkan bahwa kinerjanya mirip dengan solusi tals :

arr = np.arange(100000)
%timeit index(arr, 5)           # 1000000 loops, best of 3: 1.88 µs per loop
%timeit find_first(5, arr)      # 1000000 loops, best of 3: 1.7 µs per loop

%timeit index(arr, 99999)       # 10000 loops, best of 3: 118 µs per loop
%timeit find_first(99999, arr)  # 10000 loops, best of 3: 96 µs per loop

1
Jika Anda lebih tertarik untuk menelusuri sepanjang sumbu tertentu terlebih dahulu: Ubah urutan arraysebelum memasukkannya ke dalam np.ndenumerate, sehingga sumbu minat Anda muncul lebih dulu.
CheshireCat

Terima kasih, ini memang lipat lebih cepat: dari ~ 171ms ( np.argwhere) hingga 717ns (solusi Anda), keduanya untuk larik bentuk (3000000, 12)).
Arthur Colombini Gusmão

3

Jika daftar Anda diurutkan , Anda dapat mencapai pencarian indeks yang sangat cepat dengan paket 'bisect'. Ini adalah O (log (n)) bukan O (n).

bisect.bisect(a, x)

menemukan x dalam larik a, pasti lebih cepat dalam kasus yang diurutkan daripada rutin C yang melewati semua elemen pertama (untuk daftar yang cukup panjang).

Terkadang baik untuk mengetahuinya.


>>> cond = "import numpy as np;a = np.arange(40)" timeit("np.searchsorted(a, 39)", cond)bekerja selama 3,47867107391 detik. timeit("bisect.bisect(a, 39)", cond2)bekerja selama 7,0661458969116 detik. Sepertinya numpy.searchsortedlebih baik untuk array yang diurutkan (setidaknya untuk int).
Boris Tsema

2

Sejauh yang saya tahu hanya np.any dan np.all pada array boolean yang dihubung pendek.

Dalam kasus Anda, numpy harus melalui seluruh array dua kali, sekali untuk membuat kondisi boolean dan kedua kalinya untuk menemukan indeks.

Rekomendasi saya dalam hal ini adalah menggunakan cython. Saya pikir seharusnya mudah untuk menyesuaikan contoh untuk kasus ini, terutama jika Anda tidak memerlukan banyak fleksibilitas untuk tipe dan bentuk yang berbeda.


2

Saya membutuhkan ini untuk pekerjaan saya jadi saya belajar sendiri Python dan antarmuka C Numpy dan menulis sendiri. http://pastebin.com/GtcXuLyd Ini hanya untuk array 1-D, tetapi bekerja untuk sebagian besar tipe data (int, float, atau string) dan pengujian telah menunjukkan itu lagi sekitar 20 kali lebih cepat dari pendekatan yang diharapkan dalam Python murni- numpy.


2

Masalah ini dapat diselesaikan secara efektif dalam numpy murni dengan memproses array dalam potongan:

def find_first(x):
    idx, step = 0, 32
    while idx < x.size:
        nz, = x[idx: idx + step].nonzero()
        if len(nz): # found non-zero, return it
            return nz[0] + idx
        # move to the next chunk, increase step
        idx += step
        step = min(9600, step + step // 2)
    return -1

Array diproses dalam potongan ukuran step. Semakin steplama langkahnya, semakin cepat pemrosesan array-nol (kasus terburuk). Semakin kecil nilainya, semakin cepat pemrosesan larik dengan bukan nol di awal. Triknya adalah memulai dengan yang kecil stepdan meningkatkannya secara eksponensial. Selain itu, tidak perlu menaikkannya di atas ambang batas karena manfaat yang terbatas.

Saya telah membandingkan solusi dengan solusi ndarary.nonzero dan numba murni terhadap 10 juta array float.

import numpy as np
from numba import jit
from timeit import timeit

def find_first(x):
    idx, step = 0, 32
    while idx < x.size:
        nz, = x[idx: idx + step].nonzero()
        if len(nz):
            return nz[0] + idx
        idx += step
        step = min(9600, step + step // 2)
    return -1

@jit(nopython=True)
def find_first_numba(vec):
    """return the index of the first occurence of item in vec"""
    for i in range(len(vec)):
        if vec[i]:
            return i
    return -1


SIZE = 10_000_000
# First only
x = np.empty(SIZE)

find_first_numba(x[:10])

print('---- FIRST ----')
x[:] = 0
x[0] = 1
print('ndarray.nonzero', timeit(lambda: x.nonzero()[0][0], number=100)*10, 'ms')
print('find_first', timeit(lambda: find_first(x), number=1000), 'ms')
print('find_first_numba', timeit(lambda: find_first_numba(x), number=1000), 'ms')

print('---- LAST ----')
x[:] = 0
x[-1] = 1
print('ndarray.nonzero', timeit(lambda: x.nonzero()[0][0], number=100)*10, 'ms')
print('find_first', timeit(lambda: find_first(x), number=100)*10, 'ms')
print('find_first_numba', timeit(lambda: find_first_numba(x), number=100)*10, 'ms')

print('---- NONE ----')
x[:] = 0
print('ndarray.nonzero', timeit(lambda: x.nonzero()[0], number=100)*10, 'ms')
print('find_first', timeit(lambda: find_first(x), number=100)*10, 'ms')
print('find_first_numba', timeit(lambda: find_first_numba(x), number=100)*10, 'ms')

print('---- ALL ----')
x[:] = 1
print('ndarray.nonzero', timeit(lambda: x.nonzero()[0][0], number=100)*10, 'ms')
print('find_first', timeit(lambda: find_first(x), number=100)*10, 'ms')
print('find_first_numba', timeit(lambda: find_first_numba(x), number=100)*10, 'ms')

Dan hasil di mesin saya:

---- FIRST ----
ndarray.nonzero 54.733994480002366 ms
find_first 0.0013148509997336078 ms
find_first_numba 0.0002839310000126716 ms
---- LAST ----
ndarray.nonzero 54.56336712999928 ms
find_first 25.38929685000312 ms
find_first_numba 8.022820680002951 ms
---- NONE ----
ndarray.nonzero 24.13432420999925 ms
find_first 25.345200140000088 ms
find_first_numba 8.154927100003988 ms
---- ALL ----
ndarray.nonzero 55.753537260002304 ms
find_first 0.0014760300018679118 ms
find_first_numba 0.0004358099977253005 ms

Murni ndarray.nonzeropasti lebih longgar. Solusi numba sekitar 5 kali lebih cepat untuk kasus terbaik. Ini sekitar 3 kali lebih cepat dalam kasus terburuk.


2

Jika Anda mencari elemen bukan nol pertama, Anda dapat menggunakan peretasan berikut:

idx = x.view(bool).argmax() // x.itemsize
idx = idx if x[idx] else -1

Ini adalah solusi "numpy-pure" yang sangat cepat tetapi gagal untuk beberapa kasus yang dibahas di bawah.

Solusinya mengambil keuntungan dari kenyataan bahwa hampir semua representasi nol untuk tipe numerik terdiri dari 0byte. Ini berlaku untuk numpy booljuga. Dalam versi numpy terbaru, argmax()fungsi menggunakan logika hubung singkat saat memproses booltipe. Ukurannya bool1 byte.

Jadi, seseorang perlu:

  • buat tampilan array sebagai bool. Tidak ada salinan yang dibuat
  • digunakan argmax()untuk menemukan byte bukan-nol pertama menggunakan logika hubung singkat
  • hitung ulang offset byte ini ke indeks elemen bukan nol pertama dengan pembagian integer (operator //) offset dengan ukuran elemen tunggal yang dinyatakan dalam byte ( x.itemsize)
  • periksa apakah x[idx]sebenarnya bukan nol untuk mengidentifikasi kasus ketika tidak ada bukan nol

Saya telah membuat beberapa patokan terhadap solusi numba dan membangunnya np.nonzero.

import numpy as np
from numba import jit
from timeit import timeit

def find_first(x):
    idx = x.view(bool).argmax() // x.itemsize
    return idx if x[idx] else -1

@jit(nopython=True)
def find_first_numba(vec):
    """return the index of the first occurence of item in vec"""
    for i in range(len(vec)):
        if vec[i]:
            return i
    return -1


SIZE = 10_000_000
# First only
x = np.empty(SIZE)

find_first_numba(x[:10])

print('---- FIRST ----')
x[:] = 0
x[0] = 1
print('ndarray.nonzero', timeit(lambda: x.nonzero()[0][0], number=100)*10, 'ms')
print('find_first', timeit(lambda: find_first(x), number=1000), 'ms')
print('find_first_numba', timeit(lambda: find_first_numba(x), number=1000), 'ms')

print('---- LAST ----')
x[:] = 0
x[-1] = 1
print('ndarray.nonzero', timeit(lambda: x.nonzero()[0][0], number=100)*10, 'ms')
print('find_first', timeit(lambda: find_first(x), number=100)*10, 'ms')
print('find_first_numba', timeit(lambda: find_first_numba(x), number=100)*10, 'ms')

print('---- NONE ----')
x[:] = 0
print('ndarray.nonzero', timeit(lambda: x.nonzero()[0], number=100)*10, 'ms')
print('find_first', timeit(lambda: find_first(x), number=100)*10, 'ms')
print('find_first_numba', timeit(lambda: find_first_numba(x), number=100)*10, 'ms')

print('---- ALL ----')
x[:] = 1
print('ndarray.nonzero', timeit(lambda: x.nonzero()[0][0], number=100)*10, 'ms')
print('find_first', timeit(lambda: find_first(x), number=100)*10, 'ms')
print('find_first_numba', timeit(lambda: find_first_numba(x), number=100)*10, 'ms')

Hasil di mesin saya adalah:

---- FIRST ----
ndarray.nonzero 57.63976670001284 ms
find_first 0.0010841979965334758 ms
find_first_numba 0.0002308919938514009 ms
---- LAST ----
ndarray.nonzero 58.96685277999495 ms
find_first 5.923203580023255 ms
find_first_numba 8.762269750004634 ms
---- NONE ----
ndarray.nonzero 25.13398071998381 ms
find_first 5.924289370013867 ms
find_first_numba 8.810063839919167 ms
---- ALL ----
ndarray.nonzero 55.181210660084616 ms
find_first 0.001246920000994578 ms
find_first_numba 0.00028766007744707167 ms

Solusinya 33% lebih cepat dari numba dan ini "numpy-pure".

Kerugiannya:

  • tidak berfungsi untuk jenis yang dapat diterima numpy seperti object
  • gagal untuk nol negatif yang kadang-kadang muncul di floatatau doubleperhitungan

ini adalah solusi numpy murni terbaik yang pernah saya coba. harus diterima jawaban. @tstanisl ive telah mencoba untuk mendapatkan solusi yang sama cepatnya untuk menemukan elemen nol pertama dalam sebuah array tetapi selalu berakhir lebih lambat daripada mengonversi ke bool kemudian menjalankan argmin (). ada ide?
Ta946

1
@ Ta946. Triknya tidak dapat digunakan saat mencari entri nol. Misalnya non-zero double mungkin berisi byte nol di dalamnya. Jika Anda mencari solusi numpy-pure, coba ubah jawaban saya yang lain . Lihat stackoverflow.com/a/58294774/4989451 . Singkirkan saja sepotong xsebelum menelepon nonzero(). Ini mungkin akan lebih lambat daripada numba tetapi ** tidak akan ** mencari seluruh larik sambil mencari entri nol pertama sehingga mungkin cukup cepat untuk kebutuhan Anda.
tstanisl

1

Sebagai pengguna matlab lama saya telah mencari solusi yang efisien untuk masalah ini cukup lama. Akhirnya, termotivasi oleh diskusi proposisi di utas ini, saya telah mencoba untuk menemukan solusi yang menerapkan API yang mirip dengan apa yang disarankan di sini , untuk saat ini hanya mendukung array 1D.

Anda akan menggunakannya seperti ini

import numpy as np
import utils_find_1st as utf1st
array = np.arange(100000)
item = 1000
ind = utf1st.find_1st(array, item, utf1st.cmp_larger_eq)

Operator kondisi yang didukung adalah: cmp_equal, cmp_not_equal, cmp_larger, cmp_smaller, cmp_larger_eq, cmp_smaller_eq. Untuk efisiensi, ekstensi ditulis dalam c.

Anda dapat menemukan sumber, tolok ukur, dan detail lainnya di sini:

https://pypi.python.org/pypi?name=py_find_1st&:action=display

untuk digunakan dalam tim kami (anaconda di linux dan macos) Saya telah membuat penginstal anaconda yang menyederhanakan penginstalan, Anda dapat menggunakannya seperti yang dijelaskan di sini

https://anaconda.org/roebel/py_find_1st


"Sebagai pengguna matlab lama" - apa ejaan matlab untuk ini?
Eric

find (X, n) menemukan n indeks pertama di mana X bukan nol. mathworks.com/help/matlab/ref/find.html
Seorang Roebel

0

Hanya catatan bahwa jika Anda melakukan urutan pencarian, perolehan kinerja dari melakukan sesuatu yang pintar seperti mengonversi ke string, mungkin hilang di loop luar jika dimensi pencarian tidak cukup besar. Lihat bagaimana kinerja iterasi find1 yang menggunakan trik konversi string yang diusulkan di atas dan find2 yang menggunakan argmax di sepanjang sumbu dalam (ditambah penyesuaian untuk memastikan non-match menghasilkan -1)

import numpy,time
def find1(arr,value):
    return (arr==value).tostring().find('\x01')

def find2(arr,value): #find value over inner most axis, and return array of indices to the match
    b = arr==value
    return b.argmax(axis=-1) - ~(b.any())


for size in [(1,100000000),(10000,10000),(1000000,100),(10000000,10)]:
    print(size)
    values = numpy.random.choice([0,0,0,0,0,0,0,1],size=size)
    v = values>0

    t=time.time()
    numpy.apply_along_axis(find1,-1,v,1)
    print('find1',time.time()-t)

    t=time.time()
    find2(v,1)
    print('find2',time.time()-t)

keluaran

(1, 100000000)
('find1', 0.25300002098083496)
('find2', 0.2780001163482666)
(10000, 10000)
('find1', 0.46200013160705566)
('find2', 0.27300000190734863)
(1000000, 100)
('find1', 20.98099994659424)
('find2', 0.3040001392364502)
(10000000, 10)
('find1', 206.7590000629425)
('find2', 0.4830000400543213)

Meskipun demikian, penemuan yang ditulis dalam C setidaknya akan sedikit lebih cepat daripada salah satu pendekatan ini


0

bagaimana dengan ini

import numpy as np
np.amin(np.where(array==item))

2
Meskipun kode ini mungkin menjawab pertanyaan, memberikan konteks tambahan tentang mengapa dan / atau bagaimana kode ini menjawab pertanyaan akan secara signifikan meningkatkan nilai jangka panjangnya. Harap edit jawaban Anda untuk menambahkan penjelasan.
Toby Speight

1
Saya cukup yakin ini bahkan lebih lambat daripada where(array==item)[0][0]dari pertanyaan ...
Markus

-1

Anda dapat mengubah array Anda menjadi listdan menggunakan index()metodenya:

i = list(array).index(item)

Sejauh yang saya tahu, ini adalah metode terkompilasi C.


3
ini kemungkinan akan berkali-kali lebih lambat daripada hanya mengambil hasil pertama dari np. di mana
cwa

1
sangat benar .. Saya menggunakan timeit()array 10.000 bilangan bulat - mengubah ke daftar sekitar 100 kali lebih lambat! Saya lupa bahwa struktur data yang mendasari untuk array numpy sangat berbeda dari daftar ..
drevicko
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.