69 votes

La manière la plus efficace de remplir les valeurs NaN dans les tableaux numpy.

Exemple de problème

À titre d'exemple simple, considérons le tableau numpy arr comme défini ci-dessous :

import numpy as np
arr = np.array([[5, np.nan, np.nan, 7, 2],
                [3, np.nan, 1, 8, np.nan],
                [4, 9, 6, np.nan, np.nan]])

arr ressemble à ça en sortie de console :

array([[  5.,  nan,  nan,   7.,   2.],
       [  3.,  nan,   1.,   8.,  nan],
       [  4.,   9.,   6.,  nan,  nan]])

J'aimerais maintenant remplir les rangées en avant de l'écran. nan valeurs dans le tableau arr . J'entends par là le remplacement de chaque nan avec la valeur valide la plus proche à partir de la gauche. Le résultat souhaité ressemblerait à ceci :

array([[  5.,   5.,   5.,  7.,  2.],
       [  3.,   3.,   1.,  8.,  8.],
       [  4.,   9.,   6.,  6.,  6.]])

Essayé jusqu'à présent

J'ai essayé d'utiliser des boucles for :

for row_idx in range(arr.shape[0]):
    for col_idx in range(arr.shape[1]):
        if np.isnan(arr[row_idx][col_idx]):
            arr[row_idx][col_idx] = arr[row_idx][col_idx - 1]

J'ai également essayé d'utiliser un cadre de données pandas comme étape intermédiaire (puisque les cadres de données pandas ont une méthode intégrée très soignée pour le remplissage avant) :

import pandas as pd
df = pd.DataFrame(arr)
df.fillna(method='ffill', axis=1, inplace=True)
arr = df.as_matrix()

Les deux stratégies ci-dessus produisent le résultat souhaité, mais je continue à me demander : une stratégie qui utilise uniquement des opérations vectorielles numpy ne serait-elle pas la plus efficace ?


Résumé

Existe-t-il un autre moyen plus efficace de "remplir à l'avance" nan des valeurs dans des tableaux numpy ? (par exemple, en utilisant des opérations vectorielles numpy)


Mise à jour : Comparaison des solutions

J'ai essayé toutes les solutions jusqu'à présent. C'était ma configuration script :

import numba as nb
import numpy as np
import pandas as pd

def random_array():
    choices = [1, 2, 3, 4, 5, 6, 7, 8, 9, np.nan]
    out = np.random.choice(choices, size=(1000, 10))
    return out

def loops_fill(arr):
    out = arr.copy()
    for row_idx in range(out.shape[0]):
        for col_idx in range(1, out.shape[1]):
            if np.isnan(out[row_idx, col_idx]):
                out[row_idx, col_idx] = out[row_idx, col_idx - 1]
    return out

@nb.jit
def numba_loops_fill(arr):
    '''Numba decorator solution provided by shx2.'''
    out = arr.copy()
    for row_idx in range(out.shape[0]):
        for col_idx in range(1, out.shape[1]):
            if np.isnan(out[row_idx, col_idx]):
                out[row_idx, col_idx] = out[row_idx, col_idx - 1]
    return out

def pandas_fill(arr):
    df = pd.DataFrame(arr)
    df.fillna(method='ffill', axis=1, inplace=True)
    out = df.as_matrix()
    return out

def numpy_fill(arr):
    '''Solution provided by Divakar.'''
    mask = np.isnan(arr)
    idx = np.where(~mask,np.arange(mask.shape[1]),0)
    np.maximum.accumulate(idx,axis=1, out=idx)
    out = arr[np.arange(idx.shape[0])[:,None], idx]
    return out

suivi de cette entrée de console :

%timeit -n 1000 loops_fill(random_array())
%timeit -n 1000 numba_loops_fill(random_array())
%timeit -n 1000 pandas_fill(random_array())
%timeit -n 1000 numpy_fill(random_array())

ce qui donne cette sortie de console :

1000 loops, best of 3: 9.64 ms per loop
1000 loops, best of 3: 377 µs per loop
1000 loops, best of 3: 455 µs per loop
1000 loops, best of 3: 351 µs per loop

4 votes

Ce qui doit se passer si le premier élément d'une ligne est nan ?

0 votes

@TadhgMcDonald-Jensen Dans ce cas, pandas laisse l'élément NaN intacte. Je suppose que le PO veut le même comportement par souci de cohérence.

4 votes

65voto

Divakar Points 20144

Voici une approche -

mask = np.isnan(arr)
idx = np.where(~mask,np.arange(mask.shape[1]),0)
np.maximum.accumulate(idx,axis=1, out=idx)
out = arr[np.arange(idx.shape[0])[:,None], idx]

