Differences in slopes or residual variance

library(tidyverse)
library(brms)

Data-generating model

set.seed(201910052)
n <- 1000
people <- tibble(
  id = 1:n,
  days_WFH = rep(0:5, length.out = n),
  parent = id > n/2,
  sleep = rnorm(n, if_else(parent, 6, 8), if_else(parent, 5, 1)),
  RandomVariation = rnorm(n)
)

table(round(people$sleep))
## 
##  -7  -6  -5  -4  -3  -2  -1   0   1   2   3   4   5   6   7   8   9  10  11  12 
##   1   3   2   4   6  11  17  17  18  36  28  38  39  89 159 231 153  53  29  17 
##  13  14  15  16  17  18  21 
##  19  11   6   3   2   6   2
xtabs(~ parent + days_WFH, people)
##        days_WFH
## parent   0  1  2  3  4  5
##   FALSE 84 84 83 83 83 83
##   TRUE  83 83 84 84 83 83
table(people$days_WFH)
## 
##   0   1   2   3   4   5 
## 167 167 167 167 166 166

Variance in outcome increases with moderator, predictor variance is lower

DGM

people <- people %>% mutate(
  Productivity = 20 + sleep + if_else(parent, 0.31, 0.1) * days_WFH + 1.75 * RandomVariation
)
ggplot(data = people) + geom_histogram(aes(sleep, fill = parent), position = position_identity(), alpha = 0.4)
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

ggplot(data = people) + geom_histogram(aes(Productivity, fill = parent), position = position_identity(), alpha = 0.4)
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

Fit LM

model <- brm(Productivity ~ parent * days_WFH, data = people, cores = 4, backend = "cmdstanr", file = "models/productivity_parents_mean")
model2 <- brm(bf(Productivity ~ parent * days_WFH, 
                 sigma ~ parent), data = people, cores = 4, backend = "cmdstanr", file = "models/productivity_parents_mean_sigma")
people[,c("estimate__", "std.error__", "lower__", "upper__")] <- fitted(model, re_formula = NA)

Inspect correlations

(cors <- people %>% group_by(parent) %>%
  summarise(cor = cor(days_WFH, Productivity),
            slope = broom::tidy(lm(Productivity ~ days_WFH))[2, "estimate"][[1]],
            sd_resid = sd(resid(lm(Productivity ~ days_WFH))),
            sd_prod = sd(Productivity),
            mean_prod = mean(Productivity),
            max_js = max(days_WFH),
            mean_js = mean(days_WFH),
            mean_est = mean(estimate__),
            sd_js = sd(days_WFH))
)
## `summarise()` ungrouping output (override with `.groups` argument)
## # A tibble: 2 x 10
##   parent   cor slope sd_resid sd_prod mean_prod max_js mean_js mean_est sd_js
##   <lgl>  <dbl> <dbl>    <dbl>   <dbl>     <dbl>  <int>   <dbl>    <dbl> <dbl>
## 1 FALSE  0.131 0.157     2.03    2.05      28.2      5    2.49     28.2  1.71
## 2 TRUE   0.132 0.398     5.10    5.15      27.0      5    2.5      27.0  1.71

Scatter plot

m6 <- ggplot(people, aes(days_WFH, Productivity)) +
  facet_wrap(~ parent, labeller = labeller(parent = c("TRUE" = "parent", "FALSE" = "non-parent"))) +
  geom_point(alpha = 0.1, color = "#6E9181") +
  # geom_line(aes(color = factor(if_else(id > 25, id - 24.5, as.numeric(id)))), stat = "smooth", method = 'lm', alpha = 0.3) +
  geom_smooth(aes(days_WFH, Productivity), method = 'lm', color = 'black') +
  geom_text(aes(max_js - 3, mean_est + 1.1, label = sprintf('b==plain("%.2f")', slope)), data = cors, hjust = 0, parse = TRUE, size = 3) +

  scale_color_viridis_d(guide = F, option = "B")+
  geom_text(aes(label = sprintf('italic("r")==plain("%.2f")', cor)), x= 0.5, y=39, data = cors, hjust = 0, parse = TRUE, size = 3) +

  geom_text(aes(max_js + 0.3, y = mean_prod - 2, label = sprintf('sigma[y]==plain("%.2f")', sd_prod)),, data = cors, hjust = 0, parse = TRUE, size = 3, angle = 90) +
  geom_segment(aes(x = max_js + 0.55, y = mean_prod - sd_prod/2, xend = max_js + 0.55, yend = mean_prod + sd_prod/2), data = cors, size = 1, color = "#219FD8") +

  geom_text(aes(max_js + 0.9, y = mean_prod - 2, label = sprintf('sigma[res(y)]==plain("%.2f")', sd_resid)),, data = cors, hjust = 0, parse = TRUE, size = 3, angle = 90) +
  geom_segment(aes(x = max_js + 1.15, y = mean_prod - sd_resid/2, xend = max_js + 1.15, yend = mean_prod + sd_resid/2), data = cors, size = 1, color = "#219FD8") +

  geom_text(aes(label = sprintf('sigma[x]==plain("%.1f")', sd_js),  x= mean_js - .7, y = 16), data = cors, hjust = 0, parse = TRUE, size = 3) +
  geom_segment(aes(x = mean_js - sd_js/2, y = 15, xend = mean_js + sd_js/2, yend = 15), data = cors, size = 1, color = "#219FD8") +
  

  # coord_cartesian(ylim = c(-3.5, 3.5)) +
  scale_x_continuous("Days worked from home", breaks = 0:5) +
  ylab("Productivity (h)") +
  ggthemes::theme_clean()
