<jupyter_start><jupyter_text>Statistiche descrittiveConsideriamo nuovamente il DataFrame degli [incidenti](http://dati.comune.milano.it/dataset/9f7bcc9c-20a4-4e48-a7cd-99b15ed11102/resource/38d2171d-1067-4252-9f96-02867a2cc617/download/ds177_trafficotrasporti_incidenti_stradali_persone_infortunate_mese_zona_2001-2016.json).<jupyter_code>import pandas as pd
incidenti = pd.read_json("data/incidenti.json")<jupyter_output><empty_output><jupyter_text>La funzione `describe` permette di iniziare l'esplorazione del dataset, calcolando alcune statistiche essenziali.<jupyter_code>incidenti.describe()<jupyter_output><empty_output><jupyter_text>Notare che `count` conta il numero di righe per cui la variabile non è nulla.  Rimuovere valori mancantiLa presenza di valori mancanti può creare problemi. Per questo motivo possiamo usare `dropna` per rimuovere tutte le righe che contengono valori mancanti.<jupyter_code>incidenti_puliti = incidenti.dropna()
incidenti_puliti.head(3)<jupyter_output><empty_output><jupyter_text>Stiamo però attenti che il nuovo DataFrame potrebbe avere caratteristiche diverse dal DataFrame originale. Andiamo a calcolare il numero medio di `Feriti` e di `Incidenti` per i DataFrame `incidenti` e `incidenti_puliti`.<jupyter_code>incidenti.describe()[['Feriti', 'Incidenti']].loc['mean']
incidenti_puliti.describe()[['Feriti', 'Incidenti']].loc['mean']<jupyter_output><empty_output><jupyter_text>Creare una nuova colonnaPer creare una nuova colonna a partire da altre già esistenti, bisogna scrivere l'espressione corrispondente. Ad esempio, per creare una colonna con il numero di feriti per incidente, bisogna scrivere<jupyter_code>incidenti['rapporto'] = incidenti['Feriti'] / incidenti['Incidenti']<jupyter_output><empty_output><jupyter_text>Come usuale, è opportuno verificare il risultato dell'operazione, guardando le prime righe del dataset.<jupyter_code>incidenti.head(3)<jupyter_output><empty_output><jupyter_text>Assegnare valori ad una colonnaUn caso particolare si ha quando si vuole assegnare lo stesso valore a tutte le righe di un DataFrame. In questo caso assegnamo il valore `0` ad una nuova colonna che chiamiamo `prova`<jupyter_code>incidenti['prova'] = 0
incidenti.head(5)<jupyter_output><empty_output><jupyter_text>Convertire una colonnaPossiamo notare che la colonna `Zona` contiene numeri con virgola (*float*). Siccome in realtà `Zona` è un numero intero, convertiamo il numero usando il metodo `astype`. Dobbiamo dare un argomento a `astype` che è un dizionario che associa ad ogni colonna da convertire, il tipo da usare nella conversione.<jupyter_code>incidenti_puliti = incidenti_puliti.astype({'Zona': int})
incidenti_puliti.head(3)<jupyter_output><empty_output><jupyter_text>Però, `astype` richiede che i valori da convertire siano tutti non nulli: per questo motivo abbiamo lavorato su `incidenti_puliti`, invece che su `incidenti`. Cosa succede se si eseguono le stesse operazioni su `incidenti`?  Operare solo su alcune righe<jupyter_code>incidenti = incidenti.astype({'Zona': int})<jupyter_output><empty_output><jupyter_text>La presenza di valori mancanti rende impossibile l'esecuzione di `astype`. Possiamo sfruttare il fatto che `incidenti` e `incidenti_puliti` hanno lo stesso indice.<jupyter_code>incidenti.loc[:, 'Zonaint'] = incidenti_puliti.loc[:, 'Zona']
incidenti.head(3)<jupyter_output><empty_output><jupyter_text>La nuova colonna aggiunta (il risultato di `incidenti_puliti.loc[:, 'Zona'].astype(int)`) contiene solo alcune delle righe di `incidenti`. Ma pandas sfrutta gli indici di entrambi i DataFrame per aggiungere la colonna, mettendo valori mancanti dove non trova un indice corrispondente. Ad esempio, `indici_puliti` non ha una riga con indice `0`.Notare che `Zonaint` non ha numeri interi. Ciò è dovuto alla presenza di valori nulli (`NaN`). Per avere interi dobbiamo eliminarli.  Operare solo su alcune righe.Se non si vuole passare per un DataFrame intermedio come `incidenti_puliti`, diventa necessario estendere la chiamata a `loc` esplicitando anche una selezione delle righe, per invocare `astype(int)` solo sulle righe in cui la zona non sia mancante.Questo passo permette anche di gestire adeguatamente la zona mancante, che verrà posta a *0*.<jupyter_code>incidenti['Zona_int'] = 0
incidenti.loc[incidenti['Zona'].notnull(), 'Zona_int'] = \
incidenti.loc[incidenti['Zona'].notnull(), 'Zona'].astype(int)
incidenti.head(3)<jupyter_output><empty_output><jupyter_text>Creare una categoriaUna colonna viene chiamata *categoria* se i suoi valori permettono di classificare le righe.Adesso andremo a creare una nuova variabile `tipo` che ha tre valori:*  `a` se il rapporto fra il numero di feriti e il numero di incidenti è minore di 1.3;*  `b` se il rapporto è fra 1.3 (incluso) e 1.4 (escluso);*  `c` se il rapporto è maggiore o uguale a 1.4;Il modo più semplice, ma più lungo, è simile a quanto fatto in precedenza: per ogni valore possibile di `tipo`, si selezionano le righe e si cambia il valore solo di tali righe.<jupyter_code>incidenti.loc[incidenti['rapporto'] < 1.3, 'tipo'] = 'a'
incidenti.loc[(incidenti['rapporto'] >= 1.3) & (incidenti['rapporto'] < 1.4), 'tipo'] = 'b'
incidenti.loc[incidenti['rapporto'] >= 1.4, 'tipo'] = 'c'
incidenti.head()<jupyter_output><empty_output><jupyter_text>Cancellare una colonna<jupyter_code>del incidenti['Zonaint']
incidenti.head(3)<jupyter_output><empty_output><jupyter_text>Funzioni  Funzioni aggregate (Serie)Alcune funzioni possono essere applicate su una Serie intera. Ad esempio la funzione `len` permette di ottenere il numero di righe<jupyter_code>len(incidenti)<jupyter_output><empty_output><jupyter_text>Sicuramente più interessanti sono le funzioni che calcolano minimo, massimo o la somma. Queste funzioni sono applicate a Serie.<jupyter_code>incidenti['Feriti'].sum()
incidenti['Feriti'].min()
incidenti['Feriti'].max()<jupyter_output><empty_output><jupyter_text>Di particolare rilievo è che eventuali valori mancanti vengono gestiti correttamente (vengono ignorati)  Funzioni aggregate (DataFrame)Le stesse funzioni aggregate, se vengono invocate su un DataFrame, vengono applicate su tutte le colonne, incluse quelle che non contengono valori numerici.Il risultato è sempre una Serie.<jupyter_code>incidenti.min()<jupyter_output><empty_output><jupyter_text>Se si vuole applicare la funzione solo alle colonne numeriche, bisogna usare l'opzione `numeric_only`.<jupyter_code>incidenti.min(numeric_only = True)<jupyter_output><empty_output><jupyter_text>Raggruppare righeTalvolta il fatto che più righe condividano uno stesso valore può essere sfruttare per costruire gruppi di righe, che poi possono essere analizzati separatamente.Ad esempio, nel dataset `incidenti`, vogliamo contare quanti incidenti abbiamo avuto per ogni zona.Ciò richiede di raggruppare con una `groupby`.In realtà andiamo a raggruppare per `Zona_int` solo per motivi estetici (non è gradevole vedere una zona con il punto decimale).<jupyter_code>incidenti.groupby('Zona_int')<jupyter_output><empty_output><jupyter_text>Il risultato di una `groupby` è un oggetto opaco, che può essere sfruttato tramite operatori su gruppi. Pertanto da solo non serve a molto. Ma diventa molto utile se abbinato a `count`, `min`, `max`, `mean`, `sum`  Raggruppare righe con operatoriPer contare il numero di incidenti, dobbiamo sommare il valore della colonna `Incidenti`, distinguendo per ogni zona.Notare in questo caso che le righe che hanno *Zona* mancante non vengono considerate nei raggruppamenti.<jupyter_code>incidenti.groupby('Zona_int')['Incidenti'].sum()<jupyter_output><empty_output><jupyter_text>Estrarre un gruppoPer estrarre un singolo gruppo si può usare il metodo `get_group`. Ad esempio, per estrarre tutti i dati relativi al gruppo della zona 3:<jupyter_code>incidenti.groupby('Zona_int').get_group(3)<jupyter_output><empty_output><jupyter_text>Operatori aggregatiL'elenco delle funzioni che possono essere applicate ad una `groupby` sono chiamati operatori aggregati. Sono:*  `mean()` calcola il valore medio*  `median()` calcola il valore mediano*  `sum()`  calcola la somma*  `size()` calcola il numero di elementi dei gruppi*  `count()` calcola il numero di elementi non mancanti nei gruppi*  `std()` calcola la deviazione standard*  `var()` calcola la varianza*  `sem()` calcola l'errore standard della media*  `describe()` calcola le statistiche descrittive*  `first()` calcola il primo valore di ogni gruppo*  `last()` calcola l'ultimo valore di ogni gruppo*  `nth()` estrae alcuni valori*  `min()` calcola il minimo valore di ogni gruppo*  `max()` calcola il massimo valore di ogni gruppo  Size e CountRicordiamo che `count` esclude dal conteggio i valori mancanti, mentre `size` include i valori mancanti.Andiamo a confrontare i risultati di queste operazioni, considerano solo la colonna `Zona`, visto che è l'unica a contenere valori mancanti.Chiaramente ci aspettiamo che `count` restituisca valori minori di quelli calcolati da `size`.<jupyter_code>incidenti.groupby('Anno')['Zona'].size()
incidenti.groupby('Anno')['Zona'].count()<jupyter_output><empty_output><jupyter_text>Raggruppare su più colonnePossiamo utilizzare più di una colonna per raggruppare, fornendo una lista come argomento della `groupby`.Ad esempio, andiamo a calcolare il numero totale di incidenti, diviso per zona e mese.<jupyter_code>incidenti.groupby(['Zona', 'Mese']).sum()['Incidenti']<jupyter_output><empty_output><jupyter_text>Raggruppamenti e indiceIl comportamento di default è che le variabili indicate nella `groupby` diventino l'indice della Serie o del DataFrame risultante.<jupyter_code>gruppi = incidenti.groupby(['Zona_int', 'Mese']).sum()
gruppi.index.names<jupyter_output><empty_output><jupyter_text>Se invece si vuole che siano nuove colonne del DataFrame risultante, bisogna usare l'opzione `as_index`<jupyter_code>gruppi2 = incidenti.groupby(['Zona_int', 'Mese'], as_index = False).sum()
gruppi2.index.names<jupyter_output><empty_output><jupyter_text>Controlliamo esplicitamente che `Zona_int` e `Mese` siano colonne del DataFrame `gruppi2`<jupyter_code>gruppi2.columns<jupyter_output><empty_output><jupyter_text>Apply e Tabelle pivot  ApplyIl metodo `apply` permette di applicare una qualunque funzione a tutte le righe (o, più raramente, le colonne) di un  DataFrame o di una Serie. La funzione riceve direttamente una riga del DataFrame.Una forma più sofisticata permette di applicare una funzione che riceve in input un intero DataFrame.Come primo esempio, costruiamo una nuova variabile `pericolo` che corrisponde al numero di incidenti, più il numero di feriti moltiplicato per 100. Il primo passo è costruire la funzione che calcola tale indice.<jupyter_code>def calcola_p(riga):
    return riga['Incidenti'] + riga['Feriti'] * 100<jupyter_output><empty_output><jupyter_text>Poi si può usare la `apply`<jupyter_code>incidenti['pericolo'] = incidenti.apply(calcola_p, axis = 1)
incidenti.head()<jupyter_output><empty_output><jupyter_text>Creare una categoria con applyLa soluzione che abbiamo visto in precedenza per creare la categoria `tipo` ha due difetti: è fragile (perchè il valore soglia *1.3* compare in due istruzioni diverse, quindi è più probabile scrivere un valore sbagliato) e richiede un'istruzione distinta per ogni valore possibile di `tipo`.Quindi prima scriviamo una funzione che riceve in input un numero e calcola il valore della categoria.<jupyter_code>def categoria(n):
    soglie = [('a', 1.3), ('b', 1.4), ('c', None)]
    for (categoria, soglia) in soglie:
        if soglia is None or n < soglia:
            return categoria<jupyter_output><empty_output><jupyter_text>Poi è possibile *applicare* la funzione a `rapporto`<jupyter_code>incidenti['tipo'] = incidenti['rapporto'].apply(categoria)
incidenti.head(5)<jupyter_output><empty_output><jupyter_text>DifferenzeNelle slide precedenti abbiamo visto due diverse tipologie di utilizzo di apply:1.  `incidenti.apply(calcola_p, axis = 1)`2.  `incidenti['rapporto'].apply(categoria)`Nel caso 1 diventa essenziale l'opzione `axis = 1` che indica a pandas che la funzione deve essere invocata per ogni riga. In questo caso la funzione `calcola_p` riceve un argomento che è la riga di un DataFrame. Tecnicamente la riga è un dizionario con chiavi corrispondenti alle colonne del DataFrame.Nel caso 2 invece la `apply` viene chiamata su una Serie. In questo caso la funzione `categoria` riceve un singolo valore in input e la parte `axis = 1` non è necessaria.Un terzo caso prevede invece che la funzione riceva un intero DataFrame  Calcolo percentualeAdesso vogliamo aggiungere una colonna che contiene la percentuale del numero di incidenti in un certo mese rispetto a tutti quelli dell'anno nella zona di riferimento. Questo calcolo richiede più passi.Tramite un raggruppamento, possiamo calcolare il numero totale di incidenti per ogni anno e zona.<jupyter_code>incidenti.groupby(['Anno', 'Zona_int']).sum()['Incidenti']<jupyter_output><empty_output><jupyter_text>Tabella pivotUna tabella pivot viene ottenuta a partire da un DataFrame per riassumere i dati di quest'ultimo.In particolare, in una tabella pivot sono presenti tre elementi:*  un insieme di variabili per guidano il raggruppamento di righe*  una variabile (o un insieme di variabili) che etichetta nuove colonne*  una funzione per aggregare i valori.Ad esempio, andiamo a costruire una tabella pivot che riassume per ogni *anno* il numero totale (quindi la *somma) di incidenti (feriti, ecc.), ed in cui si ha una colonna per ogni *zona*.<jupyter_code>pd.pivot_table(incidenti, index = 'Anno', columns = ['Zona_int'], values = 'Incidenti', aggfunc = sum)<jupyter_output><empty_output><jupyter_text>Tabella pivot con medieTalvolta la funzione per aggregare i valori non è la somma, ma la media. In questo caso si può omettere l'opzione `aggfunc).<jupyter_code>pd.pivot_table(incidenti, index = 'Anno', columns = ['Zona_int'], values = 'Incidenti')<jupyter_output><empty_output><jupyter_text>I dati presenti in una tabella pivot possono essere calcolati con una `groupby`, ma la struttura del DataFrame risultante è diversa. Ogni volta bisogna capire se preferiamo la forma ottenuta con la `groupby` (poche colonne, tante righe) o la tabella pivot (meno righe, più colonne).Di solito la tabella pivot è più semplice da leggere per una persona, ma più complicata da rielaborare.  Elementi estremiDi particolare interesse è capire chi ha realizzato un valore estremo (per valore estremo si intende un minimo o un massimo). Ad esempio vogliamo sapere in quale anno si sono verificati più incidenti.Il primo passo è calcolare il numero di incidenti per ogni anno.<jupyter_code>incidenti_anno = incidenti.groupby('Anno').sum()[['Incidenti', 'Feriti', 'Morti']]
incidenti_anno.head(3)<jupyter_output><empty_output><jupyter_text>Adesso possiamo calcolare il valore massimo della colonna `Incidenti` e selezionare le righe che hanno tale valore.<jupyter_code>massimo = incidenti_anno['Incidenti'].max()
massimo
incidenti_anno[incidenti_anno['Incidenti'] == massimo]<jupyter_output><empty_output><jupyter_text>Elementi estremi 2Un'alternativa più rapida è fornita dai metodi `idxmax` e `idxmin` che restituiscono il valore dell'*indice* che realizza il valore estremo. Questo valore può essere utilizzato per estrarre la riga che ci interessa.<jupyter_code>incidenti_anno['Incidenti'].idxmax()
incidenti_anno.loc[incidenti_anno['Incidenti'].idxmax()]<jupyter_output><empty_output><jupyter_text>**Confronto***  La soluzione con `idxmax` è più veloce da scrivere e permette di individuare facilmente l'anno che ci interessa. Ma bisogna ricordare che `idxmax` permette di calcolare un solo valore (in questo caso, un solo anno).*  La soluzione con la selezione è più complicata. Inoltre per calcolare l'anno che ci interessa, dovremmo fare una nuova estrazione. Il vantaggio è che è corretta anche nel caso in cui due anni diversi realizzano entrambi il massimo valore.  Idxmax e raggruppamentiTalvolta ci interessa capire chi ha realizzato un massimo all'interno di ogni gruppo. In altre parole, vogliamo isolare non una sola riga, ma una riga per ogni gruppo.Ad esempio, per ogni anno vogliamo calcolare in quale mese si sono verificati più incidenti. Il primo passo è costruire un DataFrame che dica per ogni coppia (`Anno`, `Mese`) quanti incidenti si sono verificati.<jupyter_code>incidenti_mese = incidenti.groupby(['Anno', 'Mese'], as_index = False).sum()[['Anno', 'Mese', 'Incidenti']]
incidenti_mese<jupyter_output><empty_output><jupyter_text>Applicando `idxmax` otteniamo il singolo mese in cui si sono verificati più incidenti, ma noi vogliamo un mese per ogni anno (quindi un mese per il 2001, un mese per il 2002, ecc).<jupyter_code>incidenti_mese.loc[incidenti_mese['Incidenti'].idxmax()]<jupyter_output><empty_output><jupyter_text>Quindi sappiamo che il picco del numero di incidenti si è verificato nel mese di Giugno del 2001. Ma ciò non è quello che desideriamo.  Idxmax e raggruppamento 2Raggruppare per anno le righe di `incidenti_mese` è il primo passo: bisogna applicare la funzione `idxmax`.<jupyter_code>incidenti_mese.groupby('Anno')['Incidenti'].idxmax()<jupyter_output><empty_output><jupyter_text>Siccome `idxmax` restituisce i valori dell'indice corrispondenti alle righe che realizzano un massimo, bisogna passare per una `loc` per ottenere i mesi corrispondenti.<jupyter_code>incidenti_mese.loc[incidenti_mese.groupby('Anno')['Incidenti'].idxmax(), ['Anno', 'Mese']]<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08528284619344506, 'fraction_numerical': 0.0072070010867700054, 'mean_word_length': 6.822818791946308, 'pattern_counts': {'":': 0, '<': 160, '<?xml version=': 0, '>': 159, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '39', 'dir': 'jupyter-structured-clean-dedup', 'id': '317759', 'license': 'MIT', 'n_tokens_mistral': 6237, 'n_tokens_neox': 6215, 'n_words': 1909, 'path': '11 Lavorare su un DataFrame.ipynb', 'repo_name': 'DavideMaspero/python-alfabetizzazione-docenti', 'size': '17483'}