Si vous ne voulez pas créer un autre tableau et simplement remplir les NaNs dedans arr lui-même, remplacez la dernière étape par celle-ci -

arr[mask] = arr[np.nonzero(mask)[0], idx[mask]]

Exemple d'entrée, de sortie -

In [179]: arr
Out[179]: 
array([[  5.,  nan,  nan,   7.,   2.,   6.,   5.],
       [  3.,  nan,   1.,   8.,  nan,   5.,  nan],
       [  4.,   9.,   6.,  nan,  nan,  nan,   7.]])

In [180]: out
Out[180]: 
array([[ 5.,  5.,  5.,  7.,  2.,  6.,  5.],
       [ 3.,  3.,  1.,  8.,  8.,  5.,  5.],
       [ 4.,  9.,  6.,  6.,  6.,  6.,  7.]])

3 votes

Une solution vectorielle uniquement pour numpy, sympa. Merci ! Cette solution semble en effet plus rapide que les solutions basées sur les boucles et sur pandas (voir les temps dans la question mise à jour).

0 votes

@Xukrao Oui, je viens de les voir, merci d'avoir ajouté ces résultats de chronométrage ! C'est bien de voir des accélérations !

2 votes

Comment adapter cette solution au cas où l'arrangement est une unidimensionnel tableau numpy ? Comme numpy.array([0.83, 0.83, 0.83, 0.83, nan, nan, nan]) ?

7voto

c_c Points 8

Mise à jour : Comme l'a fait remarquer financial_physician dans les commentaires, la solution que j'avais initialement proposée peut simplement être échangée avec ffill sur le tableau inversé et ensuite inverser le résultat. Il n'y a pas de perte de performance significative. Ma solution initiale semble être 2% ou 3% plus rapide selon %timeit . J'ai mis à jour l'exemple de code ci-dessous mais j'ai laissé mon texte initial tel quel.


Pour ceux qui sont venus ici à la recherche du remplissage en amont des valeurs NaN, j'ai modifié la solution fournie par Divakar ci-dessus pour faire exactement cela. Le truc, c'est que vous devez faire l'accumulation sur le tableau inversé en utilisant le minimum sauf le maximum.

Voici le code :

# ffill along axis 1, as provided in the answer by Divakar
def ffill(arr):
    mask = np.isnan(arr)
    idx = np.where(~mask, np.arange(mask.shape[1]), 0)
    np.maximum.accumulate(idx, axis=1, out=idx)
    out = arr[np.arange(idx.shape[0])[:,None], idx]
    return out

# Simple solution for bfill provided by financial_physician in comment below
def bfill(arr): 
    return ffill(arr[:, ::-1])[:, ::-1]

# My outdated modification of Divakar's answer to do a backward-fill
def bfill_old(arr):
    mask = np.isnan(arr)
    idx = np.where(~mask, np.arange(mask.shape[1]), mask.shape[1] - 1)
    idx = np.minimum.accumulate(idx[:, ::-1], axis=1)[:, ::-1]
    out = arr[np.arange(idx.shape[0])[:,None], idx]
    return out

# Test both functions
arr = np.array([[5, np.nan, np.nan, 7, 2],
                [3, np.nan, 1, 8, np.nan],
                [4, 9, 6, np.nan, np.nan]])
print('Array:')
print(arr)

print('\nffill')
print(ffill(arr))

print('\nbfill')
print(bfill(arr))

Sortie :

Array:
[[ 5. nan nan  7.  2.]
 [ 3. nan  1.  8. nan]
 [ 4.  9.  6. nan nan]]

ffill
[[5. 5. 5. 7. 2.]
 [3. 3. 1. 8. 8.]
 [4. 9. 6. 6. 6.]]

bfill
[[ 5.  7.  7.  7.  2.]
 [ 3.  1.  1.  8. nan]
 [ 4.  9.  6. nan nan]]

Edit : Mise à jour selon le commentaire de MS_

1 votes

idx = np.where(~mask, np.arange(mask.shape[1]), mask.shape[0] + 1) en bfill devrait être idx = np.where(~mask, np.arange(mask.shape[1]), mask.shape[1] - 1)

1 votes

N'est pas flipper O(n) et vous le faites deux fois, donc ne serait-il pas aussi rapide de retourner le tableau, d'utiliser le remplissage avant, puis de le déplier, que votre méthode bfill avec le tableau original ?

0 votes

Merci ! C'est en effet un très bon point. J'ai chronométré votre solution et la mienne en utilisant %%timeit et il n'y a qu'une différence négligeable mais constante, 10.3 µs (votre solution) vs 9.95 µs (ma solution). Je vais mettre à jour ma réponse en conséquence.

