diff --git a/config.toml b/config.toml index c63b3f4cd..0c184e9b2 100644 --- a/config.toml +++ b/config.toml @@ -111,3 +111,9 @@ relativeURLs = false mission = "Develop the theory and practice of epidemiological forecasting, with a long-term vision of making this technology as universally accepted and useful as weather forecasting is today." apiUrl = "https://cmu-delphi.github.io/delphi-epidata" twitter = "CmuDelphi" + feedbackForm = "https://docs.google.com/forms/d/e/1FAIpQLSeIeOJtrAhdOriEyiRY7LkpQX8DZBY19dl6De8l56Q9CZhmxw/viewform?usp=pp_url&entry.1245962748=" + feedbackLikelihoodMobile = 0.2 + feedbackLikelihoodDesktop = 1 + feedbackDelayMin = 10 # in sec + feedbackDelayMax = 100 # in sec + feedbackDuration = 60 # show it for 60sec \ No newline at end of file diff --git a/content/blog/2021-01-15-causal-effect-mobility.Rmd b/content/blog/2021-01-15-causal-effect-mobility.Rmd new file mode 100644 index 000000000..2dfb03b89 --- /dev/null +++ b/content/blog/2021-01-15-causal-effect-mobility.Rmd @@ -0,0 +1,540 @@ +--- +title: Causal Inference for Social Mobility and COVID-19 +author: Matteo Bonvini, Edward Kennedy, Valérie Ventura, and Larry Wasserman +date: 2021-01-19 +tags: + - COVIDcast +authors: + - mbonvini + - ekennedy + - vventura + - lwasserman +heroImage: /blog/images/causal-inference-mobility-full-size.jpg +heroImageThumb: /blog/images/causal-inference-mobility-thumb.jpg +summary: | + How can we estimate the causal effect of mobility on COVID-19 when there could + be many confounding variables? +acknowledgements: | + We'd like to thank the Delphi engineering team for making this data available. + And we'd like to thank Roni Rosenfeld and Ryan Tibshirani for posing the + challenge of making counterfactual predictions. We thank Rob Tibshirani for + suggesting several improvements on this post, and Alex Reinhart for getting + our post into the appropriate format. +related: + - 2020-08-28-api +output: + blogdown::html_page: + toc: true +--- + +```{r setup, include=FALSE} +knitr::opts_chunk$set(collapse = TRUE) +``` + +Most pandemic research has focused on questions related to prediction such as: +how many COVID cases do we expect to see next week in Allegheny County? Causal +inference, on the other hand, is concerned with "what if" questions. For +example: what would happen if everyone wore masks? How many cases would there be +if there was a lockdown? How many deaths would occur if 40 percent of the +population is vaccinated? These questions involve hypothetical interventions on +a population. + +Estimating causal effects is tricky. We all know that "correlation +isn't causation." For example, mask usage and cases could both +increase over time. But that doesn't mean that masks cause more cases +to occur. Activation of "Buckle your seatbelt" signs on airplanes is +correlated with turbulence. But activating the sign does not cause +turbulence. + +So how do we estimate causal effects? There is a collection of methods +for this task. The important thing is that we can't just use standard +prediction methods. We need to use specialized methods that were designed +for causal inference. In this post we will discuss our work on +the causal effect of social mobility on deaths from COVID. The social mobility +variable we will use is the proportion of people staying home. We can think of +this of anti-mobility, and expect that higher values of this variable will lead +to fewer deaths from COVID. + +Let’s start with a brief introduction to causal inference. + + +## Causal Inference + +Causal inference +is a huge, complex topic. +We give a very brief +exposition of some key ideas here. +To learn more, +we recommend [the book +by Hernan and Robins](https://www.hsph.harvard.edu/miguel-hernan/causal-inference-book/) and +the paper +"[Statistics and Causal Inference](https://www.jstor.org/stable/2289064)" +by Paul Holland +(*Journal of the American Statistical Association* 1986, pp. 945-960). +And one can find many tutorials +on the web. + +Suppose we have +three variables +$X$, $A$ and $Y$, where +$Y$ is the outcome of interest +(such as deaths from COVID), +$A$ is a treatment or exposure +(such as mobility level), +and $X$ are confounding variables, +which are variables that affect both +$A$ and $Y$ (such as age). +The joint density is + +$$ +p(x,a,y) = p(x)p(a|x)p(y|x,a). +$$ + +We want to know: what would be the mean of $Y$ +if we could set $A$ to be equal to some value $a$? +For example, +what would the number of COVID cases be +if 92 percent of people wore masks? +We let $Y^a$ +denote this hypothetical outcome, +which is called a counterfactual or potential outcome. +And we denote the mean of $Y^a$ by +$\mathbb{E}[Y^a]$. + +If we have measured all the confounding variables +then the answer +(ignoring a few technical details) +is given by +Robins' $g$-formula: + +$$ +\mathbb{E}[Y^a] = \int \mu(x,a) p(x) dx = \mathbb{E}[\mu(X,a)] +$$ + +where +$\mu(x,a) = \mathbb{E}[Y|X=x,A=a]$ +is the mean of $Y$ given $X$ and $A$. +This is not equal to +the mean of $Y$ given $A=a$, which is +$\mathbb{E}[Y|A=a] = \int \mu(x,a) p(x|a) dx$. +This is the difference between causation +and prediction (correlation). +You can think of +the $g$-formula as the mean of $Y$ +if we replace $p(a|x)$ +in the joint density +with a point mass at $a$. +Using the $g$-formula is one example of *adjusting for +confounding*. + +Now suppose we observe data +$(X_1,A_1,Y_1),\dots,(X_n,A_n,Y_n)$. +How do we estimate +$\mathbb{E}[Y^a]$? +The simplest approach is to +get an estimate $\hat\mu(x,a)$ of $\mu(x,a)$ +(this is just regression) and replace the integral +in the $g$-formula +with a sample average to get the estimate + +$$ +\hat{\mathbb{E}(Y^a)} = \frac{1}{n}\sum_i \hat\mu(X_i,a). +$$ + +This is called the plug-in estimator. (Note that for prediction we would not use +this formula. We would just use $\hat \mu(X, A)$.) +There are often better estimators, +but we won't get into that here. +The important thing +is: +there is a formula for the causal effect +and we can estimate it. + +The first plot below shows an example where we would predict +higher values of $Y$ when $A$ is large. For pure prediction, this is +the correct conclusion. The second plot shows that once we +account for $X = \text{age}$ (corresponding to different colors) there is +a negative relationship between $Y$ and $A$: for a given person with a fixed +age, increasing $A$ would *decrease* $Y$. In this case, age is a +confounder and the $g$-formula would correctly recover the negative +relationship. For causal inference, this is the correct conclusion. + +![](/blog/images/causal-simple-confounder.svg) + +Things get trickier +when there are time varying variables. +Consider weekly mobility and death data +$(A_1,Y_1),\dots, (A_T,Y_T)$ +in one state. +For simplicity, we'll assume that there are no $X$ variables. But we'll see that at time $t$, the +variables $Y_1, \dots, Y_{t-1}$ are confounding variables for the causal effect of +mobility on $Y_t$. +Define +$\overline{A}_t = (A_1,\dots, A_t)$ and +$\overline{Y}_t = (Y_1,\dots, Y_t)$ +for $t\geq 1$. +The density +of $(\overline{y}_t,\overline{a}_t)$ +can be written as + +$$ +p(\overline{y}_t,\overline{a}_t) = +\prod_{s=1}^t p(y_s|\overline{y}_{s-1},\overline{a}_{s}) p(a_s|\overline{a}_{s-1},\overline{y}_{s-1}). +$$ + +Now consider +the causal question: + +What would $Y_t$ be if we set +$\overline{A}_t$ equal to some value +$\overline{a}_t =(a_1,\dots, a_t)$? + +Let +$Y^{\overline{a}_t}_t$ +denote this counterfactual quantity. +For example, if $A_t$ is mobility, +measuring, say, the percentage of people who stay home, +and +$\overline{a}_t = (12,12,12,6,6,6)$ +then +$Y^{\overline{a}_t}_t$ +is the number of deaths +we would observe if we set mobility to 12 for three weeks +then set it to 6 for three weeks. + +How do we +find the mean $\mathbb{E}[Y^{\overline{a}_t}_t]$? +As a general rule, +we adjust for pre-treatment variables but +not for post-treatment variables. +But in the time varying case, +this rule makes no sense. +Each $Y_t$ is both a pre-treatment variable +(it occurs before $A_{t+1}$) and +a post-treatment variable +(it occurs after $A_{t-1}$). +The correct way to +get the mean of $\mathbb{E}[Y^{\overline{a}_t}]$ +is to use the more general form +of the $g$-formula: + +$$ +\psi(\overline{a}_t)\equiv +\mathbb{E}[Y^{\overline{a}_t}_t] = +\int\cdots\int +\mathbb{E} \left[Y_t | \overline{A}_t = \overline{a}_t, \overline{Y}_{t-1}=\overline{y}_{t-1}\right] +\prod_{s=1}^{t-1} p(y_s|\overline{y}_{s-1},\overline{a}_{s}) +d y_s. +$$ + +You can ignore that equation +if you like; +suffice it to say that +there is indeed a formula +for the quantity we are interested in. + +We note, by the way, that some authors denote +$\mathbb{E}[Y^{\overline{a}_t}_t]$ by +$\mathbb{E}[Y_t| {\rm do}(\overline{a}_t)]$. +They mean the same thing. + +Having a formula for +$\psi(\overline{a}_t)$ +is great, but how do we estimate it? +Again we could plug-in estimates +of all the quantities in +the $g$-formula. +But there are +better things we can do, +as we now explain. + +## A Marginal Structural Model + +The approach we use is +to make use of +something called +a *marginal structural model* (MSM). + +Usually when we construct +a statistical model, +we are trying to +come up with +a way to express the distribution +of the data, in this case, +$(A_1,Y_1),\dots, (A_T,Y_T)$. +In effect, we want a formula +that explains how we would generate +a dataset +like the one we have. +But to do so in a tractable way +usually involves making a bunch of +assumptions about the model. +Instead, with an MSM, +we specify a plausible +model for +the form of the function +$\mathbb{E}[Y^{\overline{a}_t}]$ +but we otherwise leave the model +for the data unspecified. +For example, we might assume +that $\mathbb{E}[Y^{\overline{a}_t}]\approx \beta_0 + \beta \sum_s a_s$. +In a sense, we are spending our modeling assumptions wisely: +we want the effect of the treatment to be smooth +and fairly simple. +But we otherwise don't want to impose assumptions. + +The model we use is + +$$ +\mathbb{E}[L_t^{\overline{a}_t}]= s(t) + \beta M_t +$$ + +where +$L_t = \log (1+Y_t)$ is log-deaths, +$s(t)$ is an arbitrary, smooth, increasing function +and + +$$ +M_t = \sum_{s=1}^{t-\delta}(a_s-a_1) +$$ + +is cumulative mobility (compared to baseline mobility). +Based on published studies, we take $\delta = 4$ weeks. +(We switch from deaths to log-deaths mainly for +certain numerical reasons related to the software we are using.) +Here, $s(t)$ represents the +evolution of the pandemic if there were no +interventions and no reduction in +susceptible individuals. +The term $\beta M_t$ captures +the effect of mobility. +With no change in mobility, +deaths increase exponentially as $e^{s(t)}$. + +The intuition for using cumulative mobility +comes from ideas from epidemic modeling. +Roughly, +if $I_t$ denotes new infections in week $t$, +we might assume that +$I_t \approx e^{\nu(t) + \beta A_t} I_{t-1}$ +for some function $\nu(t)$. +This implies that +$I_{t-1} \approx e^{\nu(t-1) + \beta A_{t-1}} I_{t-2}$ +and so on. +If we continue back to $t=1$ we find that +$I_t \approx e^{s(t) + \beta \sum_s A_s} I_1$ +where $s(t)=\sum_{s=1}^t \nu(s)$. +If we further assume that +some fraction of people infected a time $t$ +will die in $\delta$ weeks, +then we can derive the above model. +(More complex models are discussed in the paper.) + +To re-cap: +we are leaving the +distribution $P$ of the data +$(A_1,Y_1),\dots, (A_T,Y_T)$ +unspecified, +except that we require: +if you apply +the $g$-formula +to $P$ you must get +$s(t) + \beta M_t$. +In other words, +the statistical model is the +set of all densities +$p(\overline{\ell}_t,\overline{a}_t)$ that satisfy the condition + +$$ +\int\cdots\int +\mathbb{E}[L_t | \overline{A}_t = \overline{a}_t, \overline{L}_{t-1}=\overline{\ell}_{t-1}] +\prod_{s=1}^{t-1} p(\ell_s|\overline{\ell}_{s-1},\overline{a}_{s}) +d \ell_s = +s(t) + \beta M_t. +$$ + +This is an example of a semiparametric model: +a model that has some sort of constraint +but is otherwise nonparametric. + + +Estimating the MSM is a bit involved. +A key step is estimating the following function + +$$ +W_t = \frac{p(a_t|\overline{a}_{t-1})}{p(a_t| \overline{y}_{t-1},\overline{a}_{t-1},\overline{x}_{t-1})} +$$ + +where $\overline{x}_{t-1}$ +represents potential confounding variables. +The model is fit with these weights +and the weights are crucial +because that is how we adjust for confounding in the MSM. +We won't go into detail here. +Instead, +let's look at the data +and the estimates. + +## The Data and the Results + +The data we use +are weekly deaths +and weekly averaged mobility +starting February 15 2019. +The mobility signal +we'll consider here is the +proportion of people of who stay home, +which is kindly provided to Delphi by +SafeGraph. +(We [publish this +data](`r blogdown::shortcode_html("apiref", "api/covidcast-signals/safegraph.html")`) +in our [COVIDcast Epidata API](`r blogdown::shortcode_html("apiref", "api/covidcast.html")`), +so it is available to anyone interested in studying mobility during the pandemic.) +We use data at the state level. +Note that this is actually a sort of anti-mobility measure: +the higher it is, the less mobility there is. +Here are plots of +weekly deaths and mobility +starting February 15, 2020, +for a few states: + +```{r deaths-by-state, message=FALSE} +library(covidcast) +library(directlabels) +library(dplyr) +library(lubridate) +library(ggplot2) + +# Top 5 most populated states according to +# https://en.wikipedia.org/wiki/List_of_states_and_territories_of_the_United_States_by_population +states_of_interest <- c("ca", "tx", "fl", "ny", "pa") + +states_deaths <- covidcast_signal( + "jhu-csse", "deaths_incidence_num", + start_day = "2020-02-15", end_day = "2020-12-16", + geo_type = "state", geo_values = states_of_interest +) + +states_mobility <- covidcast_signal( + "safegraph", "completely_home_prop", + start_day = "2020-02-15", end_day = "2020-12-16", + geo_type = "state", geo_values = states_of_interest +) + +dat <- full_join(x = select(states_deaths, time_value, geo_value, value), + y = select(states_mobility, time_value, geo_value, value), + by = c("time_value" = "time_value", "geo_value" = "geo_value")) + +dat <- dat %>% + rename(date = time_value, + state = geo_value, + stay_home = value.y, + deaths = value.x) %>% + group_by(state) %>% + # epi week is from saturday to sunday + mutate(weekday = wday(date, label = TRUE, week_start = 6), + weeknum = wday(date, week_start = 6), + week = cumsum(weeknum == 1)) + +dat_week <- dat %>% + group_by(state, week) %>% + mutate(weekly_deaths = sum(deaths), + weekly_deaths = 1 + weekly_deaths, # avoid 0 in the log scale + weekly_stay_home = mean(stay_home)) %>% + select(week, state, weekly_stay_home, weekly_deaths, weekly_deaths) %>% + unique() + +ggplot(dat_week, aes(x = week, y = weekly_deaths, color = state)) + + geom_line() + + scale_y_log10() + + geom_dl(aes(label = toupper(state)), method = "last.bumpup") + + labs(x = "Week", y = "# deaths (log scale)", + title = "Reported deaths over time", + subtitle = "From JHU CSSE COVID-19 data", + caption = "Data from Delphi COVIDcast, delphi.cmu.edu") + + theme_bw() + + guides(color = FALSE) +``` + +```{r mobility-by-state} +ggplot(dat_week, aes(x = week, y = weekly_stay_home, color = state)) + + geom_line() + + geom_dl(aes(label = toupper(state)), method = "last.bumpup") + + labs(x = "Week", y = "Proportion staying at home", + title = "(Anti)mobility over time", + subtitle = "From SafeGraph", + caption = "Data from Delphi COVIDcast, delphi.cmu.edu") + + theme_bw() + + guides(color = FALSE) +``` + +After fitting the MSM, +we can estimate various quantities. +Let's consider the effect +of changing the observed +stay-at-home series +$(A_1,\dots, A_T)$ +to the hypothetical value +$\overline{a}_T = (A_1+\gamma,\dots, A_T+\gamma)$. +In other words, +what would have happened +if we had increased stay-at-home by $\gamma$? +Below is the estimated +difference in deaths +(with 90 percent pointwise confidence bands) +for a few states: +Arizona, California, Florida and Georgia. +The plot shows the mean of + +$$ +Y_t^\gamma - Y_t +$$ + +where $Y_t$ is the number of deaths +and +$Y_t^\gamma$ +is the (counterfactual) number of deaths +we would have observed of mobility had +been increases by $\gamma$ at all times, +where we take $\gamma = 10$ +(a ten percent increase in stay-at-home). +The fact that the values +are negative +indicates that increasing +stay-at-home decreases deaths, as we would expect. +(We remind the reader +that these results are preliminary.) + + +![](/blog/images/causal-effect-mobility-bigstates.svg) + +## What's Next? + +One important question in every +causal analysis is: what if there +is unobserved confounding? +In the current problem, +a confounder +is a variable that affects +both mobility and death from COVID. +We try to include as many plausible +observed confounding variables +as possible. +So far we use all +past values of death and mobility as +confounding variables. +But of course, +it is always possible +that there are unobserved confounders. +Currently, +we are conducting a sensitivity analysis +to assess how robust the estimates are +to unobserved confounders. + +In this post we have only +addressed mobility. +As soon as the mobility analysis +is complete +we will turn our attention +to estimating +the causal effect of masks on COVID cases. diff --git a/content/blog/2021-01-15-causal-effect-mobility.html b/content/blog/2021-01-15-causal-effect-mobility.html new file mode 100644 index 000000000..17e9f640b --- /dev/null +++ b/content/blog/2021-01-15-causal-effect-mobility.html @@ -0,0 +1,499 @@ +--- +title: Causal Inference for Social Mobility and COVID-19 +author: Matteo Bonvini, Edward Kennedy, Valérie Ventura, and Larry Wasserman +date: 2021-01-19 +tags: + - COVIDcast +authors: + - mbonvini + - ekennedy + - vventura + - lwasserman +heroImage: /blog/images/causal-inference-mobility-full-size.jpg +heroImageThumb: /blog/images/causal-inference-mobility-thumb.jpg +summary: | + How can we estimate the causal effect of mobility on COVID-19 when there could + be many confounding variables? +acknowledgements: | + We'd like to thank the Delphi engineering team for making this data available. + And we'd like to thank Roni Rosenfeld and Ryan Tibshirani for posing the + challenge of making counterfactual predictions. We thank Rob Tibshirani for + suggesting several improvements on this post, and Alex Reinhart for getting + our post into the appropriate format. +related: + - 2020-08-28-api +output: + blogdown::html_page: + toc: true +--- + + + + +
+ +
+ +

Most pandemic research has focused on questions related to prediction such as: +how many COVID cases do we expect to see next week in Allegheny County? Causal +inference, on the other hand, is concerned with “what if” questions. For +example: what would happen if everyone wore masks? How many cases would there be +if there was a lockdown? How many deaths would occur if 40 percent of the +population is vaccinated? These questions involve hypothetical interventions on +a population.

+

Estimating causal effects is tricky. We all know that “correlation +isn’t causation.” For example, mask usage and cases could both +increase over time. But that doesn’t mean that masks cause more cases +to occur. Activation of “Buckle your seatbelt” signs on airplanes is +correlated with turbulence. But activating the sign does not cause +turbulence.

+

So how do we estimate causal effects? There is a collection of methods +for this task. The important thing is that we can’t just use standard +prediction methods. We need to use specialized methods that were designed +for causal inference. In this post we will discuss our work on +the causal effect of social mobility on deaths from COVID. The social mobility +variable we will use is the proportion of people staying home. We can think of +this of anti-mobility, and expect that higher values of this variable will lead +to fewer deaths from COVID.

+

Let’s start with a brief introduction to causal inference.

+
+

Causal Inference

+

Causal inference +is a huge, complex topic. +We give a very brief +exposition of some key ideas here. +To learn more, +we recommend the book +by Hernan and Robins and +the paper +“Statistics and Causal Inference” +by Paul Holland +(Journal of the American Statistical Association 1986, pp. 945-960). +And one can find many tutorials +on the web.

+

Suppose we have +three variables +\(X\), \(A\) and \(Y\), where +\(Y\) is the outcome of interest +(such as deaths from COVID), +\(A\) is a treatment or exposure +(such as mobility level), +and \(X\) are confounding variables, +which are variables that affect both +\(A\) and \(Y\) (such as age). +The joint density is

+

\[ +p(x,a,y) = p(x)p(a|x)p(y|x,a). +\]

+

We want to know: what would be the mean of \(Y\) +if we could set \(A\) to be equal to some value \(a\)? +For example, +what would the number of COVID cases be +if 92 percent of people wore masks? +We let \(Y^a\) +denote this hypothetical outcome, +which is called a counterfactual or potential outcome. +And we denote the mean of \(Y^a\) by +\(\mathbb{E}[Y^a]\).

+

If we have measured all the confounding variables +then the answer +(ignoring a few technical details) +is given by +Robins’ \(g\)-formula:

+

\[ +\mathbb{E}[Y^a] = \int \mu(x,a) p(x) dx = \mathbb{E}[\mu(X,a)] +\]

+

where +\(\mu(x,a) = \mathbb{E}[Y|X=x,A=a]\) +is the mean of \(Y\) given \(X\) and \(A\). +This is not equal to +the mean of \(Y\) given \(A=a\), which is +\(\mathbb{E}[Y|A=a] = \int \mu(x,a) p(x|a) dx\). +This is the difference between causation +and prediction (correlation). +You can think of +the \(g\)-formula as the mean of \(Y\) +if we replace \(p(a|x)\) +in the joint density +with a point mass at \(a\). +Using the \(g\)-formula is one example of adjusting for +confounding.