m6
## `geom_smooth()` using formula 'y ~ x'

ggsave("plots/parents_slope_vs_corr.pdf", width = 5.5, height = 4)
## `geom_smooth()` using formula 'y ~ x'
ggsave("plots/parents_slope_vs_corr.png", width = 4.5, height = 3)
## `geom_smooth()` using formula 'y ~ x'
IyBEaWZmZXJlbmNlcyBpbiBzbG9wZXMgb3IgcmVzaWR1YWwgdmFyaWFuY2UKCmBgYHtyfQpsaWJyYXJ5KHRpZHl2ZXJzZSkKbGlicmFyeShicm1zKQpgYGAKCiMjIyBEYXRhLWdlbmVyYXRpbmcgbW9kZWwKYGBge3J9CnNldC5zZWVkKDIwMTkxMDA1MikKbiA8LSAxMDAwCnBlb3BsZSA8LSB0aWJibGUoCiAgaWQgPSAxOm4sCiAgZGF5c19XRkggPSByZXAoMDo1LCBsZW5ndGgub3V0ID0gbiksCiAgcGFyZW50ID0gaWQgPiBuLzIsCiAgc2xlZXAgPSBybm9ybShuLCBpZl9lbHNlKHBhcmVudCwgNiwgOCksIGlmX2Vsc2UocGFyZW50LCA1LCAxKSksCiAgUmFuZG9tVmFyaWF0aW9uID0gcm5vcm0obikKKQoKdGFibGUocm91bmQocGVvcGxlJHNsZWVwKSkKeHRhYnMofiBwYXJlbnQgKyBkYXlzX1dGSCwgcGVvcGxlKQp0YWJsZShwZW9wbGUkZGF5c19XRkgpCmBgYAoKCiMjIFZhcmlhbmNlIGluIG91dGNvbWUgaW5jcmVhc2VzIHdpdGggbW9kZXJhdG9yLCBwcmVkaWN0b3IgdmFyaWFuY2UgaXMgbG93ZXIKIyMjIERHTQpgYGB7cn0KcGVvcGxlIDwtIHBlb3BsZSAlPiUgbXV0YXRlKAogIFByb2R1Y3Rpdml0eSA9IDIwICsgc2xlZXAgKyBpZl9lbHNlKHBhcmVudCwgMC4zMSwgMC4xKSAqIGRheXNfV0ZIICsgMS43NSAqIFJhbmRvbVZhcmlhdGlvbgopCmdncGxvdChkYXRhID0gcGVvcGxlKSArIGdlb21faGlzdG9ncmFtKGFlcyhzbGVlcCwgZmlsbCA9IHBhcmVudCksIHBvc2l0aW9uID0gcG9zaXRpb25faWRlbnRpdHkoKSwgYWxwaGEgPSAwLjQpCmdncGxvdChkYXRhID0gcGVvcGxlKSArIGdlb21faGlzdG9ncmFtKGFlcyhQcm9kdWN0aXZpdHksIGZpbGwgPSBwYXJlbnQpLCBwb3NpdGlvbiA9IHBvc2l0aW9uX2lkZW50aXR5KCksIGFscGhhID0gMC40KQpgYGAKCiMjIyBGaXQgTE0KYGBge3J9Cm1vZGVsIDwtIGJybShQcm9kdWN0aXZpdHkgfiBwYXJlbnQgKiBkYXlzX1dGSCwgZGF0YSA9IHBlb3BsZSwgY29yZXMgPSA0LCBiYWNrZW5kID0gImNtZHN0YW5yIiwgZmlsZSA9ICJtb2RlbHMvcHJvZHVjdGl2aXR5X3BhcmVudHNfbWVhbiIpCm1vZGVsMiA8LSBicm0oYmYoUHJvZHVjdGl2aXR5IH4gcGFyZW50ICogZGF5c19XRkgsIAogICAgICAgICAgICAgICAgIHNpZ21hIH4gcGFyZW50KSwgZGF0YSA9IHBlb3BsZSwgY29yZXMgPSA0LCBiYWNrZW5kID0gImNtZHN0YW5yIiwgZmlsZSA9ICJtb2RlbHMvcHJvZHVjdGl2aXR5X3BhcmVudHNfbWVhbl9zaWdtYSIpCnBlb3BsZVssYygiZXN0aW1hdGVfXyIsICJzdGQuZXJyb3JfXyIsICJsb3dlcl9fIiwgInVwcGVyX18iKV0gPC0gZml0dGVkKG1vZGVsLCByZV9mb3JtdWxhID0gTkEpCmBgYAoKIyMjIEluc3BlY3QgY29ycmVsYXRpb25zCmBgYHtyfQooY29ycyA8LSBwZW9wbGUgJT4lIGdyb3VwX2J5KHBhcmVudCkgJT4lCiAgc3VtbWFyaXNlKGNvciA9IGNvcihkYXlzX1dGSCwgUHJvZHVjdGl2aXR5KSwKICAgICAgICAgICAgc2xvcGUgPSBicm9vbTo6dGlkeShsbShQcm9kdWN0aXZpdHkgfiBkYXlzX1dGSCkpWzIsICJlc3RpbWF0ZSJdW1sxXV0sCiAgICAgICAgICAgIHNkX3Jlc2lkID0gc2QocmVzaWQobG0oUHJvZHVjdGl2aXR5IH4gZGF5c19XRkgpKSksCiAgICAgICAgICAgIHNkX3Byb2QgPSBzZChQcm9kdWN0aXZpdHkpLAogICAgICAgICAgICBtZWFuX3Byb2QgPSBtZWFuKFByb2R1Y3Rpdml0eSksCiAgICAgICAgICAgIG1heF9qcyA9IG1heChkYXlzX1dGSCksCiAgICAgICAgICAgIG1lYW5fanMgPSBtZWFuKGRheXNfV0ZIKSwKICAgICAgICAgICAgbWVhbl9lc3QgPSBtZWFuKGVzdGltYXRlX18pLAogICAgICAgICAgICBzZF9qcyA9IHNkKGRheXNfV0ZIKSkKKQpgYGAKCiMjIyBTY2F0dGVyIHBsb3QKYGBge3J9Cm02IDwtIGdncGxvdChwZW9wbGUsIGFlcyhkYXlzX1dGSCwgUHJvZHVjdGl2aXR5KSkgKwogIGZhY2V0X3dyYXAofiBwYXJlbnQsIGxhYmVsbGVyID0gbGFiZWxsZXIocGFyZW50ID0gYygiVFJVRSIgPSAicGFyZW50IiwgIkZBTFNFIiA9ICJub24tcGFyZW50IikpKSArCiAgZ2VvbV9wb2ludChhbHBoYSA9IDAuMSwgY29sb3IgPSAiIzZFOTE4MSIpICsKICAjIGdlb21fbGluZShhZXMoY29sb3IgPSBmYWN0b3IoaWZfZWxzZShpZCA+IDI1LCBpZCAtIDI0LjUsIGFzLm51bWVyaWMoaWQpKSkpLCBzdGF0ID0gInNtb290aCIsIG1ldGhvZCA9ICdsbScsIGFscGhhID0gMC4zKSArCiAgZ2VvbV9zbW9vdGgoYWVzKGRheXNfV0ZILCBQcm9kdWN0aXZpdHkpLCBtZXRob2QgPSAnbG0nLCBjb2xvciA9ICdibGFjaycpICsKICBnZW9tX3RleHQoYWVzKG1heF9qcyAtIDMsIG1lYW5fZXN0ICsgMS4xLCBsYWJlbCA9IHNwcmludGYoJ2I9PXBsYWluKCIlLjJmIiknLCBzbG9wZSkpLCBkYXRhID0gY29ycywgaGp1c3QgPSAwLCBwYXJzZSA9IFRSVUUsIHNpemUgPSAzKSArCgogIHNjYWxlX2NvbG9yX3ZpcmlkaXNfZChndWlkZSA9IEYsIG9wdGlvbiA9ICJCIikrCiAgZ2VvbV90ZXh0KGFlcyhsYWJlbCA9IHNwcmludGYoJ2l0YWxpYygiciIpPT1wbGFpbigiJS4yZiIpJywgY29yKSksIHg9IDAuNSwgeT0zOSwgZGF0YSA9IGNvcnMsIGhqdXN0ID0gMCwgcGFyc2UgPSBUUlVFLCBzaXplID0gMykgKwoKICBnZW9tX3RleHQoYWVzKG1heF9qcyArIDAuMywgeSA9IG1lYW5fcHJvZCAtIDIsIGxhYmVsID0gc3ByaW50Zignc2lnbWFbeV09PXBsYWluKCIlLjJmIiknLCBzZF9wcm9kKSksLCBkYXRhID0gY29ycywgaGp1c3QgPSAwLCBwYXJzZSA9IFRSVUUsIHNpemUgPSAzLCBhbmdsZSA9IDkwKSArCiAgZ2VvbV9zZWdtZW50KGFlcyh4ID0gbWF4X2pzICsgMC41NSwgeSA9IG1lYW5fcHJvZCAtIHNkX3Byb2QvMiwgeGVuZCA9IG1heF9qcyArIDAuNTUsIHllbmQgPSBtZWFuX3Byb2QgKyBzZF9wcm9kLzIpLCBkYXRhID0gY29ycywgc2l6ZSA9IDEsIGNvbG9yID0gIiMyMTlGRDgiKSArCgogIGdlb21fdGV4dChhZXMobWF4X2pzICsgMC45LCB5ID0gbWVhbl9wcm9kIC0gMiwgbGFiZWwgPSBzcHJpbnRmKCdzaWdtYVtyZXMoeSldPT1wbGFpbigiJS4yZiIpJywgc2RfcmVzaWQpKSwsIGRhdGEgPSBjb3JzLCBoanVzdCA9IDAsIHBhcnNlID0gVFJVRSwgc2l6ZSA9IDMsIGFuZ2xlID0gOTApICsKICBnZW9tX3NlZ21lbnQoYWVzKHggPSBtYXhfanMgKyAxLjE1LCB5ID0gbWVhbl9wcm9kIC0gc2RfcmVzaWQvMiwgeGVuZCA9IG1heF9qcyArIDEuMTUsIHllbmQgPSBtZWFuX3Byb2QgKyBzZF9yZXNpZC8yKSwgZGF0YSA9IGNvcnMsIHNpemUgPSAxLCBjb2xvciA9ICIjMjE5RkQ4IikgKwoKICBnZW9tX3RleHQoYWVzKGxhYmVsID0gc3ByaW50Zignc2lnbWFbeF09PXBsYWluKCIlLjFmIiknLCBzZF9qcyksICB4PSBtZWFuX2pzIC0gLjcsIHkgPSAxNiksIGRhdGEgPSBjb3JzLCBoanVzdCA9IDAsIHBhcnNlID0gVFJVRSwgc2l6ZSA9IDMpICsKICBnZW9tX3NlZ21lbnQoYWVzKHggPSBtZWFuX2pzIC0gc2RfanMvMiwgeSA9IDE1LCB4ZW5kID0gbWVhbl9qcyArIHNkX2pzLzIsIHllbmQgPSAxNSksIGRhdGEgPSBjb3JzLCBzaXplID0gMSwgY29sb3IgPSAiIzIxOUZEOCIpICsKICAKCiAgIyBjb29yZF9jYXJ0ZXNpYW4oeWxpbSA9IGMoLTMuNSwgMy41KSkgKwogIHNjYWxlX3hfY29udGludW91cygiRGF5cyB3b3JrZWQgZnJvbSBob21lIiwgYnJlYWtzID0gMDo1KSArCiAgeWxhYigiUHJvZHVjdGl2aXR5IChoKSIpICsKICBnZ3RoZW1lczo6dGhlbWVfY2xlYW4oKQptNgoKZ2dzYXZlKCJwbG90cy9wYXJlbnRzX3Nsb3BlX3ZzX2NvcnIucGRmIiwgd2lkdGggPSA1LjUsIGhlaWdodCA9IDQpCmdnc2F2ZSgicGxvdHMvcGFyZW50c19zbG9wZV92c19jb3JyLnBuZyIsIHdpZHRoID0gNC41LCBoZWlnaHQgPSAzKQpgYGAKCgo=