Open In Colab

5. Introduzione a Pandas#

La libreria Pandas è delegata alla gestione e lettura dei dati provenienti da sorgenti eterogenee, tra cui fogli Excel, file CSV, o anche JSON e database di tipo SQL.

Pandas dipenda da due strutture dati principali:

  • la Series che viene utilizzata per rappresentare righe o colonne di un DataFrame (molto simile ad un NumPy array),

  • il DataFrame, una sorta di tabella, strutturata su colonne dove i dati di ciascuna unità di osservazione sono distribuiti per righe.

Lo scopo di questo capitolo è iniziare a prendere confidenza con i DataFrame ed imparare a manipolare i dati al loro interno. Per un approfondimento, consiglio il capitolo 10 di Python for Data Analysis, 3E.

Iniziamo a caricare le librerie necessarie.

import pandas as pd
import numpy as np
import statistics as st

5.1. Series#

In Pandas, una Series è un array unidimensionale composto da una sequenza di valori omogenei, simile ad un ndarray, accompagnato da un array di etichette chiamato “index”. A differenza degli indici degli array Numpy, che sono sempre interi e partono da zero, gli oggetti Series supportano etichette personalizzate che possono essere, ad esempio, delle stringhe. Inoltre, gli oggetti Series possono contenere dati mancanti che vengono ignorati da molte delle operazioni della classe.

Il modo più semplice di creare un oggetto Series è di convertire una lista. Per esempio:

grades = pd.Series([27, 30, 24, 18, 22, 20, 29])

È possibile ottenere la rappresentazione dell’array dell’oggetto e dell’indice dell’oggetto Series tramite i suoi attributi array e index, rispettivamente.

grades.array
<PandasArray>
[27, 30, 24, 18, 22, 20, 29]
Length: 7, dtype: int64
grades.index
RangeIndex(start=0, stop=7, step=1)

Oppure, possiamo semplicemente stampare i contenuti dell’oggetto Series direttamente:

print(grades)
0    27
1    30
2    24
3    18
4    22
5    20
6    29
dtype: int64

Per accedere agli elementi di un oggetto Series si usano le parentesi quadre contenenti un indice:

grades[0]
27
grades[0:3]
0    27
1    30
2    24
dtype: int64

È possibile filtrare gli elementi di un oggetto Series con un array booleano:

grades > 24
0     True
1     True
2    False
3    False
4    False
5    False
6     True
dtype: bool
grades[grades > 24]
0    27
1    30
6    29
dtype: int64

È possibile manipolare gli elementi di un oggetto Series con le normali operazioni aritmetiche:

grades / 10
0    2.7
1    3.0
2    2.4
3    1.8
4    2.2
5    2.0
6    2.9
dtype: float64
np.sqrt(grades)
0    5.196152
1    5.477226
2    4.898979
3    4.242641
4    4.690416
5    4.472136
6    5.385165
dtype: float64

Gli oggetti Series hanno diversi metodi per svolgere varie operazioni, per esempio per ricavare alcune statistiche descrittive:

[grades.count(), grades.mean(), grades.min(), grades.max(), grades.std(), grades.sum()]
[7, 24.285714285714285, 18, 30, 4.572172558506722, 170]

Molto utile è il metodo .describe():

grades.describe()
count     7.000000
mean     24.285714
std       4.572173
min      18.000000
25%      21.000000
50%      24.000000
75%      28.000000
max      30.000000
dtype: float64

5.2. DataFrame#

Un pandas.DataFrame è composto da righe e colonne. Ogni colonna di un dataframe è un oggetto pandas.Series: quindi, un dataframe è una collezione di serie. A differenza di un array NumPy, un dataframe può combinare più tipi di dati, come numeri e testo, ma i dati in ogni colonna sono dello stesso tipo.

Esistono molti modi per costruire un DataFrame. Il più semplice è quello di utilizzare un dizionario che include una o più liste o array Numpy di uguale lunghezza. Per esempio:

data = {
    "name": [
        "Maria",
        "Anna",
        "Francesco",
        "Cristina",
        "Gianni",
        "Gabriella",
        "Stefano",
    ],
    "sex": ["f", "f", "m", "f", "m", "f", "m"],
    "group": ["a", "b", "a", "b", "b", "c", "a"],
    "x": [1, 2, 3, 4, 5, 6, 7],
    "y": [8, 9, 10, 11, 12, 13, 14],
    "z": [15, 16, 17, 18, 19, 20, 21],
}
frame = pd.DataFrame(data)
frame
name sex group x y z
0 Maria f a 1 8 15
1 Anna f b 2 9 16
2 Francesco m a 3 10 17
3 Cristina f b 4 11 18
4 Gianni m b 5 12 19
5 Gabriella f c 6 13 20
6 Stefano m a 7 14 21

