Pandas

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/

Strutture per i dati: Series

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']

Strutture per i dati:Dataframe

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

Dataframe, selezione righe e colonne

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

Dataframe, modifica righe e colonne

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.

Algebra con DataFrame

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.

Dataframe ed iteratori

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.

Funzioni per i 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: