C'è un numpy builtin per rifiutare i valori anomali da un elenco


101

C'è un numpy builtin per fare qualcosa come il seguente? Cioè, prendi un elenco de restituisci un elenco filtered_dcon tutti gli elementi esterni rimossi in base a una distribuzione presunta dei punti d.

import numpy as np

def reject_outliers(data):
    m = 2
    u = np.mean(data)
    s = np.std(data)
    filtered = [e for e in data if (u - 2 * s < e < u + 2 * s)]
    return filtered

>>> d = [2,4,5,1,6,5,40]
>>> filtered_d = reject_outliers(d)
>>> print filtered_d
[2,4,5,1,6,5]

Dico "qualcosa di simile" perché la funzione potrebbe consentire distribuzioni variabili (poisson, gaussiana, ecc.) E soglie anomale variabili all'interno di quelle distribuzioni (come quella mche ho usato qui).


Correlati: scipy.stats può identificare e mascherare evidenti valori anomali? , sebbene quella domanda sembri trattare situazioni più complesse. Per il semplice compito che hai descritto, un pacchetto esterno sembra essere eccessivo.
Sven Marnach,

Stavo pensando che, dato il numero di builtin nella libreria numpy principale, era strano che non ci fosse niente da fare. Sembra una cosa abbastanza comune da fare con dati grezzi e rumorosi.
aaren

Risposte:


104

Questo metodo è quasi identico al tuo, solo più numpyst (funziona anche solo su array numpy):

def reject_outliers(data, m=2):
    return data[abs(data - np.mean(data)) < m * np.std(data)]

3
Questo metodo funziona abbastanza bene se mè sufficientemente grande (ad es. m=6), Ma per valori piccoli mquesto soffre della media la varianza non essendo stimatori robusti.
Benjamin Bannier

30
questa non è davvero una lamentela sul metodo, ma una lamentela sulla vaga nozione di un "valore anomalo"
Eelco Hoogendoorn

come scegli una m?
john ktejik

1
Non l'ho fatto funzionare. Continuo a ricevere un errore di ritorno dati [abs (data - np.mean (data)) <m * np.std (dati)] TypeError: solo gli array scalari interi possono essere convertiti in un indice scalare OPPURE blocca il mio programma
john ktejik

@johnktejik data arg deve essere un array numpy.
Sander van Leeuwen

181

Una cosa importante quando si tratta di valori anomali è che si dovrebbe cercare di utilizzare stimatori il più robusti possibile. La media di una distribuzione sarà influenzata da valori anomali, ma ad esempio la mediana sarà molto inferiore.

Basandosi sulla risposta di eumiro:

def reject_outliers(data, m = 2.):
    d = np.abs(data - np.median(data))
    mdev = np.median(d)
    s = d/mdev if mdev else 0.
    return data[s<m]

Qui ho sostituito la media con la mediana più robusta e la deviazione standard con la distanza mediana assoluta dalla mediana. Ho quindi scalato le distanze in base al loro (di nuovo) valore mediano in modo che msia su una scala relativa ragionevole.

Si noti che affinché la data[s<m]sintassi funzioni, datadeve essere un array numpy.


5
itl.nist.gov/div898/handbook/eda/section3/eda35h.htm questo è fondamentalmente il punteggio Z modificato a cui si fa riferimento qui, ma con una soglia diversa. Se i miei calcoli sono corretti, mi consigliano un m di 3.5 / .6745 ~= 5.189(moltiplicano sper 0,6745 e specificano m3,5 ... prendono anche abs(s)). Qualcuno può spiegare la scelta di m? O è qualcosa che identificherai dal tuo particolare set di dati?
Charlie G

2
@BenjaminBannier: Puoi per favore fornire qualche spiegazione concreta per la scelta di un valore per maffermazioni piuttosto che banali come "gioco di purezza ed efficienza"?
stackoverflowuser2010

1
@ stackoverflowuser2010: Come ho detto, questo dipende dalle tue esigenze specifiche, ad esempio, quanto pulito dobbiamo segnalare al campione (falsi positivi) o quante misurazioni del segnale possiamo permetterci di buttare via per mantenere il segnale pulito (falsi negativi) . Per quanto riguarda una valutazione di esempio specifica per un determinato caso d'uso, vedere ad esempio desy.de/~blist/notes/whyeffpur.ps.gz .
Benjamin Bannier

2
Ottengo il seguente errore quando chiamo la funzione con un elenco di float:TypeError: only integer scalar arrays can be converted to a scalar index
Vasilis

2
@Charlie, se guardi la figura itl.nist.gov/div898/handbook/eda/section3/eda356.htm#MAD , vedrai che quando hai a che fare con la distribuzione normale (che in realtà non è il caso avresti bisogno del z-score modificati) con SD = 1, hai MAD ~ 0.68, che spiega il fattore di scala. La scelta di m = 3.5 implica quindi che si desidera scartare lo 0,05% dei dati.
Fato39