5voto

shx2 Points 14025

Utilice Numba . Cela devrait permettre un gain de vitesse significatif :

import numba
@numba.jit
def loops_fill(arr):
    ...

0 votes

Numba ne ferait-il qu'accélérer la solution basée sur les boucles ? Ou bien accélèrerait-il aussi les autres solutions ?

0 votes

C'est bon pour les boucles. Il n'accélère pas les fonctions implémentées dans numpy/pandas.

1 votes

Merci. J'ai inclus cette solution dans la comparaison de temps (voir la question mise à jour). Il semble que l'ajout du décorateur numba à la solution basée sur les boucles réduise son temps d'exécution d'un ordre de grandeur.

4voto

RichieV Points 4813

J'ai aimé la réponse de Divakar sur pure numpy. Voici une fonction généralisée pour les tableaux à n dimensions :

def np_ffill(arr, axis):
    idx_shape = tuple([slice(None)] + [np.newaxis] * (len(arr.shape) - axis - 1))
    idx = np.where(~np.isnan(arr), np.arange(arr.shape[axis])[idx_shape], 0)
    np.maximum.accumulate(idx, axis=axis, out=idx)
    slc = [np.arange(k)[tuple([slice(None) if dim==i else np.newaxis
        for dim in range(len(arr.shape))])]
        for i, k in enumerate(arr.shape)]
    slc[axis] = idx
    return arr[tuple(slc)]

AFIK les pandas ne peuvent travailler qu'avec deux dimensions, bien qu'ils aient le multi-index pour compenser. La seule façon d'accomplir ceci serait d'aplatir un DataFrame, de désempiler le niveau désiré, de ré-empiler, et finalement de remettre en forme comme l'original. Ce dépilage/réempilage/rafraîchissement, avec le triage pandas impliqué, n'est qu'une surcharge inutile pour obtenir le même résultat.

Test :

def random_array(shape):
    choices = [1, 2, 3, 4, np.nan]
    out = np.random.choice(choices, size=shape)
    return out

ra = random_array((2, 4, 8))
print('arr')
print(ra)
print('\nffull')
print(np_ffill(ra, 1))
raise SystemExit

Sortie :

arr
[[[ 3. nan  4.  1.  4.  2.  2.  3.]
  [ 2. nan  1.  3. nan  4.  4.  3.]
  [ 3.  2. nan  4. nan nan  3.  4.]
  [ 2.  2.  2. nan  1.  1. nan  2.]]

 [[ 2.  3.  2. nan  3.  3.  3.  3.]
  [ 3.  3.  1.  4.  1.  4.  1. nan]
  [ 4.  2. nan  4.  4.  3. nan  4.]
  [ 2.  4.  2.  1.  4.  1.  3. nan]]]

ffull
[[[ 3. nan  4.  1.  4.  2.  2.  3.]
  [ 2. nan  1.  3.  4.  4.  4.  3.]
  [ 3.  2.  1.  4.  4.  4.  3.  4.]
  [ 2.  2.  2.  4.  1.  1.  3.  2.]]

 [[ 2.  3.  2. nan  3.  3.  3.  3.]
  [ 3.  3.  1.  4.  1.  4.  1.  3.]
  [ 4.  2.  1.  4.  4.  3.  1.  4.]
  [ 2.  4.  2.  1.  4.  1.  3.  4.]]]

3voto

Charles Woo Points 21

J'aime la réponse de Divakar, mais elle ne fonctionne pas pour un cas limite où une ligne commence par np.nan, comme l'exemple suivant arr en dessous de

arr = np.array([[9, np.nan, 4, np.nan, 6, 6, 7, 2, 3, np.nan],
[ np.nan, 5, 5, 6, 5, 3, 2, 1, np.nan, 10]])

Le résultat en utilisant le code de Divakar serait :

[[ 9.  9.  4.  4.  6.  6.  7.  2.  3.  3.]
 [nan  4.  5.  6.  5.  3.  2.  1.  1. 10.]]

Le code de Divakar peut être simplifié un peu, et la version simplifiée résout ce problème en même temps :

arr[np.isnan(arr)] = arr[np.nonzero(np.isnan(arr))[0], np.nonzero(np.isnan(arr))[1]-1]

En cas de plusieurs np.nan dans une rangée (soit au début, soit au milieu), il suffit de répéter cette opération plusieurs fois. Par exemple, si le tableau comporte 5 np.nan le code suivant les remplacera tous par le numéro qui les précède. np.nan s :

for i in range(0, 5):
   value[np.isnan(value)] = value[np.nonzero(np.isnan(value))[0], np.nonzero(np.isnan(value))[1]-1]

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X