EDA in Python – Parte 1

L’EDA (Exploratory Data Analysis) è l’ analisi iniziale di un dataset. Sono i primi passi fatti per prendere confidenza con i nuovi dati che abbiamo a disposizione. Tramite semplici grafici e qualche riga di codice, ci addentreremo in una terra al momento sconosciuta e da bravi esploratori ne delineeremo i confini e i principali punti di riferimento.

Seppur si utilizzino tecniche basilari in questa fase di analisi, l’EDA è uno step molto importante da cui dipende la qualità di tutti gli step successivi.

Certamente gli algoritmi di Machine Learning, Deep Learning o elaborati metodi di clusterizzazione sono più affascinanti e ci restituiscono una sensazione quasi “magica” quando li applichiamo.

Ma proprio come il sapore di un piatto servito non può prescindere dalla qualità degli ingredienti, l’utilità di un’ analisi di dati non può prescindere dalla qualità dei dati utilizzati

Ci dobbiamo quindi chiedere:

  • Ci sono valori duplicati dove non dovrebbero esserci?
  • Quanti valori mancanti ha il nostro dataset?
  • Le variabili sono del tipo esatto (integer, string, boolean …)?
  • Chi ha creato il dataset? Quando e come sono stati raccolti i dati?

Con questo e i prossimi articoli, voglio mostrarvi una mia semplice EDA con cui sono andato ad analizzare un dataset pubblico del Ministero dello Sviluppo Economico.

Il dataset contiene i prezzi dei carburanti di tutti distributori presenti in Italia. I dati sono richiesti per legge ai proprietari degli impianti e sono disponibili in formato CSV.

La raccolta di queste informazioni è iniziata nel 3 Marzo 2015. Nel download sono però disponibili solo i dati del giorno precedente a quello di pubblicazione.

Vantaggi:

  • Possono trarre vantaggio da questo dataset tutti gli attori della filiera dei carburanti in Italia. Sapere a che prezzo è disponibile il prodotto finale permette maggiore concorrenza tra i vari attori presenti sul mercato (ed, in teoria, un conseguente abbassamento del prezzo medio per il consumatore), ma permette anche ai diversi intermediari di adattarsi conseguentemente ai cambiamenti del mercato

Dati:

Schema EDA

Per rendere più facile la lettura, ho pensato che uno schema riassuntivo potesse aiutare. Perciò eccolo qui 👍

eda python scheme
Link allo schema: https://coggle.it/diagram/Y6dfCb1Eo51jWY-J/t/-/3a2212a3b6627c7c0b673714915a582d758b494c51164db606c374abf71527b4

Overview del dataset

Prima di tutto importiamo i dati dai due CSV. In questo caso dobbiamo ricordarci di specificare il tipo di separatore. Di default si usa la virgola ma in questi file è stato usato il punto e virgola.

Codice
import pandas as pd
dati_prezzi = pd.read_csv("C:\\Users\David\Documents\Python Scripts\prezzo carburanti\dati\prezzo_alle_8.csv", sep=";")
dati_anagrafica = pd.read_csv("C:\\Users\David\Documents\Python Scripts\prezzo carburanti\dati\\anagrafica_impianti_attivi.csv", sep=";")

Utilizziamo quindi il metodo head() per avere una piccola overview di quelle che sono le prime 5 righe di entrambe le tabelle. È importante fare già delle supposizioni su quello che sarà il contenuto di ogni colonna. In questo modo potremmo verificare se le nostre ipotesi siano corrette o meno e, in caso ci fossimo sbagliati, avremmo modo di chiederci il “perché” siano state fatte delle scelte piuttosto che altre.

Codice
dati_prezzi.head()

