Skip to content

Commit 6938e80

Browse files
committed
Update tests
1 parent 76a5a76 commit 6938e80

File tree

8 files changed

+237
-307
lines changed

8 files changed

+237
-307
lines changed

src/app.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
# additional_projections_chart,
88
display_header,
99
display_sidebar,
10-
display_n_days_slider,
1110
draw_census_table,
1211
draw_projected_admissions_table,
1312
draw_raw_sir_simulation_table,
@@ -18,7 +17,7 @@
1817
write_footer,
1918
)
2019
from penn_chime.settings import DEFAULTS
21-
from penn_chime.models import sim_sir_df, build_admissions_df, build_census_df
20+
from penn_chime.models import SimSirModel
2221
from penn_chime.charts import (
2322
additional_projections_chart,
2423
admitted_patients_chart,
@@ -32,49 +31,44 @@
3231
st.markdown(hide_menu_style, unsafe_allow_html=True)
3332

3433
p = display_sidebar(st, DEFAULTS)
34+
m = SimSirModel(p)
3535

36-
display_header(st, p)
36+
display_header(st, m, p)
3737

3838
if st.checkbox("Show more info about this tool"):
3939
notes = "The total size of the susceptible population will be the entire catchment area for Penn Medicine entities (HUP, PAH, PMC, CCH)"
40-
show_more_info_about_this_tool(st=st, parameters=p, inputs=DEFAULTS, notes=notes)
40+
show_more_info_about_this_tool(st=st, model=m, parameters=p, defaults=DEFAULTS, notes=notes)
4141

4242
# PRESENTATION
4343
# Two more combination variable initialization / input element creation
4444
as_date = st.checkbox(label="Present result as dates instead of days", value=False)
45-
display_n_days_slider(st, p, DEFAULTS)
46-
47-
# begin format data
48-
admissions_df = build_admissions_df(p=p) # p.n_days, *p.dispositions)
49-
census_df = build_census_df(admissions_df, parameters=p)
50-
# end format data
5145

5246
st.subheader("New Admissions")
5347
st.markdown("Projected number of **daily** COVID-19 admissions at Penn hospitals")
5448
st.altair_chart(
55-
new_admissions_chart(alt, admissions_df, parameters=p, as_date=as_date),
49+
new_admissions_chart(alt, m.admits_df, parameters=p, as_date=as_date),
5650
use_container_width=True,
5751
)
5852
if st.checkbox("Show Projected Admissions in tabular form"):
59-
draw_projected_admissions_table(st, admissions_df, as_date=as_date)
53+
draw_projected_admissions_table(st, m.admits_df, as_date=as_date)
6054
st.subheader("Admitted Patients (Census)")
6155
st.markdown(
6256
"Projected **census** of COVID-19 patients, accounting for arrivals and discharges at Penn hospitals"
6357
)
6458
st.altair_chart(
65-
admitted_patients_chart(alt=alt, census=census_df, parameters=p, as_date=as_date),
59+
admitted_patients_chart(alt=alt, census=m.census_df, parameters=p, as_date=as_date),
6660
use_container_width=True,
6761
)
6862
if st.checkbox("Show Projected Census in tabular form"):
69-
draw_census_table(st, census_df, as_date=as_date)
63+
draw_census_table(st, m.census_df, as_date=as_date)
7064
st.markdown(
7165
"""**Click the checkbox below to view additional data generated by this simulation**"""
7266
)
7367
if st.checkbox("Show Additional Projections"):
7468
show_additional_projections(
75-
st, alt, additional_projections_chart, parameters=p, as_date=as_date
69+
st, alt, additional_projections_chart, model=m, parameters=p, as_date=as_date
7670
)
7771
if st.checkbox("Show Raw SIR Simulation Data"):
78-
draw_raw_sir_simulation_table(st, parameters=p, as_date=as_date)
72+
draw_raw_sir_simulation_table(st, model=m, parameters=p, as_date=as_date)
7973
write_definitions(st)
8074
write_footer(st)

src/cli.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from pandas import DataFrame
1010

1111
from penn_chime.parameters import Parameters
12-
from penn_chime.utils import build_admissions_df, build_census_df, RateLos
12+
from penn_chime.models import SimSirModel
13+
from penn_chime.utils import RateLos
1314

1415

1516
class FromFile(Action):
@@ -108,29 +109,22 @@ def main():
108109
doubling_time=a.doubling_time,
109110
known_infected=a.known_infected,
110111
market_share=a.market_share,
112+
n_days=a.n_days,
111113
relative_contact_rate=a.relative_contact_rate,
112114
susceptible=a.susceptible,
113-
n_days=a.n_days,
115+
114116
hospitalized=RateLos(a.hospitalized_rate, a.hospitalized_los),
115117
icu=RateLos(a.icu_rate, a.icu_los),
116118
ventilated=RateLos(a.ventilated_rate, a.ventilated_los),
117119
)
118120

