From 9a4528d1308a57a1a208517a785390fd48ffb723 Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Wed, 1 Apr 2020 13:08:43 -0400 Subject: [PATCH 01/12] Wire up input for an optional mitigation effective date --- src/penn_chime/parameters.py | 5 ++++- src/penn_chime/presentation.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/penn_chime/parameters.py b/src/penn_chime/parameters.py index 9991acc9..78a951d2 100644 --- a/src/penn_chime/parameters.py +++ b/src/penn_chime/parameters.py @@ -55,6 +55,7 @@ def __init__( hospitalized: Disposition, icu: Disposition, relative_contact_rate: float, + mitigation_date: Optional[date] = None, ventilated: Disposition, current_date: date = date.today(), date_first_hospitalized: Optional[date] = None, @@ -68,7 +69,6 @@ def __init__( region: Optional[Regions] = None, ): self.current_hospitalized = StrictlyPositive(value=current_hospitalized) - self.relative_contact_rate = Rate(value=relative_contact_rate) Rate(value=hospitalized.rate), Rate(value=icu.rate), Rate(value=ventilated.rate) StrictlyPositive(value=hospitalized.days), StrictlyPositive(value=icu.days), @@ -92,6 +92,9 @@ def __init__( self.date_first_hospitalized = OptionalDate(value=date_first_hospitalized) self.doubling_time = OptionalStrictlyPositive(value=doubling_time) + self.relative_contact_rate = Rate(value=relative_contact_rate) + self.mitigation_date = OptionalDate(value=mitigation_date) + self.infectious_days = StrictlyPositive(value=infectious_days) self.market_share = Rate(value=market_share) self.max_y_axis = OptionalStrictlyPositive(value=max_y_axis) diff --git a/src/penn_chime/presentation.py b/src/penn_chime/presentation.py index 751e20d1..87a59fcf 100644 --- a/src/penn_chime/presentation.py +++ b/src/penn_chime/presentation.py @@ -204,6 +204,10 @@ def display_sidebar(st, d: Parameters) -> Parameters: st_obj, "Date of first hospitalized case - Enter this date to have chime estimate the initial doubling time", value=d.date_first_hospitalized, ) + mitigation_date_input = DateInput( + st_obj, "Date of social distancing measures effect (may be delayed from implementation)", + value=d.mitigation_date + ) relative_contact_pct_input = PercentInput( st_obj, "Social distancing (% reduction in social contact going forward)", @@ -309,7 +313,14 @@ def display_sidebar(st, d: Parameters) -> Parameters: doubling_time = doubling_time_input() date_first_hospitalized = None - relative_contact_rate = relative_contact_pct_input() + if st.sidebar.checkbox( + "Social distancing measures have been implemented" + ): + mitigation_date = mitigation_date_input() + relative_contact_rate = relative_contact_pct_input() + else: + mitigation_date = None + relative_contact_rate = 1.e-7 st.sidebar.markdown( "### Severity Parameters [ℹ]({docs_url}/what-is-chime/parameters#severity-parameters)".format( From 1ca43ed2e718949117f0a0c5c8225bd346cbe252 Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Wed, 1 Apr 2020 13:26:42 -0400 Subject: [PATCH 02/12] Maintain defaults of social distancing, and ensure they're displayed accordingly --- src/penn_chime/presentation.py | 6 ++++-- src/penn_chime/settings.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/penn_chime/presentation.py b/src/penn_chime/presentation.py index 87a59fcf..233e9155 100644 --- a/src/penn_chime/presentation.py +++ b/src/penn_chime/presentation.py @@ -11,6 +11,7 @@ CHANGE_DATE, DATE_FORMAT, DOCS_URL, + EPSILON, FLOAT_INPUT_MIN, FLOAT_INPUT_STEP, ) @@ -314,13 +315,14 @@ def display_sidebar(st, d: Parameters) -> Parameters: date_first_hospitalized = None if st.sidebar.checkbox( - "Social distancing measures have been implemented" + "Social distancing measures have been implemented", + value=(d.relative_contact_rate > EPSILON) ): mitigation_date = mitigation_date_input() relative_contact_rate = relative_contact_pct_input() else: mitigation_date = None - relative_contact_rate = 1.e-7 + relative_contact_rate = EPSILON st.sidebar.markdown( "### Severity Parameters [ℹ]({docs_url}/what-is-chime/parameters#severity-parameters)".format( diff --git a/src/penn_chime/settings.py b/src/penn_chime/settings.py index 0bd12989..a9ccb347 100644 --- a/src/penn_chime/settings.py +++ b/src/penn_chime/settings.py @@ -16,6 +16,7 @@ def get_defaults(): infectious_days=14, market_share=0.15, n_days=100, + mitigation_date=date.today(), relative_contact_rate=0.3, ventilated=Disposition(0.005, 10), ) From d247c6e006ac0fecdd68a1a414ff3a4e1b0f8536 Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Wed, 1 Apr 2020 13:30:02 -0400 Subject: [PATCH 03/12] Set mitigation application date explicitly in tests, so their results won't change when the model does --- tests/conftest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index e822d91a..b7cf01f2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,6 +51,7 @@ def DEFAULTS(): doubling_time=4.0, n_days=60, market_share=0.15, + mitigation_date=datetime(year=2020, month=3, day=28), relative_contact_rate=0.3, hospitalized=Disposition(0.025, 7), icu=Disposition(0.0075, 9), @@ -65,6 +66,7 @@ def param(): current_hospitalized=100, doubling_time=6.0, market_share=0.05, + mitigation_date=datetime(year=2020, month=3, day=28), relative_contact_rate=0.15, population=500000, hospitalized=Disposition(0.05, 7), @@ -81,6 +83,7 @@ def halving_param(): current_hospitalized=100, doubling_time=6.0, market_share=0.05, + mitigation_date=datetime(year=2020, month=3, day=28), relative_contact_rate=0.7, population=500000, hospitalized=Disposition(0.05, 7), From b269207ff803fc9c8b54fe06f2e9847a7b315ed2 Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Wed, 1 Apr 2020 15:24:59 -0400 Subject: [PATCH 04/12] Split projection timeline by when mitigation is imposed, not day 0 --- src/penn_chime/models.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/penn_chime/models.py b/src/penn_chime/models.py index da7311fe..ad87dfe8 100644 --- a/src/penn_chime/models.py +++ b/src/penn_chime/models.py @@ -147,17 +147,29 @@ def __init__(self, p: Parameters): self.daily_growth_rate_t = get_growth_rate(self.doubling_time_t) def run_projection(self, p): + if p.mitigation_date is not None: + mitigation_day = (p.current_date - p.mitigation_date).days + else: + mitigation_day = 0 + + total_days = self.i_day + p.n_days + + if mitigation_day < -self.i_day: + mitigation_day = -self.i_day + + pre_mitigation_days = self.i_day + mitigation_day + post_mitigation_days = total_days - pre_mitigation_days + self.raw_df = sim_sir_df( self.susceptible, self.infected, p.recovered, self.gamma, -self.i_day, - self.beta, - self.i_day, - self.beta_t, - p.n_days + self.beta, pre_mitigation_days, + self.beta_t, post_mitigation_days ) + self.dispositions_df = build_dispositions_df(self.raw_df, self.rates, p.market_share, p.current_date) self.admits_df = build_admits_df(self.dispositions_df) self.census_df = build_census_df(self.admits_df, self.days) From de5d5fc9952a5b9426ef1c2b08f8a7575cda52bd Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Wed, 1 Apr 2020 15:53:31 -0400 Subject: [PATCH 05/12] Finish wiring mitigation date value through the app --- src/penn_chime/presentation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/penn_chime/presentation.py b/src/penn_chime/presentation.py index 233e9155..eb441ad8 100644 --- a/src/penn_chime/presentation.py +++ b/src/penn_chime/presentation.py @@ -356,6 +356,7 @@ def display_sidebar(st, d: Parameters) -> Parameters: hospitalized=Disposition(hospitalized_rate, hospitalized_days), icu=Disposition(icu_rate, icu_days), relative_contact_rate=relative_contact_rate, + mitigation_date=mitigation_date, ventilated=Disposition(ventilated_rate, ventilated_days), current_date=current_date, date_first_hospitalized=date_first_hospitalized, From 93edbc20e8d5c844452f316b1969a6d30b8754e9 Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Wed, 1 Apr 2020 16:15:18 -0400 Subject: [PATCH 06/12] Fix date arithmetic sign error --- src/penn_chime/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/penn_chime/models.py b/src/penn_chime/models.py index ad87dfe8..2bc5b520 100644 --- a/src/penn_chime/models.py +++ b/src/penn_chime/models.py @@ -148,7 +148,7 @@ def __init__(self, p: Parameters): def run_projection(self, p): if p.mitigation_date is not None: - mitigation_day = (p.current_date - p.mitigation_date).days + mitigation_day = -(p.current_date - p.mitigation_date).days else: mitigation_day = 0 From 90d8288f16d350fe49d21dd1d7a445862ab793d7 Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Thu, 2 Apr 2020 12:24:58 -0400 Subject: [PATCH 07/12] Convert sequence of time-evolving beta parameter to a typed list --- src/penn_chime/models.py | 15 ++++++++------- tests/penn_chime/test_models.py | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/penn_chime/models.py b/src/penn_chime/models.py index 2bc5b520..93b1c030 100644 --- a/src/penn_chime/models.py +++ b/src/penn_chime/models.py @@ -166,8 +166,10 @@ def run_projection(self, p): p.recovered, self.gamma, -self.i_day, - self.beta, pre_mitigation_days, - self.beta_t, post_mitigation_days + [ + (self.beta, pre_mitigation_days), + (self.beta_t, post_mitigation_days), + ] ) self.dispositions_df = build_dispositions_df(self.raw_df, self.rates, p.market_share, p.current_date) @@ -233,7 +235,7 @@ def sir( def gen_sir( - s: float, i: float, r: float, gamma: float, i_day: int, *args + s: float, i: float, r: float, gamma: float, i_day: int, policies: List[Tuple[float, int]] ) -> Generator[Tuple[int, float, float, float], None, None]: """Simulate SIR model forward in time yielding tuples. Parameter order has changed to allow multiple (beta, n_days) @@ -242,8 +244,7 @@ def gen_sir( s, i, r = (float(v) for v in (s, i, r)) n = s + i + r d = i_day - while args: - beta, n_days, *args = args + for beta, n_days in policies: for _ in range(n_days): yield d, s, i, r s, i, r = sir(s, i, r, beta, gamma, n) @@ -253,11 +254,11 @@ def gen_sir( def sim_sir_df( s: float, i: float, r: float, - gamma: float, i_day: int, *args + gamma: float, i_day: int, policies: List[Tuple[float, int]] ) -> pd.DataFrame: """Simulate the SIR model forward in time.""" return pd.DataFrame( - data=gen_sir(s, i, r, gamma, i_day, *args), + data=gen_sir(s, i, r, gamma, i_day, policies), columns=("day", "susceptible", "infected", "recovered"), ) diff --git a/tests/penn_chime/test_models.py b/tests/penn_chime/test_models.py index a8c4129e..9f02367b 100644 --- a/tests/penn_chime/test_models.py +++ b/tests/penn_chime/test_models.py @@ -64,7 +64,7 @@ def test_sim_sir(): Rounding to move fast past decimal place issues """ raw_df = sim_sir_df( - 5, 6, 7, 0.1, 0, 0.1, 40, # s # i # r # gamma # i_day # beta1 # n_days1 + 5, 6, 7, 0.1, 0, [(0.1, 40)], # s # i # r # gamma # i_day # beta1 # n_days1 ) first = raw_df.iloc[0, :] From 90c7b1b3f1b2a7adb313f7618268f6f1beb90f9c Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Thu, 2 Apr 2020 12:57:17 -0400 Subject: [PATCH 08/12] Factor out mitigation policy generation --- src/penn_chime/models.py | 50 +++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/src/penn_chime/models.py b/src/penn_chime/models.py index 93b1c030..451c0e46 100644 --- a/src/penn_chime/models.py +++ b/src/penn_chime/models.py @@ -28,6 +28,25 @@ class SimSirModel: + def gen_policy(self, p: Parameters) -> List[Tuple[float, int]]: + if p.mitigation_date is not None: + mitigation_day = -(p.current_date - p.mitigation_date).days + else: + mitigation_day = 0 + + total_days = self.i_day + p.n_days + + if mitigation_day < -self.i_day: + mitigation_day = -self.i_day + + pre_mitigation_days = self.i_day + mitigation_day + post_mitigation_days = total_days - pre_mitigation_days + + return [ + (self.beta, pre_mitigation_days), + (self.beta_t, post_mitigation_days), + ] + def __init__(self, p: Parameters): self.rates = { @@ -66,14 +85,13 @@ def __init__(self, p: Parameters): intrinsic_growth_rate = get_growth_rate(p.doubling_time) self.beta = get_beta(intrinsic_growth_rate, gamma, self.susceptible, 0.0) + self.beta_t = get_beta(intrinsic_growth_rate, self.gamma, self.susceptible, p.relative_contact_rate) self.i_day = 0 # seed to the full length - self.beta_t = self.beta - self.run_projection(p) + self.run_projection(p, [(self.beta, p.n_days)]) self.i_day = i_day = int(get_argmin_ds(self.census_df, p.current_hospitalized)) - self.beta_t = get_beta(intrinsic_growth_rate, self.gamma, self.susceptible, p.relative_contact_rate) - self.run_projection(p) + self.run_projection(p, self.gen_policy(p)) logger.info('Set i_day = %s', i_day) p.date_first_hospitalized = p.current_date - timedelta(days=i_day) @@ -100,7 +118,7 @@ def __init__(self, p: Parameters): self.beta = get_beta(intrinsic_growth_rate, self.gamma, self.susceptible, 0.0) self.beta_t = get_beta(intrinsic_growth_rate, self.gamma, self.susceptible, p.relative_contact_rate) - self.run_projection(p) + self.run_projection(p, self.gen_policy(p)) loss = self.get_loss() losses[i] = loss @@ -109,7 +127,7 @@ def __init__(self, p: Parameters): intrinsic_growth_rate = get_growth_rate(p.doubling_time) self.beta = get_beta(intrinsic_growth_rate, self.gamma, self.susceptible, 0.0) self.beta_t = get_beta(intrinsic_growth_rate, self.gamma, self.susceptible, p.relative_contact_rate) - self.run_projection(p) + self.run_projection(p, self.gen_policy(p)) self.population = p.population else: @@ -146,30 +164,14 @@ def __init__(self, p: Parameters): self.daily_growth_rate = get_growth_rate(p.doubling_time) self.daily_growth_rate_t = get_growth_rate(self.doubling_time_t) - def run_projection(self, p): - if p.mitigation_date is not None: - mitigation_day = -(p.current_date - p.mitigation_date).days - else: - mitigation_day = 0 - - total_days = self.i_day + p.n_days - - if mitigation_day < -self.i_day: - mitigation_day = -self.i_day - - pre_mitigation_days = self.i_day + mitigation_day - post_mitigation_days = total_days - pre_mitigation_days - + def run_projection(self, p: Parameters, policy: List[Tuple[float, int]]): self.raw_df = sim_sir_df( self.susceptible, self.infected, p.recovered, self.gamma, -self.i_day, - [ - (self.beta, pre_mitigation_days), - (self.beta_t, post_mitigation_days), - ] + policy ) self.dispositions_df = build_dispositions_df(self.raw_df, self.rates, p.market_share, p.current_date) From 4fe09200c5fb772b3bb3e7ac0a4e535edf5e56a2 Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Thu, 2 Apr 2020 13:10:40 -0400 Subject: [PATCH 09/12] Test that both fitting modes produce equivalent results when their inputs are compatible --- tests/penn_chime/test_models.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/penn_chime/test_models.py b/tests/penn_chime/test_models.py index 9f02367b..d5e6de20 100644 --- a/tests/penn_chime/test_models.py +++ b/tests/penn_chime/test_models.py @@ -3,11 +3,13 @@ import pytest import pandas as pd import numpy as np +from datetime import timedelta from src.penn_chime.models import ( sir, sim_sir_df, get_growth_rate, + SimSirModel, ) from src.penn_chime.constants import EPSILON @@ -100,6 +102,20 @@ def test_model(model, param): assert model.r_t == 2.307298374881539 assert model.r_naught == 2.7144686763312222 assert model.doubling_time_t == 7.764405988534983 + assert model.i_day == 43 + + +def test_model_first_hosp_fit(param): + param.date_first_hospitalized = param.current_date - timedelta(days=43) + param.doubling_time = None + + my_model = SimSirModel(param) + + assert my_model.intrinsic_growth_rate == 0.12246204830937302 + assert abs(my_model.beta - 4.21501347256401e-07) < EPSILON + assert my_model.r_t == 2.307298374881539 + assert my_model.r_naught == 2.7144686763312222 + assert my_model.doubling_time_t == 7.764405988534983 def test_model_raw_start(model, param): From 1ad05307aa63a272c46f5e4cc6750d3039458bb3 Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Thu, 2 Apr 2020 13:11:22 -0400 Subject: [PATCH 10/12] Move new method below constructor --- src/penn_chime/models.py | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/penn_chime/models.py b/src/penn_chime/models.py index 451c0e46..b1801229 100644 --- a/src/penn_chime/models.py +++ b/src/penn_chime/models.py @@ -27,26 +27,6 @@ class SimSirModel: - - def gen_policy(self, p: Parameters) -> List[Tuple[float, int]]: - if p.mitigation_date is not None: - mitigation_day = -(p.current_date - p.mitigation_date).days - else: - mitigation_day = 0 - - total_days = self.i_day + p.n_days - - if mitigation_day < -self.i_day: - mitigation_day = -self.i_day - - pre_mitigation_days = self.i_day + mitigation_day - post_mitigation_days = total_days - pre_mitigation_days - - return [ - (self.beta, pre_mitigation_days), - (self.beta_t, post_mitigation_days), - ] - def __init__(self, p: Parameters): self.rates = { @@ -164,6 +144,25 @@ def __init__(self, p: Parameters): self.daily_growth_rate = get_growth_rate(p.doubling_time) self.daily_growth_rate_t = get_growth_rate(self.doubling_time_t) + def gen_policy(self, p: Parameters) -> List[Tuple[float, int]]: + if p.mitigation_date is not None: + mitigation_day = -(p.current_date - p.mitigation_date).days + else: + mitigation_day = 0 + + total_days = self.i_day + p.n_days + + if mitigation_day < -self.i_day: + mitigation_day = -self.i_day + + pre_mitigation_days = self.i_day + mitigation_day + post_mitigation_days = total_days - pre_mitigation_days + + return [ + (self.beta, pre_mitigation_days), + (self.beta_t, post_mitigation_days), + ] + def run_projection(self, p: Parameters, policy: List[Tuple[float, int]]): self.raw_df = sim_sir_df( self.susceptible, From 439764efb6daf1eff7f4b187edae7d4a4f7a94a2 Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Thu, 2 Apr 2020 13:13:28 -0400 Subject: [PATCH 11/12] Re-add blank line to shrink diff --- src/penn_chime/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/penn_chime/models.py b/src/penn_chime/models.py index b1801229..989788b8 100644 --- a/src/penn_chime/models.py +++ b/src/penn_chime/models.py @@ -27,6 +27,7 @@ class SimSirModel: + def __init__(self, p: Parameters): self.rates = { From 7aca5d261b14b72c841c00f2bf55bfa2177f92d8 Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Thu, 2 Apr 2020 13:39:12 -0400 Subject: [PATCH 12/12] Clear up typing over-concreteness --- src/penn_chime/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/penn_chime/models.py b/src/penn_chime/models.py index 989788b8..6582b1c3 100644 --- a/src/penn_chime/models.py +++ b/src/penn_chime/models.py @@ -9,7 +9,7 @@ from datetime import date, datetime, timedelta from logging import INFO, basicConfig, getLogger from sys import stdout -from typing import Dict, Generator, Tuple, Sequence,Optional +from typing import Dict, Generator, Tuple, Sequence, Optional import numpy as np import pandas as pd @@ -145,7 +145,7 @@ def __init__(self, p: Parameters): self.daily_growth_rate = get_growth_rate(p.doubling_time) self.daily_growth_rate_t = get_growth_rate(self.doubling_time_t) - def gen_policy(self, p: Parameters) -> List[Tuple[float, int]]: + def gen_policy(self, p: Parameters) -> Sequence[Tuple[float, int]]: if p.mitigation_date is not None: mitigation_day = -(p.current_date - p.mitigation_date).days else: @@ -164,7 +164,7 @@ def gen_policy(self, p: Parameters) -> List[Tuple[float, int]]: (self.beta_t, post_mitigation_days), ] - def run_projection(self, p: Parameters, policy: List[Tuple[float, int]]): + def run_projection(self, p: Parameters, policy: Sequence[Tuple[float, int]]): self.raw_df = sim_sir_df( self.susceptible, self.infected, @@ -237,7 +237,7 @@ def sir( def gen_sir( - s: float, i: float, r: float, gamma: float, i_day: int, policies: List[Tuple[float, int]] + s: float, i: float, r: float, gamma: float, i_day: int, policies: Sequence[Tuple[float, int]] ) -> Generator[Tuple[int, float, float, float], None, None]: """Simulate SIR model forward in time yielding tuples. Parameter order has changed to allow multiple (beta, n_days) @@ -256,7 +256,7 @@ def gen_sir( def sim_sir_df( s: float, i: float, r: float, - gamma: float, i_day: int, policies: List[Tuple[float, int]] + gamma: float, i_day: int, policies: Sequence[Tuple[float, int]] ) -> pd.DataFrame: """Simulate the SIR model forward in time.""" return pd.DataFrame(