idImpiantodescCarburanteprezzoisSelfdtComu
06492Hi-Q Diesel1.6439999999999999111/21/2022 15:00
16612Hi-Q Diesel1.669111/21/2022 0:35
26484Hi-Q Diesel1.6739999999999999111/21/2022 15:00
36503Hi-Q Diesel1.6759999999999999111/21/2022 15:32
46632Hi-Q Diesel1.669111/21/2022 0:35
  • idImpianto: beh direi proprio che si tratta dell’indice identificatore di ciascun impianto all’interno del dataset. Forse sono solo numeri, perciò potrebbe essere un integer, ma potrebbe anche contenere stringhe
  • descCarburante: la tipologia di questa colonna dovrebbe essere stringa. Anche se ci fossero dei carburanti identificati solamente da numeri, questi verrebbero salvati come sequenza di caratteri piuttosto che come integer o float dato che non dobbiamo eseguire operazioni matematiche su questi dati
  • prezzo: data la presenza di valori decimali nei numeri di questa colonna, la sua tipologia dovrebbe essere float
  • isSelf: da una colonna con questo nome mi immagino che i dati debbano rispondere alla domanda “is it self?” e che quindi le possibili risposte possano essere solo “Si” o “No”. Questa colonna dovrebbe essere quindi di tipo Boolean. Se così non fosse bisogna indagarne i motivi e la variabile diventerebbe una stringa
  • dtComu: data in cui è stato comunicato il prezzo. Al momento c’è sia la data che l’orario. La colonna sarà di tipo date ma è da capire quale formato sia più conveniente per noi. Interessante il fatto che nel sito web del dataset è scritto che la comunicazione dei prezzi avviene alle 8:00 di ogni mattina, ma a quanto pare molte persone in Italia hanno orologi non funzionanti 😅

Vediamo ora l’altro CSV, dati_anagrafica:

Codice
dati_anagrafica.head()

idImpiantoGestoreBandieraTipo ImpiantoNome ImpiantoIndirizzoComuneProvinciaLatitudineLongitudine
052829ERRA NICOLAApi-IpStradaleNaNNaNNaNNaN40.71603900000000214.941328
153637ODELLI LUCIAPompe BiancheStradaleNaNNaNNaNNaNNaNNaN
252614FONZI PAOLOApi-IpStradalestazione di servizio IP di Fonzi Paolovia s francesco d'assisi snc - -NaNNaNNaNNaN
353552KHALIL NOMANAgip EniStradaleNaNVIA LINCOLN 69 20092CINISELLO BALSAMONaNNaNNaN
453906GE.CA.R SOCIETA' A RESPONSABILITA' LIMITATA SE...Q8StradaleNaNVIA L.CAVALLARO SNC 84018SCAFATINaNNaNNaN

Facciamo quindi delle ipotesi sulle tipologie delle colonne:

  • IdImpianto: anche in questo caso ritengo molto probabile che questa colonna contenga solo numeri interi e che sia quindi di tipo Integer. Mi aspetto poi di poter unire i due CSV tramite questa colonna
  • Gestore: qui dovrebbero essere presenti i vari nomi dei proprietari degli impianti. Quindi la tipologia dovrebbe essere stringa
  • Bandiera: anche in questo caso dovrebbe essere una stringa in quanto la colonna rappresenta il brand del distributore
  • Tipo di impianto: questa dovrebbe essere una variabile categoriale, perciò sarà di tipo stringa. È però da capire quante categorie sono presenti. Infatti anche le variabili Booleane possono essere viste come categoriali che possono avere solo 2 valori (come abbiamo visto nel caso di “isSelf”)
  • Nome impianto: dovrebbe contenere i nomi dei vari impianti. Anche questa variabile sarà quindi ti tipo stringa. Dalla preview sopra notiamo come da questa colonna in poi siano presenti molti valori NaN (Not a Number). Sicuramente ne dovremo tener conto.
  • Indirizzo/Comune/Provincia: queste colonne rappresentano la posizione geografica dell’ impianto in modo testuale. Saranno pertanto di tipologia stringa. Mi aspetto che concatenando questi 3 valori, all’interno del dataset non ci siano duplicati, altrimenti significherebbe che 2 impianti hanno la stessa posizione oppure che questi parametri non possono essere usati come chiave di ricerca
  • Latitudine e Longitudine: sono le coordinate geografiche. Dato che sono numeri con decimali, la tipologia corretta per queste colonne è float. In caso ci siano alcuni valori non conformi, sono da modificare o eliminare. Come nel caso precedente, concatenando questi 2 valori mi aspetto di non trovare duplicati e che si possano usare come chiave di ricerca.