119-
raw_df = DataFrame(
120-
{
121-
"Susceptible": p.susceptible_v,
122-
"Infected": p.infected_v,
123-
"Recovered": p.recovered_v,
124-
}
125-
)
126-
admits_df = build_admissions_df(p.n_days, *p.dispositions)
127-
census_df = build_census_df(admits_df, *p.lengths_of_stay)
121+
m = SimSirModel(p)
128122

129123
prefix = a.prefix
130124
for df, name in (
131-
(raw_df, "raw"),
132-
(admits_df, "admits"),
133-
(census_df, "census"),
125+
(m.raw_df, "raw"),
126+
(m.admits_df, "admits"),
127+
(m.census_df, "census"),
134128
):
135129
df.to_csv(prefix + name + ".csv")
136130

src/penn_chime/charts.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def new_admissions_chart(
2828

2929
return (
3030
alt.Chart(projection_admits.head(plot_projection_days))
31-
.transform_fold(fold=["Hospitalized", "ICU", "Ventilated"])
31+
.transform_fold(fold=["hospitalized", "icu", "ventilated"])
3232
.mark_line(point=True)
3333
.encode(
3434
x=alt.X(**x_kwargs),
@@ -67,7 +67,7 @@ def admitted_patients_chart(
6767

6868
return (
6969
alt.Chart(census.head(plot_projection_days))
70-
.transform_fold(fold=["Hospitalized", "ICU", "Ventilated"])
70+
.transform_fold(fold=["hospitalized", "icu", "ventilated"])
7171
.mark_line(point=True)
7272
.encode(
7373
x=alt.X(**x_kwargs),

src/penn_chime/defaults.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,29 @@ def __init__(
2525
current_hospitalized: int,
2626
doubling_time: int,
2727
known_infected: int,
28-
n_days: int,
2928
relative_contact_rate: int,
3029
region: Regions,
30+
3131
hospitalized: RateLos,
3232
icu: RateLos,
3333
ventilated: RateLos,
34+
35+
recovery_days: int = 14,
3436
market_share: float = 1.0,
37+
n_days: int = 60,
3538
):
3639
self.region = region
37-
self.known_infected = known_infected
3840
self.current_hospitalized = current_hospitalized
41+
self.known_infected = known_infected
3942
self.doubling_time = doubling_time
4043
self.market_share = market_share
44+
self.n_days = n_days
45+
self.recovery_days = recovery_days
4146
self.relative_contact_rate = relative_contact_rate
4247

4348
self.hospitalized = hospitalized
4449
self.icu = icu
4550
self.ventilated = ventilated
46-
self.n_days = n_days
4751

4852
def __repr__(self) -> str:
4953
return f"Constants(susceptible_default: {self.region.susceptible}, known_infected: {self.known_infected})"

src/penn_chime/models.py

Lines changed: 120 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,91 @@
11
"""Models."""
22

3-
from typing import Generator, Tuple
3+
from __future__ import annotations
4+
5+
from typing import Dict, Generator, Tuple
46

57
import numpy as np # type: ignore
68
import pandas as pd # type: ignore
79

10+
from .parameters import Parameters
11+
12+
13+
class SimSirModel:
14+
15+
def __init__(self, p: Parameters) -> SimSirModel:
16+
17+
# Note: this should not be an integer.
18+
# We're appoximating infected from what we do know.
19+
# TODO market_share > 0, hosp_rate > 0
20+
self.infected = infected = (
21+
p.current_hospitalized / p.market_share / p.hospitalized.rate
22+
)
23+
24+
self.detection_probability = (
25+
p.known_infected / infected if infected > 1.0e-7 else None
26+
)
27+
28+
# TODO missing initial recovered value
29+
self.recovered = recovered = 0.0
30+
31+
self.intrinsic_growth_rate = intrinsic_growth_rate = \
32+
(2.0 ** (1.0 / p.doubling_time) - 1.0) if p.doubling_time > 0.0 else 0.0
33+
34+
# TODO make this configurable, or more nuanced
35+
self.recovery_days = recovery_days = 14.0
36+
37+
self.gamma = gamma = 1.0 / recovery_days
38+
39+
# Contact rate, beta
40+
self.beta = beta = (
41+
(intrinsic_growth_rate + gamma)
42+
/ p.susceptible
43+
* (1.0 - p.relative_contact_rate)
44+
) # {rate based on doubling time} / {initial susceptible}
45+
46+
# r_t is r_0 after distancing
47+
self.r_t = beta / gamma * p.susceptible
48+
49+
# Simplify equation to avoid division by zero:
50+
# self.r_naught = r_t / (1.0 - relative_contact_rate)
51+
self.r_naught = (intrinsic_growth_rate + gamma) / gamma
52+
53+
# doubling time after distancing
54+
# TODO constrain values np.log2(...) > 0.0
55+
self.doubling_time_t = 1.0 / np.log2(
56+
beta * p.susceptible - gamma + 1)
57+
58+
self.raw_df = raw_df = sim_sir_df(
59+
p.susceptible,
60+
infected,
61+
recovered,
62+
beta,
63+
gamma,
64+
p.n_days,
65+
)
66+
67+
rates = {
68+
key: d.rate
69+
for key, d in p.dispositions.items()
70+
}
71+
72+
lengths_of_stay = {
73+
key: d.length_of_stay
74+
for key, d in p.dispositions.items()
75+
}
76+
77+
i_dict_v = get_dispositions(raw_df.infected, rates, p.market_share)
78+
r_dict_v = get_dispositions(raw_df.recovered, rates, p.market_share)
79+
80+
self.dispositions = {
81+
key: value + r_dict_v[key]
82+
for key, value in i_dict_v.items()
83+
}
84+
85+
self.dispositions_df = pd.DataFrame(self.dispositions)
86+
self.admits_df = admits_df = build_admits_df(p.n_days, self.dispositions)
87+
self.census_df = build_census_df(admits_df, lengths_of_stay)
88+
889

990
def sir(
1091
s: float, i: float, r: float, beta: float, gamma: float, n: float
@@ -30,91 +111,61 @@ def gen_sir(
30111
"""Simulate SIR model forward in time yielding tuples."""
31112
s, i, r = (float(v) for v in (s, i, r))
32113
n = s + i + r
33-
for _ in range(n_days + 1):
34-
yield s, i, r
114+
for d in range(n_days + 1):
115+
yield d, s, i, r
35116
s, i, r = sir(s, i, r, beta, gamma, n)
36117

37118

38-
def sim_sir(
39-
s: float, i: float, r: float, beta: float, gamma: float, n_days: int
40-
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
119+
def sim_sir_df(
120+
s: float, i: float, r: float, beta: float, gamma: float, n_days
121+
) -> pd.DataFrame:
41122
"""Simulate the SIR model forward in time."""
42-
s, i, r = (float(v) for v in (s, i, r))
43-
n = s + i + r
44-
s_v, i_v, r_v = [s], [i], [r]
45-
for day in range(n_days):
46-
s, i, r = sir(s, i, r, beta, gamma, n)
47-
s_v.append(s)
48-
i_v.append(i)
49-
r_v.append(r)
50-
51-
return (
52-
np.array(s_v),
53-
np.array(i_v),
54-
np.array(r_v),
55-
)
56-
57-
58-
def sim_sir_df(p) -> pd.DataFrame:
59-
"""Simulate the SIR model forward in time.
60-
61-
p is a Parameters instance. for circuluar dependency reasons i can't annotate it.
62-
"""
63123
return pd.DataFrame(
64-
data=gen_sir(p.susceptible, p.infected, p.recovered, p.beta, p.gamma, p.n_days),
65-
columns=("Susceptible", "Infected", "Recovered"),
124+
data=gen_sir(s, i, r, beta, gamma, n_days),
125+
columns=("day", "susceptible", "infected", "recovered"),
66126
)
67127

68128

69129
def get_dispositions(
70-
patient_state: np.ndarray, rates: Tuple[float, ...], market_share: float = 1.0
71-
) -> Tuple[np.ndarray, ...]:
72-
"""Get dispositions of infected adjusted by rate and market_share."""
73-
return (*(patient_state * rate * market_share for rate in rates),)
74-
75-
76-
def build_admissions_df(p) -> pd.DataFrame:
77-
"""Build admissions dataframe from Parameters."""
78-
days = np.array(range(0, p.n_days + 1))
79-
data_dict = dict(
80-
zip(
81-
["day", "Hospitalized", "ICU", "Ventilated"],
82-
[days] + [disposition for disposition in p.dispositions],
83-
)
84-
)
85-
projection = pd.DataFrame.from_dict(data_dict)
130+
patients: np.ndarray,
131+
rates: Dict[str, float],
132+
market_share: float,
133+
) -> Dict[str, np.ndarray]:
134+
"""Get dispositions of patients adjusted by rate and market_share."""
135+
return {
136+
key: patients * rate * market_share
137+
for key, rate in rates.items()
138+
}
139+
140+
141+
def build_admits_df(n_days, dispositions) -> pd.DataFrame:
142+
"""Build admits dataframe from Parameters and Model."""
143+
days = np.arange(0, n_days + 1)
144+
projection = pd.DataFrame({
145+
"day": days,
146+
**dispositions,
147+
})
86148
# New cases
87-
projection_admits = projection.iloc[:-1, :] - projection.shift(1)
88-
projection_admits["day"] = range(projection_admits.shape[0])
89-
return projection_admits
149+
admits_df = projection.iloc[:-1, :] - projection.shift(1)
150+
admits_df["day"] = range(admits_df.shape[0])
151+
return admits_df
90152

91153

92-
def build_census_df(projection_admits: pd.DataFrame, parameters) -> pd.DataFrame:
154+
def build_census_df(
155+
admits_df: pd.DataFrame, lengths_of_stay
156+
) -> pd.DataFrame:
93157
"""ALOS for each category of COVID-19 case (total guesses)"""
94-
n_days = np.shape(projection_admits)[0]
95-
hosp_los, icu_los, vent_los = parameters.lengths_of_stay
96-
los_dict = {
97-
"Hospitalized": hosp_los,
98-
"ICU": icu_los,
99-
"Ventilated": vent_los,
100-
}
101-
102-
census_dict = dict()
103-
for k, los in los_dict.items():
158+
n_days = np.shape(admits_df)[0]
159+
census_dict = {}
160+
for key, los in lengths_of_stay.items():
104161
census = (
105-
projection_admits.cumsum().iloc[:-los, :]
106-
- projection_admits.cumsum().shift(los).fillna(0)
162+
admits_df.cumsum().iloc[:-los, :]
163+
- admits_df.cumsum().shift(los).fillna(0)
107164
).apply(np.ceil)
108-
census_dict[k] = census[k]
165+
census_dict[key] = census[key]
109166

110167
census_df = pd.DataFrame(census_dict)
111168
census_df["day"] = census_df.index
112-
census_df = census_df[["day", "Hospitalized", "ICU", "Ventilated"]]
169+
census_df = census_df[["day", *lengths_of_stay.keys()]]
113170
census_df = census_df.head(n_days)
114-
census_df = census_df.rename(
115-
columns={
116-
disposition: f"{disposition}"
117-
for disposition in ("Hospitalized", "ICU", "Ventilated")
118-
}
119-
)
120171
return census_df

0 commit comments

Comments
 (0)