#!/usr/bin/env python # coding: utf-8 # # Correlazione temporale tra diversi dati Covid # ### Abstract # Nelle due ondate di Covid in Italia, è stato più volte evidenziato che "il numero dei decessi è l'ultimo a diminuire" rispetto a tutti gli indicatori legati alla pandemia. E lo stesso vale per l'andamento dei guariti e dimessi. # # Vero, ma **di quanti giorni stiamo parlando** rispetto, ad esempio, ai dati sui **nuovi positivi**? # # Rispondo a questa e altre domande con un'analisi delle **serie temporali** dei dati Covid. # ### Caricamento e overview dati Covid (da repository Protezione Civile) # In[1]: import pandas as pd import numpy as np import plotly.express as px import plotly.graph_objects as go from plotly.subplots import make_subplots # In[2]: repo_pc = 'https://raw.githubusercontent.com/pcm-dpc/COVID-19/master/dati-andamento-nazionale/' file_andam_naz = 'dpc-covid19-ita-andamento-nazionale.csv' raw_data = pd.read_csv(f'{repo_pc}{file_andam_naz}') # In[3]: raw_data.columns # Limito i dati alla sola *seconda ondata*, dal primo settembre 2020 in avanti # In[4]: raw_data = raw_data[raw_data['data']>='2020-09-01T17:00:00'] raw_data # I dati giornalieri non sono direttamente disponibili (ad eccezione dei *nuovi positivi*). # # Li calcolo puntualmente in relazione allo *shift* sul giorno precedente # In[5]: raw_data['ricoverati_con_sintomi_giorno'] = raw_data['ricoverati_con_sintomi'] - raw_data['ricoverati_con_sintomi'].shift(1) raw_data['terapia_intensiva_giorno'] = raw_data['terapia_intensiva'] - raw_data['terapia_intensiva'].shift(1) raw_data['deceduti_giorno'] = raw_data['deceduti'] - raw_data['deceduti'].shift(1) raw_data['tamponi_giorno'] = raw_data['tamponi'] - raw_data['tamponi'].shift(1) raw_data['casi_testati_giorno'] = raw_data['casi_testati'] - raw_data['casi_testati'].shift(1) raw_data['dimessi_guariti_giorno'] = raw_data['dimessi_guariti'] - raw_data['dimessi_guariti'].shift(1) # Controllo eventuali anomalie. # In[6]: fig = make_subplots(rows=4, cols=2) fig.add_trace( go.Bar(name='ricoverati_con_sintomi_giorno', x=raw_data['data'], y=raw_data['ricoverati_con_sintomi_giorno']), row=1, col=1 ) fig.add_trace( go.Bar(name='terapia_intensiva_giorno', x=raw_data['data'], y=raw_data['terapia_intensiva_giorno']), row=1, col=2 ) fig.add_trace( go.Bar(name='nuovi_positivi_giorno', x=raw_data['data'], y=raw_data['nuovi_positivi']), row=2, col=1 ) fig.add_trace( go.Bar(name='tamponi_giorno', x=raw_data['data'], y=raw_data['tamponi_giorno']), row=2, col=2 ) fig.add_trace( go.Bar(name='casi_testati_giorno', x=raw_data['data'], y=raw_data['casi_testati_giorno']), row=3, col=1 ) fig.add_trace( go.Bar(name='deceduti_giorno', x=raw_data['data'], y=raw_data['deceduti_giorno']), row=3, col=2 ) fig.add_trace( go.Bar(name='dimessi_guariti_giorno', x=raw_data['data'], y=raw_data['dimessi_guariti_giorno']), row=4, col=1 ) fig.update_layout(height=600, width=800, title_text='Variazioni giornaliere dei principali indicatori') fig.update_layout(legend=dict( orientation="h", yanchor="bottom", y=-.3, xanchor="center", x=.5 )) fig.show() # I casi testati meritano uno zoom. # In[7]: fig = px.bar(raw_data, x='data', y='casi_testati_giorno') fig.show() # Quasi 900.000 casi testati il 5 dicembre sono sicuramente un recupero nei dati o un errore. # Annullo questa misurazione. # In[8]: raw_data.at[raw_data.index[raw_data['data']=='2020-12-05T17:00:00'][0],'casi_testati_giorno'] = np.NaN # ### Auto-correlazione: quali dati presentano una vistosa periodicità # In[9]: days = 25 new_cases = \ pd.DataFrame({'giorni': range(days),\ 'autocor_tamponi_eseguiti': \ [raw_data['tamponi_giorno'].corr(raw_data['tamponi_giorno'].shift(i)) \ for i in range(days)], \ 'autocor_casi_testati': \ [raw_data['casi_testati_giorno'].corr(raw_data['casi_testati_giorno'].shift(i)) \ for i in range(days)], \ 'autocor_nuovi_positivi': \ [raw_data['nuovi_positivi'].corr(raw_data['nuovi_positivi'].shift(i)) \ for i in range(days)], \ 'autocor_nuovi_ricoverati': \ [raw_data['ricoverati_con_sintomi_giorno'].corr(raw_data['ricoverati_con_sintomi_giorno'].shift(i)) \ for i in range(days)],\ 'autocor_nuove_TI': \ [raw_data['terapia_intensiva_giorno'].corr(raw_data['terapia_intensiva_giorno'].shift(i)) \ for i in range(days)],\ 'autocor_nuovi_decessi': \ [raw_data['deceduti_giorno'].corr(raw_data['deceduti_giorno'].shift(i)) \ for i in range(days)] }) fig = px.line(new_cases, \ x='giorni', \ y=['autocor_casi_testati','autocor_tamponi_eseguiti',\ 'autocor_nuovi_positivi','autocor_nuovi_ricoverati','autocor_nuove_TI','autocor_nuovi_decessi'],\ line_shape = 'spline',\ title=f'Auto-correlazione su tamponi eseguiti / casi testati / '\ 'nuovi positivi / ricoverati / TI / decessi') fig.add_vline(x = 7, line_dash='dash', line_color='green', \ annotation_text='una
settimana', annotation_position='bottom right') fig.update_yaxes(title_text='Auto-correlazione') fig.update_xaxes(title_text='Giorni') fig.show() # Si nota chiaramente che il numero di **tamponi eseguiti** e di **casi testati** ha una fortissima **correlazione settimanale** (picchi ogni 7/14/21/.. giorni). In maniera minore, ma lo stesso si verifica anche per i **nuovi positivi**. # # I nuovi **ricoveri** e le nuove **terapie intensive non presentano periodicità**. Va detto che questi valori sono in realtà la *variazione* nel numero di attualmente ricoverati e di persone attualmente in TI (non è disponibile il numero dei nuovi ingressi in ospedale o nuovi ingressi in TI). # ### Cross-correlazione: qual è il "ritardo" tra decessi e nuovi positivi # Studiando la cross-correlazione è possibile stimare il *ritardo* tra decessi e nuovi positivi. # In[10]: days = 50 cases_death_corr = \ pd.DataFrame({'giorni': range(days),\ 'crosscor_decessi_nuovi_positivi': \ [raw_data['deceduti_giorno'].rolling(7,center=True).mean()\ .corr(raw_data['nuovi_positivi'].rolling(7,center=True).mean().shift(i))\ for i in range(days)], \ 'crosscor_decessi_nuovi_ricoverati': \ [raw_data['deceduti_giorno'].rolling(7,center=True).mean()\ .corr(raw_data['ricoverati_con_sintomi_giorno'].rolling(7,center=True).mean().shift(i))\ for i in range(days)], \ 'crosscor_decessi_nuove_TI': \ [raw_data['deceduti_giorno'].rolling(7,center=True).mean()\ .corr(raw_data['terapia_intensiva_giorno'].rolling(7,center=True).mean().shift(i))\ for i in range(days)] }) fig = px.line(cases_death_corr, \ x='giorni', \ y='crosscor_decessi_nuovi_positivi', \ line_shape = 'spline', \ title='Cross-correlazione tra decessi e nuovi positivi, in media mobile a 7 giorni') fig.update_yaxes(title_text='Cross-correlazione tra decessi e positivi') fig.update_xaxes(title_text='Giorni') fig.add_vline(x = cases_death_corr['crosscor_decessi_nuovi_positivi'].idxmax(),\ line_dash='dash', line_color='green', \ annotation_text='massima
correlazione', annotation_position='bottom right') fig.show() # In[11]: giorni_max_cor = cases_death_corr['crosscor_decessi_nuovi_positivi'].idxmax() valore_max_cor = round(cases_death_corr['crosscor_decessi_nuovi_positivi'].max(),4) print(f'La maggiore correlazione tra decessi e nuovi positivi è dopo {giorni_max_cor} giorni') print(f'Ed è pari a {valore_max_cor} su un massimo teorico di 1 (correlazione perfetta)') # In termini più intuitivi: **i numeri dei decessi di oggi sono legati fortissimamente ai positivi di 13 giorni fa**. # # Sovrapponendo i due grafici (con uno shift di 13 giorni e uso di doppia scala sulle y), si vede molto bene questa correlazione. # In[12]: fig = make_subplots(specs=[[{'secondary_y': True}]]) fig.add_trace( go.Scatter(x=raw_data['data'], y=raw_data['nuovi_positivi'], \ name='numero positivi del giorno', line_shape='spline'), secondary_y=False, ) fig.add_trace( go.Scatter(x=raw_data['data'], y=raw_data['deceduti_giorno'].shift(-13), \ name='numero decessi di 13 giorni dopo', line_shape='spline'), secondary_y=True, ) fig.update_layout( title_text='Andamento positivi e decessi (shift di 13 giorni e uso di doppia scala sulle y)' ) fig.update_xaxes(title_text='Data') fig.update_yaxes(title_text='Positivi', range=[0, 50000], secondary_y=False) fig.update_yaxes(title_text='Decessi di 13 giorni dopo', range=[0, 1000],secondary_y=True) fig.show() # ### Cross-correlazione: tempi di guarigione # E' interessante anche studiare la cross-correlazione tra nuovi positivi e dimessi/guariti. # In[13]: days = 50 healing_corr = \ pd.DataFrame({'giorni': range(days),\ 'healing_corr': \ [raw_data['dimessi_guariti_giorno'].rolling(7,center=True).mean().\ corr(raw_data['nuovi_positivi'].rolling(7,center=True).mean().shift(i))\ for i in range(days)] }) fig = px.line(healing_corr, \ x='giorni', \ y='healing_corr', \ line_shape = 'spline', \ title='Cross-correlazione tra dimessi e nuovi ricoverati con sintomi') fig.update_yaxes(title_text='Cross-correlazione tra dimessi e positivi') fig.update_xaxes(title_text='Giorni') fig.add_vline(x = healing_corr['healing_corr'].idxmax(),\ line_dash='dash', line_color='green', \ annotation_text='massima
correlazione', annotation_position='bottom right') fig.show() # In[14]: giorni_max_cor_healing = healing_corr['healing_corr'].idxmax() valore_max_cor_healing = round(healing_corr['healing_corr'].max(),4) print(f'La maggiore correlazione tra guariti e nuovi positivi è dopo {giorni_max_cor_healing} giorni') print(f'Ed è pari a {valore_max_cor_healing} su un massimo teorico di 1 (correlazione perfetta)') # In altri termini: **i numeri dei guariti e dimessi di oggi sono legati fortissimamente ai nuovi positivi di 21 giorni fa**. # # Anche in questo caso un grafico shiftato e con doppio asse y mostra chiaramente il fenomeno. # In[15]: fig = make_subplots(specs=[[{'secondary_y': True}]]) fig.add_trace( go.Scatter(x=raw_data['data'], y=raw_data['nuovi_positivi'], \ name='numero positivi del giorno', line_shape='spline'), secondary_y=False, ) fig.add_trace( go.Scatter(x=raw_data['data'], y=raw_data['dimessi_guariti_giorno'].shift(-21), \ name='numero dimessi e guariti/dimessi di 21 giorni dopo', line_shape='spline'), secondary_y=True, ) fig.update_layout( title_text='Andamento positivi e guariti/dimessi (shift di 21 giorni e uso di doppia scala sulle y)' ) fig.update_xaxes(title_text='Data') fig.update_yaxes(title_text='Positivi', range=[0, 45000], secondary_y=False) fig.update_yaxes(title_text='Guariti/dimessi di 21 giorni dopo', range=[0, 45000], secondary_y=True) fig.show()