J'ai quelques grands tableaux (~ 100 millions de points) dont j'ai besoin pour tracer de manière interactive. J'utilise actuellement Matplotlib. Tracer les tableaux tels quels devient très lent et est un gaspillage car vous ne pouvez pas visualiser autant de points de toute façon.
J'ai donc créé une fonction de décimation min / max que j'ai liée au rappel 'xlim_changed' de l'axe. J'ai opté pour une approche min / max car les données contiennent des pics rapides que je ne veux pas manquer en parcourant simplement les données. Il y a plus de wrappers qui rognent aux limites x, et ignorent le traitement sous certaines conditions, mais la partie pertinente est ci-dessous:
x_big = np.linspace(0,10,100000000) y_big = np.cos(x_big ) x_small, y_small = min_max_downsample(x_big ,y_big ,2000) #Fast but not exactly correct. x_small, y_small = min_max_downsample_v2(x_big ,y_big ,2000) #correct but not exactly fast.
Cela fonctionne plutôt bien et est suffisamment rapide (~ 80ms sur 1e8 points et 2k bacs). Il y a très peu de décalage car il recalcule et met à jour périodiquement les données x & y de la ligne.
Cependant, mon seul reproche concerne les données x. Ce code duplique la valeur x du bord gauche de chaque bac et ne renvoie pas le véritable emplacement x des paires y min / max. Je règle généralement le nombre de cases pour doubler la largeur des pixels de l'axe. Donc vous ne pouvez pas vraiment voir la différence parce que les bacs sont si petits ... mais je sais que c'est là ... et ça me dérange.
Alors essayez le numéro 2 qui renvoie les valeurs x réelles pour chaque paire min / max. Cependant, il est environ 5 fois plus lent.
def min_max_downsample_v2(x,y,num_bins): pts_per_bin = x.size // num_bins #Create temp to hold the reshaped & slightly cropped y y_temp = y[:num_bins*pts_per_bin].reshape((num_bins, pts_per_bin)) #use argmax/min to get column locations cc_max = y_temp.argmax(axis=1) cc_min = y_temp.argmin(axis=1) rr = np.arange(0,num_bins) #compute the flat index to where these are flat_max = cc_max + rr*pts_per_bin flat_min = cc_min + rr*pts_per_bin #Create a boolean mask of these locations mm_mask = np.full((x.size,), False) mm_mask[flat_max] = True mm_mask[flat_min] = True x_out = x[mm_mask] y_out = y[mm_mask] return x_out, y_out
Cela prend environ 400+ ms sur ma machine, ce qui devient assez perceptible. Ma question est donc essentiellement: y a-t-il un moyen d'aller plus vite et de fournir les mêmes résultats? Le goulot d'étranglement réside principalement dans les fonctions numpy.argmin
et numpy.argmax
qui sont un peu plus lentes que numpy.min
et numpy .max
.
La réponse pourrait être de simplement vivre avec la version n ° 1 car cela n'a pas vraiment d'importance visuelle. Ou peut-être essayer d'accélérer quelque chose comme cython (que je n'ai jamais utilisé).
Pour info en utilisant Python 3.6.4 sur Windows ... un exemple d'utilisation serait quelque chose comme ceci:
def min_max_downsample(x,y,num_bins): """ Break the data into num_bins and returns min/max for each bin""" pts_per_bin = x.size // num_bins #Create temp to hold the reshaped & slightly cropped y y_temp = y[:num_bins*pts_per_bin].reshape((num_bins, pts_per_bin)) y_out = np.empty((num_bins,2)) #Take the min/max by rows. y_out[:,0] = y_temp.max(axis=1) y_out[:,1] = y_temp.min(axis=1) y_out = y_out.ravel() #This duplicates the x-value for each min/max y-pair x_out = np.empty((num_bins,2)) x_out[:] = x[:num_bins*pts_per_bin:pts_per_bin,np.newaxis] x_out = x_out.ravel() return x_out, y_out
4 Réponses :
J'ai réussi à améliorer les performances en utilisant la sortie de arg (min | max)
directement pour indexer les tableaux de données. Cela se fait au prix d'un appel supplémentaire à np.sort
mais l'axe à trier ne comporte que deux éléments (les indices min. / Max.) Et le tableau global est plutôt petit (nombre de bins ):
def min_max_downsample_v3(x, y, num_bins): pts_per_bin = x.size // num_bins x_view = x[:pts_per_bin*num_bins].reshape(num_bins, pts_per_bin) y_view = y[:pts_per_bin*num_bins].reshape(num_bins, pts_per_bin) i_min = np.argmin(y_view, axis=1) i_max = np.argmax(y_view, axis=1) r_index = np.repeat(np.arange(num_bins), 2) c_index = np.sort(np.stack((i_min, i_max), axis=1)).ravel() return x_view[r_index, c_index], y_view[r_index, c_index]
J'ai vérifié les horaires de votre exemple et j'ai obtenu:
min_max_downsample_v1
: 110 ms ± 5 ms min_max_downsample_v2
: 240 ms ± 8,01 ms min_max_downsample_v3
: 164 ms ± 1,23 ms J'ai également vérifié le retour directement après les appels à arg (min | max)
et le résultat était également de 164 ms, c'est-à-dire qu'il n'y a plus de véritable surcharge après cela.
Belle astuce d'indexation et évitant le masque. Je n'aurais pas pensé que le tri
serait plus rapide. J'obtiens 87, 421 et 338 ms pour v1, v2 et v3. J'aimerais le ramener à cette plage inférieure à 100 ms, mais je prendrai toutes les améliorations que je peux obtenir. Cependant, j'ai remarqué que le code ci-dessus sera une erreur si x n'est pas exactement les éléments num_bin * pts_per_bin longs ... ce qui est souvent le cas. C'est pourquoi je faisais y_temp = y [: num_bins * pts_per_bin] .reshape
@AeroEngy Vous avez raison, y compris. Pourriez-vous vérifier le résultat de la synchronisation lorsque vous revenez juste après les appels arg (min | max)
afin de voir la contribution de l'appel suivant à sort
? Il existe différentes façons de fusionner les indices, mais sur ma machine, cela n'avait pas vraiment d'importance.
Je l'ai parcouru à travers un profil et j'ai obtenu un total de 339 ms (142 ms pour argmax
et 196ms pour argmin
). Le sort
était trivial à 45us. Je pense donc que vous avez supprimé tous les autres frais généraux. Pour le faire plus rapidement, il faudrait une nouvelle approche inconnue.
@AeroEngy Vous pourriez peut-être suivre cette approche afin de compiler votre propre fonction argmin_max
qui calcule le argmin et argmax en une seule fois au lieu d'itérer deux fois sur le tableau. Pour un tableau de cette taille, la différence doit être mesurable.
Merci pour le lien! Je vais d'abord essayer de faire une version numba d'un argminmax en un seul passage pour voir comment il se compare. Je n'ai jamais utilisé numba ou cython auparavant, mais cela semble intéressant. Je suis récemment converti en Python depuis Matlab, je suis donc habitué à accélérer le code avec MEX, ce qui semble être une idée similaire.
L'utilisation de votre réponse + ce lien m'a aidé à créer une version agréable et rapide avec numba. Je l'ai posté comme une réponse distincte pour la postérité, mais j'excepterai votre réponse puisque vous me conduisez sur le bon chemin. Merci encore!
Donc, cela ne concerne pas l'accélération de la fonction spécifique en question, mais cela montre quelques façons de tracer une ligne avec un grand nombre de points de manière assez efficace. Cela suppose que les x points sont ordonnés et uniformément (ou presque uniformément) échantillonnés.
Configuration
L=int(100e6) x=linspace(0,1,L) y=0.1*randn(L)+sin(2*pi*18*x) plot_bigly(x,y, red=calc_env)
Voici une fonction que j'aime qui réduit le nombre de points de manière aléatoire en choisir un dans chaque intervalle. Il n'est pas garanti d'afficher tous les pics des données, mais il n'a pas autant de problèmes que de décimer directement les données, et est rapide.
def plot_bigly(x, y, *, ax=None, M=5000, red=calc_env, **kwargs): """ x : the x data y : the y data ax : axis to plot on M : The maximum number of line points to display at any given time kwargs : passed to line """ assert x.shape == y.shape, "x and y data must have same shape!" if ax is None: ax = gca() cached = {} # Setup line to be drawn beforehand, note this doesn't increment line properties so # style needs to be passed in explicitly line = plt.Line2D([],[], **kwargs) def update(xmin, xmax): """ Update line data precomputes and caches entire line at each level, so initial display may be slow but panning and zooming should speed up after that """ # Find nearest power of two as a factor to downsample by imin = max(np.searchsorted(x, xmin)-1, 0) imax = min(np.searchsorted(x, xmax) + 1, y.shape[0]) L = imax - imin + 1 factor = max(2**int(round(np.log(L/M) / np.log(2))), 1) # only calculate reduction if it hasn't been cached, do reduction using nearest cached version if possible if factor not in cached: cached[factor] = red(y, factor=factor) ## Make sure lengths match correctly here, by ensuring at least # "factor" points for each x point, then matching y length # this assumes x has uniform sample spacing - but could be modified newx = x[imin:imin + ((imax-imin)//factor)* factor:factor] start = imin//factor newy = cached[factor][start:start + newx.shape[-1]] assert newx.shape == newy.shape, "decimation error {}/{}!".format(newx.shape, newy.shape) ## Update line data line.set_xdata(newx) line.set_ydata(newy) update(x[0], x[-1]) ax.add_line(line) ## Manually update limits of axis, as adding line doesn't do this # if drawing multiple lines this can quickly slow things down, and some # sort of check should be included to prevent unnecessary changes in limits # when a line is first drawn. ax.set_xlim(min(ax.get_xlim()[0], x[0]), max(ax.get_xlim()[1], x[1])) ax.set_ylim(min(ax.get_ylim()[0], np.min(y)), max(ax.get_ylim()[1], np.max(y))) def callback(*ignore): lims = ax.get_xlim() update(*lims) ax.callbacks.connect('xlim_changed', callback) return [line]
Et voici min et max pour voir l'enveloppe du signal
def calc_env(y, factor): """ y : 1D signal factor : amount to reduce y by (actually returns twice this for min and max) Calculate envelope (interleaved min and max points) for y """ split = y[:len(y)//factor*factor].reshape(-1, factor) upper = split.max(axis=-1) lower = split.min(axis=-1) return c_[upper, lower].flatten()
La fonction suivante peut prendre l'un de ces deux éléments et les utilise pour réduire les données en cours de dessin. Le nombre de points réellement pris est de 5000 par défaut, ce qui devrait largement dépasser la résolution d'un moniteur. Les données sont mises en cache après avoir été réduites. La mémoire peut être un problème, en particulier avec de grandes quantités de données, mais elle ne doit pas dépasser la quantité requise par le signal d'origine.
def calc_rand(y, factor): split = y[:len(y)//factor*factor].reshape(-1, factor) idx = randint(0, split.shape[-1], split.shape[0]) return split[arange(split.shape[0]), idx]
Voici un code de test
from pylab import *
Sur ma machine, cela s'affiche très rapidement. Le zoom a un peu de retard, surtout lorsqu'il est important. Le panoramique n'a aucun problème. L'utilisation de la sélection aléatoire au lieu du min et du max est un peu plus rapide et ne pose des problèmes que sur des niveaux de zoom très élevés.
Merci, je n'ai pas envisagé de stocker une version sous-échantillonnée à différents facteurs comme celui-ci pour une utilisation ultérieure. Je vais probablement l'emprunter et l'intégrer à ma routine.
EDIT: Ajout de parallel = True to numba ... encore plus rapide
J'ai fini par faire un hybride d'une seule passe argmin + max routine et de l'indexation améliorée de la réponse et du lien de @ a_guest à cette question min max simultanée associée .
Cette version renvoie les valeurs x correctes pour chaque paire y min / max et grâce à numba
est en fait un peu plus rapide que la version "rapide mais pas tout à fait correcte".
%timeit min_max_downsample_v1(x_big ,y_big ,2000) 96 ms ± 2.46 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) %timeit min_max_downsample_v2(x_big ,y_big ,2000) 507 ms ± 4.75 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) %timeit min_max_downsample_v3(x_big ,y_big ,2000) 365 ms ± 1.27 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) %timeit min_max_downsample_v4(x_big ,y_big ,2000) 36.2 ms ± 487 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
La comparaison des vitesses en utilisant timeit
montre que le code numba
est environ 2,6 fois plus rapide et fournit de meilleurs résultats que la v1. C'est un peu plus de 10 fois plus rapide que de faire l'argmin et l'argmax de numpy en série.
from numba import jit, prange @jit(parallel=True) def min_max_downsample_v4(x, y, num_bins): pts_per_bin = x.size // num_bins x_view = x[:pts_per_bin*num_bins].reshape(num_bins, pts_per_bin) y_view = y[:pts_per_bin*num_bins].reshape(num_bins, pts_per_bin) i_min = np.zeros(num_bins,dtype='int64') i_max = np.zeros(num_bins,dtype='int64') for r in prange(num_bins): min_val = y_view[r,0] max_val = y_view[r,0] for c in range(pts_per_bin): if y_view[r,c] < min_val: min_val = y_view[r,c] i_min[r] = c elif y_view[r,c] > max_val: max_val = y_view[r,c] i_max[r] = c r_index = np.repeat(np.arange(num_bins), 2) c_index = np.sort(np.stack((i_min, i_max), axis=1)).ravel() return x_view[r_index, c_index], y_view[r_index, c_index]
C'est génial! Je ne savais pas qu'avec Numba, vous pouvez tout faire en Python. Le code résultant est super facile à lire et son intégration est à portée de main. Merci d'avoir partagé le code :-)
@a_guest J'ai été très surpris de la facilité d'utilisation. Avant d'ajouter le prange
et le parallel = True
, le temps était d'environ 86 ms, ce qui m'a rendu heureux. Après avoir ajouté ceux-ci, il est étonnant de constater à quel point il est beaucoup plus rapide que Numpy seul. Cela prend environ 1/2 seconde environ pour compiler lors de la première utilisation, mais cela est souvent appelé, donc cela n'a pas d'importance du tout.
Avez-vous essayé pyqtgraph pour le traçage interactif? Il est plus réactif que matplotlib.
Une astuce que j'utilise pour le sous-échantillonnage consiste à utiliser array_split < / a> et calculez min et max pour les fractionnements. Le fractionnement est effectué en fonction du nombre d'échantillons par pixel de la zone de tracé.
J'ai essayé pyqtgraph et cela m'a semblé plus rapide. Il a également intégré une routine de sous-échantillonnage min / max, ce qui est bien. Cependant, je n'aimais pas vraiment la mécanique de l'intrigue. Si vous lisez la question / les réponses, vous verrez que je fais min / max avec un remodelage au lieu de array_split. reshape
a beaucoup moins de surcharge car il renvoie une seule vue du tableau. array_split
semble renvoyer une liste
de vues. La division d'un tableau d'éléments 1e8 prend ~ 2ms tandis que le remodelage est d'environ 520ns ... donc le remodelage est 3800x plus rapide sur ma machine.
array_split est en effet beaucoup plus lent que le remodelage
D'après votre question, je suppose que cela est appelé plusieurs fois? Si tel est le cas, vous pouvez accélérer énormément les choses en utilisant la méthode
set_data
d'une ligne existante dans matplotlib.@ user2699 Oui, c'est exactement ce que je fais avec la sortie de cette fonction. Ce que j'ai publié est le principal goulot d'étranglement, car il est appelé fréquemment lorsque je me déplace. Cependant, j'ai changé la partie externe de ce code (non montré) afin qu'il soit appelé moins fréquemment en tamponnant plus de date au-delà des limites x du tracé. Il ne doit donc recalculer que lorsque vous dépassez une certaine limite. Cela a aidé un peu l'expérience utilisateur.
J'ai rencontré des problèmes similaires (bien que sur moins de 100 millions de points) et je peux inclure une partie du code que j'ai écrit qui est pertinente. Ce que j'ai trouvé le plus efficace est la mise en cache du signal sous-échantillonné et le sous-échantillonnage uniquement lorsque cela fait une différence substantielle dans la vue. Tant que les données sont (quelque peu) uniformément échantillonnées avec des points x exacts, cela n'a pas d'importance.
@ user2699 Je voudrais voir comment vous faites la mise en cache. Je fais quelque chose comme ça maintenant. Fondamentalement, charger des données au-delà des limites x d'une certaine quantité et rendre la densité de points un peu plus que nécessaire. Ensuite, il déclenche un nouveau recalcul uniquement si vous zoomez suffisamment pour que la densité de points diminue ou si vous vous déplacez au-delà du bord des tampons. J'aimerais voir comment quelqu'un d'autre le fait puisque j'en ai fait l'essentiel et je sais qu'il y a des trous de logique. La mienne sera parfois inutilement recalculée.
Je l'ai inclus dans une réponse. Faites-moi savoir comment cela fonctionne pour vous, il a été assez robuste dans mon utilisation, mais il peut y avoir des problèmes que j'ai manqués.