13

La risposta di Benjamin Bannier fornisce un pass-through quando la mediana delle distanze dalla mediana è 0, quindi ho trovato questa versione modificata un po 'più utile per i casi come mostrato nell'esempio seguente.

def reject_outliers_2(data, m=2.):
    d = np.abs(data - np.median(data))
    mdev = np.median(d)
    s = d / (mdev if mdev else 1.)
    return data[s < m]

Esempio:

data_points = np.array([10, 10, 10, 17, 10, 10])
print(reject_outliers(data_points))
print(reject_outliers_2(data_points))

Dà:

[[10, 10, 10, 17, 10, 10]]  # 17 is not filtered
[10, 10, 10, 10, 10]  # 17 is filtered (it's distance, 7, is greater than m)

9

Basandosi su Benjamin, utilizzando pandas.Seriese sostituendo MAD con IQR :

def reject_outliers(sr, iq_range=0.5):
    pcnt = (1 - iq_range) / 2
    qlow, median, qhigh = sr.dropna().quantile([pcnt, 0.50, 1-pcnt])
    iqr = qhigh - qlow
    return sr[ (sr - median).abs() <= iqr]

Per esempio, se si imposta iq_range=0.6, i percentili della interquartile gamma diventerebbero: 0.20 <--> 0.80, in modo più valori anomali saranno inclusi.


4

Un'alternativa è fare una stima robusta della deviazione standard (assumendo statistiche gaussiane). Guardando le calcolatrici online, vedo che il percentile del 90% corrisponde a 1.2815σ e il 95% è 1.645σ ( http://vassarstats.net/tabs.html?#z )

Come semplice esempio:

import numpy as np

# Create some random numbers
x = np.random.normal(5, 2, 1000)

# Calculate the statistics
print("Mean= ", np.mean(x))
print("Median= ", np.median(x))
print("Max/Min=", x.max(), " ", x.min())
print("StdDev=", np.std(x))
print("90th Percentile", np.percentile(x, 90))

# Add a few large points
x[10] += 1000
x[20] += 2000
x[30] += 1500

# Recalculate the statistics
print()
print("Mean= ", np.mean(x))
print("Median= ", np.median(x))
print("Max/Min=", x.max(), " ", x.min())
print("StdDev=", np.std(x))
print("90th Percentile", np.percentile(x, 90))

# Measure the percentile intervals and then estimate Standard Deviation of the distribution, both from median to the 90th percentile and from the 10th to 90th percentile
p90 = np.percentile(x, 90)
p10 = np.percentile(x, 10)
p50 = np.median(x)
# p50 to p90 is 1.2815 sigma
rSig = (p90-p50)/1.2815
print("Robust Sigma=", rSig)

rSig = (p90-p10)/(2*1.2815)
print("Robust Sigma=", rSig)

L'output che ottengo è:

Mean=  4.99760520022
Median=  4.95395274981
Max/Min= 11.1226494654   -2.15388472011
Sigma= 1.976629928
90th Percentile 7.52065379649

Mean=  9.64760520022
Median=  4.95667658782
Max/Min= 2205.43861943   -2.15388472011
Sigma= 88.6263902244
90th Percentile 7.60646688694

Robust Sigma= 2.06772555531
Robust Sigma= 1.99878292462

Che è vicino al valore atteso di 2.

Se vogliamo rimuovere i punti sopra / sotto 5 deviazioni standard (con 1000 punti ci aspetteremmo 1 valore> 3 deviazioni standard):

y = x[abs(x - p50) < rSig*5]

# Print the statistics again
print("Mean= ", np.mean(y))
print("Median= ", np.median(y))
print("Max/Min=", y.max(), " ", y.min())
print("StdDev=", np.std(y))

Che dà:

Mean=  4.99755359935
Median=  4.95213030447
Max/Min= 11.1226494654   -2.15388472011
StdDev= 1.97692712883

Non ho idea di quale approccio sia il più efficace / robusto


3

Vorrei fornire due metodi in questa risposta, soluzione basata su "punteggio z" e soluzione basata su "IQR".

Il codice fornito in questa risposta funziona sia su numpyarray singolo dim che su array multipli numpy.

Importiamo prima alcuni moduli.

import collections
import numpy as np
import scipy.stats as stat
from scipy.stats import iqr

metodo basato sul punteggio z

Questo metodo verificherà se il numero non rientra nelle tre deviazioni standard. In base a questa regola, se il valore è outlier, il metodo restituirà true, in caso contrario restituirà false.

def sd_outlier(x, axis = None, bar = 3, side = 'both'):
    assert side in ['gt', 'lt', 'both'], 'Side should be `gt`, `lt` or `both`.'

    d_z = stat.zscore(x, axis = axis)

    if side == 'gt':
        return d_z > bar
    elif side == 'lt':
        return d_z < -bar
    elif side == 'both':
        return np.abs(d_z) > bar

