Obtenez efficacement des indices de bins d'histogramme en Python

Question Courte

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

Comment extraire les indices de chaque bac pour effectuer efficacement mon calcul en utilisant les valeurs des bacs?

Ce que je cherche est une solution qui évite le goulot d'étranglement d'avoir à sélectionner chaque fois ind == j de mon grand tableau. Est-il un moyen de obtenir directement, en une seule fois, les indices des éléments appartenant à chaque bin?

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 cette réponse connexe), où je numérise mes valeurs, puis j'ai une boucle j sélectionnant des indices 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 cherche est une solution qui évite le goulot d'étranglement d'avoir à sélectionner à chaque fois ind == j de mon grand tableau. Est-il possible d'obtenir directement, en une seule fois, les indices des éléments appartenant à chaque bin?

2. Utilisation de binned_statistics

L'approche ci-dessus s'avère être la même implémentée dans scipy.statistique.binned_statistic, pour le cas général d'une fonction définie par l'utilisateur. En utilisant Scipy directement, une sortie identique peut être obtenue avec

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 labeled_comprehension

Une autre alternative de Scipy est d'utiliser scipy.ndimage.mesure.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 aussi ce formulaire est inefficace et en particulier il n'a aucun avantage de vitesse sur mon exemple original.

4. Comparaison avec la langue IDL

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

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

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 Numpy, en raison du fait que les indices des bins n'ont pas besoin d'être sélectionnés pour chaque bac. Et la différence de vitesse en faveur de L'implémentation IDL augmente avec le nombre de bacs.

22
demandé sur divenex 2014-11-06 18:54:50

5 réponses

J'ai trouvé qu'un constructeur particulier de matrice clairsemée peut atteindre le résultat souhaité très efficacement. C'est un peu obscur mais nous pouvons en abuser à cette fin. La fonction ci-dessous peut être utilisée de la même manière que scipy.statistique.binned_statistic mais peut être 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 sont de largeur égale et donc lents, mais la méthode que j'ai utilisée à la place peut ne pas gérer parfaitement tous les cas de bord.

6
répondu divenex 2018-08-20 22:09:20

Je suppose que le binning, fait dans l'exemple avec digitize, ne peut pas être modifié. C'est une façon d'aller, où vous faire 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: entrez la description de l'image ici

4
répondu gg349 2014-11-11 15:10:43

Vous pouvez réduire de moitié le temps de calcul en triant d'abord le tableau, puis en utilisant 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.

1
répondu Hooked 2014-11-06 16:52:08

Une solution efficace utilise le paquetnumpy_indexed (avertissement: je suis son auteur):

import numpy_indexed as npi
npi.group_by(ind).split(vals)
0
répondu Eelco Hoogendoorn 2016-04-02 20:51:03

Pandas a un code de regroupement très rapide (je pense qu'il est écrit en C), donc si cela ne vous dérange pas de 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 ce dernier soit plus lent pour les fonctions d'agrégation standard (comme somme, moyenne, etc)

0
répondu Alcofribas Nasier 2018-02-22 00:23:58