Oppure possiamo procedere nel modo seguente:

lst1 = [1, 2, 3, 4, 5, 6, 7]
lst2 = [8, 9, 10, 11, 12, 13, 14]
lst3 = [14.4, 15.1, 16.7, 17.3, 18.9, 19.3, 20.2]
lst4 = ["a", "b", "a", "b", "b", "c", "a"]
lst5 = ["f", "f", "m", "f", "m", "f", "m"]
lst6 = ["Maria", "Anna", "Francesco", "Cristina", "Gianni", "Gabriella", "Stefano"]

df = pd.DataFrame()

df["x"] = lst1
df["y"] = lst2
df["z"] = lst3
df["group"] = lst4
df["sex"] = lst5
df["name"] = lst6

df
x y z group sex name
0 1 8 14.4 a f Maria
1 2 9 15.1 b f Anna
2 3 10 16.7 a m Francesco
3 4 11 17.3 b f Cristina
4 5 12 18.9 b m Gianni
5 6 13 19.3 c f Gabriella
6 7 14 20.2 a m Stefano

Molto spesso un DataFrame viene creato dal caricamento di dati da file.

5.3. Lettura di dati da file#

Di solito la quantità di dati da analizzare è tale che non è pensabile di poterli immettere manualmente in una o più liste. Normalmente i dati sono memorizzati su un file ed è necessario importarli. La lettura (importazione) dei file è il primo fondamentale passo nel processo più generale di analisi dei dati.

In un primo esempio, importiamo i dati da un repository remoto.

url = "https://raw.githubusercontent.com/pandas-dev/pandas/master/doc/data/titanic.csv"
titanic = pd.read_csv(url, index_col='Name')

È possibile usare il metodo .head() per visualizzare le prime cinque righe.

titanic.head()
PassengerId Survived Pclass Sex Age SibSp Parch Ticket Fare Cabin Embarked
Name
Braund, Mr. Owen Harris 1 0 3 male 22.0 1 0 A/5 21171 7.2500 NaN S
Cumings, Mrs. John Bradley (Florence Briggs Thayer) 2 1 1 female 38.0 1 0 PC 17599 71.2833 C85 C
Heikkinen, Miss. Laina 3 1 3 female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
Futrelle, Mrs. Jacques Heath (Lily May Peel) 4 1 1 female 35.0 1 0 113803 53.1000 C123 S
Allen, Mr. William Henry 5 0 3 male 35.0 0 0 373450 8.0500 NaN S

Le statistiche descrittive per ciascuna colonna si ottengono con il metodo describe.

titanic.describe()
PassengerId Survived Pclass Age SibSp Parch Fare
count 891.000000 891.000000 891.000000 714.000000 891.000000 891.000000 891.000000
mean 446.000000 0.383838 2.308642 29.699118 0.523008 0.381594 32.204208
std 257.353842 0.486592 0.836071 14.526497 1.102743 0.806057 49.693429
min 1.000000 0.000000 1.000000 0.420000 0.000000 0.000000 0.000000
25% 223.500000 0.000000 2.000000 20.125000 0.000000 0.000000 7.910400
50% 446.000000 0.000000 3.000000 28.000000 0.000000 0.000000 14.454200
75% 668.500000 1.000000 3.000000 38.000000 1.000000 0.000000 31.000000
max 891.000000 1.000000 3.000000 80.000000 8.000000 6.000000 512.329200

In questo modo possiamo ottenere informazioni sui nomi dei passeggeri, la sopravvivenza (0 o 1), l’età, il prezzo del biglietto, ecc. Con le statistiche riassuntive vediamo che l’età media è di 29,7 anni, il prezzo massimo del biglietto è di 512 USD, il 38% dei passeggeri è sopravvissuto, ecc.

Supponiamo di essere interessati alla probabilità di sopravvivenza per diverse fasce d’età. Con due righe di codice, possiamo trovare l’età media di coloro che sono sopravvissuti o non sono sopravvissuti e generare gli istogrammi corrispondenti della distribuzione dell’età:

print(titanic.groupby("Survived")["Age"].mean())
Survived
0    30.626179
1    28.343690
Name: Age, dtype: float64
titanic.hist(
    column="Age",
    by="Survived",
    bins=25,
    figsize=(12, 5),
    layout=(1, 2),
    zorder=2,
    sharey=True,
    rwidth=0.9,
)
array([<Axes: title={'center': '0'}>, <Axes: title={'center': '1'}>],
      dtype=object)
_images/bdfad0adedcf3ca946a8c9237b38e2cd50cc626a13640c7a01313ab0936fd96c.png

È chiaro che i dataframes di Pandas ci permettono di condurre analisi avanzate con pochi comandi, ma acquisire familiarità con la sintassi corretta richiede un po’ di tempo.

