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.
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
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}')
raw_data.columns
Index(['data', 'stato', 'ricoverati_con_sintomi', 'terapia_intensiva', 'totale_ospedalizzati', 'isolamento_domiciliare', 'totale_positivi', 'variazione_totale_positivi', 'nuovi_positivi', 'dimessi_guariti', 'deceduti', 'casi_da_sospetto_diagnostico', 'casi_da_screening', 'totale_casi', 'tamponi', 'casi_testati', 'note'], dtype='object')
Limito i dati alla sola seconda ondata, dal primo settembre 2020 in avanti
raw_data = raw_data[raw_data['data']>='2020-09-01T17:00:00']
raw_data
data | stato | ricoverati_con_sintomi | terapia_intensiva | totale_ospedalizzati | isolamento_domiciliare | totale_positivi | variazione_totale_positivi | nuovi_positivi | dimessi_guariti | deceduti | casi_da_sospetto_diagnostico | casi_da_screening | totale_casi | tamponi | casi_testati | note | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
190 | 2020-09-01T17:00:00 | ITA | 1380 | 107 | 1487 | 25267 | 26754 | 676 | 978 | 207944 | 35491 | 230102.0 | 40087.0 | 270189 | 8725909 | 5214766.0 | NaN |
191 | 2020-09-02T17:00:00 | ITA | 1437 | 109 | 1546 | 26271 | 27817 | 1063 | 1326 | 208201 | 35497 | 230504.0 | 41011.0 | 271515 | 8828868 | 5280948.0 | NaN |
192 | 2020-09-03T17:00:00 | ITA | 1505 | 120 | 1625 | 27290 | 28915 | 1098 | 1397 | 208490 | 35507 | 230950.0 | 41962.0 | 272912 | 8921658 | 5342150.0 | NaN |
193 | 2020-09-04T17:00:00 | ITA | 1607 | 121 | 1728 | 28371 | 30099 | 1184 | 1733 | 209027 | 35518 | 231587.0 | 43057.0 | 274644 | 9034743 | 5414708.0 | NaN |
194 | 2020-09-05T17:00:00 | ITA | 1620 | 121 | 1741 | 29453 | 31194 | 1095 | 1694 | 209610 | 35533 | 232219.0 | 44118.0 | 276337 | 9142401 | 5484345.0 | NaN |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
281 | 2020-12-01T17:00:00 | ITA | 32811 | 3663 | 36474 | 743471 | 779945 | -8526 | 19350 | 784595 | 56361 | 971784.0 | 649117.0 | 1620901 | 22127199 | 13071746.0 | NaN |
282 | 2020-12-02T17:00:00 | ITA | 32454 | 3616 | 36070 | 725160 | 761230 | -18715 | 20709 | 823335 | 57045 | 988470.0 | 653140.0 | 1641610 | 22334342 | 13167345.0 | NaN |
283 | 2020-12-03T17:00:00 | ITA | 31772 | 3597 | 35369 | 724613 | 759982 | -1248 | 23225 | 846809 | 58038 | 0.0 | 0.0 | 1664829 | 22554389 | 13264937.0 | NaN |
284 | 2020-12-04T17:00:00 | ITA | 31200 | 3567 | 34767 | 722935 | 757702 | -2280 | 24099 | 872385 | 58852 | 0.0 | 0.0 | 1688939 | 22767130 | 13348428.0 | NaN |
285 | 2020-12-05T17:00:00 | ITA | 30158 | 3517 | 33675 | 720494 | 754169 | -3533 | 21052 | 896308 | 59514 | 0.0 | 0.0 | 1709991 | 22962114 | 14243149.0 | NaN |
96 rows × 17 columns
I dati giornalieri non sono direttamente disponibili (ad eccezione dei nuovi positivi).
Li calcolo puntualmente in relazione allo shift sul giorno precedente
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.
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.
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.
raw_data.at[raw_data.index[raw_data['data']=='2020-12-05T17:00:00'][0],'casi_testati_giorno'] = np.NaN
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<br>settimana', annotation_position='bottom right')
fig.update_yaxes(title_text='<b>Auto-correlazione</b>')
fig.update_xaxes(title_text='<b>Giorni</b>')
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).
Studiando la cross-correlazione è possibile stimare il ritardo tra decessi e nuovi positivi.
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='<b>Cross-correlazione tra decessi e positivi</b>')
fig.update_xaxes(title_text='<b>Giorni</b>')
fig.add_vline(x = cases_death_corr['crosscor_decessi_nuovi_positivi'].idxmax(),\
line_dash='dash', line_color='green', \
annotation_text='massima<br>correlazione', annotation_position='bottom right')
fig.show()
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)')
La maggiore correlazione tra decessi e nuovi positivi è dopo 13 giorni Ed è pari a 0.9981 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.
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='<b>Positivi</b>', range=[0, 50000], secondary_y=False)
fig.update_yaxes(title_text='<b>Decessi di 13 giorni dopo</b>', range=[0, 1000],secondary_y=True)
fig.show()
E' interessante anche studiare la cross-correlazione tra nuovi positivi e dimessi/guariti.
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='<b>Cross-correlazione tra dimessi e positivi</b>')
fig.update_xaxes(title_text='<b>Giorni</b>')
fig.add_vline(x = healing_corr['healing_corr'].idxmax(),\
line_dash='dash', line_color='green', \
annotation_text='massima<br>correlazione', annotation_position='bottom right')
fig.show()
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)')
La maggiore correlazione tra guariti e nuovi positivi è dopo 21 giorni Ed è pari a 0.9979 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.
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='<b>Positivi</b>', range=[0, 45000], secondary_y=False)
fig.update_yaxes(title_text='<b>Guariti/dimessi di 21 giorni dopo</b>', range=[0, 45000], secondary_y=True)
fig.show()