Metodo basato su IQR

Questo metodo verificherà se il valore è minore q1 - 1.5 * iqro maggiore di q3 + 1.5 * iqr, che è simile al metodo di stampa di SPSS.

def q1(x, axis = None):
    return np.percentile(x, 25, axis = axis)

def q3(x, axis = None):
    return np.percentile(x, 75, axis = axis)

def iqr_outlier(x, axis = None, bar = 1.5, side = 'both'):
    assert side in ['gt', 'lt', 'both'], 'Side should be `gt`, `lt` or `both`.'

    d_iqr = iqr(x, axis = axis)
    d_q1 = q1(x, axis = axis)
    d_q3 = q3(x, axis = axis)
    iqr_distance = np.multiply(d_iqr, bar)

    stat_shape = list(x.shape)

    if isinstance(axis, collections.Iterable):
        for single_axis in axis:
            stat_shape[single_axis] = 1
    else:
        stat_shape[axis] = 1

    if side in ['gt', 'both']:
        upper_range = d_q3 + iqr_distance
        upper_outlier = np.greater(x - upper_range.reshape(stat_shape), 0)
    if side in ['lt', 'both']:
        lower_range = d_q1 - iqr_distance
        lower_outlier = np.less(x - lower_range.reshape(stat_shape), 0)

    if side == 'gt':
        return upper_outlier
    if side == 'lt':
        return lower_outlier
    if side == 'both':
        return np.logical_or(upper_outlier, lower_outlier)

Infine, se vuoi filtrare i valori anomali, usa un numpyselettore.

Buona giornata.


3

Considera che tutti i metodi di cui sopra falliscono quando la tua deviazione standard diventa molto grande a causa di enormi valori anomali.

( Simalar come la media caluclation fallisce e dovrebbe piuttosto calcolare la mediana. Tuttavia, la media è "più incline a un errore come stdDv". )

Potresti provare ad applicare in modo iterativo il tuo algoritmo o filtrare usando l'intervallo interquartile: (qui "factor" si riferisce a un intervallo * sigma, ma solo quando i tuoi dati seguono una distribuzione gaussiana)

import numpy as np

def sortoutOutliers(dataIn,factor):
    quant3, quant1 = np.percentile(dataIn, [75 ,25])
    iqr = quant3 - quant1
    iqrSigma = iqr/1.34896
    medData = np.median(dataIn)
    dataOut = [ x for x in dataIn if ( (x > medData - factor* iqrSigma) and (x < medData + factor* iqrSigma) ) ] 
    return(dataOut)

Scusa, ho trascurato che c'è già un suggerimento IQR sopra. Devo comunque lasciare questa risposta a causa di un codice più breve o eliminarla?
K. Foe

1

Volevo fare qualcosa di simile, tranne impostare il numero su NaN invece di rimuoverlo dai dati, poiché se lo rimuovi modifichi la lunghezza che può rovinare la stampa (cioè se rimuovi solo i valori anomali da una colonna in una tabella , ma è necessario che rimanga uguale alle altre colonne in modo da poterle tracciare l'una contro l'altra).

Per farlo ho usato le funzioni di mascheramento di numpy :

def reject_outliers(data, m=2):
    stdev = np.std(data)
    mean = np.mean(data)
    maskMin = mean - stdev * m
    maskMax = mean + stdev * m
    mask = np.ma.masked_outside(data, maskMin, maskMax)
    print('Masking values outside of {} and {}'.format(maskMin, maskMax))
    return mask

Potresti anche np. Ritagliarli ai valori minimo e massimo consentito per mantenere le dimensioni.
Andi R

0

se vuoi ottenere la posizione di indice dei valori anomali idx_list, la restituirai.

def reject_outliers(data, m = 2.):
        d = np.abs(data - np.median(data))
        mdev = np.median(d)
        s = d/mdev if mdev else 0.
        data_range = np.arange(len(data))
        idx_list = data_range[s>=m]
        return data[s<m], idx_list

data_points = np.array([8, 10, 35, 17, 73, 77])  
print(reject_outliers(data_points))

after rejection: [ 8 10 35 17], index positions of outliers: [4 5]

0

Per un set di immagini (ogni immagine ha 3 dimensioni), dove volevo rifiutare i valori anomali per ogni pixel che ho usato:

mean = np.mean(imgs, axis=0)
std = np.std(imgs, axis=0)
mask = np.greater(0.5 * std + 1, np.abs(imgs - mean))
masked = np.multiply(imgs, mask)

Quindi è possibile calcolare la media:

masked_mean = np.divide(np.sum(masked, axis=0), np.sum(mask, axis=0))

(Lo uso per la sottrazione in background)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.