Per fare un secondo esempio, importo i dati dal file penguins.csv situato nella directory “data” del mio computer. I dati relativi ai pinguini di Palmer sono resi disponibili da Kristen Gorman e dalla Palmer station, Antarctica LTER. La seguente cella legge il contenuto del file penguins.csv e lo inserisce nell’oggetto df utilizzando la funzione read_csv() di Pandas.

df = pd.read_csv("data/penguins.csv")

Per il DataFrame df il significato delle colonne è il seguente:

  • species: a factor denoting penguin type (Adélie, Chinstrap and Gentoo)

  • island: a factor denoting island in Palmer Archipelago, Antarctica (Biscoe, Dream or Torgersen)

  • bill_length_mm: a number denoting bill length (millimeters)

  • bill_depth_mm: a number denoting bill depth (millimeters)

  • flipper_length_mm: an integer denoting flipper length (millimeters)

  • body_mass_g: an integer denoting body mass (grams)

  • sex: a factor denoting sexuality (female, male)

  • year: the year of the study

Usiamo il metodo .head() per visualizzare le prime cinque righe.

df.head()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 male 2007
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 female 2007
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 female 2007
3 Adelie Torgersen NaN NaN NaN NaN NaN 2007
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 female 2007

A volte potrebbero esserci dati estranei alla fine del file, quindi è importante anche controllare le ultime righe:

df.tail()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
339 Chinstrap Dream 55.8 19.8 207.0 4000.0 male 2009
340 Chinstrap Dream 43.5 18.1 202.0 3400.0 female 2009
341 Chinstrap Dream 49.6 18.2 193.0 3775.0 male 2009
342 Chinstrap Dream 50.8 19.0 210.0 4100.0 male 2009
343 Chinstrap Dream 50.2 18.7 198.0 3775.0 female 2009

Le istruzioni seguenti ritornano le prime e le ultime tre righe del DataFrame.

display(df.head(3))
display(df.tail(3))
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 male 2007
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 female 2007
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 female 2007
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
341 Chinstrap Dream 49.6 18.2 193.0 3775.0 male 2009
342 Chinstrap Dream 50.8 19.0 210.0 4100.0 male 2009
343 Chinstrap Dream 50.2 18.7 198.0 3775.0 female 2009

Tip

Un breve tutorial in formato video è disponibile tramite il seguente collegamento, il quale illustra come effettuare la lettura dei dati da un file esterno in Visual Studio Code.

L’attributo .dtypes restituisce il tipo dei dati:

df.dtypes
species               object
island                object
bill_length_mm       float64
bill_depth_mm        float64
flipper_length_mm    float64
body_mass_g          float64
sex                   object
year                   int64
dtype: object

Gli attributi più comunemente usati sono elencati di seguito:

Attributo

Ritorna

dtypes

Il tipo di dati in ogni colonna

shape

Una tupla con le dimensioni del DataFrame object (numero di righe, numero di colonne)

index

L’oggetto Index lungo le righe del DataFrame

columns

Il nome delle colonne

values

I dati contenuti nel DataFrame

empty

Check if the DataFrame object is empty

Per esempio, l’istruzione della cella seguente restituisce l’elenco con i nomi delle colonne del DataFrame df:

df.columns
Index(['species', 'island', 'bill_length_mm', 'bill_depth_mm',
       'flipper_length_mm', 'body_mass_g', 'sex', 'year'],
      dtype='object')

L’attributo .shape ritorna il numero di righe e di colonne del DataFrame. Nel caso presente, ci sono 344 righe e 8 colonne.

df.shape
(344, 8)

Come abbiamo già visto in precedenza, un sommario dei dati si ottiene con il metodo .describe():

df.describe()
bill_length_mm bill_depth_mm flipper_length_mm body_mass_g year
count 342.000000 342.000000 342.000000 342.000000 344.000000
mean 43.921930 17.151170 200.915205 4201.754386 2008.029070
std 5.459584 1.974793 14.061714 801.954536 0.818356
min 32.100000 13.100000 172.000000 2700.000000 2007.000000
25% 39.225000 15.600000 190.000000 3550.000000 2007.000000
50% 44.450000 17.300000 197.000000 4050.000000 2008.000000
75% 48.500000 18.700000 213.000000 4750.000000 2009.000000
max 59.600000 21.500000 231.000000 6300.000000 2009.000000