Controlli livello dataset

Dataset dati_prezzi

Dataset dati_prezzi Vediamo quindi come è stato impostato il dataset tramite questa riga di codice:

Codice
dati_prezzi.dtypes

Output:

idImpianto          int64
descCarburante     object
prezzo            float64
isSelf              int64
dtComu             object
dtype: object

In Python, le colonne con tipologia str, quindi stringa, sono viste come di tipologia Object. Perciò, l’unica colonna la cui tipologia non è congruente con le nostre ipotesi è “isSelf”.

Avevamo ipotizzato boolean, ma invece è un integer. Vediamo velocemente quali dati contiene. Per fare questo possiamo utilizzare l’oggetto Counter, che andrà a controllare quali sono i diversi valori presenti in una lista e ci restituirà il numero di occorrenze di ciascun valore

Codice
from collections import Counter
Counter(dati_prezzi["isSelf"]).most_common()

Output:

[(1, 49600), (0, 41958)]

Sono presenti solo due valori, 1 e 0. Dato che questa variabile non può assumere altri valori (altrimenti si chiamerebbe qualcosa come “tipologia distributore” piuttosto che “isSelf”), è più opportuno cambiare la tipologia della colonna in boolean.

Codice
convert_dict = {'isSelf': bool}
dati_prezzi = dati_prezzi.astype(convert_dict)
print(dati_prezzi.dtypes)

Output:

idImpianto          int64
descCarburante     object
prezzo            float64
isSelf               bool
dtComu             object
dtype: object

Una variabile di tipo boolean può avere solo 2 valori: True e False. Se ora andiamo a controllare i valori della colonna “isSelf” vediamo come questi siano stati convertiti.

Codice
Counter(dati_prezzi["isSelf"]).most_common()

Output:

[(True, 49600), (False, 41958)]

Controlliamo ora con che tipo di variabile sono salvate le date, quindi la colonna “dtComu”, all’interno del dataframe.

Codice
dati_prezzi.dtypes

Output:

idImpianto          int64
descCarburante     object
prezzo            float64
isSelf               bool
dtComu             object
dtype: object

Le date sono salvate come stringhe. Python non ha infatti di default una tipologia di variabile per le date. Possiamo però risolvere questo problema importanto una libreria creata specificatamente per trattare questi oggetti.

Codice
from datetime import datetime

Convertiamo ora ogni valore della colonna “dtComu”

Codice
try:
    for i in range(0,len(dati_prezzi["dtComu"])):
        dati_prezzi["dtComu"][i] = datetime.strptime(dati_prezzi["dtComu"][i], "%d/%m/%Y %H:%M:%S")
except Exception as e: print(e)
dati_prezzi['dtComu']= pd.to_datetime(dati_prezzi['dtComu'])

Infine, andiamo a controllare che tutte le celle di questa colonna siano di tipo datetime.

Codice
z=[]
for elem in dati_prezzi["dtComu"]:
        z.append(type(elem))
Counter(z).most_common()

Output:

[(pandas._libs.tslibs.timestamps.Timestamp, 91558)]

Ha funzionato!

Dataset dati_anagrafica

Vediamo ora le tipologie delle colonne del dataset dati_anagrafica.

Codice
dati_anagrafica.dtypes

Output:

idImpianto        object
Gestore           object
Bandiera          object
Tipo Impianto     object
Nome Impianto     object
Indirizzo         object
Comune            object
Provincia         object
Latitudine       float64
Longitudine      float64
dtype: object

In questo caso “IdImpianto” è salvata come stringa, quindi la convertiamo in Integer. Questo passaggio ci permetterà di evitare un problema di compatibilità tra colonne quando andremo ad unire i due dataset (eh già, indovinate chi ha sbagliato al primo tentativo? 😑 )

Codice
convert_dict = {'idImpianto': int}
try:
    dati_anagrafica = dati_anagrafica.astype(convert_dict)
except Exception as e: print(e)
dati_anagrafica.dtypes

Output:

