Il pacchetto Pandas estende Numpy, creando strutture a una o due dimensioni, in cui righe e colonne hanno un nome che le identifica. Le prime versioni di pandas avevano anche strutture a tre dimensioni, poi abbandonate.
Le strutture ad una dimensione sono dette Series quelle a due dimensioni DataFrames.
I nomi e i tipi delle righe e delle colonne sono in oggetti di tipo Indexes. Le colonne sono oggetti: columns, le righe: index
I DataFrasmes sono particolarmente utili, in pratica e' come maneggiare direttamente una tabella di un database, o un foglio excel.
Come per gli array di numpy si possono fare operazioni algebriche con Series e DataFrames, qui pero' si combinano fra loro elementi che hanno lo stesso nome di riga e colonna, anche se il loro ordine e' diverso.
Pandas ha anche strumenti per la gestione di serie temporali, tratta valori mancanti nelle tabelle, integra matplotlib per la grafica e scipy per algoritmi matematici.
Il sito di riferimento e': https://pandas.pydata.org ove si trova anche tutta la documentazione
Il testo di riferimento e' il libro di Wes McKinney (il creatore di Pandas):
"Python for Data Analysis" by Wes McKinney 3nd Edition , O'Reilly
ISBN-13: 978-1098104030 , ISBN-10: 109810403X
Il libro e' on-line: https://wesmckinney.com/book/
Una Series e' un array monodimensionale di numpy, quindi con elementi tutti dello stesso tipo, che possono essere interi, float, stringhe o qualunque altro oggetto Python.
A questo array e' associato in index, che ha un valore per ogni dato ed e' un identificativo del dato, di default sono una sequenza di interi, cha partono da zero.
Sono ammessi dati mancanti, ovvero indici senza un dato corrispondente, in questo caso il dato e' indicato con: NaN (not a number).
Una Series puo' essere costruita a partire da una lista, un dizionario, una array di numpy, o anche un singolo valore:
import numpy as np import pandas as pd k= pd.Series(dtype='int') # una serie vuota, di interi k= pd.Series(dtype='int',name='vuota') # una Series puo' avere un nome # una serie di interi, con lettere come indici s= pd.Series([0,1,2,3,4],index=['a', 'b', 'c', 'd', 'e']) # numeri a caso, fra 0 ed 1, con interi come indice s1 = pd.Series(np.random.rand(5), index=[1,2,3,4,5]) # utilizzando un dizionatio le *key* diventano gli indici s2 = pd.Series( {'a' : 0., 'b' : 1., 'c' : 2.} ) # utilizzzando un singolo valore per tutti gli elementi s3=pd.Series(5., index=['a', 'b', 'c', 'd', 'e'])
Elementi e parti di una Series (slices), possono essere ottenuti utlizzando il valore dell'indice, fra quadre, oppure si possono usare indici interi ,come per le liste in python:
s= pd.Series([0,10,20,30,40],index=['a', 'b', 'c', 'd', 'e']) s[1] # vale 10 s[:3] # una Series con i primi 3 elementi (i numeri da 0 a 2) s['b'] # vale 10 s['b':'e'] # una serie con gli elementi con indici da 'b' ad 'e' (compresi)
L'operatore in opera sull'index:
'c' in s # e' True 10 in s # e' False
for itera normalmente sull'array, non sull'indice, per iterare sull'indice bisogna fare:
for i in s.index: print(i,s[i]) #stampa indice e valore dell'elemento
Le operazioni per gli array di numpy si applicano alle Series:
s[s > s.median()] # Series dei valori maggiori della mediana s[s == s.mean()] # Series con il valor medio s[[1,2]] # Uso di liste come indici (si ottengono Series) s[['b','c']] np.sum(s) # ma somma dei valori s+s # Series con elementi di valore doppio s*2
Usando indici per individuare elementi, si ha un errore se l'elemento non e' presente, la funzione get invece produce l'oggetto 'None':
s[f] # da errore keyerror s.get('f') # da "None"
Serie possono esere convertire in dizionari:
s.to_dict()
E' possibile cambiare l'indice riassegnando l'oggetto Index:
s.index=['a','b','c','d','e']
Queste strutture a due dimensioni, hanno identificativi per le linee, un index come le Series, ma anche un identificativo per le colonne: columns.
Un dataframe si puo' costruire a partire da un dizionario di serie, un dizionario di liste, da liste di dizionati, da dizionari di dizionari etc. etc.
Come primo esempio creiamo una dataframe da un dizionario di liste:
import pandas as pd d = {'col1': [1, 2], 'col2': [3, 4]} df = pd.DataFrame(data=d) col1 col2 0 1 3 1 2 4
Abbiamo creato un dataframe con due colonne, una colonna per lista; gli indici del dizionario diventano i nomi delle colonne:
Creiamo un DataFrame da un dizionario di Series:
d = {'one' : pd.Series([1., 2., 3.], index=['a', 'b', 'c']), 'two' : pd.Series([1., 2., 3., 4.], index=['a', 'b', 'c', 'd'])} df = DataFrame(d) one two a 1.0 1.0 b 2.0 2.0 c 3.0 3.0 d NaN 4.0
Abbiamo creato un dataframe con due colonne, una colonna per ogni chiave del dizionerio, le chiavi del dizionario diventano i nomi delle colonne. Qui abbiamo dato gli indici, per le Series, che di default sono interi crescenti che partono da zero.
Creiamo un dataframe da una lista di dizionari:
data2 = [{'a': 1, 'b': 2}, {'a': 5, 'b': 10, 'c': 20}] df2=pd.DataFrame(data2) a b c 0 1 2 NaN 1 5 10 20.0
Ogni dizionario diventa una riga, le chiavi sono nomi di colonne, i dati mancanti sono indicati con: NaN;
La funzione DataFrame.from_records costruisce il dataframe a partire da tuple; DataFrame.from_dict costruisce il dataframe da un dizionario, procedendo per righe o per colonne. df.to_dict converte un dataframe in un dizionario:
dd={ 'd1':{'da':1,'db':2},'d2':{'da':3,'dc':4}} df3=pd.DataFrame.from_dict(dd,orient='index') da db dc d1 1 2.0 NaN d2 3 NaN 4.0 df4=pd.DataFrame.from_dict(dd,orient='columns') d1 d2 da 1.0 3.0 db 2.0 NaN dc NaN 4.0
Gli attributi index e columns sono oggetti tipo: Index con la lista dei nomi dell righe e colonne; il nome delle colonne deve essere unico, invece righe diverse possono avere nomi uguali:
Ad esempio prendiamo il dataframe df con: one two a 1.0 1.0 b 2.0 2.0 c 3.0 3.0 d NaN 4.0 df.index # vediamo i nomi delle righe: Index(['a', 'b', 'c', 'd'], dtype='object') df.columns # vediamo i nomi delle colonne: Index(['one', 'two'], dtype='object')
Un dataframe ha anche l'attributo dtypes che e' una Series con i tipi di dato di ogni colonna e, come indici, i nomi delle colonne:
df.dtype # vediamo il tipo delle colonne one float64 two float64
Altri attributi sono shape : tupla con numero righe e colonne, axes : lista di oggetti index con nomi righe e colonne, values: una array di numpy con i valori,
df.shape (4, 2) df.axes [Index(['a', 'b', 'c', 'd'], dtype='object'), Index(['one', 'two'], dtype='object')]
La funzione df.describe() fa una statistica dei valori delle colonne, df.info() da una descrizione del dataframe.
Creando un dataframe da un dizionario e dando index e columns in modo esplicito, dal dizionario si prendono solo i valori indicati, e si mette NaN quando i valori indicati mancano:
w=pd.DataFrame(d, index=['d', 'b', 'k'], columns=['two', 'three']) two three d 4.0 NaN b 2.0 NaN k NaN NaN
Per copiare una dataframe:
dff=df.copy()
Al solito l'assegnazione: dff=df crea un secondo nome per il dataframe., ma non lo copia.
Come per le Series in un dataframe possono mancare alcuni valori e questi sono identificati con: NaN . La funzione dropna elimina righe o colonne con valori mancanti, fillna mette un valore al posto dei mancanti:
df.dropna(axis=0,how='any') # elimina righe con qualche dato mancante df.dropna(axis=1,how='all') # elimina colonne con tutti dati mancanti df.fillna(100) # fa copia di df con 100 al posto dei mancanti
La funzione fillna ha diversi parametri per gestire i dati mancanti: metterci valori diversi per ognuno, propagarli lungo una riga o colonna, rimpiazzarne solo alcuni.
df.T e' la trasposte del dataframe, con righe e colonne scambiate.
La funzione rename cambia nomi ad indici e colonne, anche la funzione reindex riordina indici e colonne:
dff=pd.DataFrame({'c1':[1,2],'c2':[3,4]}) c1 c2 0 1 3 1 2 4 dff.rename(index={0:'zero',1:'uno'},columns={'c1':'col1'}) col1 c2 zero 1 3 uno 2 4 dff.reindex(index=[1,0],columns=['c2','c1','c3']) c2 c1 c3 1 4.0 2.0 NaN 0 3.0 1.0 NaN reindex mette NaN per indici cui mancano valori. df.reindex_like(df2) # mette indici di altro oggetto
I nomi delle colonne permettono di selezionarne una, vista come una Series, o piu' colonne, viste come Datframe:
df['nomecol'] #: una singola Series, di nome 'nomecol' df[['nomecol','nomecol']]# un DataFrame con solo le cols selezionat df[['nomecol']] # un dataframe di una sola colonna df.nomecol # la colonna e' vista come attributo
la sintassi con ":" serve a selezionare righe:
df=pd.DataFrame({'c1':[1,2],"c2":[3.4]},index=['a','b']) c1 c2 a 1 3 b 2 4 df[0:1] c1 c2 a 1 3 df['a':'b'] c1 c2 a 1 3 b 2 4
Per selezionare sia righe che colonne:
df[0:1]['c1'] a 1 df[0:1][['c1','c2']] c1 c2 a 1 3
Anche gli attributi loc ed iloc selezionano righe:
df.iloc[0] # la prima riga, vista come una serie, df.loc['a'] # la righa con index: *a*, e' una serie # i nomi delle colonne sono gli indici c1 1 c2 3
df.loc e df.iloc possono anche essere usati per selezionare sia righe che colonne:
df.loc['a':'b','c2'] # il primo indice e' la riga a 3 b 4 Name: c2, dtype: int64 df.loc['a':'b',['c1','c2']] df.iloc[0:2,[0,1]] c1 c2 a 1 3 b 2 4 df.loc['a','c1'] # il primo indice e' la riga df.iloc[0,0] # il secondo la colonna
Per selezionare un singolo elemento si possono usare "at" od "iat":
df.at['a','c2'] # vale: 3.4; elemento della riga: 'a' , colonna: 'c2' df.iat[0,1] # vale: 3.4; elemento della prima riga, seconda colonna
Come in numpy, operazioni logiche fra colonne sono Series booleane:
df['c1']>3 a False b False
Espressioni logiche sulle colonne possono essere usate come indici per selezionare righe di un DataFrame:
df[df['c1']>1] # righe con la colonna c1 >1 c1 c2 b 2 4 df[[True,False]] # prima riga True, la prendo, l'altra no c1 c2 a 1 3
Vediamo alcuni esempi; definiamo un dataframe:
import pandas as pd d={c1:[11,21],c2:[12,22]} df=pd.DataFrame(d,index=['r1','r2']) c1 c2 r1 11 12 r2 21 22
Per eliminare colonne:
df.drop(columns='c1') # questo elimina colonne (fa una copia di df) df.drop('c1',axis=1) del df['c1'] # questo non fa una copia di *df*, ma lo modifica
Per aggiungere colonne, o cambiarene il contenuto:
df['c3'] = [13,23] # aggiungo una colonna (alla fine) df['c4'] = pd.Series(['a','b','c']) # aggiungo una Series come colonna df['c5'] = 3 # nuova colonna, ogni elemento con: '3' df.insert(1,'c1',[12,22]) # aggiungo una colonna in posizione: *'0'* c1 c2 c3 c4 c5 r1 12 12 13 NaN 3 r2 22 22 23 NaN 3 df['c1']=df['c1']*100 # moltiplica i valori delle colonna per 100
Per eliminare righe:
df.drop(['r1','r2']) # elimina righe, produce una copia del dataframe df.drop('r1',axis=0) df.drop(index=['r1','r2']) df.drop(index='r1',inplace=True] # qui non crea una copia, modifica 'df'
Per cambiare valori di righe:
df.loc['r1']=1333 # mette un unico valore in tutta la riga df.loc['r1']=[1,2,3,4,5] # ridefinisc i valori di una riga df.loc['r1','c2']=1234 # cambia un singolo valore
Si possono fare modifiche ad un dataframe usando condizioni logiche come indici:
df[df<20]=0 # tutti gli elementi minori di 20 diventano 0 df[df==0]=np.nan # al posto di '0' mette: NaN: valore mancante.
Come per le Series, quando si fanno operazioni sui dataframe vengono fatti corrispondere valori con righe e colonne di stessi nomi; mettendo: 'NaN' in caso di valori non corrispondenti:
df+df1 # somma elemento per elemento df.add(df2)
La somma di una serie ad un Daframe opera per righe, nel senso che gli indici della serie corrispondono alle colonne del dataframe e la serie e' sommata ad ogni riga; analoga cosa vale per le altre operazioni fra serie e dataframe:
df c1 c2 r1 11 12 r2 21 22 df.iloc[0] # una serie, con indici i nomi delle colonne c1 11 c2 12 Name: r1, dtype: int64 df+df.iloc[0] # i valori della serie sono sommati ad ogni riga c1 c2 r1 22 24 r2 32 34
Per sommare una serie ad ogni colonna si usa la funzione add, con il parametro axis=0
s=pd.Series([100,200],index=['r1','r2']) df.add(s,axis=0) c1 c2 r1 111 112 r2 221 222
Le operazioni con singoli valori operano su tutti gli elementi del Data Frame:
df * 5 + 2 # ogni elemento e' moltiplicato per 5 e aumentato di 2 1 / df # applicato ad ogni elemento df ** 2 # eleva al quadrato ogni elemento df.T : trasposta ; scambia righe e colonne
Se il dataframe contiene valori logici si possono utilizzare operatori logici, ottenendo sempre dataframe booleani:
df1 & df2 df1 | df2 df1 ^ df2 -df1 : muta veri in falsi e viceversa
Per applicare una funzione generica alle colonne di una dataframe, ed ottenere una serie con un valore per ogni colonna, si usa la funzione apply:
df.apply(lambda x: max(x)) La Series ottenuta ha come indici i nomi delle colonne e contiene il valore massimo di ogni colonna.
L'istruzione for itera sui nome delle colonne:
for col in df: print(col) c1 c2
Per ottenere, a ogni iterazione, una colonna:
for colname,col in df.iteritems(): # qui colname e' il nome della colonna # col e' una serie, con la colonna
Per ottenere, ad ogni iterazione, una serie con una riga:
for indice,riga in df.iterrows(): # qui indice e' il nome della riga # riga: la Series contenente la riga
La funzione "where sostituisce singoli valori in base a una condizione, iterando sul dataframe.
Per applicare una generica funzione ad ogni elemento di un dataframe si usa "applymap", ad esempio:
f = lambda x: len(str(x)) df.applymap(f)
Per ordinare le righe di una dataframe in base ai valori degli indici si puo' usare la funzione 'sort_index':
dff=pd.DataFrame({3:[20,22],2:[10,40]},index=[300,200]) 3 2 300 20 10 200 22 40 dff.sort_index() # ordino le righe 3 2 200 22 40 300 20 10 dff.sort_index(axis=1) # ordino le colonne 2 3 300 10 20 200 40 22
Si puo' ordinare un dataframe in base ai valori di una colonna, qui ordino le righe in base ai valori delle colonna: '2', in ordine inverso:
dff.sort_values(by=[2],ascending=False) 3 2 200 22 40 300 20 10
Ci sono moltissime funzionei per serie e dataframe, calcolo del minimo, del massimo, somme, correlazione, ordinamento. Le funzioni di numpy si appicano anche a Series e Dataframes. In molti casi si puo' ottenere un valore per ogni riga o colonna, restituito in una Series.
E' posibile ,con la funzione groupby, creare gruppi di righe, per poi applicare funzioni ai singoli gruppi.
Esiste la possibilita' di avere una gerarchia di indici (multi index), per trattare dati gerarchici.
Le funzioni concat, join, merge, combinano insieme dataframe, hanno funzioni simili a comandi del linguaggio SQL che si usa per i database: