library(tidyverse)
library(rlang)
library(broom)
library(quickpsy) # to use qpdat
fit_glm <- function(.data, .response, .predictor) {
predictor <- ensym(.predictor)
prop <- .data |>
group_by({{ .predictor }}) |>
summarise(k = sum({{.response}}),
n = n(),
prop = mean({{ .response }}), .groups = "keep")
model <- glm(inject(cbind(k, n - k) ~ !!predictor),
family = binomial(logit),
data = prop)
param <- tidy(model) |>
select(term, estimate) |>
pivot_wider(names_from = term, values_from = estimate) |>
rename(intercept = `(Intercept)`, sensitivity = {{ .predictor }}) |>
mutate(bias = -intercept / sensitivity)
min_max <- .data |>
distinct({{.predictor}}) |>
summarise(min = min({{ .predictor }}),
max = max({{ .predictor }}))
sequ <- tibble({{.predictor}} := seq(min_max$min,
min_max$max,
length.out = 100))
psy <- augment(model, newdata = sequ, type.predict = "response") |>
rename(prop = .fitted)
deviance <- glance(model) |>
select(null.deviance, df.null) |>
mutate(p_value = pchisq(null.deviance, df = df.null, lower.tail = FALSE)) |>
rename(null_deviance = null.deviance, df_null = df.null)
list(prop = prop,
param = param,
psy = psy,
deviance = deviance)
}
Single group case
fit_all <- fit_glm(qpdat, resp, phase)
ggplot() +
geom_point(data = fit_all$prop,
aes(x = phase, y = prop)) +
geom_line(data = fit_all$psy,
aes(x = phase, y = prop)) +
geom_segment(data = fit_all$param,
aes(x = bias, xend = bias,
y = 0, yend = .5))

Multiple groups
fits <- qpdat |>
group_by(participant, cond) |>
reframe(fit = list(fit_glm(cur_data(), resp, phase))) |>
unnest_wider(fit)
proportions <- fits |>
select(participant, cond, prop) |>
unnest(prop)
psy_functions <- fits |>
select(participant, cond, psy) |>
unnest(psy)
params <- fits |>
select(participant, cond, param) |>
unnest(param)
ggplot() +
facet_wrap(vars(participant)) +
geom_point(data = proportions,
aes(x = phase, y = prop, color = cond)) +
geom_line(data = psy_functions,
aes(x = phase, y = prop, color = cond)) +
geom_segment(data = params,
aes(x = bias, xend = bias,
y = 0, yend = .5, color = cond))

