Estou tentando calcular uma mediana móvel para cerca de 10.000 sinais, cada um com uma lista de comprimento em torno de 750.
Um exemplo de dataframe se parece com isto:
num_indices = 2000 # Set number of indices
# Generate lists of values (each a list of numbers from 0 to 1)
column_data = [np.random.random(750).tolist() for _ in range(num_indices)]
# Create DataFrame
df = pd.DataFrame({'values': column_data}, index=range(num_indices))
Encontrei esta implementação que usa np.lib.stride_tricks, mas é um pouco lenta para o meu propósito. Alguém tem uma ideia para um método mais rápido?
def moving_median(signal,n=150):
# Compute rolling median for valid windows
swindow = np.lib.stride_tricks.sliding_window_view(signal, (n,))
b = np.nanmedian(swindow, axis=1)
b_full = np.concatenate([[np.nanmedian(signal)]*(n-1), b]) # Prepend first `n-1` values unchanged
return signal - b_full
E finalmente:
df.iloc[:,0].apply(lambda x: moving_median(x))
A implementação do Scipy não parece tão eficiente, então escrevi uma em Cython para verificar se eu poderia escrever uma mais rápida. Minha implementação em Cython é muito mais rápida do que o código inicial e também supera o Scipy.
Aqui está o código Cython (
median.pyx
arquivo):Aqui está o código para construí-lo (
setup.py
arquivo):Aqui está a função modificada para usar o código Cython muito mais rápido:
Benchmark e notas
Aqui estão os resultados de desempenho da minha CPU i5-9600KF com Numpy 2.1.3 e Scipy 1.15.2:
Esta implementação baseada em Cython é cerca de 13,6 vezes mais rápida que o código inicial e 4,5 vezes mais rápida que o código Scipy!
Na verdade, a solução SciPy não opera diretamente no dataframe, não precisa converter a lista para arrays Numpy e não computa
np.nanmedian(signal)
nenhum dos dois (sem mencionar onp.concatenate
). Portanto, não deve ser comparado amoving_median
nemmoving_median_fast
, mas apenas amedian.sliding_median
.median.sliding_median
é 10 vezes mais rápido que o código SciPy. A maior parte do tempo demoving_median_fast
é, na verdade, overheads. Se você quiser reduzir tais overheads, então você deve usar matrizes Numpy no dataframe. Se todas as listas forem do mesmo tamanho (suposição feita pelo código SciPy), você deve simplesmente usar uma matriz 2D em vez de um dataframe contendo matrizes Numpy ou listas Python puras ineficientes. Você também deve usar Cython para otimizar ainda mais a linhanp.concatenate([[np.nanmedian(signal)]*(n-1), b])
(pré-alocando a matriz de saída para o tamanho certo e escrevendo diretamente o resultado denp.nanmedian
nela, e também multiplicando valores com um loop no local, etc.). Tudo isso certamente tornariamoving_median_fast
>20 vezes mais rápido quemoving_median
.Se isso não for suficiente, você pode usar vários threads com
joblib
. Observe que o GIL precisa ser liberado na função Cython (usandowith nogil
or@cython.nogil
) para que ele possa escalar bem. Observe que a paralelização só torna o código significativamente mais rápido se você usar matrizes 2D (já que conversões de lista exigem que o GIL esteja habilitado).Você não menciona ter NaNs em seus dados, então vou assumir que não. Nesse caso, acho que isso é o melhor que o SciPy tem a oferecer:
Acredito que houve algum trabalho recente feito para tornar isso mais rápido na versão de desenvolvimento do SciPy (nightly wheels aqui ) do que antes. Estou supondo que, em vez de reordenar cada janela do zero, ele atualiza uma estrutura de dados classificada ou particionada com base nos valores de entrada e saída, mas eu realmente não pesquisei sobre isso.
Observe as várias
mode
opções na documentação que controlam o que acontece no limite. Se você estiver satisfeito em obter de volta um array menor que o original em vez da condição de limite "reflect" padrão, você pode querer usar o padrãomode
e aparar as bordas depois.Se você tiver NaNs, o SciPy tem um novo
vectorized_filter
que funcionará com onp.nanmedian
, mas ele só usastride_tricks.sliding_window_view
recursos ocultos, então é improvável que seja mais rápido do que o que você tem.Se CuPy for uma opção, me avise, e eu posso sugerir algo muito mais rápido. O SciPy
median_filter
ainda não é tão rápido quanto deveria ser.Tenho trabalhado em um projeto chamado
ndfilters
, que foi criado para calcular filtros multidimensionais mais rápido que o Scipy em máquinas multicore usando o compilador Numba com paralelismo habilitado.Você pode instalar
ndilters
usandopip
,Implementei o filtro mediano
ndfilters.median_filter()
e você pode usá-lo de uma forma muito semelhante ao Scipy,Como o Numba é um compilador just-in-time, essa função será muito lenta na primeira vez, pois precisa ser compilada.