+

Now suppose we observe data +\((X_1,A_1,Y_1),\dots,(X_n,A_n,Y_n)\). +How do we estimate +\(\mathbb{E}[Y^a]\)? +The simplest approach is to +get an estimate \(\hat\mu(x,a)\) of \(\mu(x,a)\) +(this is just regression) and replace the integral +in the \(g\)-formula +with a sample average to get the estimate

+

\[ +\hat{\mathbb{E}(Y^a)} = \frac{1}{n}\sum_i \hat\mu(X_i,a). +\]

+

This is called the plug-in estimator. (Note that for prediction we would not use +this formula. We would just use \(\hat \mu(X, A)\).) +There are often better estimators, +but we won’t get into that here. +The important thing +is: +there is a formula for the causal effect +and we can estimate it.

+

The first plot below shows an example where we would predict +higher values of \(Y\) when \(A\) is large. For pure prediction, this is +the correct conclusion. The second plot shows that once we +account for \(X = \text{age}\) (corresponding to different colors) there is +a negative relationship between \(Y\) and \(A\): for a given person with a fixed +age, increasing \(A\) would decrease \(Y\). In this case, age is a +confounder and the \(g\)-formula would correctly recover the negative +relationship. For causal inference, this is the correct conclusion.

+

+

Things get trickier +when there are time varying variables. +Consider weekly mobility and death data +\((A_1,Y_1),\dots, (A_T,Y_T)\) +in one state. +For simplicity, we’ll assume that there are no \(X\) variables. But we’ll see that at time \(t\), the +variables \(Y_1, \dots, Y_{t-1}\) are confounding variables for the causal effect of +mobility on \(Y_t\). +Define +\(\overline{A}_t = (A_1,\dots, A_t)\) and +\(\overline{Y}_t = (Y_1,\dots, Y_t)\) +for \(t\geq 1\). +The density +of \((\overline{y}_t,\overline{a}_t)\) +can be written as

+

\[ +p(\overline{y}_t,\overline{a}_t) = +\prod_{s=1}^t p(y_s|\overline{y}_{s-1},\overline{a}_{s}) p(a_s|\overline{a}_{s-1},\overline{y}_{s-1}). +\]

+

Now consider +the causal question:

+

What would \(Y_t\) be if we set +\(\overline{A}_t\) equal to some value +\(\overline{a}_t =(a_1,\dots, a_t)\)?

+

Let +\(Y^{\overline{a}_t}_t\) +denote this counterfactual quantity. +For example, if \(A_t\) is mobility, +measuring, say, the percentage of people who stay home, +and +\(\overline{a}_t = (12,12,12,6,6,6)\) +then +\(Y^{\overline{a}_t}_t\) +is the number of deaths +we would observe if we set mobility to 12 for three weeks +then set it to 6 for three weeks.

+

How do we +find the mean \(\mathbb{E}[Y^{\overline{a}_t}_t]\)? +As a general rule, +we adjust for pre-treatment variables but +not for post-treatment variables. +But in the time varying case, +this rule makes no sense. +Each \(Y_t\) is both a pre-treatment variable +(it occurs before \(A_{t+1}\)) and +a post-treatment variable +(it occurs after \(A_{t-1}\)). +The correct way to +get the mean of \(\mathbb{E}[Y^{\overline{a}_t}]\) +is to use the more general form +of the \(g\)-formula:

+

\[ +\psi(\overline{a}_t)\equiv +\mathbb{E}[Y^{\overline{a}_t}_t] = +\int\cdots\int +\mathbb{E} \left[Y_t | \overline{A}_t = \overline{a}_t, \overline{Y}_{t-1}=\overline{y}_{t-1}\right] +\prod_{s=1}^{t-1} p(y_s|\overline{y}_{s-1},\overline{a}_{s}) +d y_s. +\]

+

You can ignore that equation +if you like; +suffice it to say that +there is indeed a formula +for the quantity we are interested in.

+

We note, by the way, that some authors denote +\(\mathbb{E}[Y^{\overline{a}_t}_t]\) by +\(\mathbb{E}[Y_t| {\rm do}(\overline{a}_t)]\). +They mean the same thing.

+

Having a formula for +\(\psi(\overline{a}_t)\) +is great, but how do we estimate it? +Again we could plug-in estimates +of all the quantities in +the \(g\)-formula. +But there are +better things we can do, +as we now explain.

+
+
+

A Marginal Structural Model

+

The approach we use is +to make use of +something called +a marginal structural model (MSM).

+

Usually when we construct +a statistical model, +we are trying to +come up with +a way to express the distribution +of the data, in this case, +\((A_1,Y_1),\dots, (A_T,Y_T)\). +In effect, we want a formula +that explains how we would generate +a dataset +like the one we have. +But to do so in a tractable way +usually involves making a bunch of +assumptions about the model. +Instead, with an MSM, +we specify a plausible +model for +the form of the function +\(\mathbb{E}[Y^{\overline{a}_t}]\) +but we otherwise leave the model +for the data unspecified. +For example, we might assume +that \(\mathbb{E}[Y^{\overline{a}_t}]\approx \beta_0 + \beta \sum_s a_s\). +In a sense, we are spending our modeling assumptions wisely: +we want the effect of the treatment to be smooth +and fairly simple. +But we otherwise don’t want to impose assumptions.

+

The model we use is

+

\[ +\mathbb{E}[L_t^{\overline{a}_t}]= s(t) + \beta M_t +\]

+

where +\(L_t = \log (1+Y_t)\) is log-deaths, +\(s(t)\) is an arbitrary, smooth, increasing function +and

+

\[ +M_t = \sum_{s=1}^{t-\delta}(a_s-a_1) +\]

+

is cumulative mobility (compared to baseline mobility). +Based on published studies, we take \(\delta = 4\) weeks. +(We switch from deaths to log-deaths mainly for +certain numerical reasons related to the software we are using.) +Here, \(s(t)\) represents the +evolution of the pandemic if there were no +interventions and no reduction in +susceptible individuals. +The term \(\beta M_t\) captures +the effect of mobility. +With no change in mobility, +deaths increase exponentially as \(e^{s(t)}\).

+

The intuition for using cumulative mobility +comes from ideas from epidemic modeling. +Roughly, +if \(I_t\) denotes new infections in week \(t\), +we might assume that +\(I_t \approx e^{\nu(t) + \beta A_t} I_{t-1}\) +for some function \(\nu(t)\). +This implies that +\(I_{t-1} \approx e^{\nu(t-1) + \beta A_{t-1}} I_{t-2}\) +and so on. +If we continue back to \(t=1\) we find that +\(I_t \approx e^{s(t) + \beta \sum_s A_s} I_1\) +where \(s(t)=\sum_{s=1}^t \nu(s)\). +If we further assume that +some fraction of people infected a time \(t\) +will die in \(\delta\) weeks, +then we can derive the above model. +(More complex models are discussed in the paper.)

+

To re-cap: +we are leaving the +distribution \(P\) of the data +\((A_1,Y_1),\dots, (A_T,Y_T)\) +unspecified, +except that we require: +if you apply +the \(g\)-formula +to \(P\) you must get +\(s(t) + \beta M_t\). +In other words, +the statistical model is the +set of all densities +\(p(\overline{\ell}_t,\overline{a}_t)\) that satisfy the condition

+

\[ +\int\cdots\int +\mathbb{E}[L_t | \overline{A}_t = \overline{a}_t, \overline{L}_{t-1}=\overline{\ell}_{t-1}] +\prod_{s=1}^{t-1} p(\ell_s|\overline{\ell}_{s-1},\overline{a}_{s}) +d \ell_s = +s(t) + \beta M_t. +\]

+

This is an example of a semiparametric model: +a model that has some sort of constraint +but is otherwise nonparametric.

+

Estimating the MSM is a bit involved. +A key step is estimating the following function

+

\[ +W_t = \frac{p(a_t|\overline{a}_{t-1})}{p(a_t| \overline{y}_{t-1},\overline{a}_{t-1},\overline{x}_{t-1})} +\]

+

where \(\overline{x}_{t-1}\) +represents potential confounding variables. +The model is fit with these weights +and the weights are crucial +because that is how we adjust for confounding in the MSM. +We won’t go into detail here. +Instead, +let’s look at the data +and the estimates.

+
+
+

The Data and the Results

+

The data we use +are weekly deaths +and weekly averaged mobility +starting February 15 2019. +The mobility signal +we’ll consider here is the +proportion of people of who stay home, +which is kindly provided to Delphi by +SafeGraph. +(We }}">publish this +data +in our }}">COVIDcast Epidata API, +so it is available to anyone interested in studying mobility during the pandemic.) +We use data at the state level. +Note that this is actually a sort of anti-mobility measure: +the higher it is, the less mobility there is. +Here are plots of +weekly deaths and mobility +starting February 15, 2020, +for a few states:

+
library(covidcast)
+library(directlabels)
+library(dplyr)
+library(lubridate)
+library(ggplot2)
+
+# Top 5 most populated states according to
+# https://en.wikipedia.org/wiki/List_of_states_and_territories_of_the_United_States_by_population
+states_of_interest <- c("ca", "tx", "fl", "ny", "pa")
+
+states_deaths <- covidcast_signal(
+  "jhu-csse", "deaths_incidence_num",
+  start_day = "2020-02-15", end_day = "2020-12-16",
+  geo_type = "state", geo_values = states_of_interest
+)
+
+states_mobility <- covidcast_signal(
+  "safegraph", "completely_home_prop",
+  start_day = "2020-02-15", end_day = "2020-12-16",
+  geo_type = "state", geo_values = states_of_interest
+)
+
+dat <- full_join(x = select(states_deaths, time_value, geo_value, value),
+                 y = select(states_mobility, time_value, geo_value, value),
+                 by = c("time_value" = "time_value", "geo_value" = "geo_value"))
+
+dat <- dat %>%
+  rename(date = time_value,
+         state = geo_value,
+         stay_home = value.y,
+         deaths = value.x) %>%
+  group_by(state) %>%
+  # epi week is from saturday to sunday
+  mutate(weekday = wday(date, label = TRUE, week_start = 6),
+         weeknum = wday(date, week_start = 6),
+         week = cumsum(weeknum == 1))
+
+dat_week <- dat %>%
+  group_by(state, week) %>%
+  mutate(weekly_deaths = sum(deaths),
+         weekly_deaths = 1 + weekly_deaths, # avoid 0 in the log scale
+         weekly_stay_home = mean(stay_home)) %>%
+  select(week, state, weekly_stay_home, weekly_deaths, weekly_deaths) %>%
+  unique()
+
+ggplot(dat_week, aes(x = week, y = weekly_deaths, color = state)) +
+  geom_line() +
+  scale_y_log10() +
+  geom_dl(aes(label = toupper(state)), method = "last.bumpup") +
+  labs(x = "Week", y = "# deaths (log scale)",
+       title = "Reported deaths over time",
+       subtitle = "From JHU CSSE COVID-19 data",
+       caption = "Data from Delphi COVIDcast, delphi.cmu.edu") +
+  theme_bw() +
+  guides(color = FALSE)
+

+
ggplot(dat_week, aes(x = week, y = weekly_stay_home, color = state)) +
+  geom_line() +
+  geom_dl(aes(label = toupper(state)), method = "last.bumpup") +
+  labs(x = "Week", y = "Proportion staying at home",
+       title = "(Anti)mobility over time",
+       subtitle = "From SafeGraph",
+       caption = "Data from Delphi COVIDcast, delphi.cmu.edu") +
+  theme_bw() +
+  guides(color = FALSE)
+

+

After fitting the MSM, +we can estimate various quantities. +Let’s consider the effect +of changing the observed +stay-at-home series +\((A_1,\dots, A_T)\) +to the hypothetical value +\(\overline{a}_T = (A_1+\gamma,\dots, A_T+\gamma)\). +In other words, +what would have happened +if we had increased stay-at-home by \(\gamma\)? +Below is the estimated +difference in deaths +(with 90 percent pointwise confidence bands) +for a few states: +Arizona, California, Florida and Georgia. +The plot shows the mean of

