wake-up-neo.net

Der effizienteste Weg, NaN-Werte in einem numpy-Array vorwärts zu füllen

Beispielproblem

Als einfaches Beispiel betrachten wir das numpy-Array arr wie unten definiert:

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]])

dabei sieht arr in der Konsolenausgabe folgendermaßen aus:

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

Ich möchte jetzt die nan-Werte im Array arr zeilenweise vorwärts füllen. Damit meine ich, jeden nan Wert durch den nächsten gültigen Wert von links zu ersetzen. Das gewünschte Ergebnis würde so aussehen:

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

So weit versucht

Ich habe es mit for-loops versucht:

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]

Ich habe auch versucht, einen Pandas-Datenrahmen als Zwischenschritt zu verwenden (da Pandas-Datenrahmen eine sehr gepflegte integrierte Methode für das Forward-Füllen haben):

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

Beide Strategien liefern das gewünschte Ergebnis, aber ich frage mich immer wieder: Wäre eine Strategie, die nur numpy vektorisierte Operationen verwendet, die effizienteste?


Zusammenfassung

Gibt es eine andere, effizientere Möglichkeit, nan-Werte in numpy-Arrays vorwärts zu füllen? (z. B. mit numpy vektorisierten Operationen)


Update: Lösungsvergleich

Ich habe bis jetzt alle Lösungen ausprobiert. Dies war mein Setup-Skript:

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

gefolgt von dieser Konsoleneingabe:

%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())

was zu dieser Konsolenausgabe führt:

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
27
Xukrao

Hier ist ein Ansatz -

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]

Wenn Sie kein weiteres Array erstellen möchten und einfach die NaNs in arr füllen, ersetzen Sie den letzten Schritt durch Folgendes:

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

Probeneingang, Ausgang -

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.]])
25
Divakar

Verwenden Sie Numba . Dies sollte zu einer erheblichen Beschleunigung führen:

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

Für diejenigen, die sich für das Problem interessieren, np.nan nach dem Vorfüllen zu führen, funktioniert Folgendes:

mask = np.isnan(arr)
first_non_zero_idx = (~mask!=0).argmax(axis=1) #Get indices of first non-zero values
arr = [ np.hstack([
             [arr[i,first_nonzero]]*(first_nonzero), 
             arr[i,first_nonzero:]])
             for i, first_nonzero in enumerate(first_non_zero_idx) ]
1
christian_bock

Für diejenigen, die auf der Suche nach der Rückwärtsfüllung von NaN-Werten hierher kamen, habe ich die Lösung von Divakar oben modifiziert, um genau das zu tun. Der Trick ist, dass Sie die Akkumulation auf dem umgekehrten Array mit dem Minimum durchführen müssen, mit Ausnahme des Maximums.

Hier ist der Code:



# 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

# My modification to do a backward-fill
def bfill(arr):
    mask = np.isnan(arr)
    idx = np.where(~mask, np.arange(mask.shape[1]), mask.shape[0] + 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))

Ausgabe:

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]]
0
cchwala