9. Introduzione a Pandas#
Abbiamo visto come Python, unito a numpy e matplotlib possa essere un potente strumento per il calcolo scientifico. Esistono tuttavia altre librerie che forniscono una maggiore astrazione per una più agevole gestione e analisi dei dati. In questo laboratorio vedremo le basi di della libreria Pandas, utile per l’analisi dei dati. In particolare vedremo:
Come creare e gestire serie di dati (Pandas Series);
Come creare e gestire DataFrame di Pandas;
Esempi su com manipolare i dati di un DataFrame.
Pandas è una libreria di alto livello che mette a disposizione diversi strumenti e strutture dati per l’analisi dei dati. In particolare, Pandas è molto utile per caricare, manipolare e visualizzare i dati i maniera veloce e conveniente prima di passare all’analisi vera e propria. Le due strutture dati principali di Pandas sono le Series
e i DataFrame
.
9.1. Series#
Una Series
è una struttura monodimensionale (una serie di dati) molto simile a un array di numpy. A differenza di esso, i suoi elementi possono essere indicizzati mediante delle etichette, oltre che mediante numeri. I valori contenuti in una serie possono essere di qualsiasi tipo.
9.1.1. Creazione di Series#
E’ possibile definire una Series
a partire da una lista o da un array di numpy:
import pandas as pd
import numpy as np
np.random.seed(123) #impostiamo un seed per ripetibilità
s1 = pd.Series([7,5,2,8,9,6])# alternativamente print(pd.Series(np.array([7,5,2])))
print(s1)
0 7
1 5
2 2
3 8
4 9
5 6
dtype: int64
I numeri visualizzati sulla sinistra rappresentano le etichette dei valori contenute nella serie, che, di default sono di tipo numerico e sequenziale. Durante la definizione di una serie, è possibile specificare delle opportune etichette (una per ogni valore):
valori = [7,5,2,8,9,6]
etichette = ['a','b','c','d','e','f']
s2 = pd.Series(valori, index=etichette)
print(s2)
a 7
b 5
c 2
d 8
e 9
f 6
dtype: int64
E’ possibile definire una serie anche mediante un dizionario che specifica contemporaneamente etichette e valori:
s3=pd.Series({'a':14,'h':18,'m':72})
print(s3)
a 14
h 18
m 72
dtype: int64
Opzionalmente, è possibile assegnare un nome a una serie:
pd.Series([1,2,3], name='Mia Serie')
0 1
1 2
2 3
Name: Mia Serie, dtype: int64
🙋♂️ Domanda 1
Qual è la principale differenza tra
Series
di Pandas earray
di Numpy?
9.1.2. Indicizzazione di Series#
Quando gli indici sono numerici, le serie possono essere indicizzate come gli array di numpy:
print(s1[0]) #indicizzazione
print(s1[0:4:2]) #slicing
7
0 7
2 2
dtype: int64
Quando gli indici sono delle più generiche etichette, l’indicizzazione avviene in maniera simile, ma non è possibile utilizzare lo slicing:
print(s2['c'])
2
Chiaramente, è possibile modificare i valori di una serie mediante l’indicizzazione:
s2['c']=4
s2
a 7
b 5
c 4
d 8
e 9
f 6
dtype: int64
Qualora l’indice specificato non esistesse, un nuovo elemento verrà creato:
s2['z']=-2
s2
a 7
b 5
c 4
d 8
e 9
f 6
z -2
dtype: int64
Qualora volessimo specificare più di una etichetta alla volta, potremmo passare una lista di etichette:
print(s2[['a','c','d']])
a 7
c 4
d 8
dtype: int64
Le serie con etichette alfanumeriche possono essere indicizzate anche seguendo l’ordine in cui i dati sono inseriti nella serie, in un certo senso “scartando” le etichette alfanumeriche e indicizzando gli elementi in maniera posizionale. Questo effetti si ottiene mediante il metodo iloc
:
print(s2,'\n')
print("Elemento di indice 'a':",s2['a'])
print("Primo elemento della serie:",s2.iloc[0])
a 7
b 5
c 4
d 8
e 9
f 6
z -2
dtype: int64
Elemento di indice 'a': 7
Primo elemento della serie: 7
In certi casi, può essere utile ripristinare la numerazione degli indici. Ciò può essere fatto usando il metodo reset_index
:
print(s3,'\n')
print(s3.reset_index(drop=True))#drop=True indica di scartare i vecchi indici
a 14
h 18
m 72
dtype: int64
0 14
1 18
2 72
dtype: int64
Le serie ammettono anche l’indicizzazione logica:
print(s1,'\n') #serie s1
print(s1>2,'\n') #indicizzazione logica per selezionare gli elementi maggiori di 2
print(s1[s1>2],'\n') #applicazione dell'indicizzazione logica
0 7
1 5
2 2
3 8
4 9
5 6
dtype: int64
0 True
1 True
2 False
3 True
4 True
5 True
dtype: bool
0 7
1 5
3 8
4 9
5 6
dtype: int64
E’ possibile specificare la combinazione tra due condizione mediante gli operatori logici “|” (or) e “&” (and), ricordando di racchiudere gli operandi tra parentesi tonde:
(s1>2) & (s1<6)
0 False
1 True
2 False
3 False
4 False
5 False
dtype: bool
s1[(s1>2) & (s1<6)]
1 5
dtype: int64
Analogamente per l’or:
(s1<2) | (s1>6)
0 True
1 False
2 False
3 True
4 True
5 False
dtype: bool
print(s1[(s1<2) | (s1>6)])
0 7
3 8
4 9
dtype: int64
Come nel caso degli array di Numpy, l’allocazione della memoria è gestita dinamicamente per le Serie. Pertanto, se assegno una serie ad una nuova variabile e modifico la seconda variabile, verrà modificata anche la prima:
s11=pd.Series([1,2,3])
s12=s11
s12[0]=-1
s11
0 -1
1 2
2 3
dtype: int64
Per ottenere una nuova Serie indipendente, possiamo usare il metodo copy
:
s11=pd.Series([1,2,3])
s12=s11.copy()
s12[0]=-1
s11
0 1
1 2
2 3
dtype: int64
🙋♂️ Domanda 2
Cosa stampa a schermo il seguente codice?
s=pd.Series([1,2,3,4,6]) print(s[s%2])
9.1.3. Tipi di dato#
Le Series
possono contenere diversi tipi di dato:
pd.Series([2.5,3.4,5.2])
0 2.5
1 3.4
2 5.2
dtype: float64
Ad una Series
viene associato un unico tipo di dato. Se specifichiamo dati di tipi eterogenei, la Series
sarà di tipo “object”:
s=pd.Series([2.5,'A',5.2])
s
0 2.5
1 A
2 5.2
dtype: object
E’ possibile cambiare il tipo di dato di una serie al volo con astype
in maniera simile a quanto avviene con gli array di Numpy:
s=pd.Series([2,3,8,9,12,45])
print(s)
print(s.astype(float))
0 2
1 3
2 8
3 9
4 12
5 45
dtype: int64
0 2.0
1 3.0
2 8.0
3 9.0
4 12.0
5 45.0
dtype: float64
🙋♂️ Domanda 3
Considerata la Series definita come segue:
s=pd.Series([2.5,'A',5.2])quali delle seguenti operazioni restituiscono un errore? Perché?
s.astype(str), s.astype(int)
9.1.4. Dati Mancanti#
In Pandas, è possibile specificare Series (e come vedremo anche DataFrame) con dati mancanti. Per indicare tali valori, viene utilizzato il valore np.nan
:
missing = pd.Series([2,5,np.nan,8])
missing
0 2.0
1 5.0
2 NaN
3 8.0
dtype: float64
I valori NaN
(not a number) indicano dei valori che possono essere mancanti per diversi motivi (es. dati risultato di operazioni non validi, dati che non sono stati raccolti correttamente, ecc).
In molti casi, può essere utile scartare i dati NaN
. Ciò si può fare con l’operazione dropna()
:
missing.dropna()
0 2.0
1 5.0
3 8.0
dtype: float64
Alternativamente, possiamo sostituire i valori NaN
con un valore specifico con fillna
. Sostituiamo i NaN
con zero:
missing = pd.Series([2,5,np.nan,8])
print(missing)
missing.fillna(0)
0 2.0
1 5.0
2 NaN
3 8.0
dtype: float64
0 2.0
1 5.0
2 0.0
3 8.0
dtype: float64
9.1.5. Operazioni su e tra Series#
Sono definite sulle serie le principali operazioni disponibili per gli array di numpy:
print("Min:",s1.min())
print("Max:",s1.max())
print("Mean:",s1.mean())
Min: 2
Max: 9
Mean: 6.166666666666667
Vediamo come sostiuire i valori mancanti di una serie con il valore medio:
data = pd.Series([1,5,2,np.nan,5,2,6,np.nan,9,2,3])
data
data.fillna(data.mean())
0 1.000000
1 5.000000
2 2.000000
3 3.888889
4 5.000000
5 2.000000
6 6.000000
7 3.888889
8 9.000000
9 2.000000
10 3.000000
dtype: float64
Si noti che la funzione mean
ha semplicemente ignorato i valori NaN
.
E’ possibile ottenere la dimensione di una serie mediante la funzione len
:
print(len(s1))
6
E’ possibile ottenere delle statistiche sui valori univoci (ovvero i valori della serie, una volta rimossi i duplicati) contenuti in una serie mediante il metodo unique
:
print("Unique:",s1.unique()) #restituisce i valori univoci
Unique: [7 5 2 8 9 6]
Per conoscere il numero di valori univoci in una serie, possiamo utilizzare il metodo nunique
:
s1.nunique()
6
E’ possibile ottenere i valori univoci di una serie insieme alle frequenze con le quali essi appaiono nella serie mediante il metodo value_counts
:
tmp = pd.Series(np.random.randint(0,10,100))
print(tmp.unique()) #valori univoci
tmp.value_counts() #valori univoci con relative frequenze
[2 6 1 3 9 0 4 7 8 5]
3 15
6 14
1 13
2 10
9 10
0 10
4 10
7 8
8 5
5 5
Name: count, dtype: int64
Il risultato di value_counts
è una Series
in cui gli indici rappresentano i valori univoci, mentre i valori sono le frequenze con cui essi appaiono nella serie. La serie è ordinata per valori.
Il metodo describe
permette di calcolare diverse statistiche dei valori contenuti nella serie:
tmp.describe()
count 100.000000
mean 4.130000
std 2.830765
min 0.000000
25% 2.000000
50% 4.000000
75% 6.000000
max 9.000000
dtype: float64
Nelle operazioni tra serie, gli elementi vengono associati in base agli indici. Nel caso in cui esiste una perfetta corrispondenza tra gli elementi otteniamo il seguente risultato:
print(pd.Series([1,2,3])+pd.Series([4,4,4]),'\n')
print(pd.Series([1,2,3])*pd.Series([4,4,4]),'\n')
0 5
1 6
2 7
dtype: int64
0 4
1 8
2 12
dtype: int64
Nel caso in cui alcuni indici dovessero essere mancanti, le caselle corrispondenti verranno riempite con NaN
(not a number) per indicare l’assenza del valore:
s1 = pd.Series([1,4,2], index = [1,2,3])
s2 = pd.Series([4,2,8], index = [0,1,2])
print(s1,'\n')
print(s2,'\n')
print(s1+s2)
1 1
2 4
3 2
dtype: int64
0 4
1 2
2 8
dtype: int64
0 NaN
1 3.0
2 12.0
3 NaN
dtype: float64
In questo caso, l’indice 0
era presente solo nella seconda serie (s2
), mentre l’indice 3
era presente solo nella prima serie (s1
).
Se vogliamo escludere i valori NaN
(inclusi i relativi indici) possiamo utilizzare il metodo dropna
:
s3=s1+s2
print(s3)
print(s3.dropna())
0 NaN
1 3.0
2 12.0
3 NaN
dtype: float64
1 3.0
2 12.0
dtype: float64
E’ possibile applicare una funzione a tutti gli elementi di una serie mediante il metodo apply
. Supponiamo ad esempio di voler trasformare delle stringhe contenute in una serie in uppercase. Possiamo specificare la funzione str.upper
mediante il metodo apply
:
s=pd.Series(['aba','cda','daf','acc'])
s.apply(str.upper)
0 ABA
1 CDA
2 DAF
3 ACC
dtype: object
Mediante apply possiamo applicare anche funzioni definite dall’utente mediante espressioni lambda o mediante la consueta sintassi:
def miafun(x):
y="Stringa: "
return y+x
s.apply(miafun)
0 Stringa: aba
1 Stringa: cda
2 Stringa: daf
3 Stringa: acc
dtype: object
La stessa funzione si può scrivere in maniera più compatta come espressione lambda:
s.apply(lambda x: "Stringa: "+x)
0 Stringa: aba
1 Stringa: cda
2 Stringa: daf
3 Stringa: acc
dtype: object
E’ possibile modificare tutte le occorrenze di un dato valore di una serie mediante il metodo replace
:
ser = pd.Series([1,2,15,-1,7,9,2,-1])
print(ser)
ser=ser.replace({-1:0}) #sostituisci tutti le occorrenze di "-1" con zeri
print(ser)
0 1
1 2
2 15
3 -1
4 7
5 9
6 2
7 -1
dtype: int64
0 1
1 2
2 15
3 0
4 7
5 9
6 2
7 0
dtype: int64
🙋♂️ Domanda 4
Si provi ad applicare il metodo
apply
ad unaSeries
specificando una funzione che prende in unput due argomenti. E’ possibile farlo? Perché?
9.1.6. Conversione in array di Numpy#
E’ possibile accedere ai valori della series in forma di array di numpy mediante la proprietà values
:
print(s3.values)
[nan 3. 12. nan]
Bisogna fare attenzione al fatto che values
non crea una copia indipendente della serie, per cui, se modifichiamo l’array di numpy accessibile mediante values
, di fatto modifichiamo anche la serie:
a = s3.values
print(s3)
a[0]=-1
print(s3)
0 NaN
1 3.0
2 12.0
3 NaN
dtype: float64
0 -1.0
1 3.0
2 12.0
3 NaN
dtype: float64
🙋♂️ Domanda 5
Come si può ottenere un array di Numpy da un Series in modo che le due entità siano indipendenti?
9.2. DataFrame#
Un DataFrame è sostanzialmente una tabella di numeri in cui:
Ogni riga rappresenta una osservazione diversa;
Ogni colonna rappresenta una variabile.
Righe e colonne possono avere dei nomi. In particolare, è molto comune assegnare dei nomi alle colonne per indicare a quale variabile corrisponde ognuna di esse.
9.2.1. Costruzione e visualizzazione di DataFrame#
E’ possibile costruire un DataFrame a partire da un array bidimensionale (una matrice) di numpy:
dati = np.random.rand(10,3) #matrice di valori random 10 x 3
#si tratta di una matrice di 10 osservazioni, ognuna caratterizzata da 3 variabili
df = pd.DataFrame(dati,columns=['A','B','C'])
df #in jupyter o in una shell ipython possiamo stampare il dataframe
#semplicemente scrivendo "df". In uno script dovremmo scrivere "print df"
A | B | C | |
---|---|---|---|
0 | 0.309884 | 0.507204 | 0.280793 |
1 | 0.763837 | 0.108542 | 0.511655 |
2 | 0.909769 | 0.218376 | 0.363104 |
3 | 0.854973 | 0.711392 | 0.392944 |
4 | 0.231301 | 0.380175 | 0.549162 |
5 | 0.556719 | 0.004135 | 0.638023 |
6 | 0.057648 | 0.043027 | 0.875051 |
7 | 0.292588 | 0.762768 | 0.367865 |
8 | 0.873502 | 0.029424 | 0.552044 |
9 | 0.240248 | 0.884805 | 0.460238 |
Ad ogni riga è stato assegnato in automatico un indice numerico. Se vogliamo possiamo specificare nomi anche per le righe:
np.random.seed(123)#imposiamo un seed per ripetitibilità
df = pd.DataFrame(np.random.rand(4,3),columns=['A','B','C'],index=['X','Y','Z','W'])
df
A | B | C | |
---|---|---|---|
X | 0.696469 | 0.286139 | 0.226851 |
Y | 0.551315 | 0.719469 | 0.423106 |
Z | 0.980764 | 0.684830 | 0.480932 |
W | 0.392118 | 0.343178 | 0.729050 |
Analogamente a quanto visto nel caso delle serie, è possibile costruire un DataFrame mediante un dizionario che specifica nome e valori di ogni colonna:
pd.DataFrame({'A':np.random.rand(10), 'B':np.random.rand(10), 'C':np.random.rand(10)})
A | B | C | |
---|---|---|---|
0 | 0.438572 | 0.724455 | 0.430863 |
1 | 0.059678 | 0.611024 | 0.493685 |
2 | 0.398044 | 0.722443 | 0.425830 |
3 | 0.737995 | 0.322959 | 0.312261 |
4 | 0.182492 | 0.361789 | 0.426351 |
5 | 0.175452 | 0.228263 | 0.893389 |
6 | 0.531551 | 0.293714 | 0.944160 |
7 | 0.531828 | 0.630976 | 0.501837 |
8 | 0.634401 | 0.092105 | 0.623953 |
9 | 0.849432 | 0.433701 | 0.115618 |
Nel caso di dataset molto grandi, possiamo visualizzare solo le prime righe usando il metodo head:
df_big = pd.DataFrame(np.random.rand(100,3),columns=['A','B','C'])
df_big.head()
A | B | C | |
---|---|---|---|
0 | 0.317285 | 0.414826 | 0.866309 |
1 | 0.250455 | 0.483034 | 0.985560 |
2 | 0.519485 | 0.612895 | 0.120629 |
3 | 0.826341 | 0.603060 | 0.545068 |
4 | 0.342764 | 0.304121 | 0.417022 |
E’ anche possibile specificare il numero di righe da mostrare:
df_big.head(2)
A | B | C | |
---|---|---|---|
0 | 0.317285 | 0.414826 | 0.866309 |
1 | 0.250455 | 0.483034 | 0.985560 |
In maniera simile, possiamo mostrare le ultime righe con tail:
df_big.tail(3)
A | B | C | |
---|---|---|---|
97 | 0.811953 | 0.335544 | 0.349566 |
98 | 0.389874 | 0.754797 | 0.369291 |
99 | 0.242220 | 0.937668 | 0.908011 |
Possiamo ottenere il numero di righe del DataFrame mediante la funzione len:
print(len(df_big))
100
Se vogliamo conoscere sia il numero di righe che il numero di colonne, possiamo richiamare la proprietà shape:
print(df_big.shape)
(100, 3)
Analogamente a quanto visto per le Series, possiamo visualizzare un DataFrame come un array di numpy richiamando la proprietà values
:
print(df,'\n')
print(df.values)
A B C
X 0.696469 0.286139 0.226851
Y 0.551315 0.719469 0.423106
Z 0.980764 0.684830 0.480932
W 0.392118 0.343178 0.729050
[[0.69646919 0.28613933 0.22685145]
[0.55131477 0.71946897 0.42310646]
[0.9807642 0.68482974 0.4809319 ]
[0.39211752 0.34317802 0.72904971]]
Anche per i DataFrame valgono le stesse considerazioni sulla memoria fatte per le Series. Per ottenere una copia indipendente di una DataFrame, è possibile utilizzare il metodo copy
:
df2=df.copy()
🙋♂️ Domanda 6
In cosa differisce principalmente un DataFrame da un array bidimensionale di Numpy? Qual è il vantaggio dei DataFrame?
9.2.2. Indicizzazione#
Pandas mette a disposizione una serie di strumenti per indicizzare i DataFrame
selezionandone righe o colonne. Ad esempio, possiamo selezionare la colonna B come segue:
s=df['B']
print(s,'\n')
print("Tipo di s:",type(s))
X 0.286139
Y 0.719469
Z 0.684830
W 0.343178
Name: B, dtype: float64
Tipo di s: <class 'pandas.core.series.Series'>
Da notare che il risultato di questa operazione è una Series
(un DataFrame
è in fondo una collezione di Series
, ognuna rappresentante una colonna) che ha come nome il nome della colonna considerata. Possiamo selezionare più di una riga specificando una lista di righe:
dfAB = df[['A','C']]
dfAB
A | C | |
---|---|---|
X | 0.696469 | 0.226851 |
Y | 0.551315 | 0.423106 |
Z | 0.980764 | 0.480932 |
W | 0.392118 | 0.729050 |
Il risultato di questa operazione è invece un DataFrame
. La selezione delle righe avviene mediante la proprietà loc
:
df5 = df.loc['X']
df5
A 0.696469
B 0.286139
C 0.226851
Name: X, dtype: float64
Anche il risultato di questa operazione è una Series
, ma in questo caso gli indici rappresentano i nomi delle colonne, mentre il nome della serie corrisponde all’indice della riga selezionata. Come nel caso delle Series
, possiamo usare iloc
per indicizzare le righe in maniera posizionale:
df.iloc[1] #equivalente a df.loc['Y']
A 0.551315
B 0.719469
C 0.423106
Name: Y, dtype: float64
E’ anche possibile concatenare le operazioni di indicizzazione per selezionare uno specifico valore:
print(df.iloc[1]['A'])
print(df['A'].iloc[1])
0.5513147690828912
0.5513147690828912
Anche in questo caso possiamo utilizzare l’indicizzazione logica in in maniera simile a quanto visto per numpy:
df_big2=df_big[df_big['C']>0.5]
print(len(df_big), len(df_big2))
df_big2.head() #alcuni indici sono mancanti in quanto le righe corrispondenti sono state rimosse
100 53
A | B | C | |
---|---|---|---|
0 | 0.317285 | 0.414826 | 0.866309 |
1 | 0.250455 | 0.483034 | 0.985560 |
3 | 0.826341 | 0.603060 | 0.545068 |
5 | 0.681301 | 0.875457 | 0.510422 |
6 | 0.669314 | 0.585937 | 0.624904 |
E’ possibile combinare quanto visto finora per manipolare i dati in maniera semplice e veloce. Supponiamo di voler selezionare le righe per le quali la somma tra i valori di B e C è minore di \(0.7\) e supponiamo di essere interessati solo ai valori A di tali righe (e non all’intera riga di valori A,B,C). Il risultato che ci aspettiamo è un array monodimensionale di valori. Possiamo ottenere il risultato voluto come segue:
res = df[(df['B']+df['C'])>0.7]['A']
print(res.head(), res.shape)
Y 0.551315
Z 0.980764
W 0.392118
Name: A, dtype: float64 (3,)
Possiamo applicare l’indicizzazione anche a livello dell’intera tabella (oltre che al livello delle singole colonne):
df>0.3
A | B | C | |
---|---|---|---|
X | True | False | False |
Y | True | True | True |
Z | True | True | True |
W | True | True | True |
Se applichiamo questa indicizzazione al DataFrame
otterremo l’apparizione di alcuni NaN
, che indicano la presenza degli elementi che non rispettano la condizione considerata:
df[df>0.3]
A | B | C | |
---|---|---|---|
X | 0.696469 | NaN | NaN |
Y | 0.551315 | 0.719469 | 0.423106 |
Z | 0.980764 | 0.684830 | 0.480932 |
W | 0.392118 | 0.343178 | 0.729050 |
Possiamo rimuovere i valori NaN
mediante il metodo dropna
come visto nel caso delle Series
. Tuttavia, in questo caso, verranno rimosse tutte le righe che presentano almeno un NaN
:
df[df>0.3].dropna()
A | B | C | |
---|---|---|---|
Y | 0.551315 | 0.719469 | 0.423106 |
Z | 0.980764 | 0.684830 | 0.480932 |
W | 0.392118 | 0.343178 | 0.729050 |
Possiamo chiedere a dropna
di rimuovere le colonne che presentano almeno un NaN specificando axis=1
:
df[df>0.3].dropna(axis=1)
A | |
---|---|
X | 0.696469 |
Y | 0.551315 |
Z | 0.980764 |
W | 0.392118 |
Alternativamente possiamo sostituire i valori NaN
al volo mediante la funzione fillna
:
df[df>0.3].fillna('VAL') #sostiuisce i NaN con 'VAL'
A | B | C | |
---|---|---|---|
X | 0.696469 | VAL | VAL |
Y | 0.551315 | 0.719469 | 0.423106 |
Z | 0.980764 | 0.68483 | 0.480932 |
W | 0.392118 | 0.343178 | 0.72905 |
Anche in questo caso, come visto nel caso delle Series
possiamo ripristinare l’ordine degli indici mediante il metodo reset_index
:
print(df)
print(df.reset_index(drop=True))
A B C
X 0.696469 0.286139 0.226851
Y 0.551315 0.719469 0.423106
Z 0.980764 0.684830 0.480932
W 0.392118 0.343178 0.729050
A B C
0 0.696469 0.286139 0.226851
1 0.551315 0.719469 0.423106
2 0.980764 0.684830 0.480932
3 0.392118 0.343178 0.729050
Se non specifichiamo drop=True
, i vecchi indici verranno mantenuti come nuova colonna:
print(df.reset_index())
index A B C
0 X 0.696469 0.286139 0.226851
1 Y 0.551315 0.719469 0.423106
2 Z 0.980764 0.684830 0.480932
3 W 0.392118 0.343178 0.729050
Possiamo impostare qualsiasi colonna come nuovo indice. Ad esempio:
df.set_index('A')
B | C | |
---|---|---|
A | ||
0.696469 | 0.286139 | 0.226851 |
0.551315 | 0.719469 | 0.423106 |
0.980764 | 0.684830 | 0.480932 |
0.392118 | 0.343178 | 0.729050 |
Va notato che questa operazione non modifica di fatto il DataFrame, ma crea una nuova “vista” dei dati con la modifica richiesta:
df
A | B | C | |
---|---|---|---|
X | 0.696469 | 0.286139 | 0.226851 |
Y | 0.551315 | 0.719469 | 0.423106 |
Z | 0.980764 | 0.684830 | 0.480932 |
W | 0.392118 | 0.343178 | 0.729050 |
Per utilizzare questa versione modificata del dataframe, possiamo salvarla in un’altra variabile:
df2=df.set_index('A')
df2
B | C | |
---|---|---|
A | ||
0.696469 | 0.286139 | 0.226851 |
0.551315 | 0.719469 | 0.423106 |
0.980764 | 0.684830 | 0.480932 |
0.392118 | 0.343178 | 0.729050 |
🙋♂️ Domanda 7
Si consideri il seguente DataFrame:
pd.DataFrame({'A':[1,2,3,4],'B':[3,2,6,7],'C':[0,2,-1,12]})Si utilizzi l’indicizzazione logica per selezionare le righe in cui la somma dei valori di ‘C’ ed ‘A’ sia maggiore di \(1\).
9.2.3. Manipolazione di DataFrame#
I valori contenuti nelle righe e nelle colonne del dataframe possono essere facilmente modificati. Il seguente esempio moltiplica tutti i valori della colonna B per 2:
df['B']*=2
df.head()
A | B | C | |
---|---|---|---|
X | 0.696469 | 0.572279 | 0.226851 |
Y | 0.551315 | 1.438938 | 0.423106 |
Z | 0.980764 | 1.369659 | 0.480932 |
W | 0.392118 | 0.686356 | 0.729050 |
Analogamente possiamo dividere tutti i valori della riga di indice 2 per 3:
df.iloc[2]=df.iloc[2]/3
df.head()
A | B | C | |
---|---|---|---|
X | 0.696469 | 0.572279 | 0.226851 |
Y | 0.551315 | 1.438938 | 0.423106 |
Z | 0.326921 | 0.456553 | 0.160311 |
W | 0.392118 | 0.686356 | 0.729050 |
Possiamo definire una nuova colonna con una semplice operazione di assegnamento:
df['D'] = df['A'] + df['C']
df['E'] = np.ones(len(df))*5
df.head()
A | B | C | D | E | |
---|---|---|---|---|---|
X | 0.696469 | 0.572279 | 0.226851 | 0.923321 | 5.0 |
Y | 0.551315 | 1.438938 | 0.423106 | 0.974421 | 5.0 |
Z | 0.326921 | 0.456553 | 0.160311 | 0.487232 | 5.0 |
W | 0.392118 | 0.686356 | 0.729050 | 1.121167 | 5.0 |
Possiamo rimuovere una colonna mediante il metodo drop
e specificando axis=1
per indicare che vogliamo rimuovere una colonna:
df.drop('E',axis=1).head()
A | B | C | D | |
---|---|---|---|---|
X | 0.696469 | 0.572279 | 0.226851 | 0.923321 |
Y | 0.551315 | 1.438938 | 0.423106 | 0.974421 |
Z | 0.326921 | 0.456553 | 0.160311 | 0.487232 |
W | 0.392118 | 0.686356 | 0.729050 | 1.121167 |
Il metodo drop
non modifica il DataFrame ma genera solo una nuova “vista” senza la colonna da rimuovere:
df.head()
A | B | C | D | E | |
---|---|---|---|---|---|
X | 0.696469 | 0.572279 | 0.226851 | 0.923321 | 5.0 |
Y | 0.551315 | 1.438938 | 0.423106 | 0.974421 | 5.0 |
Z | 0.326921 | 0.456553 | 0.160311 | 0.487232 | 5.0 |
W | 0.392118 | 0.686356 | 0.729050 | 1.121167 | 5.0 |
Possiamo rimuovere la colonna effettivamente mediante un assegnamento:
df=df.drop('E',axis=1)
df.head()
A | B | C | D | |
---|---|---|---|---|
X | 0.696469 | 0.572279 | 0.226851 | 0.923321 |
Y | 0.551315 | 1.438938 | 0.423106 | 0.974421 |
Z | 0.326921 | 0.456553 | 0.160311 | 0.487232 |
W | 0.392118 | 0.686356 | 0.729050 | 1.121167 |
La rimozione delle righe avviene allo stesso modo, ma bisogna specificare axis =0
:
df.drop('X', axis=0)
A | B | C | D | |
---|---|---|---|---|
Y | 0.551315 | 1.438938 | 0.423106 | 0.974421 |
Z | 0.326921 | 0.456553 | 0.160311 | 0.487232 |
W | 0.392118 | 0.686356 | 0.729050 | 1.121167 |
E’ inoltre possibile aggiungere una nuova riga in coda al DataFrame mediante il metodo append
. Dato che le righe di un DataFrame sono delle Series, dovremo costruire una serie con i giusti indici (corrispondenti alle colonne del DataFrame) e il giusto nome (corrispondente al nuovo indice):
new_row=pd.Series([1,2,3,4], index=['A','B','C','D'], name='H')
print(new_row)
df.append(new_row)
A 1
B 2
C 3
D 4
Name: H, dtype: int64
A | B | C | D | |
---|---|---|---|---|
X | 0.696469 | 0.572279 | 0.226851 | 0.923321 |
Y | 0.551315 | 1.438938 | 0.423106 | 0.974421 |
Z | 0.326921 | 0.456553 | 0.160311 | 0.487232 |
W | 0.392118 | 0.686356 | 0.729050 | 1.121167 |
H | 1.000000 | 2.000000 | 3.000000 | 4.000000 |
Possiamo aggiungere più di una riga alla volta specificando un DataFrame:
new_rows = pd.DataFrame({'A':[0,1],'B':[2,3],'C':[4,5],'D':[6,7]}, index=['H','K'])
new_rows
A | B | C | D | |
---|---|---|---|---|
H | 0 | 2 | 4 | 6 |
K | 1 | 3 | 5 | 7 |
df.append(new_rows)
A | B | C | D | |
---|---|---|---|---|
X | 0.696469 | 0.572279 | 0.226851 | 0.923321 |
Y | 0.551315 | 1.438938 | 0.423106 | 0.974421 |
Z | 0.326921 | 0.456553 | 0.160311 | 0.487232 |
W | 0.392118 | 0.686356 | 0.729050 | 1.121167 |
H | 0.000000 | 2.000000 | 4.000000 | 6.000000 |
K | 1.000000 | 3.000000 | 5.000000 | 7.000000 |
🙋♂️ Domanda 8
Si consideri il seguente DataFrame:
pd.DataFrame({'A':[1,2,3,4],'B':[3,2,6,7],'C':[0,2,-1,12]})Si inserisca nel DataFrame una nuova colonna ‘D’ che contenga il valore \(1\) in tutte le righe in cui il valore di B è superiore al valore di C.
Suggerimento: si usi “astype(int)” per trasformare i booleani in interi.
9.2.4. Operazioni tra e su DataFrame#
Restano definite sui DataFrame, con le opportune differenze, le operazioni viste nel caso delle serie. In genere, queste vengono applicate a tutte le colonne del DataFrame in maniera indipendente:
df.mean() #media di ogni colonna
A 0.491706
B 0.788531
C 0.384830
D 0.876535
dtype: float64
df.max() #massimo di ogni colonna
A 0.696469
B 1.438938
C 0.729050
D 1.121167
dtype: float64
df.describe() #statistiche per ogni colonna
A | B | C | D | |
---|---|---|---|---|
count | 4.000000 | 4.000000 | 4.000000 | 4.000000 |
mean | 0.491706 | 0.788531 | 0.384830 | 0.876535 |
std | 0.165884 | 0.443638 | 0.255159 | 0.272747 |
min | 0.326921 | 0.456553 | 0.160311 | 0.487232 |
25% | 0.375818 | 0.543347 | 0.210216 | 0.814298 |
50% | 0.471716 | 0.629317 | 0.324979 | 0.948871 |
75% | 0.587603 | 0.874502 | 0.499592 | 1.011108 |
max | 0.696469 | 1.438938 | 0.729050 | 1.121167 |
Dato che le colonne di un DataFrame sono delle serie, ad esse può essere applicato il metodo apply
:
df['A']=df['A'].apply(lambda x: "Numero: "+str(x))
df
A | B | C | D | |
---|---|---|---|---|
X | Numero: 0.6964691855978616 | 0.572279 | 0.226851 | 0.923321 |
Y | Numero: 0.5513147690828912 | 1.438938 | 0.423106 | 0.974421 |
Z | Numero: 0.3269213994615385 | 0.456553 | 0.160311 | 0.487232 |
W | Numero: 0.3921175181941505 | 0.686356 | 0.729050 | 1.121167 |
E’ possibile ordinare le righe di un DataFrame rispetto ai valori di una delle colonne mediante il metodo sort_values
:
df.sort_values(by='D')
A | B | C | D | |
---|---|---|---|---|
Z | Numero: 0.3269213994615385 | 0.456553 | 0.160311 | 0.487232 |
X | Numero: 0.6964691855978616 | 0.572279 | 0.226851 | 0.923321 |
Y | Numero: 0.5513147690828912 | 1.438938 | 0.423106 | 0.974421 |
W | Numero: 0.3921175181941505 | 0.686356 | 0.729050 | 1.121167 |
Per rendere l’ordinamento permanente, dobbiamo effettuare un assegnamento:
df=df.sort_values(by='D')
df
A | B | C | D | |
---|---|---|---|---|
Z | Numero: 0.3269213994615385 | 0.456553 | 0.160311 | 0.487232 |
X | Numero: 0.6964691855978616 | 0.572279 | 0.226851 | 0.923321 |
Y | Numero: 0.5513147690828912 | 1.438938 | 0.423106 | 0.974421 |
W | Numero: 0.3921175181941505 | 0.686356 | 0.729050 | 1.121167 |
🙋♂️ Domanda 9
Si consideri il seguente DataFrame:
pd.DataFrame({'A':[1,2,3,4],'B':[3,2,6,7],'C':[0,2,-1,12]})Si trasformi la colonna “B” in modo che i nuovi valori siano:
uguali a zero se precedentemente pari;
uguali a -1 se precedentemente dispari.
9.2.5. Rimuovere righe e colonne#
È possibile rimuovere una riga o una colonna da un DataFrame:
data = pd.DataFrame({'A':[1,2,3,4],'B':[3,2,6,7],'C':[0,2,-1,12]})
print(data)
data.drop(1) #rimuoviamo la riga 1
data.drop('B', axis=1) #rimuoviamo la colonna B
A B C
0 1 3 0
1 2 2 2
2 3 6 -1
3 4 7 12
A | C | |
---|---|---|
0 | 1 | 0 |
1 | 2 | 2 |
2 | 3 | -1 |
3 | 4 | 12 |
Si noti che le operazioni viste sopra non modificano l’array di origine, ma ne restituiscono una copia modificata:
data = pd.DataFrame({'A':[1,2,3,4],'B':[3,2,6,7],'C':[0,2,-1,12]})
print(data)
data.drop(1) #rimuoviamo la riga 1
data
A B C
0 1 3 0
1 2 2 2
2 3 6 -1
3 4 7 12
A | B | C | |
---|---|---|---|
0 | 1 | 3 | 0 |
1 | 2 | 2 | 2 |
2 | 3 | 6 | -1 |
3 | 4 | 7 | 12 |
Possiamo effettuare una modifica persistente mediante inplace=True
:
data = pd.DataFrame({'A':[1,2,3,4],'B':[3,2,6,7],'C':[0,2,-1,12]})
print(data)
data.drop(1, inplace=True) #rimuoviamo la riga 1
data
A B C
0 1 3 0
1 2 2 2
2 3 6 -1
3 4 7 12
A | B | C | |
---|---|---|---|
0 | 1 | 3 | 0 |
2 | 3 | 6 | -1 |
3 | 4 | 7 | 12 |
O in maniera più esplicita come segue:
data = pd.DataFrame({'A':[1,2,3,4],'B':[3,2,6,7],'C':[0,2,-1,12]})
print(data)
data = data.drop(1) #rimuoviamo la riga 1
data
A B C
0 1 3 0
1 2 2 2
2 3 6 -1
3 4 7 12
A | B | C | |
---|---|---|---|
0 | 1 | 3 | 0 |
2 | 3 | 6 | -1 |
3 | 4 | 7 | 12 |
9.2.6. Groupby#
Il metodo groupby
permette di raggruppare le righe di un DataFrame
e richiamare delle funzioni aggreggate su di esse. Consideriamo un DataFrame
un po’ più rappresentativo:
df=pd.DataFrame({'income':[10000,11000,9000,3000,1000,5000,7000,2000,7000,12000,8000],\
'age':[32,32,45,35,28,18,27,45,39,33,32],\
'sex':['M','F','M','M','M','F','F','M','M','F','F'],\
'company':['CDX','FLZ','PTX','CDX','PTX','CDX','FLZ','CDX','FLZ','PTX','FLZ']})
df
income | age | sex | company | |
---|---|---|---|---|
0 | 10000 | 32 | M | CDX |
1 | 11000 | 32 | F | FLZ |
2 | 9000 | 45 | M | PTX |
3 | 3000 | 35 | M | CDX |
4 | 1000 | 28 | M | PTX |
5 | 5000 | 18 | F | CDX |
6 | 7000 | 27 | F | FLZ |
7 | 2000 | 45 | M | CDX |
8 | 7000 | 39 | M | FLZ |
9 | 12000 | 33 | F | PTX |
10 | 8000 | 32 | F | FLZ |
Il metodo groupby
ci permette di raggruppare le righe del DataFrame
per valore, rispetto a una colonna specificata. Supponiamo di voler raggruppare tutte le righe che hanno lo stesso valore di sex:
df.groupby('sex')
<pandas.core.groupby.groupby.DataFrameGroupBy object at 0x0000029E02F239E8>
Questa operazione restituisce un oggetto di tipo DataFrameGroupby
sul quale sarà possibile effettuare delle operazioni aggregate (ad esempio, somme e medie). Supponiamo adesso di voler calcolare la media di tutti i valori che ricadono nello stesso gruppo (ovvero calcoliamo la media delle righe che hanno lo stesso valore di sex):
df.groupby('sex').mean()
income | age | |
---|---|---|
sex | ||
F | 8600.000000 | 28.400000 |
M | 5333.333333 | 37.333333 |
Se siamo interessati a una sola delle variabili, possiamo selezionarla prima o dopo l’operazione sui dati aggregati:
df.groupby('sex')['age'].mean() #equivalentemente: df.groupby('sex').mean()['age']
sex
F 28.400000
M 37.333333
Name: age, dtype: float64
La tabella mostra il reddito medio e l’età media dei soggetti di sesso maschile e femminile. Dato che l’operazione di media si può applicare solo a valori numerici, la colonna Company è stata esclusa. Possiamo ottenere una tabella simile in cui mostriamo la somma dei redditi e la somma delle età cambiando mean
in sum
:
df.groupby('sex').sum()
income | age | |
---|---|---|
sex | ||
F | 43000 | 142 |
M | 32000 | 224 |
In generale, è possibile utilizzare diverse funzioni aggregate oltre a mean
e sum
. Alcuni esempi sono min
, max
, std
. Due funzioni particolarmente interessanti da usare in questo contesto sono count
e describe
. In particolare, count
conta il numero di elementi interessati, mentre describe calcola diverse statistiche dei valori interessati. Vediamo due esempi:
df.groupby('sex').count()
income | age | company | |
---|---|---|---|
sex | |||
F | 5 | 5 | 5 |
M | 6 | 6 | 6 |
Il numero di elementi è uguale per le varie colonne in quanto non ci sono valori NaN
nel DataFrame.
df.groupby('sex').describe()
age | income | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
count | mean | std | min | 25% | 50% | 75% | max | count | mean | std | min | 25% | 50% | 75% | max | |
sex | ||||||||||||||||
F | 5.0 | 28.400000 | 6.268971 | 18.0 | 27.00 | 32.0 | 32.0 | 33.0 | 5.0 | 8600.000000 | 2880.972058 | 5000.0 | 7000.0 | 8000.0 | 11000.0 | 12000.0 |
M | 6.0 | 37.333333 | 6.947422 | 28.0 | 32.75 | 37.0 | 43.5 | 45.0 | 6.0 | 5333.333333 | 3829.708431 | 1000.0 | 2250.0 | 5000.0 | 8500.0 | 10000.0 |
Per ogni variabile numerica (age e income) sono state calcolate diverse statistiche. A volte può essere più chiaro visualizzare il dataframe trasposto:
df.groupby('sex').describe().transpose()
sex | F | M | |
---|---|---|---|
age | count | 5.000000 | 6.000000 |
mean | 28.400000 | 37.333333 | |
std | 6.268971 | 6.947422 | |
min | 18.000000 | 28.000000 | |
25% | 27.000000 | 32.750000 | |
50% | 32.000000 | 37.000000 | |
75% | 32.000000 | 43.500000 | |
max | 33.000000 | 45.000000 | |
income | count | 5.000000 | 6.000000 |
mean | 8600.000000 | 5333.333333 | |
std | 2880.972058 | 3829.708431 | |
min | 5000.000000 | 1000.000000 | |
25% | 7000.000000 | 2250.000000 | |
50% | 8000.000000 | 5000.000000 | |
75% | 11000.000000 | 8500.000000 | |
max | 12000.000000 | 10000.000000 |
Questa vista ci permette di comparare diverse statistiche delle due variabili age
e income
per M
e F
.
🙋♂️ Domanda 10
Considerando il DataFrame precedentemente creato, si usi
groupby
per ottenere la somma dei redditi dei dipendenti di una data compagnia.
9.2.7. Crosstab#
Le Crosstab
permettono di descrivere le relazioni tra due o più variabili categoriche. Una volta specificata una
coppia di variabili categoriche, le righe e colonne della crosstab (nota anche come “tabella di contingenza”) enumerano indipendentemente tutti i valori univoci delle due variabili categoriche, così che ogni cella della crosstab identifica una determinata coppia di volari. All’interno delle celle, vengono dunque riportati i numeri di elementi per i quali le due variabili categoriche assumono una determinata coppia di valori.
Supponiamo di voler studiare le relazioni tra company
e sex
:
pd.crosstab(df['sex'],df['company'])
company | CDX | FLZ | PTX |
---|---|---|---|
sex | |||
F | 1 | 3 | 1 |
M | 3 | 1 | 2 |
La tabella sopra ci dice ad esempio che nella compagnia CDX, \(1\) soggetto è di sesso femminile, mentre \(3\) soggetti sono di sesso maschile. Allo stesso modo, un soggetto della compagnia FLZ è di sesso maschile, mentre tre soggetti sono di sesso femminile. E’ possibile ottenere delle frequenze invece che dei conteggi, mediante normalize=True
:
pd.crosstab(df['sex'],df['company'], normalize=True)
company | CDX | FLZ | PTX |
---|---|---|---|
sex | |||
F | 0.090909 | 0.272727 | 0.090909 |
M | 0.272727 | 0.090909 | 0.181818 |
Alternativamente, possiamo normalizzare la tabella solo per righe o solo per colonne specificando normalize='index'
o normalize='columns'
:
pd.crosstab(df['sex'],df['company'], normalize='index')
company | CDX | FLZ | PTX |
---|---|---|---|
sex | |||
F | 0.2 | 0.600000 | 0.200000 |
M | 0.5 | 0.166667 | 0.333333 |
Questa tabella riporta le percentuali di persone che lavorano nelle tre diverse compagnie per ciascun sesso, es., “il 20% delle donne lavora presso CDX”. Analogamente possiamo normalizzare per colonne come segue:
pd.crosstab(df['sex'],df['company'], normalize='columns')
company | CDX | FLZ | PTX |
---|---|---|---|
sex | |||
F | 0.25 | 0.75 | 0.333333 |
M | 0.75 | 0.25 | 0.666667 |
Questa tabella riporta le percentuali di uomini e donne che lavorano in ciascuna compagnia, es., “il 25% dei lavoratori di CDX sono donne”.
Se vogliamo studiare le relazioni tra più di due variabili categoriche, possiamo specificare una lista di colonne quando costruiamo la crosstab:
pd.crosstab(df['sex'],[df['age'],df['company']])
age | 18 | 27 | 28 | 32 | 33 | 35 | 39 | 45 | ||
---|---|---|---|---|---|---|---|---|---|---|
company | CDX | FLZ | PTX | CDX | FLZ | PTX | CDX | FLZ | CDX | PTX |
sex | ||||||||||
F | 1 | 1 | 0 | 0 | 2 | 1 | 0 | 0 | 0 | 0 |
M | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 1 |
Ofni cella della crosstab sopra conta il numero di osservazioni che riportano una determinata terna di valori. Ad esempio, \(1\) soggetto è di sesso maschile, lavora per PTX e ha \(28\) anni, mentre \(2\) soggetti sono di sesso femminile, lavorano per FLZ e hanno \(32\) anni.
Oltre a riportare conteggi e frequenze, una crosstab permette di calcolare statistiche di terze variabili considerate non categoriche. Supponiamo di voler conoscere l’età media delle persone di un dato sesso che lavorano in una data azienda. Possiamo costruire una crosstab specificando una nuova variabile (age) per values
. Dato che di questa variabile bisognerà calcolare un qualche valore aggregato, dobbiamo anche specificare aggfunc
per esempio pari a mean
(per calcolare la media dei valori interessati:
pd.crosstab(df['sex'],df['company'], values=df['age'], aggfunc='mean')
company | CDX | FLZ | PTX |
---|---|---|---|
sex | |||
F | 18.000000 | 30.333333 | 33.0 |
M | 37.333333 | 39.000000 | 36.5 |
La tabella ci dice che l’età media delle persone di sesso maschile che lavorano per CDX è di \(37,33\) anni.
🙋♂️ Domanda 11
Si costruisca una crosstab che, per ogni compagnia, riporti il numero di dipendenti di una data età.
9.2.8. Manipolazione “esplicita” di DataFrame#
In alcuni casi può essere utile trattare i DataFrame “esplicitamente” come matrici di valori. Consideriamo ad esempio il seguente DataFrame:
df123 = pd.DataFrame({'Category':[1,2,3], 'NumberOfElements':[3,1,2]})
df123
Category | NumberOfElements | |
---|---|---|
0 | 1 | 3 |
1 | 2 | 1 |
2 | 3 | 2 |
Supponiamo di voler costruire un nuovo DataFrame che, per ogni riga del dataframe df123
contenga esattamente “NumberOfElements” righe con valore di “NumberOfElements” pari a uno. Vogliamo in pratica “espandere” il DataFrame sopra come segue:
df123 = pd.DataFrame({'Category':[1,1,1,2,3,3], 'NumberOfElements':[1,1,1,1,1,1]})
df123
Category | NumberOfElements | |
---|---|---|
0 | 1 | 1 |
1 | 1 | 1 |
2 | 1 | 1 |
3 | 2 | 1 |
4 | 3 | 1 |
5 | 3 | 1 |
Per farlo in maniera automatica, possiamo trattare il DataFrame più “esplicitamente” come una matrice di valori iterandone le righe. Il nuovo DataFrame sarà dapprima costruito come una lista di Series (le righe del DataFrame) e poi trasformato in un DataFrame:
newdat = []
for i, row in df123.iterrows(): #iterrows permette di iterare le righe di un DataFrame
for j in range(row['NumberOfElements']):
newrow = row.copy()
newrow['NumberOfElements']=1
newdat.append(newrow)
pd.DataFrame(newdat)
Category | NumberOfElements | |
---|---|---|
0 | 1 | 1 |
1 | 1 | 1 |
2 | 1 | 1 |
3 | 2 | 1 |
4 | 3 | 1 |
5 | 3 | 1 |
pd.DataFrame({'Category':[1,2,3], 'NumberOfElements':[3,5,3], 'CheckedElements':[1,2,1]})
Category | NumberOfElements | CheckedElements | |
---|---|---|---|
0 | 1 | 3 | 1 |
1 | 2 | 5 | 2 |
2 | 3 | 3 | 1 |
9.2.9. Concatenazione di DataFrame#
I DataFrame possono essere concatenati per righe come mostrato di seguito:
d1 = pd.DataFrame({'A':[1,2,3,4],'B':[3,2,6,7],'C':[0,2,-1,12]})
d2 = pd.DataFrame({'A':[92,8],'B':[44,-2],'C':[0,2]})
print(d1)
print(d2)
A B C
0 1 3 0
1 2 2 2
2 3 6 -1
3 4 7 12
A B C
0 92 44 0
1 8 -2 2
pd.concat([d1,d2])
A | B | C | |
---|---|---|---|
0 | 1 | 3 | 0 |
1 | 2 | 2 | 2 |
2 | 3 | 6 | -1 |
3 | 4 | 7 | 12 |
0 | 92 | 44 | 0 |
1 | 8 | -2 | 2 |
Se le colonne non sono compatibili, appariranno dei NaN
:
d1 = pd.DataFrame({'A':[1,2,3,4],'B':[3,2,6,7],'C':[0,2,-1,12]})
d2 = pd.DataFrame({'A':[92,8],'D':[0,2]})
pd.concat([d1,d2])
A | B | C | D | |
---|---|---|---|---|
0 | 1 | 3.0 | 0.0 | NaN |
1 | 2 | 2.0 | 2.0 | NaN |
2 | 3 | 6.0 | -1.0 | NaN |
3 | 4 | 7.0 | 12.0 | NaN |
0 | 92 | NaN | NaN | 0.0 |
1 | 8 | NaN | NaN | 2.0 |
La concatenazione può avvenire anche per colonne:
d1 = pd.DataFrame({'A':[1,2],'B':[3,7],'C':[-1,12]})
d2 = pd.DataFrame({'A':[92,8],'D':[0,2]})
pd.concat([d1,d2], axis=1)
A | B | C | A | D | |
---|---|---|---|---|---|
0 | 1 | 3 | -1 | 92 | 0 |
1 | 2 | 7 | 12 | 8 | 2 |
9.2.10. Merge di DataFrame#
Due DataFrame possono essere uniti mediante una operazione di merge
simile a quanto avviene nei database. Consideriamo due dataframe di esempio:
a = pd.DataFrame({'key': ['k0', 'k1', 'k2'],
'a': ['v1', 'v2', 'v3'],
'b': ['v4', 'v5', 'v6'],
'c': ['v7', 'v8', 'v9']})
a
key | a | b | c | |
---|---|---|---|---|
0 | k0 | v1 | v4 | v7 |
1 | k1 | v2 | v5 | v8 |
2 | k2 | v3 | v6 | v9 |
b = pd.DataFrame({'key': ['k0', 'k1', 'k2'],
'd': ['v9', 'v11', 'v12'],
'e': ['v13', 'v4', 'v5']})
b
key | d | e | |
---|---|---|---|
0 | k0 | v9 | v13 |
1 | k1 | v11 | v4 |
2 | k2 | v12 | v5 |
I due dataframe hanno una colonna in comune che agisce da chiave e altre colonne che agiscono da valori. L’operazione di merge permette di unire i dataframe sulla base delle chiavi:
pd.merge(a,b,on='key')
key | a | b | c | d | e | |
---|---|---|---|---|---|---|
0 | k0 | v1 | v4 | v7 | v9 | v13 |
1 | k1 | v2 | v5 | v8 | v11 | v4 |
2 | k2 | v3 | v6 | v9 | v12 | v5 |
9.2.11. Join di DataFrame#
Le operazioni di join
possono essere applicate per unire dataframe indicizzati in maniera compatibile:
a = pd.DataFrame({'a': ['v1', 'v2', 'v3'],
'b': ['v4', 'v5', 'v6'],
'c': ['v7', 'v8', 'v9']},
index = ['k0', 'k1', 'k2'])
a
a | b | c | |
---|---|---|---|
k0 | v1 | v4 | v7 |
k1 | v2 | v5 | v8 |
k2 | v3 | v6 | v9 |
b = pd.DataFrame({'d': ['v9', 'v11', 'v12'],
'e': ['v13', 'v4', 'v5']},
index = ['k0', 'k3', 'k2'],)
b
d | e | |
---|---|---|
k0 | v9 | v13 |
k3 | v11 | v4 |
k2 | v12 | v5 |
a.join(b)
a | b | c | d | e | |
---|---|---|---|---|---|
k0 | v1 | v4 | v7 | v9 | v13 |
k1 | v2 | v5 | v8 | NaN | NaN |
k2 | v3 | v6 | v9 | v12 | v5 |
Come si può notare, l’operazione di join avviene solo utilizzando gli indici in comune.
9.2.12. Input/Output#
Pandas mette a disposizione diverse funzioni per leggere e scrivere dati. Vedremo in particolare le funzioni per leggere e scrivere file csv
. E’ possibile leggere file csv (https://it.wikipedia.org/wiki/Comma-separated_values) mediante la funzione pd.read_csv
. La lettura può avvenire da file locali passando il path relativo o da URL:
data=pd.read_csv('http://iplab.dmi.unict.it/furnari/downloads/students.csv')
data.head()
admit | gre | gpa | prestige | |
---|---|---|---|---|
0 | 0 | 380 | 3.61 | 3 |
1 | 1 | 660 | 3.67 | 3 |
2 | 1 | 800 | 4.00 | 1 |
3 | 1 | 640 | 3.19 | 4 |
4 | 0 | 520 | 2.93 | 4 |
Allo stesso modo, è possibile scrivere un DataFrame su file csv come segue:
data.to_csv('file.csv')
Dato che gli indici possono non essere sequenziali in un DataFrame, Pandas li inserisce nel csv come “colonna senza nome”. Ad esempio, le prime righe del file che abbiamo appena scritto (file.csv) sono le seguenti:
,admit,gre,gpa,prestige
0,0,380,3.61,3
1,1,660,3.67,3
2,1,800,4.0,1
3,1,640,3.19,4
4,0,520,2.93,4
Come si può vedere, la prima colonna contiene i valori degli indici, ma non ha nome. Ciò può causare problemi quando carichiamo nuovamente il DataFrame:
data=pd.read_csv('file.csv')
data.head()
Unnamed: 0 | admit | gre | gpa | prestige | |
---|---|---|---|---|---|
0 | 0 | 0 | 380 | 3.61 | 3 |
1 | 1 | 1 | 660 | 3.67 | 3 |
2 | 2 | 1 | 800 | 4.00 | 1 |
3 | 3 | 1 | 640 | 3.19 | 4 |
4 | 4 | 0 | 520 | 2.93 | 4 |
Possiamo risolvere il problema di diversi modi:
Eliminare la colonna “Unnamed: 0” dal DataFrame appena caricato (solo se gli indici sono sequenziali);
Specificare di usare la colonna “Unnamed: 0” come colonna degli indici durante il caricamento;
Salvare il DataFrame senza indici (solo se gli indici sono sequenziali).
Vediamo i tre casi:
data=pd.read_csv('file.csv')
data.drop('Unnamed: 0', axis=1).head()
admit | gre | gpa | prestige | |
---|---|---|---|---|
0 | 0 | 380 | 3.61 | 3 |
1 | 1 | 660 | 3.67 | 3 |
2 | 1 | 800 | 4.00 | 1 |
3 | 1 | 640 | 3.19 | 4 |
4 | 0 | 520 | 2.93 | 4 |
data=pd.read_csv('file.csv', index_col='Unnamed: 0')
data.head()
admit | gre | gpa | prestige | |
---|---|---|---|---|
0 | 0 | 380 | 3.61 | 3 |
1 | 1 | 660 | 3.67 | 3 |
2 | 1 | 800 | 4.00 | 1 |
3 | 1 | 640 | 3.19 | 4 |
4 | 0 | 520 | 2.93 | 4 |
data.to_csv('file.csv', index=False)
pd.read_csv('file.csv').head()
admit | gre | gpa | prestige | |
---|---|---|---|---|
0 | 0 | 380 | 3.61 | 3 |
1 | 1 | 660 | 3.67 | 3 |
2 | 1 | 800 | 4.00 | 1 |
3 | 1 | 640 | 3.19 | 4 |
4 | 0 | 520 | 2.93 | 4 |
9.3. Esempio di manipolazione dati#
Spesso, i dati devono essere opportunamente trattati per poterli analizzare. Vediamo un esempio di come combinare gli strumenti discussi sopra per manipolare un dataset reale.
Considereremo dataset disponibile a questo URL: https://www.kaggle.com/lava18/google-play-store-apps. Una copia del dataset è disponibile al seguente URL per scopi didattici: http://iplab.dmi.unict.it/furnari/downloads/googleplaystore.csv. Carichiamo il dataset e visualizziamo le prime righe per assicurarci che sia stato caricato correttamente:
data = pd.read_csv('http://iplab.dmi.unict.it/furnari/downloads/googleplaystore.csv')
data.head()
App | Category | Rating | Reviews | Size | Installs | Type | Price | Content Rating | Genres | Last Updated | Current Ver | Android Ver | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Photo Editor & Candy Camera & Grid & ScrapBook | ART_AND_DESIGN | 4.1 | 159 | 19M | 10,000+ | Free | 0 | Everyone | Art & Design | January 7, 2018 | 1.0.0 | 4.0.3 and up |
1 | Coloring book moana | ART_AND_DESIGN | 3.9 | 967 | 14M | 500,000+ | Free | 0 | Everyone | Art & Design;Pretend Play | January 15, 2018 | 2.0.0 | 4.0.3 and up |
2 | U Launcher Lite – FREE Live Cool Themes, Hide ... | ART_AND_DESIGN | 4.7 | 87510 | 8.7M | 5,000,000+ | Free | 0 | Everyone | Art & Design | August 1, 2018 | 1.2.4 | 4.0.3 and up |
3 | Sketch - Draw & Paint | ART_AND_DESIGN | 4.5 | 215644 | 25M | 50,000,000+ | Free | 0 | Teen | Art & Design | June 8, 2018 | Varies with device | 4.2 and up |
4 | Pixel Draw - Number Art Coloring Book | ART_AND_DESIGN | 4.3 | 967 | 2.8M | 100,000+ | Free | 0 | Everyone | Art & Design;Creativity | June 20, 2018 | 1.1 | 4.4 and up |
Visualizziamo le proprietà del dataset:
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10841 entries, 0 to 10840
Data columns (total 13 columns):
App 10841 non-null object
Category 10841 non-null object
Rating 9367 non-null float64
Reviews 10841 non-null object
Size 10841 non-null object
Installs 10841 non-null object
Type 10840 non-null object
Price 10841 non-null object
Content Rating 10840 non-null object
Genres 10841 non-null object
Last Updated 10841 non-null object
Current Ver 10833 non-null object
Android Ver 10838 non-null object
dtypes: float64(1), object(12)
memory usage: 1.1+ MB
Il dataset contiene \(10841\) osservazioni e \(13\) variabili. Osserviamo che molte delle variabili (tranne “Rating”) sono di tipo “object”, anche se rappresentano dei valori numerici (es., Reviews, Size, Installs e Price). Osservando le prime righe del DataFrame visualizzate sopra possiamo dedurre che:
Size non è rappresentato come valore numerico in quanto contiene l’unità di misura “M”;
Installs è in realtà una variabile categorica (riporta un “+” alla fine, quindi indica la classe “più di xxxx installazioni”);
Non ci sono motivazioni apparenti per cui Reviews e Rating non siano stati interpretati come numeri. Costruiamo una funzione filtro che identifichi se un valori è convertibile in un dato tipo o meno:
def cannot_convert(x, t=float):
try:
t(x)
return False
except:
return True
print(cannot_convert('12'))
print(cannot_convert('12f'))
False
True
Applichiamo il filtro alla colonna Review per visualizzare i valori che non possono essere convertiti:
list(filter(cannot_convert,data['Reviews']))
['3.0M']
Possiamo sostituire questo valore con la sua versione per esteso mediante il metodo replace
:
data['Reviews']=data['Reviews'].replace({'3.0M':3000000})
A questo punto possiamo convertire i tipi dei valori della colonna in interi:
data['Reviews']=data['Reviews'].astype(int)
Visualizziamo nuovamente le informazioni del DataFrame:
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10841 entries, 0 to 10840
Data columns (total 13 columns):
App 10841 non-null object
Category 10841 non-null object
Rating 9367 non-null float64
Reviews 10841 non-null int32
Size 10841 non-null object
Installs 10841 non-null object
Type 10840 non-null object
Price 10841 non-null object
Content Rating 10840 non-null object
Genres 10841 non-null object
Last Updated 10841 non-null object
Current Ver 10833 non-null object
Android Ver 10838 non-null object
dtypes: float64(1), int32(1), object(11)
memory usage: 1.0+ MB
Reviews è adesso un intero. Eseguiamo una analisi simile sulla variabile Price:
list(filter(cannot_convert, data['Price']))[:10] #visualizziamo solo i primi 10 elementi
['$4.99',
'$4.99',
'$4.99',
'$4.99',
'$3.99',
'$3.99',
'$6.99',
'$1.49',
'$2.99',
'$3.99']
Possiamo convertire le stringhe in float eliminando il dollaro iniziale mediante apply
:
def strip_dollar(x):
if x[0]=='$':
return x[1:]
else:
return x
data['Price']=data['Price'].apply(strip_dollar)
Vediamo se ci sono ancora valori che non possono essere convertiti:
list(filter(cannot_convert, data['Price']))
['Everyone']
Dato che non sappiamo come interpretare il valore “Everyone”, sostituiamolo con un NaN:
data['Price']=data['Price'].replace({'Everyone':np.nan})
Adesso possiamo procedere alla conversione:
data['Price']=data['Price'].astype(float)
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10841 entries, 0 to 10840
Data columns (total 13 columns):
App 10841 non-null object
Category 10841 non-null object
Rating 9367 non-null float64
Reviews 10841 non-null int32
Size 10841 non-null object
Installs 10841 non-null object
Type 10840 non-null object
Price 10840 non-null float64
Content Rating 10840 non-null object
Genres 10841 non-null object
Last Updated 10841 non-null object
Current Ver 10833 non-null object
Android Ver 10838 non-null object
dtypes: float64(2), int32(1), object(10)
memory usage: 1.0+ MB
Modifichiamo anche “Size” eliminando la “M” finale:
data['Size']=data['Size'].apply(lambda x : x[:-1])
Verifichiamo l’esistenza di valori non convertibili:
list(filter(cannot_convert,data['Size']))[:10]
['Varies with devic',
'Varies with devic',
'Varies with devic',
'Varies with devic',
'Varies with devic',
'Varies with devic',
'Varies with devic',
'Varies with devic',
'Varies with devic',
'Varies with devic']
Sostituiamo questi valori con NaN:
data['Size']=data['Size'].replace({'Varies with devic':np.nan})
Verifichiamo nuovamente la presenza di valori che non possono essere convertiti:
list(filter(cannot_convert,data['Size']))
['1,000']
Possiamo rimuovere la virgola usata per indicare le migliaia e convertire in float:
data['Size']=data['Size'].apply(lambda x: float(str(x).replace(',','')))
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10841 entries, 0 to 10840
Data columns (total 13 columns):
App 10841 non-null object
Category 10841 non-null object
Rating 9367 non-null float64
Reviews 10841 non-null int32
Size 9146 non-null float64
Installs 10841 non-null object
Type 10840 non-null object
Price 10840 non-null float64
Content Rating 10840 non-null object
Genres 10841 non-null object
Last Updated 10841 non-null object
Current Ver 10833 non-null object
Android Ver 10838 non-null object
dtypes: float64(3), int32(1), object(9)
memory usage: 1.0+ MB
Valutiamo se è il caso di trasformare anche “Installs” in un valore numerico. Vediamo quanti valori univoci di “Installs” ci sono nel dataset:
data['Installs'].nunique()
22
Abbiamo solo \(22\) valori, che paragonati alle 10841 osservazioni, indicano che “Installs” è una variabile molto quantizzata, per cui probabilmente ha senso considerarla come categorica, piuttosto che come un valore numerico.
Il DataFrame contiene dei NaN
. A seconda dei casi può avere senso tenerli o rimuoverli. Dato che i dati sono molti, possiamo rimuoverli con dropna
:
data=data.dropna()
Possiamo quindi iniziare a esplorare i dati con gli strumenti visti. Visualizziamo ad esempio i valori medi delle variabili numeriche per categoria:
data.groupby('Category').mean()
Rating | Reviews | Size | Price | |
---|---|---|---|---|
Category | ||||
ART_AND_DESIGN | 4.381034 | 1.874517e+04 | 12.939655 | 0.102931 |
AUTO_AND_VEHICLES | 4.147619 | 1.575057e+04 | 24.728571 | 0.000000 |
BEAUTY | 4.291892 | 5.020243e+03 | 15.513514 | 0.000000 |
BOOKS_AND_REFERENCE | 4.320139 | 2.815291e+04 | 23.543750 | 0.145069 |
BUSINESS | 4.119919 | 2.497765e+04 | 26.217480 | 0.249593 |
COMICS | 4.130612 | 1.254822e+04 | 34.626531 | 0.000000 |
COMMUNICATION | 4.102844 | 5.549962e+05 | 60.765403 | 0.197773 |
DATING | 3.957803 | 2.254489e+04 | 18.312717 | 0.086590 |
EDUCATION | 4.387273 | 6.435159e+04 | 30.588182 | 0.163273 |
ENTERTAINMENT | 4.146667 | 1.621530e+05 | 21.853333 | 0.033222 |
EVENTS | 4.478947 | 3.321605e+03 | 23.213158 | 0.000000 |
FAMILY | 4.190347 | 1.801297e+05 | 37.459963 | 1.390074 |
FINANCE | 4.112030 | 3.903023e+04 | 31.249624 | 9.172444 |
FOOD_AND_DRINK | 4.097619 | 5.103793e+04 | 24.163095 | 0.059405 |
GAME | 4.269507 | 1.386276e+06 | 46.827823 | 0.281704 |
HEALTH_AND_FITNESS | 4.223767 | 4.519072e+04 | 29.658296 | 0.154350 |
HOUSE_AND_HOME | 4.162500 | 2.935998e+04 | 17.505357 | 0.000000 |
LIBRARIES_AND_DEMO | 4.204918 | 1.632359e+04 | 225.267213 | 0.000000 |
LIFESTYLE | 4.093571 | 3.109027e+04 | 28.650714 | 6.976429 |
MAPS_AND_NAVIGATION | 4.013684 | 3.858909e+04 | 26.282105 | 0.157474 |
MEDICAL | 4.184259 | 4.392454e+03 | 49.643827 | 3.077901 |
NEWS_AND_MAGAZINES | 4.143787 | 5.826802e+04 | 14.948521 | 0.023550 |
PARENTING | 4.347727 | 1.999950e+04 | 21.579545 | 0.113409 |
PERSONALIZATION | 4.323381 | 1.257211e+05 | 50.115827 | 0.427338 |
PHOTOGRAPHY | 4.147034 | 3.260313e+05 | 18.970763 | 0.331992 |
PRODUCTIVITY | 4.143830 | 1.854692e+05 | 38.899149 | 0.225362 |
SHOPPING | 4.227933 | 2.624461e+05 | 33.397207 | 0.030615 |
SOCIAL | 4.257062 | 1.830882e+05 | 24.317514 | 0.011186 |
SPORTS | 4.204858 | 2.128411e+05 | 30.159109 | 0.324818 |
TOOLS | 4.010742 | 1.663130e+05 | 47.929068 | 0.290047 |
TRAVEL_AND_LOCAL | 4.043125 | 4.824554e+04 | 25.357500 | 0.165687 |
VIDEO_PLAYERS | 4.025862 | 2.074172e+05 | 22.881897 | 0.008534 |
WEATHER | 4.241176 | 7.639225e+04 | 24.805882 | 0.459608 |
🙋♂️ Domanda 12
Visualizzare il numero medio di review per tipo (variabile Type).
data=pd.read_csv('https://raw.githubusercontent.com/agconti/kaggle-titanic/master/data/train.csv')
data[data['Ticket']=='345779']
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
81 | 82 | 1 | 3 | Sheerlinck, Mr. Jan Baptist | male | 29.0 | 0 | 0 | 345779 | 9.5 | NaN | S |
9.4. Esercizi#
🧑💻 Esercizio 1
Si carichi il dataset disponibile al seguente URL: https://raw.githubusercontent.com/agconti/kaggle-titanic/master/data/train.csv in modo da utilizzare i valori della colonna “PassengerId” come indici e si applichino le seguenti trasfromazioni:
Si rimuovano le righe che contengono dei valori NaN;
Si modifichi il DataFrame sostituendo nella colonna “Sex” ogni occorrenza di “male” con “M” e ogni occorrenza di “female” con “F”;
Si inserisca una nuova colonna “Old” con valore pari a “1” se Age è superiore all’età media dei passeggeri e “0” altrimenti;
Si convertano i valori della colonna “Age” in interi.
🧑💻 Esercizio 2
Si consideri il dataset caricato e modificato nell’esercizio precedente. Si trovi il nome del passeggero con ticket dal codice “345779”.
🧑💻 Esercizio 3
Quali sono i nomei dei 5 passeggeri più giovani?
🧑💻 Esercizio 4
Si consideri il dataset caricato e modificato nell’esercizio precedente. Si selezionino tutte le osservazioni relative a passeggeri di età compresa tra 20 e 30 anni.
🧑💻 Esercizio 5
Si consideri il dataset caricato e modificato negli esercizi precedenti. Si calcoli la tariffa media pagata dai passeggeri di sesso femminile in seconda classe.
🧑💻 Esercizio 6
Si consideri il dataset caricato e modificato negli esercizi precedenti. Si costruisca una crosstab che permetta di rispondere alle seguenti domande:
Quanti passeggeri di sesso maschile sono sopravvisuti?
Quanti passeggeri di sesso femminile non sono sopravvisuti?
🧑💻 Esercizio 7
Si consideri il dataset caricato e modificato negli esercizi precedenti. Si costruisca una crosstab che permetta di rispondere alle seguenti domande:
Quanti passeggeri di sesso maschile sono sopravvisuti in prima classe?
Quanti passeggeri di sesso femminile sono sopravvisuti in terza classe?
Quanti passeggeri di sesso maschile sono sopravvisuti in terza classe?
🧑💻 Esercizio 8
Si consideri il seguente DataFrame:
pd.DataFrame({'Category':[1,2,3], 'NumberOfElements':[3,2,3], 'CheckedElements':[1,2,1]})Si costruisca un nuovo DataFrame che contenga le stesse colonne del DataFrame considerato e che per ogni riga di esso contenga
NumberOfElements
nuove righe con categoria uguale aCategory
di cuiCheckedElements
con valore diCheckedElements
pari a uno e le restanti con valore diCheckedElements
pari a zero.Il risultato della manipolazione dovrà essere uguale al seguente dataset:
pd.DataFrame({'Category':[1,1,1,2,2,3,3,3], 'NumberOfElements':[1,1,1,1,1,1,1,1], 'CheckedElements':[1,0,0,1,1,1,0,0]})
9.5. Referenze#
Documentazione di Pandas: https://pandas.pydata.org/pandas-docs/stable/
Sito Kaggle (dove trovare dati da analizzare): http://kaggle.com/
Pagina dalla quale scaricare diversi csv di dataset della libreria R: https://vincentarelbundock.github.io/Rdatasets/datasets.html