All the fits are different from a fit with a flat slope
fits |>
select(participant, cond, deviance) |>
unnest(deviance)
LS0tCnRpdGxlOiAiRml0IGdsbSBmdW5jdGlvbiIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKYGBge3J9CmxpYnJhcnkodGlkeXZlcnNlKQpsaWJyYXJ5KHJsYW5nKQpsaWJyYXJ5KGJyb29tKQpsaWJyYXJ5KHF1aWNrcHN5KSAjIHRvIHVzZSBxcGRhdApgYGAKCgpgYGB7cn0KZml0X2dsbSA8LSBmdW5jdGlvbiguZGF0YSwgLnJlc3BvbnNlLCAucHJlZGljdG9yKSB7CiAgCiAgcHJlZGljdG9yIDwtIGVuc3ltKC5wcmVkaWN0b3IpCgogIHByb3AgPC0gLmRhdGEgfD4KICAgIGdyb3VwX2J5KHt7IC5wcmVkaWN0b3IgfX0pIHw+CiAgICBzdW1tYXJpc2UoayA9IHN1bSh7ey5yZXNwb25zZX19KSwgCiAgICAgICAgICAgICAgbiA9IG4oKSwKICAgICAgICAgICAgICBwcm9wID0gbWVhbih7eyAucmVzcG9uc2UgfX0pLCAuZ3JvdXBzID0gImtlZXAiKQoKICBtb2RlbCA8LSBnbG0oaW5qZWN0KGNiaW5kKGssIG4gLSBrKSB+ICEhcHJlZGljdG9yKSwKICAgICAgICAgICAgICAgICAgICAgIGZhbWlseSA9IGJpbm9taWFsKGxvZ2l0KSwKICAgICAgICAgICAgICAgICAgICAgIGRhdGEgPSBwcm9wKQoKICBwYXJhbSA8LSB0aWR5KG1vZGVsKSB8PgogICAgc2VsZWN0KHRlcm0sIGVzdGltYXRlKSB8PgogICAgcGl2b3Rfd2lkZXIobmFtZXNfZnJvbSA9IHRlcm0sIHZhbHVlc19mcm9tID0gZXN0aW1hdGUpIHw+CiAgICByZW5hbWUoaW50ZXJjZXB0ID0gYChJbnRlcmNlcHQpYCwgc2Vuc2l0aXZpdHkgPSB7eyAucHJlZGljdG9yIH19KSB8PgogICAgbXV0YXRlKGJpYXMgPSAtaW50ZXJjZXB0IC8gc2Vuc2l0aXZpdHkpCgogIG1pbl9tYXggPC0gLmRhdGEgfD4KICAgIGRpc3RpbmN0KHt7LnByZWRpY3Rvcn19KSB8PgogICAgc3VtbWFyaXNlKG1pbiA9IG1pbih7eyAucHJlZGljdG9yIH19KSwKICAgICAgICAgICAgICBtYXggPSBtYXgoe3sgLnByZWRpY3RvciB9fSkpCgogIHNlcXUgPC0gdGliYmxlKHt7LnByZWRpY3Rvcn19IDo9IHNlcShtaW5fbWF4JG1pbiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWluX21heCRtYXgsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxlbmd0aC5vdXQgPSAxMDApKQoKICBwc3kgPC0gYXVnbWVudChtb2RlbCwgbmV3ZGF0YSA9IHNlcXUsIHR5cGUucHJlZGljdCA9ICJyZXNwb25zZSIpIHw+CiAgICByZW5hbWUocHJvcCA9IC5maXR0ZWQpCiAgCiAgZGV2aWFuY2UgPC0gZ2xhbmNlKG1vZGVsKSB8PiAKICAgIHNlbGVjdChudWxsLmRldmlhbmNlLCBkZi5udWxsKSB8PiAKICAgIG11dGF0ZShwX3ZhbHVlID0gcGNoaXNxKG51bGwuZGV2aWFuY2UsIGRmID0gZGYubnVsbCwgbG93ZXIudGFpbCA9IEZBTFNFKSkgfD4KICAgIHJlbmFtZShudWxsX2RldmlhbmNlID0gbnVsbC5kZXZpYW5jZSwgZGZfbnVsbCA9IGRmLm51bGwpCgogIGxpc3QocHJvcCA9IHByb3AsCiAgICAgICBwYXJhbSA9IHBhcmFtLAogICAgICAgcHN5ID0gcHN5LCAKICAgICAgIGRldmlhbmNlID0gZGV2aWFuY2UpCn0KYGBgCgoKIyMgU2luZ2xlIGdyb3VwIGNhc2UKCmBgYHtyfQpmaXRfYWxsIDwtIGZpdF9nbG0ocXBkYXQsIHJlc3AsIHBoYXNlKQpgYGAKCgpgYGB7cn0KZ2dwbG90KCkgKwogIGdlb21fcG9pbnQoZGF0YSA9IGZpdF9hbGwkcHJvcCwKICAgICAgICAgICAgIGFlcyh4ID0gcGhhc2UsIHkgPSBwcm9wKSkgKwogIGdlb21fbGluZShkYXRhID0gZml0X2FsbCRwc3ksIAogICAgICAgICAgICBhZXMoeCA9IHBoYXNlLCB5ID0gcHJvcCkpICsKICBnZW9tX3NlZ21lbnQoZGF0YSA9IGZpdF9hbGwkcGFyYW0sIAogICAgICAgICAgICAgICBhZXMoeCA9IGJpYXMsIHhlbmQgPSBiaWFzLCAKICAgICAgICAgICAgICAgICAgIHkgPSAwLCB5ZW5kID0gLjUpKQpgYGAKCgpNdWx0aXBsZSBncm91cHMKCmBgYHtyfQpmaXRzIDwtIHFwZGF0IHw+IAogIGdyb3VwX2J5KHBhcnRpY2lwYW50LCBjb25kKSB8PiAKICByZWZyYW1lKGZpdCA9IGxpc3QoZml0X2dsbShjdXJfZGF0YSgpLCByZXNwLCBwaGFzZSkpKSB8PiAKICB1bm5lc3Rfd2lkZXIoZml0KQpgYGAKCmBgYHtyfQpwcm9wb3J0aW9ucyA8LSBmaXRzIHw+IAogIHNlbGVjdChwYXJ0aWNpcGFudCwgY29uZCwgcHJvcCkgfD4gCiAgdW5uZXN0KHByb3ApCmBgYAoKYGBge3J9CnBzeV9mdW5jdGlvbnMgPC0gZml0cyB8PiAKICBzZWxlY3QocGFydGljaXBhbnQsIGNvbmQsIHBzeSkgfD4gCiAgdW5uZXN0KHBzeSkKYGBgCgpgYGB7cn0KcGFyYW1zIDwtIGZpdHMgfD4gCiAgc2VsZWN0KHBhcnRpY2lwYW50LCBjb25kLCBwYXJhbSkgfD4gCiAgdW5uZXN0KHBhcmFtKQpgYGAKCmBgYHtyfQpnZ3Bsb3QoKSArCiAgZmFjZXRfd3JhcCh2YXJzKHBhcnRpY2lwYW50KSkgKwogIGdlb21fcG9pbnQoZGF0YSA9IHByb3BvcnRpb25zLAogICAgICAgICAgICAgYWVzKHggPSBwaGFzZSwgeSA9IHByb3AsIGNvbG9yID0gY29uZCkpICsKICBnZW9tX2xpbmUoZGF0YSA9IHBzeV9mdW5jdGlvbnMsIAogICAgICAgICAgICBhZXMoeCA9IHBoYXNlLCB5ID0gcHJvcCwgY29sb3IgPSBjb25kKSkgKwogIGdlb21fc2VnbWVudChkYXRhID0gcGFyYW1zLCAKICAgICAgICAgICAgICAgYWVzKHggPSBiaWFzLCB4ZW5kID0gYmlhcywgCiAgICAgICAgICAgICAgICAgICB5ID0gMCwgeWVuZCA9IC41LCBjb2xvciA9IGNvbmQpKQpgYGAKCgpBbGwgdGhlIGZpdHMgYXJlIGRpZmZlcmVudCBmcm9tIGEgZml0IHdpdGggYSBmbGF0IHNsb3BlCgpgYGB7cn0KIGZpdHMgfD4gCiAgc2VsZWN0KHBhcnRpY2lwYW50LCBjb25kLCBkZXZpYW5jZSkgfD4gCiAgdW5uZXN0KGRldmlhbmNlKQpgYGAKCg==