Una descrizione del DataFrame si ottiene con il metodo .info().

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 344 entries, 0 to 343
Data columns (total 8 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   species            344 non-null    object 
 1   island             344 non-null    object 
 2   bill_length_mm     342 non-null    float64
 3   bill_depth_mm      342 non-null    float64
 4   flipper_length_mm  342 non-null    float64
 5   body_mass_g        342 non-null    float64
 6   sex                333 non-null    object 
 7   year               344 non-null    int64  
dtypes: float64(4), int64(1), object(3)
memory usage: 21.6+ KB

Warning

Si noti che, alle volte, abbiamo utilizzato la sintassi df.word e talvolta la sintassi df.word(). Tecnicamente, la classe Pandas Dataframe ha sia attributi che metodi. Gli attributi sono .word, mentre i metodi sono .word() o .word(arg1, arg2, ecc.). Per sapere se qualcosa è un metodo o un attributo è necessario leggere la documentazione.

Abbiamo visto in precedenza come possiamo leggere i dati in un dataframe utilizzando la funzione read_csv(). Pandas comprende anche molti altri formati, ad esempio utilizzando le funzioni read_excel(), read_hdf(), read_json(), ecc. (e i corrispondenti metodi per scrivere su file: to_csv(), to_excel(), to_hdf(), to_json(), ecc.).

5.4. Gestione dei dati mancanti#

Nell’output di .info() troviamo la colonna “Non-Null Count”, ovvero il numero di dati non mancanti per ciascuna colonna del DataFrame. Da questo si nota che le colonne del DataFrame df contengono alcuni dati mancanti. La gestione dei dati mancanti è un argomento complesso. Per ora ci limitiamo ad escludere tutte le righe che, in qualche colonna, contengono dei dati mancanti.

Ottengo il numero di dati per ciascuna colonna del DataFrame:

df.isnull().sum()
species               0
island                0
bill_length_mm        2
bill_depth_mm         2
flipper_length_mm     2
body_mass_g           2
sex                  11
year                  0
dtype: int64

Rimuovo i dati mancanti con il metodo .dropna(). L’argomento inplace=True specifica il DataFrame viene trasformato in maniera permanente.

df.dropna(inplace=True)

Verifico che i dati mancanti siano stati rimossi.

df.shape
(333, 8)

5.5. Rinominare le colonne#

È possibile rinominare tutte le colonne passando al metodo .rename() un dizionario che specifica quali colonne devono essere mappate a cosa. Nella cella seguente faccio prima una copia del DataFrame con il metodo copy() e poi rinomino sex che diventa gender e year che diventa year_of_the_study:

df1 = df.copy()

# rename(columns={"OLD_NAME": "NEW_NAME"})
df1.rename(columns={"sex": "gender", "year": "year_of_the_study"}, inplace=True)
df1.head()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g gender year_of_the_study
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 male 2007
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 female 2007
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 female 2007
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 female 2007
5 Adelie Torgersen 39.3 20.6 190.0 3650.0 male 2007

Warning

Si noti che in Python valgono le seguenti regole.

  • Il nome di una variabile deve iniziare con una lettera o con il trattino basso (underscore) _.

  • Il nome di una variabile non può iniziare con un numero.

  • Un nome di variabile può contenere solo caratteri alfanumerici e il trattino basso (A-z, 0-9 e _).

  • I nomi delle variabili fanno distinzione tra maiuscole e minuscole (age, Age e AGE sono tre variabili diverse).

Gli spazi non sono consentiti nel nome delle variabili: come separatore usate il trattino basso.

5.6. Estrarre i dati dal DataFrame#

Una parte cruciale del lavoro con i DataFrame è l’estrazione di sottoinsiemi di dati: vogliamo trovare le righe che soddisfano un determinato insieme di criteri, vogliamo isolare le colonne/righe di interesse, ecc. Per rispondere alle domande di interesse dell’analisi dei dati, molto spesso è necessario selezionare un sottoinsieme del DataFrame.

5.6.1. Colonne#

È possibile estrarre una colonna da un DataFrame usando una notazione simile a quella che si usa per il dizionario (DataFrame['word']) o utilizzando la notazione DataFrame.word. Per esempio:

df["bill_length_mm"]
0      39.1
1      39.5
2      40.3
4      36.7
5      39.3
       ... 
339    55.8
340    43.5
341    49.6
342    50.8
343    50.2
Name: bill_length_mm, Length: 333, dtype: float64
df.bill_length_mm
0      39.1
1      39.5
2      40.3
4      36.7
5      39.3
       ... 
339    55.8
340    43.5
341    49.6
342    50.8
343    50.2
Name: bill_length_mm, Length: 333, dtype: float64

Se tra parentesi quadre indichiamo una lista di colonne come df[['bill_length_mm','species']] otteniamo un nuovo DataFrame costituito da queste colonne:

df[["bill_length_mm", "species"]]
bill_length_mm species
0 39.1 Adelie
1 39.5 Adelie
2 40.3 Adelie
4 36.7 Adelie
5 39.3 Adelie
... ... ...
339 55.8 Chinstrap
340 43.5 Chinstrap
341 49.6 Chinstrap
342 50.8 Chinstrap
343 50.2 Chinstrap

333 rows × 2 columns

5.6.2. Righe#

In un pandas.DataFrame, anche le righe hanno un nome. I nomi delle righe sono chiamati index:

df.index
Int64Index([  0,   1,   2,   4,   5,   6,   7,  12,  13,  14,
            ...
            334, 335, 336, 337, 338, 339, 340, 341, 342, 343],
           dtype='int64', length=333)

Ci sono vari metodi per estrarre sottoinsimi di righe da un DataFrame. È possibile fare riferimento ad un intervallo di righe mediante un indice di slice. Per esempio, possiamo ottenere le prime 3 righe del DataFrame df nel modo seguente:

df[0:3]
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 male 2007
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 female 2007
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 female 2007

Si noti che in Python una sequenza è determinata dal valore iniziale e quello finale ma si interrompe ad n-1. Pertanto, per selezionare una singola riga (per esempio, la prima) dobbiamo procedere nel modo seguente:

df[0:1]
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 male 2007

5.6.3. Indicizzazione, selezione e filtraggio#

Poiché l’oggetto DataFrame è bidimensionale, è possibile selezionare un sottoinsieme di righe e colonne utilizzando le etichette degli assi (loc) o gli indici delle righe (iloc).

Per esempio, usando l’attributo iloc posso selezionare la prima riga del DataFrame:

df.iloc[0]
species                 Adelie
island               Torgersen
bill_length_mm            39.1
bill_depth_mm             18.7
flipper_length_mm        181.0
body_mass_g             3750.0
sex                       male
year                      2007
Name: 0, dtype: object

La cella seguene seleziona le prime tre righe del DataFrame:

df.iloc[0:3]
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 male 2007
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 female 2007
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 female 2007

L’attributo loc consente di selezionare simultaneamente righe e colonne per “nome”. Il “nome” delle righe è l’indice di riga. Per esempio, visualizzo il quinto valore della colonna body_mass_g:

df.loc[4, "body_mass_g"]
3450.0

oppure, il quinto valore delle colonne bill_length_mm, bill_depth_mm, flipper_length_mm:

df.loc[4, ["bill_length_mm", "bill_depth_mm", "flipper_length_mm"]] 
bill_length_mm        36.7
bill_depth_mm         19.3
flipper_length_mm    193.0
Name: 4, dtype: object

Visualizzo ora le prime tre righe sulle tre colonne precedenti. Si noti l’uso di : per definire un intervallo di valori sull’indice di riga.

df.loc[0:2, ["bill_length_mm", "bill_depth_mm", "flipper_length_mm"]]
bill_length_mm bill_depth_mm flipper_length_mm
0 39.1 18.7 181.0
1 39.5 17.4 186.0
2 40.3 18.0 195.0

Una piccola variante della sintassi precedente si rivela molto utile. Qui, il segno di due punti (:) signfica “tutte le righe”:

keep_cols = ["bill_length_mm", "bill_depth_mm", "flipper_length_mm"]
print(df.loc[:, keep_cols])
     bill_length_mm  bill_depth_mm  flipper_length_mm
0              39.1           18.7              181.0
1              39.5           17.4              186.0
2              40.3           18.0              195.0
4              36.7           19.3              193.0
5              39.3           20.6              190.0
..              ...            ...                ...
339            55.8           19.8              207.0
340            43.5           18.1              202.0
341            49.6           18.2              193.0
342            50.8           19.0              210.0
343            50.2           18.7              198.0

[333 rows x 3 columns]

5.6.4. Filtrare righe in maniera condizionale#

In precedenza abbiamo utilizzato la selezione delle righe in un DataFrame in base alla loro posizione. Tuttavia, è più comune selezionare le righe del DataFrame utilizzando una condizione logica, cioè tramite l’indicizzazione booleana.

Iniziamo con un esempio relativo ad una condizione specificata sui valori di una sola colonna. Quando applichiamo un operatore logico come >, <, ==, != ai valori di una colonna del DataFrame, il risultato è una sequenza di valori booleani (True, False), uno per ogni riga nel DataFrame, i quali indicano se, per quella riga, la condizione è vera o falsa. Ad esempio:

df["island"] == "Torgersen"
0       True
1       True
2       True
4       True
5       True
       ...  
339    False
340    False
341    False
342    False
343    False
Name: island, Length: 333, dtype: bool

Utilizzando i valori booleani che sono stati ottenuti in questo modo è possibile filtrare le righe del DataFrame, ovvero, ottenere un nuovo DataFrame nel quale la condizione logica specificata è vera su tutte le righe. Per esempio, nella cella seguente selezioniamo solo le osservazioni relative all’isola Torgersen, ovvero tutte le righe del DataFrame nelle quali la colonna island assume il valore Torgersen.

only_torgersen = df[df["island"] == "Torgersen"]
only_torgersen.head()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 male 2007
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 female 2007
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 female 2007
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 female 2007
5 Adelie Torgersen 39.3 20.6 190.0 3650.0 male 2007

In maniera equivalente, possiamo scrivere:

only_torgersen = df.loc[df["island"] == "Torgersen"]
only_torgersen.head()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 male 2007
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 female 2007
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 female 2007
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 female 2007
5 Adelie Torgersen 39.3 20.6 190.0 3650.0 male 2007

È possibile combinare più condizioni logiche usando gli operatori & (e), | (oppure). Si presti attenzione all’uso delle parentesi.

df.loc[(df["island"] == "Torgersen") & (df["sex"] == "female")].head()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 female 2007
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 female 2007
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 female 2007
6 Adelie Torgersen 38.9 17.8 181.0 3625.0 female 2007
12 Adelie Torgersen 41.1 17.6 182.0 3200.0 female 2007

5.6.5. Metodo .query#

È anche possibile filtrare le righe del DataFrame usando il metodo query(). Ci sono diversi modi per generare sottoinsiemi con Pandas. I metodi loc e iloc consentono di recuperare sottoinsiemi in base alle etichette di riga e colonna o all’indice intero delle righe e delle colonne. E Pandas ha una notazione a parentesi quadre che consente di utilizzare condizioni logiche per recuperare righe di dati specifiche. Ma la sintassi di questi metodi non è la più trasparente. Inoltre, tali metodi sono difficili da usare insieme ad altri metodi di manipolazione dei dati in modo organico.

Il metodo .query di Pandas cerca di risolve questi problemi. Il metodo .query consente di “interrogare” un DataFrame e recuperare sottoinsiemi basati su condizioni logiche. La sintassi è un po’ più snella rispetto alla notazione a parentesi quadre di Pandas. Inoltre, il metodo .query può essere utilizzato con altri metodi di Pandas in modo snello e semplice, rendendo la manipolazione dei dati maggiormente fluida e diretta.

La sintassi è la seguente:

your_data_frame.query(expression, inplace = False)

L’espressione utilizzata nella query è una sorta di espressione logica che descrive quali righe restituire in output. Se l’espressione è vera per una particolare riga, la riga verrà inclusa nell’output. Se l’espressione è falsa per una particolare riga, quella riga verrà esclusa dall’output.

Il parametro inplace consente di specificare se si desidera modificare direttamente il DataFrame con cui si sta lavorando.

Per esempio:

eval_string = "island == 'Torgersen' & sex == 'female' & year != 2009"
df.query(eval_string)[["bill_depth_mm", "flipper_length_mm"]]
bill_depth_mm flipper_length_mm
1 17.4 186.0
2 18.0 195.0
4 19.3 193.0
6 17.8 181.0
12 17.6 182.0
15 17.8 185.0
16 19.0 195.0
18 18.4 184.0
68 16.6 190.0
70 19.0 190.0
72 17.2 196.0
74 17.5 190.0
76 16.8 191.0
78 16.1 187.0
80 17.2 189.0
82 18.8 187.0

Un altro esempio usa la keyword in per selezionare solo le righe relative alle due isole specificate.

eval_string = "island in ['Torgersen', 'Dream']"
df.query(eval_string)[["bill_depth_mm", "flipper_length_mm"]]
bill_depth_mm flipper_length_mm
0 18.7 181.0
1 17.4 186.0
2 18.0 195.0
4 19.3 193.0
5 20.6 190.0
... ... ...
339 19.8 207.0
340 18.1 202.0
341 18.2 193.0
342 19.0 210.0
343 18.7 198.0

170 rows × 2 columns

Il metodo query() può anche essere utilizzato per selezionare le righe di un DataFrame in base alle relazioni tra le colonne. Ad esempio,

df.query("bill_length_mm > 3*bill_depth_mm")[["bill_depth_mm", "flipper_length_mm"]]
bill_depth_mm flipper_length_mm
152 13.2 211.0
153 16.3 230.0
154 14.1 210.0
155 15.2 218.0
156 14.5 215.0
... ... ...
272 14.3 215.0
273 15.7 222.0
274 14.8 212.0
275 16.1 213.0
293 17.8 181.0

106 rows × 2 columns

È anche possibile fare riferimento a variabili non contenute nel DataFrame usando il carattere @.

outside_var = 21
df.query("bill_depth_mm > @outside_var")[["bill_depth_mm", "flipper_length_mm"]]
bill_depth_mm flipper_length_mm
13 21.2 191.0
14 21.1 198.0
19 21.5 194.0
35 21.1 196.0
49 21.2 191.0
61 21.1 195.0

5.7. Selezione casuale di un sottoinsieme di righe#

Il metodo sample() viene usato per ottenere un sottoinsieme casuale di righe del DataFrame. L’argomento replace=False indica l’estrazione senza rimessa (default); se specifichiamo replace=True otteniamo un’estrazione con rimessa. L’argomento n specifica il numero di righe che vogliamo ottenere. Ad esempio

df_sample = df.sample(4)
df_sample
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
142 Adelie Dream 32.1 15.5 188.0 3050.0 female 2009
259 Gentoo Biscoe 53.4 15.8 219.0 5500.0 male 2009
20 Adelie Biscoe 37.8 18.3 174.0 3400.0 female 2007
329 Chinstrap Dream 50.7 19.7 203.0 4050.0 male 2009
df_sample = df[["bill_length_mm", "bill_depth_mm"]].sample(4)
df_sample
bill_length_mm bill_depth_mm
31 37.2 18.1
306 40.9 16.6
302 50.5 18.4
140 40.2 17.1

5.8. Selezione di colonne#

Il metodo drop() prende in input una lista con i nomi di colonne che vogliamo escludere dal DataFrame e può essere usato per creare un nuovo DataFrame o per sovrascrivere quello di partenza. È possibile usare le espressioni regolari (regex) per semplificare la ricerca dei nomi delle colonne.

Tip

In regex il simbolo $ significa “la stringa finisce con”; il simbolo ^ significa “la stringa inizia con”. L’espressione regex può contenere (senza spazi) il simbolo | che significa “oppure”.

Nel codice della cella seguente, alla funzione .columns.str.contains() viene passata l’espressione regolare mm$|year che significa: tutte le stringhe (in questo caso, nomi di colonne) che finiscono con mm oppure la stringa (nome di colonna) year.

mask = df.columns.str.contains("mm$|year", regex=True)
columns_to_drop = df.columns[mask]
columns_to_drop
Index(['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'year'], dtype='object')
df_new = df.drop(columns=columns_to_drop)
df_new.head()
species island body_mass_g sex
0 Adelie Torgersen 3750.0 male
1 Adelie Torgersen 3800.0 female
2 Adelie Torgersen 3250.0 female
4 Adelie Torgersen 3450.0 female
5 Adelie Torgersen 3650.0 male

In un altro esempio, creaiamo l’elenco delle colonne che iniziano con la lettera “b”, insieme a year e sex.

mask = df.columns.str.contains("^b|year|sex", regex=True)
columns_to_drop = df.columns[mask]
columns_to_drop
Index(['bill_length_mm', 'bill_depth_mm', 'body_mass_g', 'sex', 'year'], dtype='object')

Oppure l’elenco delle colonne che contengono il patten “length”.

mask = df.columns.str.contains("length")
columns_to_drop = df.columns[mask]
columns_to_drop
Index(['bill_length_mm', 'flipper_length_mm'], dtype='object')

5.9. Creare nuove colonne#

Per ciascuna riga, calcoliamo

  • bill_length_mm - bill_depth_mm

  • bill_length_mm / (body_mass_g / 1000)

Per ottenere questo risultato possiamo usare una lambda function.

df = df.assign(
    bill_difference=lambda x: x.bill_length_mm - x.bill_depth_mm,
    bill_ratio=lambda x: x.bill_length_mm / (x.body_mass_g / 1000),
)
df.head()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year bill_difference bill_ratio
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 male 2007 20.4 10.426667
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 female 2007 22.1 10.394737
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 female 2007 22.3 12.400000
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 female 2007 17.4 10.637681
5 Adelie Torgersen 39.3 20.6 190.0 3650.0 male 2007 18.7 10.767123

In maniera più semplice possiamo procedere nel modo seguente:

df["bill_ratio2"] = df["bill_length_mm"] / (df["body_mass_g"] / 1000)
df.head()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year bill_difference bill_ratio bill_ratio2
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 male 2007 20.4 10.426667 10.426667
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 female 2007 22.1 10.394737 10.394737
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 female 2007 22.3 12.400000 12.400000
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 female 2007 17.4 10.637681 10.637681
5 Adelie Torgersen 39.3 20.6 190.0 3650.0 male 2007 18.7 10.767123 10.767123

Un’utile funzionalità è quella che consente di aggiungere una colonna ad un DataFrame (o di mofificare una colonna già esistente) sulla base di una condizione True/False. Questo risultato può essere raggiunto usando np.where(), con la seguente sintassi:

np.where(condition, value if condition is true, value if condition is false)

Supponiamo di avere un DataFrame df con due colonne, A e B, e vogliamo creare una nuova colonna C che contenga il valore di A quando questo è maggiore di 0, e il valore di B altrimenti. Possiamo utilizzare la funzione where() per ottenere ciò come segue:

# Creiamo un DataFrame di esempio
df = pd.DataFrame({'A': [-1, 2, 3, -4], 'B': [5, 6, 0, 8]})

# Creiamo una nuova colonna 'C' usando la funzione where()
df['C'] = df['A'].where(df['A'] > 0, df['B'])

print(df)
   A  B  C
0 -1  5  5
1  2  6  2
2  3  0  3
3 -4  8  8

5.10. Formato long e wide#

Nella data analysis, i termini “formato long” e “formato wide” sono usati per descrivere la struttura di un set di dati. l formato wide (in inglese “wide format”) rappresenta una struttura di dati in cui ogni riga rappresenta una singola osservazione e ogni variabile è rappresentata da più colonne. Un esempio è il seguente, nel quale per ciascun partecipante, identificato da Name e ID abbiamo i punteggi di un ipotetico test per 6 anni consecutivi.

scores = {
    "Name": ["Maria", "Carlo", "Giovanna", "Irene"],
    "ID": [1, 2, 3, 4],
    "2017": [85, 87, 89, 91],
    "2018": [96, 98, 100, 102],
    "2019": [100, 102, 106, 106],
    "2020": [89, 95, 98, 100],
    "2021": [94, 96, 98, 100],
    "2022": [100, 104, 104, 107],
}

wide_data = pd.DataFrame(scores)
wide_data
Name ID 2017 2018 2019 2020 2021 2022
0 Maria 1 85 96 100 89 94 100
1 Carlo 2 87 98 102 95 96 104
2 Giovanna 3 89 100 106 98 98 104
3 Irene 4 91 102 106 100 100 107

Il formato long (in inglese “long format”) rappresenta una struttura di dati in cui ogni riga rappresenta una singola osservazione e ogni colonna rappresenta una singola variabile. Questo formato è quello che viene richiesto per molte analisi statistiche. In Pandas è possibile usare la funzione melt per trasformare i dati dal formato wide al formato long. Un esempio è riportato qui sotto. Sono state mantenute le due colonne che identificano ciascun partecipante, ma i dati del test, che prima erano distribuiti su sei colonne, ora sono presenti in una singola colonna. Al DataFrame, inoltre, è stata aggiunta una colonna che riporta l’anno.

long_data = wide_data.melt(
    id_vars=["Name", "ID"], var_name="Year", value_name="Score"
)
long_data
Name ID Year Score
0 Maria 1 2017 85
1 Carlo 2 2017 87
2 Giovanna 3 2017 89
3 Irene 4 2017 91
4 Maria 1 2018 96
5 Carlo 2 2018 98
6 Giovanna 3 2018 100
7 Irene 4 2018 102
8 Maria 1 2019 100
9 Carlo 2 2019 102
10 Giovanna 3 2019 106
11 Irene 4 2019 106
12 Maria 1 2020 89
13 Carlo 2 2020 95
14 Giovanna 3 2020 98
15 Irene 4 2020 100
16 Maria 1 2021 94
17 Carlo 2 2021 96
18 Giovanna 3 2021 98
19 Irene 4 2021 100
20 Maria 1 2022 100
21 Carlo 2 2022 104
22 Giovanna 3 2022 104
23 Irene 4 2022 107

Per migliorare la leggibilità dei dati, è possibile riordinare le righe del set di dati utilizzando la funzione sort_values. In questo modo, le informazioni saranno presentate in un ordine specifico, che può rendere più facile la lettura dei dati.

long_data.sort_values(by=["ID", "Year"])
Name ID Year Score
0 Maria 1 2017 85
4 Maria 1 2018 96
8 Maria 1 2019 100
12 Maria 1 2020 89
16 Maria 1 2021 94
20 Maria 1 2022 100
1 Carlo 2 2017 87
5 Carlo 2 2018 98
9 Carlo 2 2019 102
13 Carlo 2 2020 95
17 Carlo 2 2021 96
21 Carlo 2 2022 104
2 Giovanna 3 2017 89
6 Giovanna 3 2018 100
10 Giovanna 3 2019 106
14 Giovanna 3 2020 98
18 Giovanna 3 2021 98
22 Giovanna 3 2022 104
3 Irene 4 2017 91
7 Irene 4 2018 102
11 Irene 4 2019 106
15 Irene 4 2020 100
19 Irene 4 2021 100
23 Irene 4 2022 107

5.11. Watermark#

%load_ext watermark
%watermark -n -u -v -iv -w
Last updated: Sat Jun 17 2023

Python implementation: CPython
Python version       : 3.11.3
IPython version      : 8.12.0

pandas: 1.5.3
numpy : 1.24.3

Watermark: 2.3.1