+

\[ +Y_t^\gamma - Y_t +\]

+

where \(Y_t\) is the number of deaths +and +\(Y_t^\gamma\) +is the (counterfactual) number of deaths +we would have observed of mobility had +been increases by \(\gamma\) at all times, +where we take \(\gamma = 10\) +(a ten percent increase in stay-at-home). +The fact that the values +are negative +indicates that increasing +stay-at-home decreases deaths, as we would expect. +(We remind the reader +that these results are preliminary.)

+

+
+
+

What’s Next?

+

One important question in every +causal analysis is: what if there +is unobserved confounding? +In the current problem, +a confounder +is a variable that affects +both mobility and death from COVID. +We try to include as many plausible +observed confounding variables +as possible. +So far we use all +past values of death and mobility as +confounding variables. +But of course, +it is always possible +that there are unobserved confounders. +Currently, +we are conducting a sensitivity analysis +to assess how robust the estimates are +to unobserved confounders.

+

In this post we have only +addressed mobility. +As soon as the mobility analysis +is complete +we will turn our attention +to estimating +the causal effect of masks on COVID cases.

+
diff --git a/content/covidcast/_index.md b/content/covidcast/_index.md index 20a090b1f..5b01dd126 100644 --- a/content/covidcast/_index.md +++ b/content/covidcast/_index.md @@ -7,4 +7,5 @@ order: 1 modeTitle: Map Overview icon: solid/map heroImage: /images/landing-page/hero-images/covidcast_withfill.jpg +feedback: true --- diff --git a/content/covidcast/export.md b/content/covidcast/export.md index 926fef821..4f359ccb0 100644 --- a/content/covidcast/export.md +++ b/content/covidcast/export.md @@ -7,4 +7,5 @@ app_mode: export order: 6 icon: solid/download heroImage: /images/landing-page/hero-images/covidcast_withfill.jpg +feedback: true --- diff --git a/content/covidcast/release-log/headless/v1.12.4.md b/content/covidcast/release-log/headless/v1.12.4.md new file mode 100644 index 000000000..7ed26a873 --- /dev/null +++ b/content/covidcast/release-log/headless/v1.12.4.md @@ -0,0 +1,14 @@ +--- +title: v1.12.4 +date: 2021-01-19 +--- + +This release improves the UX of the survey dashboard for both desktop and mobile experience. + +#### New Features + +- [#724](https://github.com/cmu-delphi/www-covidcast/pull/724), [#717](https://github.com/cmu-delphi/www-covidcast/pull/717) Survey dashboards improvements + +#### Bug-fixes + +- [#719](https://github.com/cmu-delphi/www-covidcast/pull/719) (re: [#718](https://github.com/cmu-delphi/www-covidcast/issues/718)) fix Export Data bug diff --git a/content/covidcast/single.md b/content/covidcast/single.md index 0ef1ab851..bbc62999d 100644 --- a/content/covidcast/single.md +++ b/content/covidcast/single.md @@ -7,4 +7,5 @@ app_mode: single order: 4 icon: location-solid heroImage: /images/landing-page/hero-images/covidcast_withfill.jpg +feedback: true --- diff --git a/content/covidcast/survey-results.md b/content/covidcast/survey-results.md index 80894e212..27f352c26 100644 --- a/content/covidcast/survey-results.md +++ b/content/covidcast/survey-results.md @@ -8,4 +8,5 @@ app_mode: survey-results order: 5 icon: solid/poll heroImage: /images/landing-page/hero-images/covidcast_survey.jpg +feedback: true --- diff --git a/content/covidcast/timelapse.md b/content/covidcast/timelapse.md index 931c0241f..fd6c999ff 100644 --- a/content/covidcast/timelapse.md +++ b/content/covidcast/timelapse.md @@ -7,4 +7,5 @@ app_mode: timelapse order: 2 icon: solid/clock heroImage: /images/landing-page/hero-images/covidcast_withfill.jpg +feedback: true --- diff --git a/content/covidcast/top10.md b/content/covidcast/top10.md index 263badb3b..2a2b3aefe 100644 --- a/content/covidcast/top10.md +++ b/content/covidcast/top10.md @@ -7,4 +7,5 @@ app_mode: top10 order: 3 icon: solid/list heroImage: /images/landing-page/hero-images/covidcast_withfill.jpg +feedback: true --- diff --git a/data/authors.yaml b/data/authors.yaml index da08074f5..2d4d7e4b5 100644 --- a/data/authors.yaml +++ b/data/authors.yaml @@ -22,3 +22,19 @@ name: Kathryn Mazaitis link: https://cs.cmu.edu/~krivard description: manages Delphi's engineering team, and is a Senior Research Programmer in the Machine Learning Department at CMU. +- key: mbonvini + name: Matteo Bonvini + link: https://www.linkedin.com/in/matteobonvini + description: is a graduate student in the Department of Statistics & Data Science at CMU. +- key: ekennedy + name: Edward Kennedy + link: http://www.ehkennedy.com/ + description: is an Assistant Professor in the Department of Statistics & Data Science at CMU +- key: vventura + name: Valérie Ventura + link: http://www.stat.cmu.edu/~vventura/ + description: is a Professor in the Department of Statistics & Data Science at CMU and a member of Delphi. +- key: lwasserman + name: Larry Wasserman + link: http://www.stat.cmu.edu/~larry/ + description: is a Professor in the Department of Statistics & Data Science at CMU as well as in the Machine Learning Department and is a member of Delphi. diff --git a/package-lock.json b/package-lock.json index 57980328e..7877755c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "www-main", - "version": "1.12.3", + "version": "1.12.4", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2673,8 +2673,8 @@ "dev": true }, "www-covidcast": { - "version": "https://github.com/cmu-delphi/www-covidcast/releases/download/v1.12.3/www-covidcast-1.12.3.tgz", - "integrity": "sha512-cpigyX/+azM9/mZzcXhq986vy1j9CeWGElUs+xhu8uoFVaLsm8cr+okFiCM+Bm/RJUe80P3yalY8yyn9U9M2WA==", + "version": "https://github.com/cmu-delphi/www-covidcast/releases/download/v1.12.4/www-covidcast-1.12.4.tgz", + "integrity": "sha512-02Fg7/yXqYAbZPy8jsWvTUpFSNtIGUR9Iq2oAeQgz5PRAew/beeJjv6bT6u0cJrOb5cvIGxsparqgwuhbaIudg==", "requires": { "uikit": "^3.6.5" } diff --git a/package.json b/package.json index 67c334de2..54f5cf88c 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "www-main", - "version": "1.12.3", + "version": "1.12.4", "private": true, "dependencies": { "@fortawesome/fontawesome-free": "^5.15.1", "highlight.js": "^10.5.0", "katex": "^0.12.0", "uikit": "^3.6.5", - "www-covidcast": "https://github.com/cmu-delphi/www-covidcast/releases/download/v1.12.3/www-covidcast-1.12.3.tgz" + "www-covidcast": "https://github.com/cmu-delphi/www-covidcast/releases/download/v1.12.4/www-covidcast-1.12.4.tgz" }, "devDependencies": { "hugo-bin": "^0.67.1", diff --git a/static/blog/2021-01-15-causal-effect-mobility_files/anchor-sections-1.0/anchor-sections.css b/static/blog/2021-01-15-causal-effect-mobility_files/anchor-sections-1.0/anchor-sections.css new file mode 100644 index 000000000..07aee5fcb --- /dev/null +++ b/static/blog/2021-01-15-causal-effect-mobility_files/anchor-sections-1.0/anchor-sections.css @@ -0,0 +1,4 @@ +/* Styles for section anchors */ +a.anchor-section {margin-left: 10px; visibility: hidden; color: inherit;} +a.anchor-section::before {content: '#';} +.hasAnchor:hover a.anchor-section {visibility: visible;} diff --git a/static/blog/2021-01-15-causal-effect-mobility_files/anchor-sections-1.0/anchor-sections.js b/static/blog/2021-01-15-causal-effect-mobility_files/anchor-sections-1.0/anchor-sections.js new file mode 100644 index 000000000..570f99a0a --- /dev/null +++ b/static/blog/2021-01-15-causal-effect-mobility_files/anchor-sections-1.0/anchor-sections.js @@ -0,0 +1,33 @@ +// Anchor sections v1.0 written by Atsushi Yasumoto on Oct 3rd, 2020. +document.addEventListener('DOMContentLoaded', function() { + // Do nothing if AnchorJS is used + if (typeof window.anchors === 'object' && anchors.hasOwnProperty('hasAnchorJSLink')) { + return; + } + + const h = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); + + // Do nothing if sections are already anchored + if (Array.from(h).some(x => x.classList.contains('hasAnchor'))) { + return null; + } + + // Use section id when pandoc runs with --section-divs + const section_id = function(x) { + return ((x.classList.contains('section') || (x.tagName === 'SECTION')) + ? x.id : ''); + }; + + // Add anchors + h.forEach(function(x) { + const id = x.id || section_id(x.parentElement); + if (id === '') { + return null; + } + let anchor = document.createElement('a'); + anchor.href = '#' + id; + anchor.classList = ['anchor-section']; + x.classList.add('hasAnchor'); + x.appendChild(anchor); + }); +}); diff --git a/static/blog/2021-01-15-causal-effect-mobility_files/figure-html/deaths-by-state-1.svg b/static/blog/2021-01-15-causal-effect-mobility_files/figure-html/deaths-by-state-1.svg new file mode 100644 index 000000000..cd1c1c794 --- /dev/null +++ b/static/blog/2021-01-15-causal-effect-mobility_files/figure-html/deaths-by-state-1.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +FL +NY +TX +PA +CA + + + + + + +1 +10 +100 +1000 +10000 + + + + + + + + + + +0 +10 +20 +30 +40 +Week +# deaths (log scale) +From JHU CSSE COVID-19 data +Reported deaths over time +Data from Delphi COVIDcast, delphi.cmu.edu + diff --git a/static/blog/2021-01-15-causal-effect-mobility_files/figure-html/mobility-by-state-1-1.svg b/static/blog/2021-01-15-causal-effect-mobility_files/figure-html/mobility-by-state-1-1.svg new file mode 100644 index 000000000..642fa9d73 --- /dev/null +++ b/static/blog/2021-01-15-causal-effect-mobility_files/figure-html/mobility-by-state-1-1.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +TX +FL +PA +NY +CA + + + + + + +0.3 +0.4 +0.5 + + + + + + + + +0 +10 +20 +30 +40 +Week +Proportion staying at home +From SafeGraph +(Anti)mobility over time +Data from Delphi COVIDcast, delphi.cmu.edu + diff --git a/static/blog/2021-01-15-causal-effect-mobility_files/figure-html/mobility-by-state-1.svg b/static/blog/2021-01-15-causal-effect-mobility_files/figure-html/mobility-by-state-1.svg new file mode 100644 index 000000000..642fa9d73 --- /dev/null +++ b/static/blog/2021-01-15-causal-effect-mobility_files/figure-html/mobility-by-state-1.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +TX +FL +PA +NY +CA + + + + + + +0.3 +0.4 +0.5 + + + + + + + + +0 +10 +20 +30 +40 +Week +Proportion staying at home +From SafeGraph +(Anti)mobility over time +Data from Delphi COVIDcast, delphi.cmu.edu + diff --git a/static/blog/images/causal-effect-mobility-bigstates.svg b/static/blog/images/causal-effect-mobility-bigstates.svg new file mode 100644 index 000000000..da8f42919 --- /dev/null +++ b/static/blog/images/causal-effect-mobility-bigstates.svg @@ -0,0 +1,1349 @@ + +image/svg+xml−1500−1000−5000010203040WeekYγYCalifornia−1000−750−500−2500010203040WeekYγYFlorida−10000−7500−5000−25000010203040WeekYγYNew York−2000−1500−1000−5000010203040WeekYγYPennsylvania−3000−2000−10000010203040WeekYγYTexas diff --git a/static/blog/images/causal-inference-mobility-full-size.jpg b/static/blog/images/causal-inference-mobility-full-size.jpg new file mode 100644 index 000000000..8afe34890 Binary files /dev/null and b/static/blog/images/causal-inference-mobility-full-size.jpg differ diff --git a/static/blog/images/causal-inference-mobility-full-size.webp b/static/blog/images/causal-inference-mobility-full-size.webp new file mode 100644 index 000000000..c225766ae Binary files /dev/null and b/static/blog/images/causal-inference-mobility-full-size.webp differ diff --git a/static/blog/images/causal-inference-mobility-thumb.jpg b/static/blog/images/causal-inference-mobility-thumb.jpg new file mode 100644 index 000000000..306a0c13c Binary files /dev/null and b/static/blog/images/causal-inference-mobility-thumb.jpg differ diff --git a/static/blog/images/causal-inference-mobility-thumb.webp b/static/blog/images/causal-inference-mobility-thumb.webp new file mode 100644 index 000000000..05f425ee9 Binary files /dev/null and b/static/blog/images/causal-inference-mobility-thumb.webp differ diff --git a/static/blog/images/causal-simple-confounder.svg b/static/blog/images/causal-simple-confounder.svg new file mode 100644 index 000000000..699763a0a --- /dev/null +++ b/static/blog/images/causal-simple-confounder.svg @@ -0,0 +1,900 @@ + +image/svg+xml0.10.20.30.40.50.01.02.03.0AY0.10.20.30.40.50.01.02.03.0AY diff --git a/themes/delphi/assets/css/components/_feedback.scss b/themes/delphi/assets/css/components/_feedback.scss new file mode 100644 index 000000000..b75189ed4 --- /dev/null +++ b/themes/delphi/assets/css/components/_feedback.scss @@ -0,0 +1,62 @@ +.feedback-modal { + padding-top: 1.5em; + height: calc(100vh - 100px); + display: block; + position: relative; +} + +.feedback-modal > iframe { + width: 100%; + height: 100%; +} + +.feedback-message { + width: unset; + + .uk-notification-message { + background: #60a5fa; + color: white; + border-radius: 5px; + padding: 0; + font-size: 1rem; + cursor: inherit; + box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.25); + + > div { + padding: 1.5rem 3rem 1.5rem 1.5rem; + display: flex; + justify-content: flex-start; + align-items: center; + } + } + + .uk-notification-close { + color: white; + right: 1rem; + top: 50%; + transform: translateY(-50%); + display: block !important; + } + + .uk-button { + background: #2563eb; + border-radius: 3px; + // padding: 14px 24px 14px 24px; + } +} + +.feedback-text { + margin-right: 1em; +} + +@media only screen and (max-width: 550px) { + .feedback-message { + .uk-notification-message > div { + flex-direction: column; + } + + .uk-button { + margin-top: 0.5em; + } + } +} diff --git a/themes/delphi/assets/css/main.scss b/themes/delphi/assets/css/main.scss index 3ff6dfdf5..04021c1b0 100644 --- a/themes/delphi/assets/css/main.scss +++ b/themes/delphi/assets/css/main.scss @@ -23,6 +23,7 @@ @import "./components/card_grid"; @import "./components/latest_card"; @import "./components/toc"; +@import "./components/feedback"; // Page Designs @import "./pages/about"; diff --git a/themes/delphi/assets/css/pages/_covidcast.scss b/themes/delphi/assets/css/pages/_covidcast.scss index eb2d85998..178db8f83 100644 --- a/themes/delphi/assets/css/pages/_covidcast.scss +++ b/themes/delphi/assets/css/pages/_covidcast.scss @@ -11,3 +11,20 @@ .covidcast_wrapper > footer { display: none; } + +.covidcast-modes { + position: relative; + + &::before { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + border-bottom: 1px solid #e5e5e5; + } + + .uk-tab::before { + display: none; + } +} diff --git a/themes/delphi/assets/js/feedback-button.js b/themes/delphi/assets/js/feedback-button.js new file mode 100644 index 000000000..245e7046f --- /dev/null +++ b/themes/delphi/assets/js/feedback-button.js @@ -0,0 +1,88 @@ +import UIkit from "uikit/dist/js/uikit.js"; + +(() => { + const markSubmitted = () => { + if (!localStorage) { + return; + } + // expires in a month + const expiresAt = new Date(); + expiresAt.setMonth(expiresAt.getMonth() + 1); + localStorage.setItem("feedback-submitted", expiresAt.toString()); + + // send a google analytics event + if (window.ga) { + window.ga("send", "event", "feedback", "open", "show feedback form", "true"); + } + }; + const wasRecentlySubmitted = () => { + if (!localStorage) { + return false; + } + const r = localStorage.getItem("feedback-submitted"); + if (!r) { + return false; + } + const expiresAt = new Date(r); + // check if expired + return expiresAt > new Date(); + }; + + const showForm = (formLink) => { + const url = `${formLink}${encodeURIComponent(location.href)}&embedded=true`; + UIkit.modal.dialog( + `
` + ); + }; + const btn = document.querySelector(".feedback-button"); + if (btn) { + btn.addEventListener("click", (e) => { + e.preventDefault(); + markSubmitted(); + showForm(btn.href); + }); + } + + const container = document.querySelector(".feedback-popup-container"); + + if (container && !wasRecentlySubmitted()) { + const delayMin = Number.parseInt(container.dataset.delayMin, 10) * 1000; + const delayMax = Number.parseInt(container.dataset.delayMax, 10) * 1000; + const delay = delayMin + Math.random() * (delayMax - delayMin); + + const feedbackLikelihoodDesktop = Number.parseFloat(container.dataset.likeDesktop); + const feedbackLikelihoodMobile = Number.parseFloat(container.dataset.likeMobile); + const isMobile = window.matchMedia("only screen and (max-width: 700px)").matches; + + const showByChance = + (isMobile && Math.random() <= feedbackLikelihoodMobile) || + (!isMobile && Math.random() <= feedbackLikelihoodDesktop); + + const duration = Number.parseInt(container.dataset.duration, 10) * 1000; + const formLink = container.dataset.href; + + const showFeedbackNotification = () => { + const elem = UIkit.notification({ + message: container.innerHTML, + pos: "bottom-right", + timeout: duration, + clsContainer: "feedback-message uk-notification", + }); + elem.$el.querySelector("div").addEventListener("click", (e) => { + // stop auto close + e.stopPropagation(); + e.preventDefault(); + }); + elem.$el.querySelector(".feedback-button").addEventListener("click", (e) => { + e.preventDefault(); + elem.close(true); + markSubmitted(); + showForm(formLink); + }); + }; + // initial delay + if (showByChance) { + setTimeout(showFeedbackNotification, delay); + } + } +})(); diff --git a/themes/delphi/assets/js/main.js b/themes/delphi/assets/js/main.js index 97b340962..dee102a9e 100644 --- a/themes/delphi/assets/js/main.js +++ b/themes/delphi/assets/js/main.js @@ -1,5 +1,6 @@ import UIkit from "uikit/dist/js/uikit.js"; import plugin from "uikit/dist/js/uikit-icons.js"; +import "./feedback-button"; UIkit.use(plugin); // re export for COVIDcast diff --git a/themes/delphi/layouts/_default/baseof.html b/themes/delphi/layouts/_default/baseof.html index f70a407fa..f2389b0a8 100644 --- a/themes/delphi/layouts/_default/baseof.html +++ b/themes/delphi/layouts/_default/baseof.html @@ -8,7 +8,7 @@ {{ partial "nav.html" . }} - {{ partial "menu/breadcrumb.html" . }} + {{ block "breadcrumb" . }}{{ partial "menu/breadcrumb.html" . }}{{ end }} {{ block "main" . }} {{ end }} {{ partial "footer.html" . }} {{ partial "scripts.html" . }} diff --git a/themes/delphi/layouts/_default/covidcast_app.html b/themes/delphi/layouts/_default/covidcast_app.html index ca4038f1c..1f11dcda3 100644 --- a/themes/delphi/layouts/_default/covidcast_app.html +++ b/themes/delphi/layouts/_default/covidcast_app.html @@ -5,6 +5,7 @@ {{ end }} +{{ define "breadcrumb" }}{{ end }} {{ define "body_class" }}covidcast_wrapper{{ end }} {{ define "main" }} {{ partial "covidcast/modes.html" . }} diff --git a/themes/delphi/layouts/partials/footer.html b/themes/delphi/layouts/partials/footer.html index fe94b349a..683e0b1ea 100644 --- a/themes/delphi/layouts/partials/footer.html +++ b/themes/delphi/layouts/partials/footer.html @@ -40,8 +40,31 @@
Contact
  • Twitter
  • +
  • + Submit Feedback +
  • +{{ if .Params.feedback }} + +{{ end }}