Analisi dati esplorativa rapida con una riga di codice
Utilizzando il pacchetto open-source di Python: fasteda
Articolo in lingua originale di Benedict Neo
State scrivendo lo stesso codice per creare plot per dataset diversi? Con FastEDA potete fare analisi dati esplorativa (EDA) con una sola linea di codice 🚀.
Questo articolo esplorerà cosa FastEDA può fare andando nel dettaglio del codice e analizzando gli output su un dataset reale.
Immergiamoci!
Codice → Deepnote
FastEDA
Esaminando il Repository di Github, il codice è costituito da appena 300 righe di Python in un unico __init__.py.
Utilizza queste librerie: numpy, pandas, matplotlib, seaborn, scipy.stats, missingno, colorama
Missingno è un pacchetto per la visualizzazione semplice di valori mancanti, mentre colorama aiuta ad ottenere testi colorati. Gli altri pacchetti dovrebbero essere familiari.
Cosa si può fare?
Attualmente questo pacchetto può avere come output:
- Prime tre righe del dataframe (head) e ultime tre righe (tail)
- Conteggio dei valori mancanti
- Matrice MSNO
- Shape
- Info()
- Describe()
- Correlazione
- Pairplot
- Histplot(s) e Boxplot(s)
- Countplot(s)
Esaminiamo il codice con un dataset reale!
Spiegazione passo-passo
Utilizzeremo il dataset contenente prezzi delle case (ottenuto con la funzione fetch_openml) per spiegare il codice di FastEDA
# import iris dataset from sklearn from sklearn.datasets import fetch_openml housing = fetch_openml(name="house_prices", as_frame=True) # create a dataframe df = housing.frame # select 3 categorical and 3 numerical columns df = df[['MSZoning', 'LotArea', 'GrLivArea', 'Street', 'Neighborhood', 'OverallQual', 'SalePrice']] # randomly impute with null values except for SalePrice column df = df.apply(lambda x: np.where(np.random.random(len(x)) < 0.1, np.nan, x) if x.name != 'SalePrice' else x)
In questo caso, filtriamo il tutto su 3 colonne categoriche e numeriche, in modo che sia più facile lavorarci, e imputiamo casualmente i valori mancanti a scopo dimostrativo.
Esplorando i dati
Ecco come sono i nostri dati:
df.head()
df.info()
Dati mancanti
Il codice qua sotto filtra le colonne con dati mancanti. Siccome li abbiamo imputati, tutte le colonne saranno in null_cols
null_cols = [i for i in df.columns if df[i].isna().sum() > 0] df0 = df[null_cols] null_cols output: ['MSZoning', 'LotArea', 'GrLivArea', 'Street', 'Neighborhood', 'OverallQual']
Qui calcoliamo il totale dei valori mancati per ogni colonna, lo trasformiamo in dataframe e applichiamo gli stili.
display( df0.isna() .sum() .to_frame() .style.set_properties( **{ "background-color": "#000000", "color": "#ff0000", "font-weight": "bold", } ) )
Qui vediamo missingno in azione, le parti bianche della matrice rappresentano i valori mancanti
import missingno as msno msno.matrix(df, color=(0, 0.5, 0), figsize=(12, 8)) plt.show() print("-" * 100)
Statistiche
Qui calcoliamo skew e curtosi. Se non sapete di cosa si tratta, ecco una breve lezione:
Skew e curtosi sono misure della forma di una distribuzione.
Lo skew misura l’asimmetria. Una distribuzione è asimmetrica a destra se presenta una coda più lunga a destra, viceversa se presenta una coda più lunga a sinistra. Una distribuzione simmetrica ha skew pari a zero.
La curtosi misura la pesantezza delle code di una distribuzione. Una distribuzione con curtosi elevata presenta probabilità maggiori sulle code più che al centro, rispetto ad una distribuzione normale. Una distribuzione con curtosi bassa avrà meno probabilità sulle code piuttosto che al centro. La curtosi di una distribuzione normale è 3.
Queste misure ci danno quindi un’idea di come una distribuzione differisca da una distribuzione normale – se ha code più pesanti, più lunghe, ecc. Possono essere utili per verificare l’adattamento del modello o per rilevare anomalie nei dati.
from scipy.stats import skew, kurtosis skew_ = ( df._get_numeric_data().dropna().apply(lambda x: skew(x)).to_frame(name="skewness") ) kurt_ = ( df._get_numeric_data() .dropna() .apply(lambda x: kurtosis(x)) .to_frame(name="kurtosis") ) skew_kurt = pd.concat([skew_, kurt_], axis=1) skew_kurt
Basandoci sulle statistiche di LotArea possiamo già dire che la distribuzione di LotArea è asimmetrica (skew diverso da zero)
Qui è mostrato l’output del metodo standard describe()
desc_df = df.describe().T desc_df
Ora raggruppiamo tutto in un unico dataframe, includendo le informazioni su skew, kurtosis e mediana.
full_info = pd.concat([desc_df, skew_kurt], ignore_index=True, axis=1) full_info.columns = list(desc_df.columns) + list(skew_kurt.columns) full_info.insert( loc=2, column="median", value=df.median(skipna=True, numeric_only=True) ) full_info.iloc[:, :-2] = full_info.iloc[:, :-2].applymap( lambda x: format(x, ".3f").rstrip("0").rstrip(".") if isinstance(x, (int, float)) else x ) full_info
E ora aggiungiamo del colore:
def color_negative_red(value): if value < 0: color = "#ff0000" elif value > 0: color = "#00ff00" else: color = "#FFFFFF" return "color: %s" % color info_cols = ["skewness", "kurtosis"] display( full_info.style.background_gradient(cmap="Spectral", subset=full_info.columns[:-2]) .applymap(color_negative_red, subset=info_cols) .set_properties( **{"background-color": "#000000", "font-weight": "bold"}, subset=info_cols, ) .set_properties(**{"font-weight": "bold"}, subset=full_info.columns[:-2]) )
Tipi di dati
Ora analizziamo i tipi di dati.
Qui filtriamo le colonne categoriche ed estraiamo le variabili codificate come numeriche, ma con meno di 15 classi in modo da codificarle come categoriche.
Nota: 15 è arbitrario. Può cambiare in base al dominio del dataset.
Estraiamo anche i valori categorici con troppe classi e li trattiamo come numerici.
cat_cols = [col for col in df.columns if df[col].dtypes == "O"] print(f"{cat_cols=}") # Catch variables that are numeric but have less than 15 classes num_but_cat = [ col for col in df.columns if df[col].nunique() <= 15 and df[col].dtypes != "O" ] print(f"{num_but_cat=}") cat_cols = cat_cols + num_but_cat print(f"{cat_cols=}") # catch variables that are categorical but have an immeasurably large number of classes (>15 in this case) cat_but_car = [ col for col in df.columns if df[col].nunique() > 15 and df[col].dtypes == "O" ] print(f"{cat_but_car=}") cat_cols = [col for col in cat_cols if col not in cat_but_car] print(f"{cat_cols=}") Output: cat_cols=['MSZoning', 'Street', 'Neighborhood'] num_but_cat=['OverallQual'] cat_cols=['MSZoning', 'Street', 'Neighborhood', 'OverallQual'] cat_but_car=['Neighborhood'] cat_cols=['MSZoning', 'Street', 'OverallQual']
Notate che Neighborhood è stato sostituito da OverallQual come la colonna categorica.
num_cols = [col for col in df.columns if df[col].dtypes != "O"] # Catch variables that are numeric but have less than 15 classes num_but_cat = [ col for col in df.columns if df[col].nunique() <= 15 and df[col].dtypes != "O" ] num_cols = [col for col in num_cols if col not in num_but_cat] num_cols Output: ['LotArea', 'GrLivArea', 'SalePrice']
Correlazione
Ora prendiamo le colonne numeriche e calcoliamo la correlazione tra loro
import seaborn as sns sns.heatmap( df[num_cols].corr(), annot=True, cmap="Spectral", linewidths=2, linecolor="#000000", fmt=".3f", );
Il resto del codice ritorna un pairplot e, per ogni variable, istogramma, boxplot e countplot.
Lo vediamo nell’esecuzione di una riga sola.
Ora tutto insieme!
from fasteda import fast_eda fast_eda(df)
DataFrame Head:
DataFrame Tail:
Missing values:
MSNO Matrix
Shape of DataFrame:
(1460, 7)
DataFrame Info:
Describe DataFrame:
DataFrame Correlation:
DataFrame pairplot:
Histogram(s) & Boxplot(s):
Countplot(s)
Questo conclude la presentazione di FastEDA! Se siete ancora curiosi, ecco un altro esempio sul dataset Titanic.
Conclusione
Se volete fare dell’EDA velocce, controllate il pacchetto FastEDA. Consiglio di clonare il repo e personalizzare il codice in base ai vostri scopi. Se cercate strumenti simili per eseguire EDA in modo rapido e con poche righe di codice, date un’occhiata a pandas-profiling e sweetviz!
Grazie per l’attenzione!
Assicuratevi di seguire bitgrit Data Science Publication per rimanere aggiornati. Seguite Bitgrit per rimanere aggiornati sui workshop e le future competizioni!
Discord | Website | Twitter | LinkedIn | Instagram | Facebook | YouTube