La forma más eficiente de reenviar valores de NaN en una matriz numpy

Problema de ejemplo

Como un ejemplo simple, considere la matriz numpyarr Como es definido debajo:

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

dóndearr se ve así en la salida de la consola:

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

Ahora me gustaría "llenar hacia adelante" en filanan valores en la matrizarr. Con eso me refiero a reemplazar cadanan valor con el valor válido más cercano desde la izquierda. El resultado deseado se vería así:

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

He intentado usar for-loops:

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]

También he intentado usar un marco de datos de pandas como un paso intermedio (ya que los marcos de datos de pandas tienen un método incorporado muy bueno para el relleno directo):

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

Ambas estrategias anteriores producen el resultado deseado, pero sigo preguntándome: ¿no sería una estrategia que usa solo operaciones vectorizadas numpy la más eficiente?

Resumen

¿Hay otra manera más eficiente de 'reenviar'nan valores en matrices numpy? (por ejemplo, mediante el uso de operaciones vectorizadas numpy)

Actualización: Comparación de soluciones

He intentado cronometrar todas las soluciones hasta ahora. Este fue mi script de configuración:

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

seguido de esta entrada de consola:

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

resultando en esta salida de consola:

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

Respuestas a la pregunta(4)

Su respuesta a la pregunta