invalid literal for int() with base 10: '24895;SHABANI ENVER DISTRIBUTORE DI CARBURANTI E LUBRIFICANTI "API";Q8;Stradale;SHABANI ENVER DISTRIBUTORE DI CARBURANTI E LUBRIFICANTI "Q8";16 Adriatica Km. 285+460 sud - 60015;FALCONARA MARITTIMA;A

Otteniamo però un errore.

Significa che non possiamo convertire la nostra colonna da stringa a integer in quanto c’è almeno un valore che non può essere formattato come numero intero. Controllando nel dataset infatti, alla riga 466 osserviamo come il testo mostrato nell’errore sia tutto inserito all’interno della cella “idImpianto”. Dobbiamo scoprire ora quante righe hanno lo stesso problema. Per fare questo utilizziamo il costrutto try-except in modo da gestire in modo agile eventuali errori. Nel codice qui sotto stiamo chiedendo:

Prova a convertire in Integer ogni elemento di “idImpianto”. Quando trovi un elemento che non riesci a convertire, incrementa il contatore “x” di 1.

Codice
x=0
for elem in dati_anagrafica["idImpianto"]:
    try:
        int(elem)
    except:
        x += 1
x

Output:

181

L’ultima riga di codice stampa a video il valore di x, che in questo caso è 181. Non sono quindi molte le righe con questo problema, visto che la lunghezza del dataset è di 22154. Possiamo perciò semplicemente eliminarle.

Codice
y=[]
for elem in dati_anagrafica["idImpianto"]:
    try:
        int(elem)a
    except:
        y.append(dati_anagrafica[dati_anagrafica["idImpianto"] == elem].index[0])

In questo modo sarà molto semplice procedere con l’eliminazione, ci basterà utilizzare la seguente riga di codice:

Codice
update_dati_anagrafica = dati_anagrafica.drop(y)

Per essere sicuri che la modifica sia andata a buon fine, ricontrolliamo quante righe del dataset presentano questo problema.

Codice
x=0
for elem in update_dati_anagrafica["idImpianto"]:
    try:
        int(elem)
    except:
        z += 1
x

Output:

0

Il risultato è 0 💪

Andiamo finalmente ad aggiornare la tipologia della colonna.

Codice
convert_dict = {'idImpianto': int}
update_dati_anagrafica = update_dati_anagrafica.astype(convert_dict)
update_dati_anagrafica.dtypes

Output:

idImpianto         int32
Gestore           object
Bandiera          object
Tipo Impianto     object
Nome Impianto     object
Indirizzo         object
Comune            object
Provincia         object
Latitudine       float64
Longitudine      float64
dtype: object

