Repository: CodeForPhilly/chime (python)

Issue:
Add implied percentage daily growth rate
In the description text at the top. In addition to having implied R0 computed and displayed, also display the implied daily percentage growth rate for both initial and mitigation cases.

Pull request description:
Add implied daily growth rate
- Adds `daily_growth` and `daily_growth_t` instance attributes on `Parameters` class.
- Adds `daily_growth_helper` function to `src/penn_chime/parameters.py` to calculate daily growth rate as a function of doubling time.
- Updates `display_header` function in `src/penn_chime/presentation.py` to display initial and mitigated daily growth rates.
- Adds `test_daily_growth` test to `tests/test_app.py` to test above changes

Fixes #90

Patch:
diff --git a/src/penn_chime/models.py b/src/penn_chime/models.py
index 97fcba8..21abab2 100644
--- a/src/penn_chime/models.py
+++ b/src/penn_chime/models.py
@@ -93,6 +93,8 @@ class SimSirModel:
         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)
 
 
 def sir(
@@ -171,3 +173,11 @@ def build_census_df(
             for key, los in lengths_of_stay.items()
         }
     })
+
+  
+def daily_growth_helper(doubling_time):
+    """Calculates average daily growth rate from doubling time"""
+    result = 0
+    if doubling_time != 0:
+        result = (np.power(2, 1.0 / doubling_time) - 1) * 100
+    return result
diff --git a/src/penn_chime/presentation.py b/src/penn_chime/presentation.py
index 8cd6dd2..49c2255 100644
--- a/src/penn_chime/presentation.py
+++ b/src/penn_chime/presentation.py
@@ -81,10 +81,11 @@ def display_header(st, m, p):
 {infected_population_warning_str}
 
 An initial doubling time of **{doubling_time}** days and a recovery time of **{recovery_days}** days imply an $R_0$ of
-**{r_naught:.2f}**.
-
+ **{r_naught:.2f}** and daily growth rate of **{daily_growth:.2f}%**. 
+ 
 **Mitigation**: A **{relative_contact_rate:.0%}** reduction in social contact after the onset of the
