-
Notifications
You must be signed in to change notification settings - Fork 151
Fix issue 255 #273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix issue 255 #273
Changes from 7 commits
7e39b63
11c6e2a
0a978ff
a3d1a9a
1404983
e9b4fbf
124a3fd
13f82f8
0c96064
2a8469e
ef736d6
9ab74a7
97f3722
490fa2d
545a68c
ecf51ee
8e7f8e9
6f8aa06
2dd1bb0
714bbb0
7db3096
05ae862
448700d
77a85ef
e78b6a6
a794ea5
f75e381
469daaa
023a1b5
e6430f6
d1ff2b4
969c44f
bfe74cd
a7fc457
e23ca69
3066e26
dea9361
4d8358a
12d594f
25dde08
1c658da
def26d7
b0da967
ad3851f
02f53ea
cdb574d
7a0c4f5
fd6db51
c168a75
394a350
b38ddc8
3b040e9
be114a3
22b6df9
3175b3b
b07730d
5bed704
03691bd
8d2cdcb
2d153b8
252f5bd
f21fad6
45c55dc
25bdbb0
bd1d060
ce6fa6c
392e907
57f3b99
a562191
2bdf133
e160cbc
ef8e99b
ba8d23a
d2d06f9
38c2901
7755807
da33480
5c3d3b6
5dab315
3e7612c
fa2f740
6e4b2d2
c8cad06
0eed3c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,17 +7,17 @@ | |
|
||
from __future__ import annotations | ||
|
||
from typing import Dict, Generator, Tuple | ||
from typing import Dict, Generator, Tuple, Optional | ||
|
||
import numpy as np # type: ignore | ||
import pandas as pd # type: ignore | ||
|
||
from .parameters import Parameters | ||
|
||
from .utils import SimSirModelAttributes | ||
|
||
class SimSirModel: | ||
|
||
def __init__(self, p: Parameters) -> SimSirModel: | ||
def __init__(self, p: Parameters): | ||
# TODO missing initial non-zero 'recovered' value | ||
recovered = 0.0 | ||
recovery_days = p.recovery_days | ||
|
@@ -32,6 +32,9 @@ def __init__(self, p: Parameters) -> SimSirModel: | |
for key, d in p.dispositions.items() | ||
} | ||
|
||
self._rates = rates | ||
self._lengths_of_stay = lengths_of_stay | ||
|
||
# Note: this should not be an integer. | ||
# We're appoximating infected from what we do know. | ||
# TODO market_share > 0, hosp_rate > 0 | ||
|
@@ -45,8 +48,8 @@ def __init__(self, p: Parameters) -> SimSirModel: | |
p.known_infected / infected if infected > 1.0e-7 else None | ||
) | ||
|
||
intrinsic_growth_rate = \ | ||
(2.0 ** (1.0 / p.doubling_time) - 1.0) if p.doubling_time > 0.0 else 0.0 | ||
# (2.0 ** (1.0 / p.doubling_time) - 1.0) if p.doubling_time > 0.0 else 0.0 | ||
intrinsic_growth_rate = self._intrinsic_growth_rate(p.doubling_time) | ||
|
||
gamma = 1.0 / recovery_days | ||
|
||
|
@@ -75,8 +78,8 @@ def __init__(self, p: Parameters) -> SimSirModel: | |
p.n_days, | ||
) | ||
dispositions_df = build_dispositions_df(raw_df, rates, p.market_share) | ||
admits_df = build_admits_df(dispositions_df) | ||
census_df = build_census_df(admits_df, lengths_of_stay) | ||
admits_df = build_admits_df(dispositions_df, p.n_days_since_first_hospitalized) | ||
census_df = build_census_df(admits_df, lengths_of_stay, p.n_days_since_first_hospitalized) | ||
|
||
self.susceptible = susceptible | ||
self.infected = infected | ||
|
@@ -94,10 +97,128 @@ def __init__(self, p: Parameters) -> SimSirModel: | |
self.dispositions_df = dispositions_df | ||
self.admits_df = admits_df | ||
self.census_df = census_df | ||
|
||
if p.n_days_since_first_hospitalized is not None and p.doubling_time is None: | ||
# optimize doubling_time | ||
argmin_dt = None | ||
min_loss = 2.0**99 | ||
censes = dict() | ||
for dt in np.linspace(1,15,29): | ||
censes[dt] = self.run_projection(p, dt) | ||
self.census_df = censes[dt] # log it into state for loss | ||
loss_dt = self.loss_dt(p) | ||
if loss_dt < min_loss: | ||
min_loss = loss_dt | ||
argmin_dt = dt | ||
self.census_df = censes[dt] | ||
p.doubling_time = argmin_dt | ||
|
||
# update all state that is dependent on doubling time. | ||
intrinsic_growth_rate = self._intrinsic_growth_rate(p.doubling_time) | ||
gamma = 1 / recovery_days | ||
beta = self._beta(intrinsic_growth_rate, gamma, susceptible, p.relative_contact_rate) | ||
r_t = beta / gamma * susceptible | ||
r_naught = (intrinsic_growth_rate + gamma) / gamma | ||
doubling_time_t = 1.0 / np.log2(beta * susceptible - gamma + 1) | ||
raw_df = sim_sir_df( | ||
susceptible, | ||
infected, | ||
recovered, | ||
beta, | ||
gamma, | ||
p.n_days | ||
) | ||
dispositions_df = build_dispositions_df(raw_df, rates, p.market_share) | ||
admits_df = build_admits_df(dispositions_df, p.n_days_since_first_hospitalized) | ||
|
||
census_df = build_census_df(admits_df, lengths_of_stay, p.n_days_since_first_hospitalized) | ||
|
||
self.intrinsic_growth_rate = intrinsic_growth_rate | ||
self.gamma = gamma | ||
self.beta = beta | ||
self.r_t = r_t | ||
self.r_naught = r_naught | ||
self.doubling_time_t = doubling_time_t | ||
self.raw_df = raw_df | ||
self.dispositions_df = dispositions_df | ||
self.admits_df = admits_df | ||
self.census_df = census_df | ||
|
||
self.daily_growth = daily_growth_helper(p.doubling_time) | ||
self.daily_growth_t = daily_growth_helper(doubling_time_t) | ||
|
||
return None | ||
|
||
def run_projection(self, p: Parameters, doubling_time: float) -> pd.DataFrame: | ||
intrinsic_growth_rate = self._intrinsic_growth_rate(doubling_time) | ||
|
||
recovery_days = p.recovery_days | ||
market_share = p.market_share | ||
initial_i = 1 / p.hospitalized.rate / market_share | ||
|
||
S, I, R = self.susceptible, self.infected, self.recovered | ||
|
||
# mean recovery rate (inv_recovery_days) | ||
gamma = 1 / recovery_days | ||
|
||
# contact rate | ||
beta = (intrinsic_growth_rate + gamma) / S | ||
|
||
n_days = p.n_days | ||
|
||
raw_df = sim_sir_df(S,I,R,beta,gamma,n_days) | ||
|
||
# dispositions_df = build_dispositions_df(raw_df, self._rates, p.market_share) | ||
|
||
i_dict_v = get_dispositions(raw_df.infected, self._rates, market_share) | ||
r_dict_v = get_dispositions(raw_df.recovered, self._rates, market_share) | ||
|
||
dispositions = { | ||
key: value + r_dict_v[key] | ||
for key, value in i_dict_v.items() | ||
} | ||
|
||
dispositions_df = pd.DataFrame(dispositions) | ||
dispositions_df = dispositions_df.assign(day=dispositions_df.index) | ||
|
||
admits_df = build_admits_df(dispositions_df, p.n_days_since_first_hospitalized) | ||
census_df = build_census_df(admits_df, self._lengths_of_stay, p.n_days_since_first_hospitalized) | ||
return census_df | ||
|
||
def loss_dt(self, p: Parameters) -> float: | ||
"""Squared error: predicted_current_hospitalized vs. actual current hospitalized | ||
|
||
gets prediction of current hospitalized from a census_df which | ||
is dependent on a given doubling_time in state. | ||
""" | ||
# get the predicted number of hospitalized today | ||
predicted_current_hospitalized = self.census_df.hospitalized.loc[p.n_days_since_first_hospitalized] | ||
|
||
# compare against actual / user inputted number | ||
# we shall optimize squared distance | ||
return (p.current_hospitalized - predicted_current_hospitalized) ** 2 | ||
|
||
|
||
@staticmethod | ||
def _intrinsic_growth_rate(doubling_time: Optional[float]) -> float: | ||
if doubling_time is not None: | ||
return (2.0 ** (1.0 / doubling_time) - 1.0) if doubling_time > 0.0 else 0.0 | ||
return 0.0 | ||
|
||
@staticmethod | ||
def _beta( | ||
intrinsic_growth_rate: float, | ||
gamma: float, | ||
susceptible: float, | ||
relative_contact_rate: float) -> float: | ||
return ( | ||
(intrinsic_growth_rate + gamma) | ||
/ susceptible | ||
* (1.0 - relative_contact_rate) | ||
) | ||
|
||
################### | ||
## MODEL FUNCS ## | ||
################### | ||
def sir( | ||
s: float, i: float, r: float, beta: float, gamma: float, n: float | ||
) -> Tuple[float, float, float]: | ||
|
@@ -118,7 +239,7 @@ def sir( | |
|
||
def gen_sir( | ||
s: float, i: float, r: float, beta: float, gamma: float, n_days: int | ||
) -> Generator[Tuple[float, float, float], None, None]: | ||
) -> Generator[Tuple[int, float, float, float], None, None]: | ||
"""Simulate SIR model forward in time yielding tuples.""" | ||
s, i, r = (float(v) for v in (s, i, r)) | ||
n = s + i + r | ||
|
@@ -136,6 +257,19 @@ def sim_sir_df( | |
columns=("day", "susceptible", "infected", "recovered"), | ||
) | ||
|
||
|
||
def get_dispositions( | ||
|
||
patients: np.ndarray, | ||
rates: Dict[str, float], | ||
market_share: float, | ||
) -> Dict[str, np.ndarray]: | ||
"""Get dispositions of patients adjusted by rate and market_share.""" | ||
return { | ||
key: patients * rate * market_share | ||
for key, rate in rates.items() | ||
} | ||
|
||
|
||
def build_dispositions_df( | ||
sim_sir_df: pd.DataFrame, | ||
rates: Dict[str, float], | ||
|
@@ -152,18 +286,22 @@ def build_dispositions_df( | |
}) | ||
|
||
|
||
def build_admits_df(dispositions_df: pd.DataFrame) -> pd.DataFrame: | ||
def build_admits_df(dispositions_df: pd.DataFrame, n_days_since_first_hospitalized: int) -> pd.DataFrame: | ||
"""Build admits dataframe from dispositions.""" | ||
admits_df = dispositions_df.iloc[:-1, :] - dispositions_df.shift(1) | ||
admits_df.day = dispositions_df.day | ||
if n_days_since_first_hospitalized is not None: | ||
admits_df.day = dispositions_df.day - n_days_since_first_hospitalized | ||
else: | ||
admits_df.day = dispositions_df.day | ||
return admits_df | ||
|
||
|
||
def build_census_df( | ||
admits_df: pd.DataFrame, | ||
lengths_of_stay: Dict[str, int], | ||
n_days_since_first_hospitalized: int | ||
) -> pd.DataFrame: | ||
"""ALOS for each disposition of COVID-19 case (total guesses)""" | ||
"""Average Length of Stay for each disposition of COVID-19 case (total guesses)""" | ||
return pd.DataFrame({ | ||
'day': admits_df.day, | ||
**{ | ||
|
@@ -176,9 +314,13 @@ def build_census_df( | |
}) | ||
|
||
|
||
def daily_growth_helper(doubling_time): | ||
|
||
############# | ||
## UTILS ## | ||
############# | ||
def daily_growth_helper(doubling_time: float) -> float: | ||
"""Calculates average daily growth rate from doubling time""" | ||
result = 0 | ||
if doubling_time != 0: | ||
if doubling_time != 0 and doubling_time is not None: | ||
result = (np.power(2, 1.0 / doubling_time) - 1) * 100 | ||
return result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be p.n_days + p.n_days_since_first_hospitalization?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all i'll say is that wouldn't be parity with the notebook. @cjbayesian
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, so we'll fix this later then. We really want the raw_df to hold all of the data instead of having to bodge n_days_since... into every function.
I'll get a copy of the notebook and test for equivalent output later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, this input can no longer be described as the number of days to project as stated in the ui. It is projecting less than n_days, it is projecting from the first infection. The ui / users here will be confusing / confused.