web-dev-qa-db-fra.com

Obtenir efficacement des index des histogrammes dans Python

Question courte

J'ai une grande image 10000x10000 éléments, que je bin dans quelques centaines de secteurs/bacs différents. Je dois ensuite effectuer un calcul itératif sur les valeurs contenues dans chaque bac.

Comment extraire les indices de chaque groupe pour effectuer efficacement mes calculs en utilisant les valeurs des groupes?

Ce que je recherche, c’est une solution qui évite le goulet d’étranglement de devoir sélectionner chaque fois ind == j dans mon grand tableau. Existe-t-il un moyen d'obtenir directement, en une fois, les indices des éléments appartenant à chaque casier?

Explication détaillée

1. Solution simple

Une façon de réaliser ce dont j'ai besoin est d'utiliser un code comme celui-ci (voir, par exemple, THIS réponse associée), où je numérise mes valeurs, puis une boucle en j sélectionnant des index numérisés égaux à j comme ci-dessous.

import numpy as np

# This function func() is just a place mark for a much more complicated function.
# I am aware that my problem could be easily speed up in the specific case of
# of the sum() function, but I am looking for a general solution to the problem.
def func(x):
    y = np.sum(x)
    return y

vals = np.random.random(1e8)
nbins = 100
bins = np.linspace(0, 1, nbins+1)
ind = np.digitize(vals, bins)

result = [func(vals[ind == j]) for j in range(1, nbins)]

Ce que je recherche, c’est une solution qui évite le goulet d’étranglement de devoir sélectionner chaque fois ind == j dans mon grand tableau. Existe-t-il un moyen d'obtenir directement, en une fois, les indices des éléments appartenant à chaque casier?

2. Utilisation de binned_statistics

L’approche ci-dessus s’avère être la même que celle appliquée dans scipy.stats.binned_statistic , dans le cas général d’une fonction définie par l’utilisateur. En utilisant directement Scipy, une sortie identique peut être obtenue avec ce qui suit

import numpy as np
from scipy.stats import binned_statistics

vals = np.random.random(1e8)
results = binned_statistic(vals, vals, statistic=func, bins=100, range=[0, 1])[0]

3. Utilisation de Label_Compréhension

Une autre alternative à Scipy consiste à utiliser scipy.ndimage.measurements.labeled_comprehension . En utilisant cette fonction, l'exemple ci-dessus deviendrait

import numpy as np
from scipy.ndimage import labeled_comprehension

vals = np.random.random(1e8)
nbins = 100
bins = np.linspace(0, 1, nbins+1)
ind = np.digitize(vals, bins)

result = labeled_comprehension(vals, ind, np.arange(1, nbins), func, float, 0)

Malheureusement, cette forme est également inefficace et, en particulier, elle n’a aucun avantage en termes de vitesse par rapport à mon exemple initial.

4. Comparaison avec le langage IDL

Pour clarifier davantage, ce que je recherche, c’est une fonctionnalité équivalente au mot clé REVERSE_INDICES dans la fonction HISTOGRAM du langage IDL HERE . Cette fonctionnalité très utile peut-elle être efficacement répliquée en Python?

Plus précisément, en utilisant le langage IDL, l’exemple ci-dessus pourrait être écrit comme suit:

vals = randomu(s, 1e8)
nbins = 100
bins = [0:1:1./nbins]
h = histogram(vals, MIN=bins[0], MAX=bins[-2], NBINS=nbins, REVERSE_INDICES=r)
result = dblarr(nbins)

for j=0, nbins-1 do begin
    jbins = r[r[j]:r[j+1]-1]  ; Selects indices of bin j
    result[j] = func(vals[jbins])
endfor

L'implémentation IDL ci-dessus est environ 10 fois plus rapide que celle de Numpy, en raison du fait qu'il n'est pas nécessaire de sélectionner les index des bacs pour chaque bac. Et la différence de vitesse en faveur de la mise en œuvre IDL augmente avec le nombre de bacs.

22
divenex

J'ai constaté qu'un constructeur de matrice clairsemée peut atteindre le résultat souhaité de manière très efficace. C'est un peu obscur mais on peut en abuser pour cela. La fonction ci-dessous peut être utilisée presque de la même manière que scipy.stats.binned_statistic mais peut être beaucoup plus rapide

import numpy as np
from scipy.sparse import csr_matrix

def binned_statistic(x, values, func, nbins, range):
    '''The usage is nearly the same as scipy.stats.binned_statistic''' 

    N = len(values)
    r0, r1 = range

    digitized = (float(nbins)/(r1 - r0)*(x - r0)).astype(int)
    S = csr_matrix((values, [digitized, np.arange(N)]), shape=(nbins, N))

    return [func(group) for group in np.split(S.data, S.indptr[1:-1])]

J'ai évité np.digitize car il n'utilise pas le fait que tous les bacs ont la même largeur et sont donc lents, mais la méthode que j'ai utilisée risque de ne pas gérer tous les cas Edge parfaitement.

6
user2379410

Je suppose que le binning, effectué dans l'exemple avec digitize, ne peut pas être modifié. C'est une façon de faire, où vous faites le tri une fois pour toutes.

vals = np.random.random(1e4)
nbins = 100
bins = np.linspace(0, 1, nbins+1)
ind = np.digitize(vals, bins)

new_order = argsort(ind)
ind = ind[new_order]
ordered_vals = vals[new_order]
# slower way of calculating first_hit (first version of this post)
# _,first_hit = unique(ind,return_index=True)
# faster way:
first_hit = searchsorted(ind,arange(1,nbins-1))
first_hit.sort()

#example of using the data:
for j in range(nbins-1):
    #I am using a plotting function for your f, to show that they cluster
    plot(ordered_vals[first_hit[j]:first_hit[j+1]],'o')

La figure montre que les bacs sont en fait des grappes comme prévu: enter image description here

4
gg349

Vous pouvez diviser par deux le temps de calcul en triant d'abord le tableau, puis utilisez np.searchsorted .

vals = np.random.random(1e8)
vals.sort()

nbins = 100
bins = np.linspace(0, 1, nbins+1)
ind = np.digitize(vals, bins)

results = [func(vals[np.searchsorted(ind,j,side='left'):
                     np.searchsorted(ind,j,side='right')])
           for j in range(1,nbins)]

En utilisant 1e8 comme cas de test, je passe de 34 secondes de calcul à environ 17 secondes.

1
Hooked

Pandas a un code de regroupement très rapide (je pense que c'est écrit en C), donc si vous voulez bien charger la bibliothèque, vous pouvez le faire:

import pandas as pd

pdata=pd.DataFrame({'vals':vals,'ind':ind})
resultsp = pdata.groupby('ind').sum().values

ou plus généralement:

pdata=pd.DataFrame({'vals':vals,'ind':ind})
resultsp = pdata.groupby('ind').agg(func).values

Bien que cette dernière soit plus lente pour les fonctions d’agrégation standard (comme somme, moyenne, etc.)

0
Alcofribas Nasier

Une solution efficace consiste à utiliser le paquet numpy_indexed package (disclaimer: je suis son auteur):

import numpy_indexed as npi
npi.group_by(ind).split(vals)
0