""" # Simulación del avance del COVID_19 en Madrid ## Datos: * Tamaño población Madrid: 6.6M * Camas hospitales por capita: 0.3% * Porcentage casos asintomaticos: 17% (15.5-20.2) ([referencia](https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2020.25.10.2000180)) * Mortalidad: 3.4% ([referencia](https://www.worldometers.info/coronavirus/coronavirus-death-rate/)) * Casos que requieren UCI: 5% (a partir de las estadísticas publicadas para España). * Población de Madrid por debajo de los 50 años: 68% ([INE](https://www.ine.es/jaxi/Tabla.htm?path=/t20/e245/p05/a2011/l0/&file=00028001.px&L=0)) """ """ ## Modelado: * Cada enfermo genera un número de boletos diario que se sortean entre la población. * Si alguno de esos boletos le toca a una persona sana, esta se infecta. * Los enfermos pueden estar en 4 estados: * incubando * enfermo asintomatico (asymptomatic) * enfermo leve (mild) * enfermo grave (severe), que requiere UCI * Solo los enfermos que están incubando la enfermedad y los asintomaticos pueden transmitir la enfermedad. Se supone que para el resto, la probabilidad de contagio se vuelve 0 al ser puestos en cuarentena. * Para cada tipo de enfermo, se contemplan tiempos medios de convalecencia y varianzas distintos. * Los periodos de convalecencia siguen una distribución de [Gauss inversa](https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution). * Los periodos de incubación siguen una distribución [log-normal](https://en.wikipedia.org/wiki/Log-normal_distribution). * Durante el periodo de incubación, solo se puede contagiar la enfermedad en los últimos días. El número de boletos se calcula según la siguiente formula: $b = r^{t_1-t}*(t-t_0)/(t_1-t_0)$, siendo $t_0$ el momento en el que la persona se contagia, $t_1$ el momento en el que desarrolla la enfermedad y $r$ un parametro de amortiguamiento. Por defecto se calcula, suponiendo que el número de boletos se dobla cada dos días. * Solo los enfermos graves pueden morir. * Una vez un enfermo se recupera, se vuelve immune y no puede adquirir o transmitir la enfermedad de nuevo. * El modelo contempla la posibilidad de aplicar distintas estrategias para paliar los efectos de la epidemia. * El modelado se hace en función a macroparametros (no a individuos) por lo que su validez sera mayor cuando los números de enfermos, infectados, etc., [sean altos](https://es.wikipedia.org/wiki/Ley_de_los_grandes_n%C3%BAmeros) y tendra poca precisión cuando estos sean bajos debido a la aleatoriedad. ## Parametrización: Estos son los parametros utilizados en la modelización y los valores por defecto asignados: * `population_size=6.662e6` - poblacion de Madrid * `asymptomatic_ratio=0.17` - porcentage de enfermos que pasan la enfermedad sin manifestar ningún síntoma. * `severe_ratio=0.05` - porcentage de enfermos que en situación critca que necesitan ser ingresados en la UCI. * `mortality_ratio=0.035` - porcentage de enfermos que acaban muriendo * `asymptomatic_convalescent_period_mean=2` - tiempo medio de convalecencia para enfermos asintomaticos, notese que este es el periodo medio durante el cual un enfermos asintomatico puede seguir transmitiendo el virus. * `asymptomatic_convalescent_period_variance=3` - varianza * `mild_convalescent_period_mean=8` - tiempo medio de convalecencia para enfermos que no requiren UCI. * `mild_convalescent_period_variance=4` - varianza * `severe_convalescent_period_mean=16` - tiempo medio de convalecencia para enfermos que requieren UCI. * `severe_convalescent_period_variance=8` - varianza * `daily_transmission_rate=2.0` - posibilidades de contagio diarias (boletos) generadas por un enfermo que no está en cuarentena. El valor por defecto se ha ajustado para reproducir la velocidad de contagio de España, +40% diario. * `preinfectious_half_period=2` - para personas en periodo de incubación, tiempo medio en el cual se duplican los boletos de contagio * `incubation_mean=5` - tiempo medio de incubación * `incubation_variance=7` - varianza * `infected_seed=10` - número de infectados iniciales * `days=100` - tamaño de la simulación * `ticks_per_day=4` - número de cortes por día, para aumentar la precisión de la integración. ## Variables de salida El simulador calcula y devuelve una tabla con las siguientes columnas: * `day` - tiempo transcurrido en dias (numero de punto flotante) * `infectious` - número de personas infecciosas * `asymptomatic` - número de enfermos asintomaticos * `mild` - numero de enfermos leves * `severe` - número de enfermos graves (UCI) * `immune` - número de personas immunes * `infected` - número de personas infectadas (incubando + enfermas + asintomáticas) * `death` - número acumulado de muertos * `healthy` - número de personas sanas * `incubating` - número de personas incubando la enfermedad * `sick` - número de personas enfermas (no se contabilizan aquí los enfermos asintomáticos) * `ever_sick`- número de personas que están o han estado enfermas * `new_sick` - número de personas que caen enfermas en el intervalo al que referencia la fila de la tabla * `infectable` - personas susceptibles de ser infectadas (las que no son ni inmunes, ni están infectadas, ni han muerto) * `velocity` - velocidad a la que aumenta el número de personas infectadas en el espacio logaritmico. * `sick_velocity` - velocidad a la que aumenta el número de personas enfermas. * `ever_sick_velocity` - velocidad a la que aumenta el número de personas que han estado alguna vez enfermas * `status` - al simular estrategias de contención de la epidemia, este campo indica la etapa. ## Programación de la simulación """ import io import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import scipy.optimize from math import erf, log, exp, sqrt from scipy.stats import invgauss """ ### Cálculo de distribuciones de probabilidad ([CDF](https://en.wikipedia.org/wiki/Cumulative_distribution_function)) """ def lognorm_cdf(mean, var, n_days=20, ticks_per_day=10): sigma = sqrt(log(var/mean**2 + 1)) mu = log(mean) - 0.5*sigma**2 sqrt2sigma_inv = 1.0 / (sqrt(2) * sigma) def cdf(x): return (0.5 + 0.5*erf(sqrt2sigma_inv*(log(x) - mu)) if x > 0 else 0) n_ticks = n_days * ticks_per_day days = list([i/ticks_per_day for i in range(n_ticks)]) acu = list([cdf(d) for d in days]) return acu def invgauss_cdf(mean, var, n_days=20, ticks_per_day=10): la = mean**3/var mu = mean/la x = list([i/ticks_per_day for i in range(n_days*ticks_per_day)]) return invgauss.cdf(x, mu, scale=la) """ ### El simulador """ class SIRwhatever: def __init__(self, population_size=6.662e6, asymptomatic_ratio=0.17, severe_ratio=0.05, #hospital_beds_ratio=0.003, #no_bed_death_rate_multiplier=2, mortality_ratio=0.035, asymptomatic_convalescent_period_mean=2, asymptomatic_convalescent_period_variance=3, mild_convalescent_period_mean=8, mild_convalescent_period_variance=4, severe_convalescent_period_mean=16, severe_convalescent_period_variance=8, daily_transmission_rate=2.0, preinfectious_half_period=2, incubation_mean=5, incubation_variance=7, herd_immunity_ratio=0.0, days = 100, ticks_per_day = 4, infected_seed = 10, observation_delay = 0, quiet = False): self.population_size = population_size self.asymptomatic_ratio = asymptomatic_ratio self.severe_ratio = severe_ratio self.mortality_ratio = mortality_ratio self.daily_transmission_rate = daily_transmission_rate self.days = days self.ticks_per_day = ticks_per_day self.infected_seed = infected_seed self.incubation_mean = incubation_mean self.incubation_variance = incubation_variance self.preinfectious_half_period = preinfectious_half_period self.mild_convalescent_period_mean = mild_convalescent_period_mean self.mild_convalescent_period_variance = mild_convalescent_period_variance self.severe_convalescent_period_mean = severe_convalescent_period_mean self.severe_convalescent_period_variance = severe_convalescent_period_variance self.asymptomatic_convalescent_period_mean = asymptomatic_convalescent_period_mean self.asymptomatic_convalescent_period_variance = asymptomatic_convalescent_period_variance self.herd_immunity_ratio=herd_immunity_ratio self.observation_delay=observation_delay self.quiet=quiet def strategy(self, t, **kargs): return 'normal' # can I do nothing? def print_extrema(self, i, t, **kargs): if self.quiet: return if t >= 2: # we ignore the first two days in order to let the model stabilize for k in sorted(kargs.keys()): extreme = None v = kargs[k] if v[i-2] < v[i-1]: if v[i-1] > v[i]: extreme = '▲' elif (v[i-1] < v[i]) and (v[i-2] > v[i-1]): extreme = '▼' if extreme: line = "%s day: %f, %s: %f" % (extreme, t, k, v[i-1]) if k.startswith('new_'): line += ", %s: %f" % (k[4:], kargs[k[4:]][i-1]) if ("ever_" + k) in kargs.keys(): line += ", ever_%s: %f" % (k, kargs["ever_" + k][i-1]) print(line) def run(self): ticks = self.days * self.ticks_per_day # loop iterations Dt = 1 / self.ticks_per_day z = np.zeros(ticks) healthy = z.copy() # people that is not infected death = z.copy() # acumulated deaths incubating = z.copy() # infected in the incubation stage infected = z.copy() # number of currently infected persons velocity = z.copy() # contagion velocity sick = z.copy() # people with desease simptoms new_sick = z.copy() # new people getting sick at the tick sick_velocity = z.copy() # velocity of sick cases appearing ever_sick = z.copy() # people that has ever been sick ever_sick_velocity = z.copy() # velocity of ever sick severe = z.copy() # people in severe condition mild = z.copy() # people in mild condition asymptomatic = z.copy() # infected people not showing symptoms infectious = z.copy() # infected people that is able to transmit the disease infectable = z.copy() # people that can still get the dissease symptomatic = z.copy() # sick people showing symptoms new_symptomatic = z.copy() # new people getting symptoms at the tick symptomatic_velocity = z.copy() # velocity of symptomatic cases appearing ever_symptomatic = z.copy() # people that have ever had symptoms ever_symptomatic_velocity = z.copy() # velocity of ever symptomatic cases appearing observed_ever_symptomatic = z.copy() # people that has ever been symptomatic and observed observed_ever_symptomatic_velocity = z.copy() # velocity of observed ever symptomatic # new_sick_velocity = z.copy() # velocity of new sick day = z.copy() immune = np.full(ticks, # people that has healed from the disease self.herd_immunity_ratio*self.population_size) status = list(['normal' for i in range(ticks)]) # Here the probability distributions are tabled and cached. incubation_cdf = lognorm_cdf(self.incubation_mean, self.incubation_variance, ticks_per_day = self.ticks_per_day, n_days=self.days) mild_convalescent_period_cdf = invgauss_cdf(self.mild_convalescent_period_mean, self.mild_convalescent_period_variance, ticks_per_day = self.ticks_per_day, n_days=self.days) severe_convalescent_period_cdf = invgauss_cdf(self.severe_convalescent_period_mean, self.severe_convalescent_period_variance, ticks_per_day = self.ticks_per_day, n_days=self.days) asymptomatic_convalescent_period_cdf = invgauss_cdf(self.asymptomatic_convalescent_period_mean, self.asymptomatic_convalescent_period_variance, ticks_per_day = self.ticks_per_day, n_days=self.days) preinfection_amortiguation_rate = 0.5**(1.0/(self.preinfectious_half_period * self.ticks_per_day)) for i in range(ticks): ps = self.population_size t = i * Dt day[i] = t # For convenience, we also store the time # Number of coupons an infectious person generates at his maximum transmission_rate = self.daily_transmission_rate / self.ticks_per_day # Every infectious person gives away a number of coupons # everyday (actually, every tick) coupons = infectious[i] * transmission_rate # A single player may get several coupons, so we need to calculate the probability # that has a player of at least getting a coupon. We do it calculating first the # probability of not getting any coupon at all. p_contagied = 1 - exp(-coupons/ps) # We can calculate it using an aproximation # in order to avoid numerical inestabilities # It works because ps is quite big # p_contagied = 1 - ((ps - 1)/ps) ** coupons # probability of somebody # # not getting any ticket # And how many infectable persons do we have to play today? players = ps if i == 0: d_infected = self.infected_seed else: players -= infected[i-1] + immune[i-1] + death[i-1] d_infected = players * p_contagied # number of persons that have # been infected today infectable[i] = players - d_infected # Spread the infected over the following days, recording when they are going # to get sick filling new_sick for j in range(i+1, ticks): t1 = j - i # time elapsed since the contagion cdf0 = incubation_cdf[t1 - 1] if cdf0 == 1.0: break cdf1 = incubation_cdf[t1] d_new_sick = d_infected * (cdf1 - cdf0) new_sick[j] += d_new_sick incubating[j-1] += d_infected*(1-cdf0) # Here we also fill infectious which counts how many people is able # to generate coupons for k in range(j, i+1, -1): preinfection_rate = preinfection_amortiguation_rate**(j-k) * (k-i)/(j-i) if preinfection_rate < 1e-4: break infectious[k] += d_new_sick * preinfection_rate # Now, we see what happens which people getting sick right now. new_sick_i = new_sick[i] # How many is there in every class? d_asymptomatic = new_sick_i * self.asymptomatic_ratio d_severe = new_sick_i * self.severe_ratio d_mild = new_sick_i - d_asymptomatic - d_severe new_symptomatic[i] = new_sick_i - d_asymptomatic # Only the severyly ill can die, so we calculate and adjusted # mortality ratio just for those severe_mortality_ratio = self.mortality_ratio / self.severe_ratio # The future of those getting ill today is set in stone, so we can fill # the asymptomatic, mild, severe, imune, death until the end of time for them. for j in range(i, ticks): t1 = j - i asymptomatic_cdf0=asymptomatic_convalescent_period_cdf[t1] mild_cdf0=mild_convalescent_period_cdf[t1] severe_cdf0=severe_convalescent_period_cdf[t1] severe[j] += (1-severe_cdf0)*d_severe mild[j] += (1-mild_cdf0)*d_mild asymptomatic[j] += (1-asymptomatic_cdf0)*d_asymptomatic immune[j] += ((1-severe_mortality_ratio)*severe_cdf0*d_severe + mild_cdf0*d_mild + asymptomatic_cdf0*d_asymptomatic) death[j] += severe_mortality_ratio*severe_cdf0*d_severe infectious[j] += (1-asymptomatic_cdf0)*d_asymptomatic # infected, sick and healthy are just combinations of other parameters # we keep for convenience. infected[i] = asymptomatic[i] + mild[i] + severe[i] + incubating[i] symptomatic[i] = mild[i] + severe[i] sick[i] = symptomatic[i] + asymptomatic[i] healthy[i] = infectable[i] + immune[i] # Finally, also for convenience, we calculate the velocities for some parameters. # In this context, velocity is the increase per time unit of some parameter in # the exponencial space: # velocity(a) = exp(log(a[i]) - log(a[i-1])) = a[i] / a[i-1] if i == 0: velocity[i] = 0 sick_velocity[i] = 0 ever_sick[i] = new_sick[0] ever_sick_velocity[i] = 0 ever_symptomatic[i] = 0 else: velocity[i] = ((infected[i]/infected[i-1])**self.ticks_per_day - 1) sick_velocity[i] = (((sick[i]+1)/(sick[i-1]+1))**self.ticks_per_day - 1) ever_sick[i] = ever_sick[i-1] + new_sick[i] ever_sick_velocity[i] = (((ever_sick[i] + 1) / (ever_sick[i-1] + 1))**self.ticks_per_day - 1) # new_sick_velocity[i] = (((new_sick[i] + 1) / # (new_sick[i-1] + 1))**self.ticks_per_day - 1) symptomatic_velocity[i] = (((symptomatic[i]+1)/(symptomatic[i-1]+1)) ** self.ticks_per_day - 1) ever_symptomatic[i] = ever_symptomatic[i-1] + new_symptomatic[i] ever_symptomatic_velocity[i] = (((ever_symptomatic[i]+1) / (ever_symptomatic[i-1]+1)) ** self.ticks_per_day - 1) observation_delay_ticks = self.observation_delay * self.ticks_per_day if i >= observation_delay_ticks: observed_ever_symptomatic[i] = ever_symptomatic[i-observation_delay_ticks] observed_ever_symptomatic_velocity[i] = ever_symptomatic_velocity[i-observation_delay_ticks] self.print_extrema(i, t=t, sick=sick, new_sick=new_sick, ever_sick=ever_sick, severe=severe, symptomatic=symptomatic, ever_symptomatic=ever_symptomatic, new_symptomatic=new_symptomatic, asymptomatic=asymptomatic, infected=infected, incubating=incubating, infectious=infectious) # This method call allows us to simulate external actions over # the model: The Goberment! status[i] = self.strategy(t=t, sick=sick[i], ever_sick=ever_sick[i], sick_velocity=sick_velocity[i], new_sick=new_sick[i]*self.ticks_per_day, symptomatic=symptomatic[i], ever_symptomatic=ever_symptomatic[i], observed_ever_symptomatic=observed_ever_symptomatic[i], symptomatic_velocity=symptomatic_velocity[i], observed_ever_symptomatic_velocity=observed_ever_symptomatic_velocity[i], new_symptomatic=new_symptomatic[i]*self.ticks_per_day, infectious=infectious[i], infectable=infectable[i]) if i > 0 and status[i] != status[i-1] and not self.quiet: print(("status flip: %s -> %s, day: %f, " + "sick: %f, daily_new_sick: %f, ever_sick: %f, "+ "symptomatic: %f, daily_new_symptomatic: %f, ever_symptomatic: %f, "+ "observed_ever_symptomatic: %f, "+ "infectious: %f, severe: %f, immune: %f, death: %f") % (status[i-1], status[i], t, sick[i], new_sick[i]*self.ticks_per_day, ever_sick[i], symptomatic[i], new_symptomatic[i]*self.ticks_per_day, ever_symptomatic[i], observed_ever_symptomatic[i], infectious[i], severe[i], immune[i], death[i])) return pd.DataFrame({'day': day, 'infectious': infectious, 'asymptomatic': asymptomatic, 'symptomatic': symptomatic, 'mild': mild, 'severe': severe, 'immune': immune, 'infected': infected, 'death': death, 'healthy': healthy, 'incubating': incubating, 'sick': sick, 'ever_sick': ever_sick, 'new_sick': new_sick, 'ever_symptomatic': ever_symptomatic, 'new_symptomatic': new_symptomatic, 'infectable': infectable, 'velocity': velocity, 'sick_velocity': sick_velocity, 'ever_sick_velocity': ever_sick_velocity, 'symptomatic_velocity': symptomatic_velocity, 'ever_symptomatic_velocity': ever_symptomatic_velocity, 'observed_ever_symptomatic': observed_ever_symptomatic, 'observed_ever_symptomatic_velocity': observed_ever_symptomatic_velocity, 'status': status }) """ # Evaluación Ahora que tenemos un programa que es capaz de simular el avance de la epidemia, vamos a utilizarlo para similar diversos escenarios en cada uno de los cuales seguiremos una linea de acción distinta. ## Caso 1: No hacemos nada Este es el caso de referencia, que pasaría de no haberse hecho nada, si dejasemos a la epidemia seguir su curso natural. """ case1 = SIRwhatever(days = 100).run() """ En la siguiente gráfica se muestran las distintas curvas que caracterizan la evolución de la epidemia. """ with sns.axes_style("whitegrid"): case1.plot(x='day', y=['infected', 'sick', 'symptomatic', 'asymptomatic', 'immune', 'incubating', 'infectable', 'severe', 'death', 'healthy', 'ever_sick', 'observed_ever_symptomatic'], logy=False, ylim=(0, 7e6), figsize=(20,10)) """ Las gráficas de velocidad nos permiten comprobar el ajuste del parametro `daily_transmission_rate` de manera que la simulación tenga un valor similar al de la realidad (en España este valor ha fluctuado entre 0.35 y 0.45). """ with sns.axes_style("whitegrid"): case1.plot(x='day', y=['velocity', 'ever_symptomatic_velocity'], ylim=(-0.4, 0.7), figsize=(20,10)) case1.plot(x='day', y=['velocity', 'ever_symptomatic_velocity'], ylim=(0, 0.6), xlim=(5,40), figsize=(20,10)) """ Dibujar las curvas en escala logaritmica nos permite ver la evolución de variables cuyos valores son siempre relativamente pequeños, como es el caso de `severe`, que indica el número de enfermos que requieren tratamiento en la UCI. """ with sns.axes_style("whitegrid"): case1.plot(x='day', y=['infected', 'sick', 'immune', 'incubating', 'infectable', 'severe', 'death', 'healthy', 'ever_sick'], logy=True, ylim=(10, 7e6), figsize=(20,10)) severe_max = case1.severe.max() public_hospital_beds = 6.662e6 * 0.003 print("max severe: %d, public hospital beds: %d, ratio: %1.1f%%" % (severe_max, public_hospital_beds, 100*public_hospital_beds/severe_max)) """ Y es fácil ver cual sería el problema de la sanidad si no se para la epidemia: solo tendríamos camas de hospital para el 7.3% de los pacientes muy graves que requeririan ser ingresados en la UCI... y si, ya no hablamos de que entren en la UCI, solo de darles una cama en un hospital!!! Con el apoyo de la sanidad privada a lo mejor llegamos al 10 o al 12%, no cambia mucho la situacion. Y eso sin tener en cuenta, que normalmente, las camas de los hospitales ya tienen un grado de ocupación alto, sin necesidad de que vengan los enfermos de coronavirus. """ print("max ever sick: %f, deaths: %f" % (case1.ever_sick.max(), case1.death.max())) """ Practicamente la totalidad de la población pasaría la enfermedad. Con la tasa de mortalidad establecida del 3.4%, habría 227k muertes solo en la Comunidad de Madrid. """ """ ## Caso 2: Paramos el pais para siempre Solo como caso hipotetico que nos permitira ver algunos números interesantes. En esta simulación, al llegar a los 2900 casos (número de casos en Madrid a 14 de marzo, cuando el gobiernos declaro el estado de alerta), se aislara a la población para siempre. Durante el periodo de aislamiento suponemos que la tasa diaria de transmision de la epidemia (`daily_transmission_rate`) se divide por 10 (más adelante contemplaremos otros escenarios, cambiando dicho parametro). """ ## Implementation of the Isolate N days strategy class IsolateNDays(SIRwhatever): def __init__(self, isolation_days = 15, ever_symptomatic_threshold = 2900, isolation_transmission_rate_divider = 10, after_isolation_transmission_rate_divider = 1, **kargs): super().__init__(**kargs) self.isolation_days = isolation_days self.ever_symptomatic_threshold = ever_symptomatic_threshold self.isolation_transmission_rate_divider = isolation_transmission_rate_divider self.after_isolation_transmission_rate_divider = after_isolation_transmission_rate_divider self.isolated = False self.isolated_countdown = 0 def strategy(self, t, observed_ever_symptomatic, **kargs): if self.isolated: if self.isolated_countdown < 1: self.isolated = False self.daily_transmission_rate *= (self.isolation_transmission_rate_divider / self.after_isolation_transmission_rate_divider) self.ever_symptomatic_threshold = self.population_size * 10 # never ever stop # the country, you lazy! else: self.isolated_countdown -= 1 else: if observed_ever_symptomatic > self.ever_symptomatic_threshold: self.isolation_day = t self.isolated = True self.isolated_countdown = self.isolation_days * self.ticks_per_day self.daily_transmission_rate /= self.isolation_transmission_rate_divider if self.isolated: return 'isolated' else: return 'normal' case2 = IsolateNDays(days = 120, isolation_days=1000).run() with sns.axes_style("whitegrid"): case2.plot(x='day', y=['infected', 'sick', 'immune', 'incubating', 'infectable', 'symptomatic', 'infectious', 'severe', 'death', 'healthy', 'ever_sick'], logy=True, ylim=(1, 3e4), figsize=(20,10)) case2.plot(x='day', y=['infected', 'sick', 'immune', 'incubating', 'infectable', 'symptomatic', 'infectious', 'severe', 'death', 'healthy', 'ever_sick'], logy=True, ylim=(1, 1.6e4), xlim=(18, 60), figsize=(20,10)) print("max ever_symptomatic: %d, max symptomatic: %d, max severe: %d, max death: %d" % (case2.ever_symptomatic.max(), case2.symptomatic.max(), case2.severe.max(), case2.death.max())) """ La conclusión es que el número de enfermos sintomaticos en Madrid aun seguirá creciendo durante unos 7 dias hasta casi llegar a los 11k. El número de enfermos que tendran que ingresar en la UCI rondara los 800. En Madrid [hay 641 camas de UCI](https://elpais.com/espana/madrid/2020-03-14/casi-el-30-de-camas-de-la-uci-en-madrid-ya-se-destina-a-pacientes-con-coronavirus.html) así que llegaremos al limite de la capacidad (y eso sin tener en cuenta el grado de ocupación normal de esas camas). Por otro lado vemos que hasta aproximadamente 40 días despues de tomar medidas, no se reduce el número de infecciosos a 10, y aun tardará en torno a 12 días más en llegar a 1 (aunque como ya comente al inicio del notebook, cuando el número de elementos es tan bajo, la precisión del modelo es muy baja y podra haber grandes variaciones entre la predicción y la realidad). Y todo esto, claro, suponiendo que las medidas tomadas por el gobierno son eficaces y que realmente se va a reducir la tasa de transmisión diaria a la décima parte de la actual. """ """ ### Caso 2b: ¿y si estamos siendo muy optimistas? ¿Que pasa si las medidas del gobierno no son tan eficaces y por ejemplo, solo conseguimos reducir el ratio de transmisión a la tercera parte? """ case2b = IsolateNDays(days=300, isolation_transmission_rate_divider=3, isolation_days=1000).run() with sns.axes_style("whitegrid"): case2b.plot(x='day', y=['infected', 'sick', 'immune', 'incubating', 'infectable', 'symptomatic', 'infectious', 'severe', 'death', 'healthy', 'ever_sick'], logy=True, ylim=(10, 7e6), figsize=(20,10)) print("max ever_symptomatic: %d, max symptomatic: %d, max severe: %d" % (case2b.ever_symptomatic.max(), case2b.symptomatic.max(), case2b.severe.max())) """ Ese es, más o menos, el escenario [Flatten The Curve](https://thespinoff.co.nz/society/09-03-2020/the-three-phases-of-covid-19-and-how-we-can-make-it-manageable/) que algunos plantean como la solución ideal... Se ve que no han hecho muchos números, porque por ejemplo, en este caso, la epidemia no empieza a retroceder hasta dentro de medio año. Llegariamos a tener 234k enfermos simultaneos de los cuales unos 25k serian muy graves. Y la relación siempre es igual, si se acorta la epidemia, aumenta el numero de enfermos... si se recorta el número de enfermos simultaneos, se alarga la epidemia... no se pueden tener las dos cosas a la vez. Además, en una simulación de este tipo, siempre hay parametros que pueden estar equivocados o relaciones que pueden ser diferentes a como hemos supuesto e introducir errores, pero precisamente, la relación entre duración y número de enfermos máximo se puede comprobar de otras maneras que es correcta (el area bajo la curva de enfermos es constante cuando prácticamente toda la población se contagia). """ """ ### Caso 2c: ¿Y si estamos siendo muy pesimistas? Tambien puede ser que las medidas del gobierno pulvericen el ratio de transmision diario a por ejemplo, la centesima parte. """ case2c = IsolateNDays(days = 70, isolation_transmission_rate_divider = 100, isolation_days=1000).run() with sns.axes_style("whitegrid"): case2c.plot(x='day', y=['infected', 'immune', 'incubating', 'infectable', 'symptomatic', 'infectious', 'severe', 'death', 'healthy', 'ever_sick'], logy=True, ylim=(1, 2e4), figsize=(20,10)) """ ### Caso 2d: ¿y si consiguiesemos cortar los contagios completamente? Estudiemos el caso limite, en el que cortamos completamente los contagios (en el simulador vamos a dividir el ratio de contagio diario por $10^8$). Aunque sea un caso irreal, esta simulación nos va a permitir conocer cual es el tope inferior de la duración de la epidemia. """ case2d = IsolateNDays(days=80, isolation_transmission_rate_divider=1e8, isolation_days=1000).run() with sns.axes_style("whitegrid"): case2d.plot(x='day', y=['infected', 'symptomatic', 'immune', 'incubating', 'infectable', 'infectious', 'severe', 'death', 'healthy', 'ever_sick'], logy=True, ylim=(1, 2e4), figsize=(20,10)) """ Vemos que el resultado es practicamente identico al caso anterior, en el que reduciamos el ratio de transferencia diario a la centesima parte. La gráfica nos muestra que habría que esperar aproximadamente 3 semanas para reducir el numero de infecciosos a 10. Esas tres semanas de riesgo corresponden exclusivamente a las personas que se infectaron antes de que se tomasen medidas de aislamiento y vienen determinadas por las distribuciones de probabilidad que hemos seleccionado para los tiempos de incubación y de convalecencia. Se tardo tanto en tomar medidas de aislamiento, que la masa de personas infecciosas es enorme, con un pico por encima de las 3k personas. Eso hace que aunque la probabilidad de que el proceso de incubación sea superior a tres semanas es muy bajo, en números absolutos nos movemos en el rango de las decenas. """ with sns.axes_style("whitegrid"): case2d.plot(x='day', y=['velocity', 'ever_symptomatic_velocity'], ylim=(-0.4, 0.6), figsize=(20,10)) """ ### Comparación de los subcasos Ahora comparemos las curvas de infecciosos de los cuatro subcasos: """ with sns.axes_style("whitegrid"): fig, ax = plt.subplots(figsize=(20, 10)) ax.set(ylim=(-0.6, 0.5)) case2.plot(x='day', y='velocity', logy=False, ax=ax, xlim=(15, 70), label='infectious case 2') case2b.plot(x='day', y='velocity', logy=False, ax=ax, xlim=(15, 70), label='infectious case 2b') case2c.plot(x='day', y='velocity', logy=False, ax=ax, xlim=(15, 70), label='infectious case 2c') case2d.plot(x='day', y='velocity', logy=False, ax=ax, xlim=(15, 70), label='infectious case 2d') with sns.axes_style("whitegrid"): fig, ax = plt.subplots(figsize=(20, 10)) ax.set(ylim=(1, 1e4)) case2.plot(x='day', y='infectious', logy=True, ax=ax, xlim=(15, 70), label='infectious case 2') case2b.plot(x='day', y='infectious', logy=True, ax=ax, xlim=(15, 70), label='infectious case 2b') case2c.plot(x='day', y='infectious', logy=True, ax=ax, xlim=(15, 70), label='infectious case 2c') case2d.plot(x='day', y='infectious', logy=True, ax=ax, xlim=(15, 70), label='infectious case 2d') """ Podemos ver que el efecto de reducir en 100 veces el ratio de transmision aun tiene una importante influencia en el ritmo al que se reduce el número de personas contagiosas dentro de la población pero más alla de ese punto, ya no influye casi nada. En cualquier caso, viendo esta gráfica la cuestión que cabe plantearse es si vale la pena mantener el pais en un estado de aislamiento no completo, permitiendo que la gente acuda a su trabajo e incluso utilice el transporte publico. ¿Es mejor este aislamiento parcial al que estamos sometidos que uno total que haga que la epidemia pase lo antes posible? """ """ ## Caso 3: Paramos el pais durante 15 días En este escenario, de nuevo, al llegar a los 2900 casos, todo la población de la comunidad se aisla durante 15 días. Pasado ese periodo, volvemos al estado normal de libre circulación de personas. Igual que en el caso 2, suponemos que con el aislamiento se reduce el ratio de transmision diario a la décima parte. """ case3 = IsolateNDays(days = 120, isolation_days=15).run() with sns.axes_style("whitegrid"): case3.plot(x='day', y=['infected', 'symptomatic', 'immune', 'incubating', 'infectable', 'infectious', 'severe', 'death', 'healthy', 'ever_sick', 'new_sick'], logy=True, ylim=(10, 7e6), figsize=(20,10)) case3.plot(x='day', y=['infected', 'symptomatic', 'immune', 'incubating', 'infectable', 'infectious', 'severe', 'death', 'healthy', 'ever_sick', 'new_sick'], logy=False, ylim=(10, 2e4), xlim=(18, 50), figsize=(20,10)) """ De nuevo llegariamos a cerca de los 11k enfermos sintomáticos para luego descender hasta 3k. Una vez levantado el aislamiento la epidemia volveria a extenderse y dos semanas despues estariamos en el mismo punto que cuando se instauro el aislamiento. El último día de aislamiento aun estaríamos viendo un incremento de 250 nuevos enfermos cada día. ### Caso 3b: ¿Probamos a parar un mes? En vista de que parar 15 días no es suficiente, ¡probemos a hacerlo durante un mes! """ case3b = IsolateNDays(days = 120, isolation_days=30).run() with sns.axes_style("whitegrid"): case3b.plot(x='day', y=['infected', 'symptomatic', 'immune', 'incubating', 'infectable', 'severe', 'death', 'healthy', 'ever_sick', 'infectious'], logy=True, ylim=(10, 7e6), figsize=(20,10)) """ Pasados los 30 días de confinamiento, aun estariamos viendo alrededor de 20 enfermos nuevos cada día. Aproximadamente, aun tendriamos 40 personas contagiosas desperdigadas por la comunidad (es importante tener en cuenta que en la realidad esas personas contagiosas no sabriamos quienes son ni donde están, por lo que no se podrían aislar). Al levantar el confinamiento, esas 40 personas reiniciarian la expansión de la epidemia y 2 semanas despues otra vez estaríamos en el punto de partida. La reexpansión de la epidemia iría esta vez tan rapido como la primera y sería completamente estúpido pensar de nuevo que podríamos frenarla. """ """ ### Caso 3c: paramos el pais completamente durante un mes y medio y luego mantenemos unas condiciones menos severas En este caso se estudia el escenario en el que se mantiene la situación de aislamiento total (ratio de transmision diario reducido a la decima parte) durante 45 días y despues de ese periodo se implanta un aislamiento menos estricto (ratio de transmision diario reducido a la tercera parte). """ case3c = IsolateNDays(days = 150, isolation_days=45, isolation_transmission_rate_divider=10, after_isolation_transmission_rate_divider=3).run() with sns.axes_style("whitegrid"): case3c.plot(x='day', y=['infected', 'symptomatic', 'immune', 'incubating', 'infectable', 'severe', 'death', 'healthy', 'ever_sick', 'infectious'], logy=True, ylim=(10, 7e6), figsize=(20,10)) """ Al levantar la politica de aislamiento estricta aun tendriamos algunas personas contagiosas desperdigadas por la comunidad. Mantener a partir de ese punto ciertas medidas de aislamiento relentizaría la expansión de la epidemia y quiza, nos permitiese manejar esos pocos focos de contagío que pudiesen quedar, reduciendolos a tiempo. En cualquier caso, notese que los factores multiplicadores del ratio de transmisión son valores puestos completamente a ojo (muy dificiles de medir en la practica) y cualquier variación de los mismos significaría un cambio radical en la velocidad de re-expansion de la enfermedad. Podemos tratar de ver la influencia de esos factores probando con varios distintos: """ case3d2 = IsolateNDays(days = 120, isolation_days=30, isolation_transmission_rate_divider=10, after_isolation_transmission_rate_divider=2).run() case3d4 = IsolateNDays(days = 120, isolation_days=30, isolation_transmission_rate_divider=10, after_isolation_transmission_rate_divider=4).run() case3d5 = IsolateNDays(days = 120, isolation_days=30, isolation_transmission_rate_divider=10, after_isolation_transmission_rate_divider=5).run() """ Representemos el número de infectados y la velocidad a la que evolucionan. """ with sns.axes_style("whitegrid"): fig, ax = plt.subplots(figsize=(20, 10)) ax.set(ylim=(1, 1e4)) case3b.plot(x='day', y='infectious', logy=True, ax=ax, xlim=(15, 70), label='after divider 1.0') case3c.plot(x='day', y='infectious', logy=True, ax=ax, xlim=(15, 70), label='after divider 3.0') case3d2.plot(x='day', y='infectious', logy=True, ax=ax, xlim=(15, 70), label='after divider 2.0') case3d4.plot(x='day', y='infectious', logy=True, ax=ax, xlim=(15, 70), label='after divider 4.0') case3d5.plot(x='day', y='infectious', logy=True, ax=ax, xlim=(15, 70), label='after divider 5.0') fig, ax = plt.subplots(figsize=(20, 10)) ax.set(ylim=(-0.4, 0.6)) case3b.plot(x='day', y='velocity', logy=False, ax=ax, xlim=(15, 70), label='after divider 1.0') case3c.plot(x='day', y='velocity', logy=False, ax=ax, xlim=(15, 70), label='after divider 3.0') case3d2.plot(x='day', y='velocity', logy=False, ax=ax, xlim=(15, 70), label='after divider 2.0') case3d4.plot(x='day', y='velocity', logy=False, ax=ax, xlim=(15, 70), label='after divider 4.0') case3d5.plot(x='day', y='velocity', logy=False, ax=ax, xlim=(15, 70), label='after divider 5.0') """ La conclusión que podemos sacar de estas gráficas es que habra que ser muy cautos en la fase post-aislamiento y seguir reduciendo las relaciones humanas al máximo, mientras se localizan personas o lugares expuestos porque si las condiciones de aislamiento resultan en un ratio de transmisión más alto de lo esperado, otra vez se podría disparar la epidemia. """ """ ## Caso 4: Paramos el pais hasta que dejen de aparecer nuevos enfermos En este escenario aislamos el pais completamente hasta que dejen de aparecer enfermos, luego por si acaso aun esperamos unos días más antes de levantar las restricciones. Además, en el supuesto de que volviesen a aparecer nuevos casos, de nuevo iniciariamos el aislamiento, pero cada vez poniendo el umbral de nuevos casos mas bajo. """ class EverybodyIsHealthy(SIRwhatever): def __init__(self, isolation_safe_days = 10, isolation_transmission_rate_divider = 10, ever_symptomatic_thresholds = [2900, 50, 10, 10, 1], **kargs): super().__init__(**kargs) self.isolated = False self.isolation_transmission_rate_divider = isolation_transmission_rate_divider self.isolation_safe_days = isolation_safe_days self.ever_symptomatic_thresholds = ever_symptomatic_thresholds self.flips = 0 self.ever_symptomatic_at_flop = 0 def strategy(self, t, sick, new_symptomatic, infectious, observed_ever_symptomatic, **_): ever_symptomatic_threshold_ix = (self.flips if self.flips < len(self.ever_symptomatic_thresholds) else -1) ever_symptomatic_threshold = self.ever_symptomatic_thresholds[ever_symptomatic_threshold_ix] if self.isolated: if new_symptomatic < 1: self.isolation_safe_days_countdown -= 1 if self.isolation_safe_days_countdown < 1: self.isolated=False self.daily_transmission_rate *= self.isolation_transmission_rate_divider self.ever_symptomatic_at_flop = observed_ever_symptomatic else: if (observed_ever_symptomatic - self.ever_symptomatic_at_flop > ever_symptomatic_threshold): self.isolated=True self.isolation_safe_days_countdown = self.isolation_safe_days self.daily_transmission_rate /= self.isolation_transmission_rate_divider self.flips += 1 return ('isolated' if self.isolated else 'normal') case4 = EverybodyIsHealthy(days = 250).run() with sns.axes_style("whitegrid"): case4.plot(x='day', y=['infected', 'symptomatic', 'immune', 'incubating', 'severe', 'asymptomatic', 'ever_sick', 'infectable'], logy=False, figsize=(20,10), ylim=(0, 2e4)) case4.plot(x='day', y=['infected', 'symptomatic', 'immune', 'incubating', 'severe', 'asymptomatic', 'ever_sick', 'infectable'], logy=True, figsize=(20,10), ylim=(1, 2e4)) """ Este escenario pueda parecer absurdo pero cobra sentido cuando tenemos en cuenta nuestro contexto global. Aunque en un tiempo razonable consiguiesemos eliminar completamente el virus en Madrid y en toda España, este seguirá desarrollandose en otras partes del mundo y a no ser que cerremos nuestras fronteras a cal y canto, seguiremos recibiendo un goteo poco predecible de personas infectadas que en cualquier momento pueden reiniciar la epidemia. Confiemos en que para llegado ese momento, nuestro nivel de concienciación tanto a nivel personal como de nuestros gobernantes, nos fuerce a actuar de una manera más eficaz, permitiendonos detener la epidema con prontitud, por ejemplo, aplicando aislamientos con premura en aquellos lugares donde se detecten nuevos casos. Pero aun así, parece poco razonable pensar que mantener al pais en ese estado de alerta, sea el contexto adecuado para propiciar la recuperación economica. ¿Podriamos estar aislando ciudades de España durante periodos de varias semanas cada vez que se detecte un nuevo caso? En realidad este escenario lo que pone de manifiesto es la cortedad de miras de nuestros gobernantes, que han sido incapaces de coordinarse a nivel europeo para plantar cara de forma conjunta a la epidemia. El aislamiento hubiese sido mucho más eficaz si toda Europa lo hiciese a la vez. Ahora tendremos paises que van a distintas velocidades y eso nos va a obligara a mantener las fronteras cerradas durante mucho tiempo. """ """ ## Caso 5: Contagio estratificado controlado Con las soluciones de aislamiento estudiadas hasta ahora, aunque se consiga parar la epidemia en España, no se habrá desarrollado immunidad de grupo y eso da lugar al problema mencionado con anterioridad relativo a que cualquier nuevo caso importado puede desencadenar de nuevo la epidemia. Por ese motivo, tambien hay quien proopone aislar unicamente a las personas de riesgo y dejar que el resto de la población, se contagie. Por ejemplo, ¿que pasaría si una vez reducida la epidemia actual enviasemos a todas las personas mayores de 50 años (el 68% de la población de Madrid segun datos del INE) en autobuses camino de Benidorm al tanto que dejamos que la epidemia se extienda de nuevo en Madrid entre el resto de la población? Pasado un tiempo, la mayoria de los madrileños menores de 50 años habrán pasado la enfermedad y serán immunes. Puede que si entonces dejamos que nuestros mayores vuelvan, aun nos mantengamos por debajo del punto critico a partir del cual la epidemia es viable. Como sociedad, ¿seremos immunes al coronavirus? En concreto, vamos a realizar la simulación de un escenario con las siguientes etapas: 1. Crecimiento rampante de la epidemia 2. Como en los casos anteriores, al alcanzar los 2900 enfermos symptomaticos se aplican medidas de aislamiento que reducen el ratio de transmisión de la enfermedad a la decima parte. 3. Una vez se alcanza el punto donde ya no aparecen más enfermos durante varios días, simulamos eliminar a las personas mayores de 50 años y dejamos que la enfermedad progrese entre el resto. 4. La enfermedad se propaga, contagía a la mayor parte de la población y cae. 5. Reintroducimos a las personas mayores de 50 años sin tomar ningun otro tipo de medida y vemos que pasa. La mortalidad por edades viene reflejada en la siguiente tabla: """ mortality_table=pd.read_csv(io.StringIO(""" population,start_age,end_age,mortality 704650,0,9,0.0 590337,10,19,0.002 834247,20,29,0.002 1227008,30,39,0.002 1049187,40,49,0.004 782873,50,59,0.013 597152,60,69,0.036 420139,70,79,0.08 284087,80,200,0.148 """)) """ Para mantener la coherencia con el resto de simulaciones, no usaremos dicha tabla tal cual, si no que la escalaremos hasta conseguir que la mortalidad global tenga el valor 3.4% (el mismo valor que hemos utilizado en el resto de casos). """ class StratifiedSpread(SIRwhatever): def __init__(self, rampant_ever_symptomatic_threshold = 2900, isolated_new_symptomatic_threshold = 1, isolated_transmission_rate_divider = 10, countdown_days = 10, stratified_transmission_rate_multiplier = 1, stratified_exclusion_age = 60, mortality_table = mortality_table, stratified_new_symptomatic_threshold = 1, **kargs): super().__init__(**kargs) self.ss_state = 'rampant' self.rampant_ever_symptomatic_threshold = rampant_ever_symptomatic_threshold self.isolated_new_symptomatic_threshold = isolated_new_symptomatic_threshold self.isolated_transmission_rate_divider = isolated_transmission_rate_divider self.countdown_days = countdown_days self.stratified_exclusion_age = stratified_exclusion_age self.stratified_new_symptomatic_threshold = stratified_new_symptomatic_threshold self.mortality_table = mortality_table self.strategies = { k: getattr(self, "strategy_" + k) for k in ['rampant', 'isolated', 'countdown', 'stratified', 'everybody'] } self.starts = { k: getattr(self, "start_" + k) for k in ['rampant', 'isolated', 'countdown', 'stratified', 'everybody'] } # we keep using the data given by the user, so we calculate the scaling # factors for the mortality table accordingly: mt_population_size = mortality_table.population.sum() self.mt_population_multiplier = self.population_size / mt_population_size mt_mortality_ratio = ((mortality_table.population * mortality_table.mortality).sum() / mt_population_size) self.mt_mortality_multiplier = self.mortality_ratio / mt_mortality_ratio def strategy(self, **kargs): old_state = self.ss_state state_strategy = self.strategies[old_state] new_state = state_strategy(**kargs) if new_state is None or new_state == old_state: return old_state self.ss_state = new_state state_start = self.starts[new_state] state_start(**kargs) return new_state def start_rampant(self, **kargs): pass # def strategy_rampant(self, observed_ever_symptomatic, **kargs): if (observed_ever_symptomatic > self.rampant_ever_symptomatic_threshold): return 'isolated' def start_isolated(self, **kargs): self.daily_transmission_rate /= self.isolated_transmission_rate_divider def strategy_isolated(self, new_symptomatic, **kargs): if new_symptomatic < self.isolated_new_symptomatic_threshold: return 'countdown' def start_countdown(self, **kargs): self.countdown_ticks = self.countdown_days * self.ticks_per_day def strategy_countdown(self, **kargs): if self.countdown_ticks < 1: self.daily_transmission_rate *= self.isolated_transmission_rate_divider return 'stratified' self.countdown_ticks -= 1 def start_stratified(self, observed_ever_symptomatic, **kargs): sxa = self.stratified_exclusion_age mt = self.mortality_table mt_below_sxa = mt[mt.start_age < sxa] mt_over_sxa = mt[mt.end_age > sxa] mt_below_sxa_population = mt_below_sxa.population.sum() mt_over_sxa_population = mt_over_sxa.population.sum() mortality_ratio = ((mt_below_sxa.mortality*mt_below_sxa.population).sum()/ mt_below_sxa_population)*self.mt_mortality_multiplier multiplier = mortality_ratio/self.mortality_ratio self.mortality_ratio *= multiplier self.severe_ratio *= multiplier self.population_size = (self.population_size * mt_below_sxa_population / (mt_below_sxa_population + mt_over_sxa_population)) print("multiplier: %f, mortality_ratio: %f, population_size: %f" % (multiplier, self.mortality_ratio, self.population_size)) def strategy_stratified(self, new_symptomatic, observed_ever_symptomatic, **kargs): if (new_symptomatic < self.stratified_new_symptomatic_threshold and observed_ever_symptomatic > 0.33 * self.population_size): return 'everybody' def start_everybody(self, infectable, **kargs): sxa = self.stratified_exclusion_age mt = self.mortality_table mt_below_sxa = mt[mt.start_age < sxa] mt_over_sxa = mt[mt.end_age > sxa] mt_below_sxa_population = mt_below_sxa.population.sum() mt_over_sxa_population = mt_over_sxa.population.sum() stratified_population_size = self.population_size excluded_population_size = (stratified_population_size * mt_over_sxa_population / mt_below_sxa_population) excluded_population_size_adjusted = excluded_population_size * self.mt_population_multiplier stratified_mortality_rate = ((mt_below_sxa.mortality*mt_below_sxa.population).sum()/ mt_below_sxa_population) excluded_mortality_rate = ((mt_over_sxa.mortality*mt_over_sxa.population).sum()/ mt_over_sxa_population) combined_mortality_rate = ((stratified_mortality_rate * infectable + excluded_mortality_rate * excluded_population_size_adjusted) / (infectable + excluded_population_size_adjusted)) multiplier = combined_mortality_rate / self.mortality_ratio self.mortality_ratio *= multiplier self.severe_ratio *= multiplier self.population_size = ((self.population_size / stratified_population_size) * stratified_population_size + excluded_population_size) print("multiplier: %f, mortality_ratio: %f, population_size: %f" % (multiplier, self.mortality_ratio, self.population_size)) def strategy_everybody(self, **kargs): pass case5 = StratifiedSpread(days=400, stratified_exclusion_age=50).run() with sns.axes_style("whitegrid"): case5.plot(x='day', y=['infected', 'sick', 'immune', 'incubating', 'infectable', 'asymptomatic', 'infectious', 'severe', 'death', 'healthy', 'ever_sick', 'new_sick'], logy=True, ylim=(1, 9e6), figsize=(20,10)) case5.plot(x='day', y='velocity', logy=False, ylim=(-0.3, 0.6), figsize=(20,5)) case5.velocity[case5.day==350] """ Vemos que aunque finalmente no se detenga la epidemia, sí conseguimos reducir considerablemente el factor de crecimiento diario de la misma a la decima parte, de 0.4 a a 0.04. Eso hará sin duda que la epidemia sea mucho más controlable. """ case5.death[case5.day==173] case5.severe[case5.day < 173].max() """ Desafortunadamente el número de muertes entre la población menor de 50 años sería de 19k y llegariamos a tener a 22k enfermos graves simultaneamente. No obstante, habría que plantearse si es posible reducir el ratio de muertos particionando a la población en base a criterios más sofisticados, como por ejemplo, teniendo en cuenta el estado fisico de las personas. Tambien hay que tener en cuenta que los ratios de mortalidad y de enfermos que necesitan hospitalización son por ahora muy poco precisos, existiendo gran divergencia entre los distintos estudios publicados. """ """ ### Caso 5b: Aislamos a los mayores de 60 años Probemos a dejar fuera a la población mayor de 60 años que representa el 20% del total. """ case5b = StratifiedSpread(days=300, stratified_exclusion_age=60).run() with sns.axes_style("whitegrid"): case5b.plot(x='day', y=['infected', 'sick', 'immune', 'incubating', 'infectable', 'infectious', 'severe', 'death', 'healthy', 'ever_sick', 'new_sick'], logy=True, ylim=(1, 7e6), figsize=(20,10)) case5b.plot(x='day', y='velocity', logy=False, ylim=(-0.3, 0.6), figsize=(20,5)) """ En este caso podemos ver que la epidemia desaparece. La immunidad de grupo no le permite desarrollarse ya más en nuestra comunidad. """ case5b.death.max() case5b.severe.max() """ Vemos que el coste es alto, llegando a morir mas de 38k personas solo en la Comunidad de Madrid. """ """ ### Caso 5c: Aislamos a los mayores de 40 años Dejando que la enfermendad se extienda entre los menores de 50 o 60 años hemos visto que tiene un coste muy alto en muertes. Vamos a probar con los menores de 40 para ver si eso proporcionaría alguna ventaja. """ case5c = StratifiedSpread(days=300, stratified_exclusion_age=40).run() with sns.axes_style("whitegrid"): case5c.plot(x='day', y=['infected', 'sick', 'immune', 'incubating', 'infectable', 'infectious', 'severe', 'death', 'healthy', 'ever_sick', 'new_sick'], logy=True, ylim=(1, 7e6), figsize=(20,10)) case5c.plot(x='day', y='velocity', logy=False, ylim=(-0.3, 0.6), figsize=(20,5)) case5c[case5c.day==200].velocity """ Como en el caso de 50 años, vemos que no se detiene la epidemia, pero aun así se relentiza su expansion. En este caso el factor de velocidad se habra reducido a casi la tercera parte, de 0.4 a 0.14. El coste en vidas, antes de la etapa final sería todavía extremadamente alto, aproximadamente de 11k, todos menores de 40 años. """ """ # Efecto de los parametros en el modelo Algunos de los parametros utilizados en el simulador han sido elegidos sin demasiada rigurosidad. Antes de acabar vamos a estudiar el efecto de perturbar esos parametros sobre los resultados obtenidos de cara a determinar si introducen mucha o poca variacion en los mismos. Centremonos en los 3 parametros que pueden tener un efecto mayor en la salida del simulador: 1. Variancia del periodo de incubación. 2. Tiempo medio de convalecencia de los enfermos asintomaticos. 3. Variancia del periodo de convalecencia de los enfermos asintomaticos. ## Efecto de las varianzas Primero repetimos la simulacíon del caso 2 con variaciones en los parametros de varianza. """ fig, ax = plt.subplots(figsize=(20, 10)) ax.set(ylim=(1, 1e4)) for incubation_variance in [3, 7, 12]: for asymptomatic_convalescent_period_variance in [3, 6, 12]: df = IsolateNDays(days = 70, isolation_transmission_rate_divider = 10, asymptomatic_convalescent_period_variance=asymptomatic_convalescent_period_variance, incubation_variance=incubation_variance, isolation_days=70, quiet=True).run() df.plot(x='day', y='infectious', logy=True, ax=ax, xlim=(15, 70), label=('iv: %f, acpv: %f' % (incubation_variance, asymptomatic_convalescent_period_variance))) """ Se puede apreciar que el efecto de dichos parametros en los resultados de la simulación es relativamente pequeño. Eso nos indica que un error en la estimación de esos parametros no genera un error del mismo calibre en el resultado de la simulación. ## Efecto del tiempo medio de convalecencia asintomática Experimentemos ahora variando el tiempo medio de convalecencia asintomatica. """ fig, axs = plt.subplots(2, figsize=(20, 15)) axs[0].set(ylim=(1, 2e4)) axs[1].set(ylim=(-0.2, 0.45)) for asymptomatic_convalescent_period_mean in [0.5, 1, 2, 3, 5, 8]: def f(x): df = SIRwhatever(days = 20, asymptomatic_convalescent_period_mean=asymptomatic_convalescent_period_mean, quiet=True, daily_transmission_rate=x).run() v = df[df.day==10].symptomatic_velocity - 0.4 # print("m: %f, x: %f, v: %f" % (asymptomatic_convalescent_period_mean, x, v)) return v daily_transmission_rate = scipy.optimize.bisect(f, 1, 4, rtol=0.002) df = IsolateNDays(days = 120, daily_transmission_rate = daily_transmission_rate, isolation_transmission_rate_divider = 20, asymptomatic_convalescent_period_mean=asymptomatic_convalescent_period_mean, isolation_days=100, quiet=True).run() print(("asymptomatic_convalescent_period_mean: %f, "+ "daily_transmission_rate: %f, symptomatic_velocity: %f") % (asymptomatic_convalescent_period_mean, daily_transmission_rate, df.symptomatic_velocity[df.day == 10])) df.plot(x='day', y='infectious', logy=True, ax=axs[0], xlim=(15,100), label=('m: %f, dtr: %f' % (asymptomatic_convalescent_period_mean, daily_transmission_rate))) df.plot(x='day', y='velocity', logy=False, ax=axs[1], xlim=(15,100), label=('m: %f, dtr: %f' % (asymptomatic_convalescent_period_mean, daily_transmission_rate))) """ Se aprecia que, una vez ajustado el ratio de transmisión diario para que la velocidad de infección se mantenga en el valor 0.4, la influencia del tiempo de convalecencia asintomatico medio tampoco es muy grande. En cualquier caso, a lo largo del presente estudio hemos utilizado el valor de 2 dias que es conservador y se encuentra en una franja de poca variación. Si el valor real del parametro resultase ser mayor, el efecto sería tiempos de evolución más largos a los aquí obtenidos. """ """ # Conclusiones """ """ 1. En el mejor de los casos, el número de enfermos en la Comunidad de Madrid parara de crecer hasta llegar a 11k. El número maximo de enfermos que necesiten ser ingresados en la UCI de manera simultanea rondara los 800. 2. Igualmente, en el caso ideal, suponiendo un aislamiento completo de la población capaz de eliminar cualquier nuevo contagio, aun tendrían que pasar 3 semanas para que el numero de personas capaz de transmitir la enfermedad en la comunidad bajase a 10. 3. Asumiendo hipotesis más realistas sobre la eficacia de las medidas del gobierno, el tiempo necesario para llegar a reducir el número de personas contagiosas a 10, pasa a ser de 40 días. 4. Si se para la epidemia, no habremos desarrollado immunidad de grupo por lo que cualquier nuevo caso podría relanzar el proceso. Esto puede ser causado tanto por personas contagiosas aun presentes en la población o por casos importados. Estamos hablando de un problema a nivel global. No es suficiente con eliminar el virus en Madrid, España o Europa. 5. Igualmente, una vez parada la epidemia, sera aconsejable mantener un nivel de aislamiento parcial que limite la velocidad de propagación de nuevos focos. 6. Permitir que la epidemia se expanda de manera controlada entre los estratos de la población con menos riesgo nos permitiría lograr immunidad de grupo, pero el número de muertos seria notable. Añadir por último que como en cualquier simulación, la validez de los resultados depende en gran medida de las hipotesis realizadas, especialmente de los valores de parametrización, por lo que cualquier valor aquí dado puede tener un grado de error importante. No obstante a nivel cualitativo si podemos comparar las curvas de evolución aquí generadas con la realidad para permitirnos determinar en que escenarios nos encontramos y tomar medidas correctivas si resulta no ser el que esperamos! Reciprocamente, según pase el tiempo, podremos ir ajustando los parametros de la simulación tratando de ajustarlos a lo que vemos en la realidad de cara a predecir con mayor precisión el futuro. """ """ # Epílogo Voy a utilizar los comentarios y preguntas sobre la simulación que me esta enviando la gente para expandirla con algunos nuevos escenarios que creo que son interesantes. Tambien voy a aprovechar para responder a algunas preguntas que considero que son interesantes. ## ¿Y las vacunas? En la simulación no se habla para nada de lo que va a pasar cuando aparezca la vacuna (suponiendo que se consigue, ¡claro!). Para poder introducir el efecto de la vacuna en la simulación tendriamos que conocer diversos parametros sobre la misma, siendo el más importante cuando va a estar lista, y eso sin una bola de cristal... De todas formas, en cuanto exista la vacuna y se administre a la población el problema se acaba. Como se mostraba en el caso de propagacion controlada estratificada, una vez se immunice a al menos, el 80% de la población la epidemia ya no es viable y se extinguira por si sola. ## Ratio de enfermos asintomáticos El ratio de enfermos asintomáticos esta extraido de un estudio que se hizo con los pasajeros del crucero Diana Princess. Probablemente los turistas de un crucero no sea un grupo representivo de la población general, especialmente en lo que a distribución de edades se refiere. ¿Que pasaría si dicho paramero estuviese muy subestimado? Vamos a verlo probemos de nuevo a simular algunos de los escenarios anteriores con valores distintos del ratio de enfermos asintomaticos. ### Caso E1a: Aislamiento de la población para siempre cuando hay muchos enfermos asintomáticos En la siguiente ejecución del simulador se utilizan los siguientes valores del parametro `asymptomatic_rate`: 17%, 30%, 50% y 80%. El código es un poco más complicado que en los casos estudiados hasta ahora porque hay que ajustar el resto de parametros de manera acorde. Se tienen que mantener las proporciones entre los enfermos leves y severos y a la vez conseguir que la velocidad de infección inicial sea 0.4. """ with sns.axes_style("whitegrid"): fig, axs = plt.subplots(2, figsize=(20, 15)) axs[0].set(ylim=(1, 8e4)) axs[1].set(ylim=(-0.2, 0.45)) for asymptomatic_ratio in [0.17, 0.3, 0.5, 0.8]: # we need to adjust the severe and mortality ratios accordingly severe_ratio = 0.05 / (1 - 0.17) * (1 - asymptomatic_ratio) mortality_ratio = 0.034 / (1 - 0.17) * (1 - asymptomatic_ratio) # we also need to find the daily_transmission_rate for which ever_sick_velocity is 0.4 def f(x): df = SIRwhatever(days = 40, asymptomatic_ratio=asymptomatic_ratio, mortality_ratio=mortality_ratio, severe_ratio=severe_ratio, quiet=True, daily_transmission_rate=x).run() v = df[df.day==20].observed_ever_symptomatic_velocity - 0.4 # print("m: %f, x: %f, v: %f" % (asymptomatic_convalescent_period_mean, x, v)) return v daily_transmission_rate = scipy.optimize.bisect(f, 1, 4, rtol=0.002) print("asymptomatic_ratio: %f, severe_ratio: %f, mortality_ratio: %f, daily_transmission_rate: %f" % (asymptomatic_ratio, severe_ratio, mortality_ratio, daily_transmission_rate)) df = IsolateNDays(days = 150, daily_transmission_rate=daily_transmission_rate, isolation_transmission_rate_divider=10, asymptomatic_ratio=asymptomatic_ratio, mortality_ratio=mortality_ratio, severe_ratio=severe_ratio, isolation_days=200).run() label = " ar: %f" % asymptomatic_ratio ys = ['infectious', 'symptomatic', 'death', 'immune', 'infectable'] df.plot(x='day', y=ys, logy=True, ax=axs[0], xlim=(15,100), label=[y + label for y in ys]) velocity_ys = ['velocity', 'symptomatic_velocity'] df.plot(x='day', y=velocity_ys, logy=False, ax=axs[1], xlim=(15,100), label=[y + label for y in velocity_ys]) """ Viendo las gráficas se puede apreciar que se relentizaría el tiempo de extinción de la epidemia. La razón es que la masa total de infectados/contagiosos crece proporcionalmente al ratio de enfermos asintomáticos. """ """ ### Caso E1b: ¿que pasa si no hacemos nada y el ratio de enfermos asintomáticos es muy alto? Podemos hacer la misma simulación para el caso en el que no se toman medidas, que sería el peor caso posible, para ver como los distintos valores del ratio de enfermos asintomaticos afectan al número de muertes y de enfermos graves. """ with sns.axes_style("whitegrid"): fig, axs = plt.subplots(2, figsize=(20, 15)) axs[0].set(ylim=(1, 7e6)) axs[1].set(ylim=(-0.4, 0.45)) for asymptomatic_ratio in [0.17, 0.4, 0.8]: # we need to adjust the severe and mortality ratios accordingly severe_ratio = 0.05 / (1 - 0.17) * (1 - asymptomatic_ratio) mortality_ratio = 0.034 / (1 - 0.17) * (1 - asymptomatic_ratio) # we also need to find the daily_transmission_rate for which ever_sick_velocity is 0.4 def f(x): df = SIRwhatever(days = 40, asymptomatic_ratio=asymptomatic_ratio, mortality_ratio=mortality_ratio, severe_ratio=severe_ratio, quiet=True, daily_transmission_rate=x).run() v = df[df.day==20].observed_ever_symptomatic_velocity - 0.4 # print("m: %f, x: %f, v: %f" % (asymptomatic_convalescent_period_mean, x, v)) return v daily_transmission_rate = scipy.optimize.bisect(f, 1, 4, rtol=0.002) print("asymptomatic_ratio: %f, severe_ratio: %f, mortality_ratio: %f, daily_transmission_rate: %f" % (asymptomatic_ratio, severe_ratio, mortality_ratio, daily_transmission_rate)) df = SIRwhatever(days = 80, daily_transmission_rate=daily_transmission_rate, asymptomatic_ratio=asymptomatic_ratio, mortality_ratio=mortality_ratio, severe_ratio=severe_ratio).run() print("asymptomatic_ratio: %f, deaths: %f" % (asymptomatic_ratio, df.death.max())) label = " ar: %f" % asymptomatic_ratio ys = ['infectious', 'symptomatic', 'death', 'severe', 'immune', 'infectable'] df.plot(x='day', y=ys, logy=True, ax=axs[0], xlim=(15,100), label=[y + label for y in ys]) velocity_ys = ['velocity', 'symptomatic_velocity'] df.plot(x='day', y=velocity_ys, logy=False, ax=axs[1], xlim=(15,100), label=[y + label for y in velocity_ys]) """ Como era de esperar, vemos que el número de muertes cae al crecer el ratio de enfermos asintomaticos. No obstante, aun en el mejor caso, cuando le damos el valor 0.8, el número de muertes estaria por encima de 50k (y solo en la Comunidad de Madrid). """ """ ## Caso E2: 9 días despues Ya han pasado 9 días desde que se declaro el estado de alerta y la velocidad de crecimiento de la epidemia en España parece que se ha estabilizado en torno al 20%. Podemos utilizar ese dato para ajustar la simulación y ver en que escenario nos encontramos. """ days_after = 9 observed_velocity = 0.19 # First we need to look for the daily_transmission_rate_divisor that # makes the velocity go down to 0.19 one week after def f(x): sim = IsolateNDays(days=80, quiet=True, isolation_transmission_rate_divider=x) df = sim.run() v = df[df.day==(sim.isolation_day + days_after)].observed_ever_symptomatic_velocity - observed_velocity return v isolation_transmission_rate_divider_e2 = scipy.optimize.bisect(f, 1, 10, rtol=0.002) print("isolation_transmission_rate_divider: %f" % isolation_transmission_rate_divider_e2) """ El valor que optenemos para el divisor del ratio de transmisión diario es muy malo. Como habíamos visto anteriormente, estariamos en el escenario "FlattenTheCurve" donde la velocidad de la epidemia se relentiza pero no lo suficiente como para pararla. Veamos que pasa si ejecutamos la simulación con dicho valor: """ case_e2 = IsolateNDays(days = 300, isolation_transmission_rate_divider=isolation_transmission_rate_divider_e2, isolation_days=500).run() """ Vemos un pico de 120k pacientes severos (y solo en la Comunidad de Madrid, recordemos que la simulación esta restringida a ese territorio) que es completamente inasumible por muchos hospitales de campaña que monten. """ max(case_e2.death) """ Y ese es quiza el peor dato: más de 173k muertos. Echemos ahora un vistazo a las curvas: """ with sns.axes_style("whitegrid"): fig, axs = plt.subplots(2, figsize=(20, 15)) axs[0].set(ylim=(1, 7e6)) axs[1].set(ylim=(-0.2, 0.5)) case_e2.plot(x='day', y=['infectious', 'symptomatic', 'death', 'immune', 'infectable'], logy=True, ax=axs[0], xlim=(15,230)) case_e2.plot(x='day', y=['velocity', 'symptomatic_velocity', 'ever_symptomatic_velocity'], logy=False, ax=axs[1], xlim=(15,230)) with sns.axes_style("whitegrid"): fig, axs = plt.subplots(2, figsize=(20, 15)) axs[0].set(ylim=(1, 7e6)) axs[1].set(ylim=(-0.1, 0.45)) case_e2.plot(x='day', y=['infectious', 'symptomatic', 'death', 'immune', 'infectable'], logy=True, ax=axs[0], xlim=(20,80)) case_e2.plot(x='day', y=['velocity', 'symptomatic_velocity', 'ever_symptomatic_velocity'], logy=False, ax=axs[1], xlim=(20,80)) """ Vemos que la velocidad de propagación de la epidemia se estabiliza en torno al 15% y sigue así durante aproximandamente un mes. En ese punto, ya hay tantas personas immunes que la propagación se relentiza por si misma. Si comparamos las curvas de velocidad simuladas con la realidad, vemos que probablemente el simulador este arrojando un resultado más optimista que el real puesto que la pendiente de la curva simulada es mucho mayor de lo que se observa en la realidad. """ """ ### Caso E2b: Reajustando la velocidad inicial Hasta ahora habíamos supuesto que la velocidad de la epidemia en España antes de tomar medidas estaba en torno al 40%, pero si nos fijamos solo en los días previos a la toma de medidas, ese valor en realidad estaba en torno al 0.45%. Vamos a repetir la simulación utilizando ese valor de partida. """ def f1(x): sim = SIRwhatever(days = 45, quiet=True, daily_transmission_rate=x) df = sim.run() v = df[df.day==(22)].observed_ever_symptomatic_velocity - 0.45 return v daily_transmission_rate_e2b = scipy.optimize.bisect(f1, 1, 3, rtol=0.002) print("daily_transmission_rate: %f" % daily_transmission_rate_e2b) def f2(x): sim = IsolateNDays(days = 80, quiet=True, daily_transmission_rate = daily_transmission_rate_e2b, isolation_transmission_rate_divider=x) df = sim.run() v = df[df.day==(sim.isolation_day + days_after)].observed_ever_symptomatic_velocity - observed_velocity return v isolation_transmission_rate_divider_e2b = scipy.optimize.bisect(f2, 1, 10, rtol=0.002) print("isolation_transmission_rate_divider: %f" % isolation_transmission_rate_divider_e2b) case_e2b = IsolateNDays(days = 300, daily_transmission_rate=daily_transmission_rate_e2b, isolation_transmission_rate_divider=isolation_transmission_rate_divider_e2b, isolation_days=500).run() with sns.axes_style("whitegrid"): fig, axs = plt.subplots(2, figsize=(20, 15)) axs[0].set(ylim=(1, 7e6)) axs[1].set(ylim=(-0.2, 0.5)) case_e2b.plot(x='day', y=['infectious', 'symptomatic', 'death', 'immune', 'infectable'], logy=True, ax=axs[0], xlim=(15,230)) case_e2b.plot(x='day', y=['velocity', 'symptomatic_velocity', 'ever_symptomatic_velocity'], logy=False, ax=axs[1], xlim=(15,230)) """ Vemos que a groso modo, no hay muchos cambios con respecto al caso E2. Seguimos en el escenario donde practicamente toda la población se infecta. """ """ ### Caso E2c: el escenario más optimista Para finalizar voy a simular el escenario más optimista posible que aun es compatible con lo que hemos observado en la realidad. Tambien he introducido un parametro nuevo que es el retardo de observación, el