-outbreak **{impact_statement:s} {doubling_time_t:.1f}** days, implying an effective $R_t$ of **${r_t:.2f}$**.
+outbreak **{impact_statement:s} {doubling_time_t:.1f}** days, implying an effective $R_t$ of **${r_t:.2f}$** 
+and daily growth rate of **{daily_growth_t:.2f}%**.
 """.format(
             total_infections=m.infected,
             initial_infections=p.known_infected,
@@ -100,6 +101,8 @@ outbreak **{impact_statement:s} {doubling_time_t:.1f}** days, implying an effect
             r_t=m.r_t,
             doubling_time_t=abs(m.doubling_time_t),
             impact_statement=("halves the infections every" if m.r_t < 1 else "reduces the doubling time to"),
+            daily_growth=m.daily_growth,
+            daily_growth_t=m.daily_growth_t,
             docs_url=DOCS_URL,
             infection_warning_str=infection_warning_str,
             infected_population_warning_str=infected_population_warning_str


Test patch:
diff --git a/tests/test_app.py b/tests/test_app.py
index 3b13a29..f5dc5dc 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -8,7 +8,7 @@ import numpy as np  # type: ignore
 import altair as alt  # type: ignore
 
 from src.penn_chime.charts import new_admissions_chart, admitted_patients_chart, chart_descriptions
-from src.penn_chime.models import SimSirModel, sir, sim_sir_df, build_admits_df
+from src.penn_chime.models import SimSirModel, sir, sim_sir_df, build_admits_df, daily_growth_helper
 from src.penn_chime.parameters import Parameters
 from src.penn_chime.presentation import display_header
 from src.penn_chime.settings import DEFAULTS
@@ -27,7 +27,21 @@ PARAM = Parameters(
     n_days=60,
 )
 
+HALVING_PARAM = Parameters(
+    current_hospitalized=100,
+    doubling_time=6.0,
+    known_infected=5000,
+    market_share=0.05,
+    relative_contact_rate=0.7,
+    susceptible=500000,
+    hospitalized=RateLos(0.05, 7),
+    icu=RateLos(0.02, 9),
+    ventilated=RateLos(0.01, 10),
+    n_days=60,
+)
+
 MODEL = SimSirModel(PARAM)
+HALVING_MODEL = SimSirModel(HALVING_PARAM)
 
 
 # set up
@@ -58,46 +72,38 @@ st = MockStreamlit()
 # test presentation
 
 
+def header_test_helper(expected_str, model, param):
+    st.cleanup()
+    display_header(st, model, param)
+    assert [s for s in st.render_store if expected_str in s],\
+        "Expected the string '{expected}' in the display header".format(expected=expected_str)
+    st.cleanup()
+
+
 def test_penn_logo_in_header():
     penn_css = '<link rel="stylesheet" href="https://www1.pennmedicine.org/styles/shared/penn-medicine-header.css">'
-    display_header(st, MODEL, PARAM)
-    assert len(
-        list(filter(lambda s: penn_css in s, st.render_store))
-    ), "The Penn Medicine header should be printed"
+    header_test_helper(penn_css, MODEL, PARAM)
 
 
 def test_the_rest_of_header_shows_up():
     random_part_of_header = "implying an effective $R_t$ of"
-    assert len(
-        list(filter(lambda s: random_part_of_header in s, st.render_store))
-    ), "The whole header should render"
+    header_test_helper(random_part_of_header, MODEL, PARAM)
 
 
 def test_mitigation_statement():
-    st.cleanup()
     expected_doubling = "outbreak **reduces the doubling time to 7.8** days"
-    display_header(st, MODEL, PARAM)
-    assert [s for s in st.render_store if expected_doubling in s]
-    # assert len((list(filter(lambda s: expected_doubling in s, st.render_store))))
-    st.cleanup()
     expected_halving = "outbreak **halves the infections every 51.9** days"
-    halving_params = Parameters(
-        current_hospitalized=100,
-        doubling_time=6.0,
-        known_infected=5000,
-        market_share=0.05,
-        relative_contact_rate=0.7,
-        susceptible=500000,
-        hospitalized=RateLos(0.05, 7),
-        icu=RateLos(0.02, 9),
-        ventilated=RateLos(0.01, 10),
-        n_days=60,
-    )
-    halving_model = SimSirModel(halving_params)
-    display_header(st, halving_model, halving_params)
-    assert [s for s in st.render_store if expected_halving in s]
-    #assert len((list(filter(lambda s: expected_halving in s, st.render_store))))
-    st.cleanup()
+    header_test_helper(expected_doubling, MODEL, PARAM)
+    header_test_helper(expected_halving, HALVING_MODEL, HALVING_PARAM)
+
+
+def test_daily_growth_presentation():
+    initial_growth = "and daily growth rate of **12.25%**."
+    mitigated_growth = "and daily growth rate of **9.34%**."
+    mitigated_halving = "and daily growth rate of **-1.33%**."
+    header_test_helper(initial_growth, MODEL, PARAM)
+    header_test_helper(mitigated_growth, MODEL, PARAM)
+    header_test_helper(mitigated_halving, HALVING_MODEL, HALVING_PARAM)
 
 
 st.cleanup()
@@ -262,6 +268,12 @@ def test_model(model=MODEL, param=PARAM):
     assert (diff.abs() < 0.1).all()
 
 
+def test_daily_growth_helper():
+    assert np.round(daily_growth_helper(5), decimals=4) == 14.8698
+    assert np.round(daily_growth_helper(0), decimals=4) == 0.0
+    assert np.round(daily_growth_helper(-4), decimals=4) == -15.9104
+
+
 def test_chart_descriptions(p=PARAM):
     # new admissions chart
     projection_admits = pd.read_csv('tests/projection_admits.csv')