La tipologia di “idImpianto” è ora int32 (nell’altro CSV questa variabile è un int64. Per semplicità ci fermiamo qui, altrimenti dovremmo importare la libreria “numpy”. Se vuoi saperne di più dai un’occhiata qui https://www.tutorialspoint.com/numpy/numpy_data_types.htm )

Valori nulli e duplicati – dati_prezzi

Una volta appurate e corrette le tipologie delle colonne, passiamo alla ricerca dei valori nulli tra i nostri dati. La funzione “info()” ci viene in grande aiuto.

Codice
dati_prezzi.info()

Output:

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 91558 entries, 0 to 91557
Data columns (total 5 columns):
 #   Column          Non-Null Count  Dtype         
---  ------          --------------  -----         
 0   idImpianto      91558 non-null  int64         
 1   descCarburante  91558 non-null  object        
 2   prezzo          91558 non-null  float64       
 3   isSelf          91558 non-null  bool          
 4   dtComu          91558 non-null  datetime64[ns]
dtypes: bool(1), datetime64[ns](1), float64(1), int64(1), object(1)
memory usage: 2.9+ MB

Vediamo subito come nel dataset dati_prezzi ci siano 91558 valori non null su 91558 righe del dataset. Siamo fortunati, in questo caso non ci sono righe da modificare o eliminare. Vedremo più avanti in dettaglio ciascuna colonna.

Controlliamo quindi se sono presenti valori duplicati. Questo controllo viene fatto prendendo in considerazione l’intera riga. Quindi, perchè si verifichi un valore duplicato bisogna che l’intera riga sia uguale ad un’altra riga.

Codice
print("valori duplicati dataset dati_prezzi: " + str((sum(dati_prezzi.duplicated()))))

Output:

valori duplicati dataset dati_prezzi: 0

Valori nulli e duplicati – dati_anagrafica

Passiamo quindi al secondo dataset.

Codice
update_dati_anagrafica.info()

Output:

<class 'pandas.core.frame.DataFrame'>
Int64Index: 21973 entries, 0 to 22153
Data columns (total 10 columns):
 #   Column         Non-Null Count  Dtype  
---  ------         --------------  -----  
 0   idImpianto     21973 non-null  int32  
 1   Gestore        21972 non-null  object 
 2   Bandiera       21973 non-null  object 
 3   Tipo Impianto  21973 non-null  object 
 4   Nome Impianto  21965 non-null  object 
 5   Indirizzo      21971 non-null  object 
 6   Comune         21970 non-null  object 
 7   Provincia      21185 non-null  object 
 8   Latitudine     21965 non-null  float64
 9   Longitudine    21965 non-null  float64
dtypes: float64(2), int32(1), object(7)
memory usage: 1.8+ MB

In questo caso ci sono invece delle righe contenenti valori nulli. La colonna con più valori nulli è “Provincia”. Non sono tuttavia molti, ma il 4,37% ((22153-21185)/22153*100)

Ci sarebbero vari modi per tentare di recuperare queste righe: inserire i valori delle mode, i valori mediani o anche soluzioni di intelligenza artificiale per stimare i dati mancanti (recentemente Google ha implementato una funzionalità molto interessante in tal senso, ma sarà materiale di un altro articolo https://blog.tensorflow.org/2022/12/introducing-simple-ml-for-sheets.html )

In questo caso preferiamo semplicemente eliminare tutte quelle righe che contengono valori nulli. Per fare ciò, utilizziamo il seguente codice.

Codice
dati_anagrafica_cl = update_dati_anagrafica.dropna()
dati_anagrafica_cl.reset_index(drop=True, inplace=True)

Con la prima riga di codice andiamo ad eliminare tutte le righe all’interno del dataset che contengono ALMENO un valore nullo, mentre con la seconda ricalcoliamo l’indice delle righe.

Nello specifico, la funzione dropna() va a caccia dei valori:

  • None
  • Na (Not Available, dalla libreria Pandas)
  • NaT (Not a Time, dalla libreria Pandas)
  • NaN (Not a Number, dalla libreria Numpy)
Codice
dati_anagrafica_cl.info()

Output:

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 21178 entries, 0 to 21177
Data columns (total 10 columns):
 #   Column         Non-Null Count  Dtype  
---  ------         --------------  -----  
 0   idImpianto     21178 non-null  int32  
 1   Gestore        21178 non-null  object 
 2   Bandiera       21178 non-null  object 
 3   Tipo Impianto  21178 non-null  object 
 4   Nome Impianto  21178 non-null  object 
 5   Indirizzo      21178 non-null  object 
 6   Comune         21178 non-null  object 
 7   Provincia      21178 non-null  object 
 8   Latitudine     21178 non-null  float64
 9   Longitudine    21178 non-null  float64
dtypes: float64(2), int32(1), object(7)
memory usage: 1.5+ MB

L’output appena stampato ci conferma le modifiche appena apportate.

Controlliamo adesso quante righe duplicate ci sono all’interno del dataset.

Codice
print("valori duplicati dataset dati_anagrafica_cl: " + str((sum(dati_anagrafica_cl.duplicated()))))

Output:

valori duplicati dataset dati_anagrafica_cl: 0

Anche in questo dataset non ci sono righe duplicate 💪

Il prossimo step dell’EDA è l’analisi di ogni variabile all’interno dei dataset che dobbiamo utilizzare, ma lo vedremo in un prossimo articolo (Link seconda parte: https://www.davidemarcon.com/eda-in-python-parte-2/)

Se hai consigli o richieste, commenta pure nel box che vedi qui sotto 👍

Lascia un commento