Sign in

Daniel 🕹️

@strengejacke.de
628 followers 113 following 522 posts

He/she/it - 's' muss mit. We're lower than the world! R easystats project: easystats.github.io/easystats

PostsRepliesMedia
Daniel 🕹️ @strengejacke.de · 05/08/2026
Still, "evidence" does not indicate "clinical importance". You should take these two into account (not a new insight, I know...).
130
Daniel 🕹️ @strengejacke.de · 05/08/2026
Just for the record, this is a good example for the `post_process` argument in {modelbased}...
library(modelbased)
data(mtcars)
m <- glm(am ~ vs, family = binomial, data = mtcars)

estimate_means(
  m,
  "vs",
  transform = effectsize::probs_to_odds,
  predict = "response",
  post_process = difference ~ pairwise
)
#> Post-processing difference ~ pairwise...
#> Estimated Marginal Means
#> 
#> Parameter   | Probability |   SE |       95% CI |    z
#> ------------------------------------------------------
#> (b2) - (b1) |        0.50 | 0.17 | [0.16, 0.84] | 2.88
#> 
#> Variable predicted: am
#> Predictors modulated: vs
#> Predictions are on the response-scale.
030
Daniel 🕹️ @strengejacke.de · 27/06/2026
Only payable with this plectrum
000
Daniel 🕹️ @strengejacke.de · 27/06/2026
110
Daniel 🕹️ @strengejacke.de · 24/06/2026
A new paper by @dominiquemakowski.bsky.social, @mattansb.msbstats.info, me and colleagues just out! We show how to choose informative priors in Bayesian regression models using a systematic simulation study and a practical step-by-step tutorial in #Rstats and #Stan! doi.org/10.3389/fpsy... >
17623
Daniel 🕹️ @strengejacke.de · 15/06/2026
Not a book, but a monthly magazine. Listings were available in Basic and ASM language.
020
Daniel 🕹️ @strengejacke.de · 20/05/2026
Still not get it working 🙈 Can you rewrite this code with a different hypothesis argument and make it return the same results? (code in alt text)
set.seed(123)
n <- 200
d <- data.frame(
  outcome = rnorm(n),
  grp = as.factor(sample(c("treatment", "control"), n, TRUE)),
  episode = as.factor(sample(1:3, n, TRUE)),
  sex = as.factor(sample(c("female", "male"), n, TRUE, prob = c(0.4, 0.6)))
)
model2 <- lm(outcome ~ grp * episode, data = d)

avg_predictions(
  model2,
  by = c("episode", "grp"),
  hypothesis = "(b1 - b3) = (b2 - b4)"
)
100
Daniel 🕹️ @strengejacke.de · 20/05/2026
Code in Alt-text... You always have to look at the table of predictions, to find out the b-terms/rows. Since the order of coefficients is different between modelbased and marginaleffects, you see different values for the hypothesis-argument here.
library(modelbased)
library(marginaleffects)

set.seed(123)
n <- 200
d <- data.frame(
  outcome = rnorm(n),
  grp = as.factor(sample(c("treatment", "control"), n, TRUE)),
  episode = as.factor(sample(1:3, n, TRUE)),
  sex = as.factor(sample(c("female", "male"), n, TRUE, prob = c(0.4, 0.6)))
)
model2 <- lm(outcome ~ grp * episode, data = d)

estimate_contrasts(model2, c("episode", "grp"), comparison = "(b1 - b2) = (b4 - b5)", estimate = "average")

avg_predictions(
  model2,
  by = c("episode", "grp"),
  hypothesis = "(b1 - b3) = (b2 - b4)"
)
030
Daniel 🕹️ @strengejacke.de · 17/05/2026
000
Daniel 🕹️ @strengejacke.de · 05/05/2026
And note the changes in syntax highlighting: (package names have a different color now... Sometimes it’s the little things...)
1100
Daniel 🕹️ @strengejacke.de · 22/04/2026
Here's a short vignette on within-between effects: easystats.github.io/parameters/a... From there, you can calculate the context effect by contrasting the (average) slopes of those two effects, see attached screenshots (code in ALT text).
library(parameters)
library(modelbased)
library(marginaleffects)
library(lme4)


data("qol_cancer")
qol_cancer <- datawizard::demean(qol_cancer, select = c("phq4", "QoL"), by = "ID")
mixed_1 <- lmer(
  QoL ~ time + phq4_within + phq4_between + (1 | ID),
  data = qol_cancer
)
# within- and between effects
model_parameters(mixed_1)

# context effect - difference between within- and between-effect
estimate_contrasts(mixed_1, c("phq4_within", "phq4_between"), comparison = "slope")

# context effect - using marginal effects
avg_comparisons(
  mixed_1,
  variables = c("phq4_within", "phq4_between"),
  hypothesis = ~pairwise
)
070
Daniel 🕹️ @strengejacke.de · 03/03/2026
I just realized the small post-it in the left corner... 🤣 maybe better to see in this hires-image
000
Daniel 🕹️ @strengejacke.de · 03/03/2026
I'm pasting one of these memes into every manuscript where I'm asked to write the methods section after I did the analyses.
110
Daniel 🕹️ @strengejacke.de · 21/02/2026
Since I had some experiences with tibbles (the colleague did not and thought function doesn't work), it meanwhile doesn't take that long to figure it out. Note the `as.data.frame()`.
210
Daniel 🕹️ @strengejacke.de · 21/02/2026
One of the many, many issues I faced with tibbles in my 30 years of programming and 10 years of R experience (not joking, it's almost the same dates). What is wrong here?
110
Daniel 🕹️ @strengejacke.de · 20/02/2026
150
Daniel 🕹️ @strengejacke.de · 19/02/2026
@vincentab.bsky.social, the guy whose days have more than 24 hours. (ok, altdoc is mainly @etiennebacher.bsky.social, with support from Vincent)
150
Daniel 🕹️ @strengejacke.de · 03/02/2026
Hm, doesn't work for me. I'm using the latest daily build: Positron Version: 2026.03.0 (system setup) build 14 Code - OSS Version: 1.106.0 Date: 2026-02-03T08:15:39.951Z Electron: 37.7.0 Chromium: 138.0.7204.251 Node.js: 22.20.0 V8: 13.8.258.32-electron.0 OS: Windows_NT x64 10.0.26200
210
Daniel 🕹️ @strengejacke.de · 02/02/2026
You should switch to Positron! (and here's how to do it there)
210
Daniel 🕹️ @strengejacke.de · 16/10/2025
These are my #Positron extensions I've currently installed. Are there any "must haves" missing in my list, what other extensions would you recommend etc.? (answers including "python" will be ignored... 😎) #rstats #rstudio #vscode
8213
Daniel 🕹️ @strengejacke.de · 08/09/2025
This weekend I dug out my very old #starwars #ccg cards (decipher) because my son really wanted to play the game. Now I'm thinking about checking out Star Wars Unlimited, because it's no longer easy to get cards for the CCG. Anyone experience with Star Wars Unlimited? Would you recommend it? #tcg
320
Daniel 🕹️ @strengejacke.de · 05/09/2025
R is not Nintendo. #rstats
2433
Daniel 🕹️ @strengejacke.de · 03/09/2025
If this is not of particular interest, i.e. you're not investigating "treatment methods", I'd suggest adding "hospital ID" as random effect. As said before, in the "worst case", we end up with the same accuracy as for simpler models (that's again Gelman/Hill 2007)
020
Daniel 🕹️ @strengejacke.de · 03/09/2025
from: Douglas M. Bateslme4: Mixed-effects modeling with R
182
Daniel 🕹️ @strengejacke.de · 01/09/2025
This is how table printing in #easystats look like - nice tables out-of-the-box thanks to #rstats packages like {gt} or {tinytable}, which is now fully supported across easystats📦
0122
Daniel 🕹️ @strengejacke.de · 23/08/2025
Something like:
120
Daniel 🕹️ @strengejacke.de · 06/08/2025
paramters::model_parameters() has the `ci_method` argument with similar options . however, broom.mixed returns NA for Wald-CIs?
120
Daniel 🕹️ @strengejacke.de · 06/08/2025
(code in ALT text)
# variance components
parameters::model_parameters(m, effects = "random")

# BLUPs
parameters::model_parameters(m, effects = "grouplevel")

# fixef+ranef
parameters::model_parameters(m, effects = "random_total")

# same as
modelbased::estimate_grouplevel(m)

# dot plot of BLUPs
parameters::model_parameters(m, effects = "grouplevel") |> plot()

# dot plot of BLUPs
modelbased::estimate_grouplevel(m) |> plot()
110
Daniel 🕹️ @strengejacke.de · 02/08/2025
- modern look'n'feel - fully customizable layout - absolutely easy to handle GitHub integration - code assist / LLM integration, if desired - rather simple and intuitive UI - hide/show relevant panes with a keystroke What's not to like about it? 😎
230
Daniel 🕹️ @strengejacke.de · 17/07/2025
After several years, I noticed that the first author of a co-authored article had corrected what he believed to be a ‘spelling mistake’ in the name of an R package.
121
Daniel 🕹️ @strengejacke.de · 17/07/2025
See the slides 21-23 from "Introduction into predictions and the {modelbased} package", where I tried to write up my understanding/definition: easystats.github.io/easystats/ar...
120
Daniel 🕹️ @strengejacke.de · 08/07/2025
That's ("answer is indeed ME") probably a too fast conclusion (depending on what you mean by "marginal effects" - if a single number, that's not always a good idea. In this example, we used cubic age, and the interpretation makes sense... 1/2
110
Daniel 🕹️ @strengejacke.de · 06/07/2025
Ok, works for simple models, must check for more complex like mixed effects / zero-inflated models.
m <- lm(mpg ~ wt + hp, data = mtcars)
ci <- 0.95
dof <- insight::get_df(m)
set.seed(123)
crit <- mvtnorm::qmvt(ci, df = dof, tail = "both.tails", corr = cov2cor(vcov(m)))$quantile

# 95% level%
confint(m)
#>                   2.5 %      97.5 %
#> (Intercept) 33.95738245 40.49715778
#> wt          -5.17191604 -2.58374544
#> hp          -0.05024078 -0.01330512

parameters::model_parameters(m)
#> Parameter   | Coefficient |       SE |         95% CI | t(29) |      p
#> ----------------------------------------------------------------------
#> (Intercept) |       37.23 |     1.60 | [33.96, 40.50] | 23.28 | < .001
#> wt          |       -3.88 |     0.63 | [-5.17, -2.58] | -6.13 | < .001
#> hp          |       -0.03 | 9.03e-03 | [-0.05, -0.01] | -3.52 | 0.001
#> 
#> Uncertainty intervals (equal-tailed) and p-values (two-tailed) computed
#>   using a Wald t-distribution approximation.

# sup-t adjustment
confint(m, level = 1 - 2 * pt(-abs(crit), df = dof))
#>                  1.04 %      98.96 %
#> (Intercept) 33.31930882 41.135231417
#> wt          -5.42443900 -2.331222484
#> hp          -0.05384452 -0.009701374

set.seed(123)
parameters::model_parameters(m, p_adjust = "sup-t")
#> Parameter   | Coefficient |       SE |         95% CI | t(29) |      p
#> ----------------------------------------------------------------------
#> (Intercept) |       37.23 |     1.60 | [33.32, 41.14] | 23.28 | < .001
#> wt          |       -3.88 |     0.63 | [-5.42, -2.33] | -6.13 | < .001
#> hp          |       -0.03 | 9.03e-03 | [-0.05, -0.01] | -3.52 | 0.003 
#> 
#> p-value adjustment method: Simultaneous confidence bands
#> 
#> Uncertainty intervals (equal-tailed) and p-values (two-tailed) computed
#>   using a Wald t-distribution approximation.
030
Daniel 🕹️ @strengejacke.de · 05/07/2025
Would it be something like this?
m <- lm(mpg ~ wt + hp, data = mtcars)
x <- 0.95
l <- mvtnorm::qmvnorm(x, tail = "both.tails", corr = cov2cor(vcov(m)))$quantile
1 - 2 * pnorm(-abs(l))
#> [1] 0.9797365

# 95% level%
confint(m)
#>                   2.5 %      97.5 %
#> (Intercept) 33.95738245 40.49715778
#> wt          -5.17191604 -2.58374544
#> hp          -0.05024078 -0.01330512

# new "95%" level
confint(m, level = 1 - 2 * pnorm(-abs(l)))
#>                  1.01 %      98.99 %
#> (Intercept) 33.30014759 41.154392645
#> wt          -5.43202222 -2.323639269
#> hp          -0.05395274 -0.009593154
210
Daniel 🕹️ @strengejacke.de · 01/07/2025
Time for a new wallpaper... #easystats #insight
100
Daniel 🕹️ @strengejacke.de · 01/07/2025
I mean, it even appears if you just select a single char.
010
Daniel 🕹️ @strengejacke.de · 01/07/2025
010
Daniel 🕹️ @strengejacke.de · 04/06/2025
Here's a way how I handled it in sjPlot, *if* you really have neutral categories. Such thing would be nice to have in a future update, because you don't have much tools for plotting "Likert" scales (especially not in Excel/Office diagrams, but who makes figures with office anyway?)
120
Daniel 🕹️ @strengejacke.de · 04/06/2025
I often have "neutral" categories, like "don't know" or similar, and these are neither positive nor negative, so it would be great to account for their proportion, but placing it in the middle would add half of their counts to both sides, which can be misleading when interpreting "totals".
library(tinyplot)
tinytheme(
  "clean2",
  palette.qualitative = c("black", "sienna", "grey", "indianred", "goldenrod")
)
hec <- as.data.frame(proportions(HairEyeColor, 2:3))
# Add a new factor level "Dont know" to the variable "Hair"
hec$Hair <- factor(hec$Hair, levels = c(levels(hec$Hair)[1:2], "Dont know", levels(hec$Hair)[3:4]))
hec$Hair[5] <- "Dont know"

tinyplot(
  Freq ~ Eye | Hair,
  facet = ~Sex,
  data = hec,
  type = "barplot",
  center = TRUE,
  flip = TRUE,
  facet.args = list(ncol = 1),
  yaxl = "percent"
)library(tinyplot)
tinytheme(
  "clean2",
  palette.qualitative = c("black", "sienna", "grey", "indianred", "goldenrod")
)
hec <- as.data.frame(proportions(HairEyeColor, 2:3))
# Add a new factor level "Dont know" to the variable "Hair"
hec$Hair <- factor(hec$Hair, levels = c(levels(hec$Hair)[1:2], "Dont know", levels(hec$Hair)[3:4]))
hec$Hair[5] <- "Dont know"

tinyplot(
  Freq ~ Eye | Hair,
  facet = ~Sex,
  data = hec,
  type = "barplot",
  center = TRUE,
  flip = TRUE,
  facet.args = list(ncol = 1),
  yaxl = "percent"
)
120
Daniel 🕹️ @strengejacke.de · 04/06/2025
😎
010
Daniel 🕹️ @strengejacke.de · 03/06/2025
I often show students this figure and ask, how different is the green distribution (p < 0.05) from the blue distribution (p = 0.10)? Just to raise some awareness that the difference between "statistical significant" and "not significant" is not always that significant...
32810
Daniel 🕹️ @strengejacke.de · 21/05/2025
You may also find `convert_to_na()` and `convert_na_to()` helpful (easystats.github.io/datawizard/r...), or maybe also `replace_nan_inf()` from the #rstats {datawizard} package.
060
Daniel 🕹️ @strengejacke.de · 20/05/2025
And although the scales are different for predictions and coefficients, the statistical test for the difference between levels of a factor (= p-values) are literally identical, meaning that if you work with predicted probabilities anyway, it almost doesn't matter which model you take.
data(heart, package = "glm2")
start.p <- sum(heart$Deaths) / sum(heart$Patients)

fit.glm <- glm(
  cbind(Deaths, Patients - Deaths) ~
    factor(AgeGroup) + factor(Severity) + factor(Delay) + factor(Region),
  family = binomial(),
  data = heart
)

fit.logbin <- logbin::logbin(
  formula(fit.glm),
  data = heart,
  start = c(log(start.p), rep(c(0.2, 0.4), 4)),
  trace = 1
)

modelbased::estimate_contrasts(fit.logbin, "Delay")
#> Marginal Contrasts Analysis
#> 
#> Level1 | Level2 | Difference |   SE |        95% CI | t(65) |     p
#> -------------------------------------------------------------------
#> 2      | 1      |       0.01 | 0.01 | [-0.01, 0.04] |  0.86 | 0.394
#> 3      | 1      |       0.03 | 0.02 | [ 0.00, 0.07] |  2.13 | 0.037
#> 3      | 2      |       0.02 | 0.02 | [-0.01, 0.05] |  1.56 | 0.124
#> 
#> Contrasts are on the response-scale (in %-points).

parameters::model_parameters(fit.logbin, exponentiate = TRUE, keep = "Delay")
#> Parameter | Risk Ratio |   SE |       95% CI | Statistic | df |     p
#> ---------------------------------------------------------------------
#> Delay [2] |       1.06 | 0.07 | [0.93, 1.22] |      0.85 | 65 | 0.395
#> Delay [3] |       1.19 | 0.10 | [1.01, 1.39] |      2.13 | 65 | 0.034
#> 
#> Uncertainty intervals (profile-likelihood) and p-values (two-tailed)
#>   computed using a Wald distribution approximation.
120
Daniel 🕹️ @strengejacke.de · 20/05/2025
Nice video, as always! One conclusion would be to use marginal means / adjusted predictions, because this will give consistent results for both models.
data(heart, package = "glm2")
start.p <- sum(heart$Deaths) / sum(heart$Patients)

fit.glm <- glm(
  cbind(Deaths, Patients - Deaths) ~
    factor(AgeGroup) + factor(Severity) + factor(Delay) + factor(Region),
  family = binomial(),
  data = heart
)

fit.logbin <- logbin::logbin(
  formula(fit.glm),
  data = heart,
  start = c(log(start.p), rep(c(0.2, 0.4), 4)),
  trace = 1
)
#> logbin parameterisation 1/81
#> Deviance = 149.321 Iterations - 9052

modelbased::estimate_means(fit.glm, "AgeGroup") |> print(select = "minimal")

modelbased::estimate_means(fit.logbin, "AgeGroup") |> print(select = "minimal")
130
Daniel 🕹️ @strengejacke.de · 06/05/2025
Here's another example. Instead of running multiple models for each "factor contrast" of interest and look at regression coefficients, you can also specify these contrasts directly in {modelbased} or {marginaleffects} (modelbased is just a convenient wrapper around marginaleffects and emmeans)
library(easystats)

# sample data
data(contrast_example, package = "modelbased")

# center predictor of interaction term, for consistent results
contrast_example$score <- center(contrast_example$score)

# model with default factor-contrasts
model1 <- lm(outcome ~ score * tx, data = contrast_example)

# create custom contrasts for the "tx" factor
treat_vs_none <- c(-2/3, 1/3, 1/3)
short_vs_long <- c(0, -1/2, 1/2)

contrasts(contrast_example$tx) <- cbind(treat_vs_none, short_vs_long)

# model with custom factor-contrasts
model2 <- lm(outcome ~ score * tx, data = contrast_example)

# see parameter "tx [treat_vs_none]", value 1.94 - we got this results
# due to custom contrasts...
model_parameters(model2)

# instead of re-running the model each time with different contrasts, simply
# formulate what you want to do as "comparison" (or "hypothesis" in
# {marginaleffects}): we want to compare the average effect of short + long
# treatment against no treatment. We find these coefficients in rows 2+3
# (short+long) and row 1 (none).
estimate_means(model1, "tx")

# we now formulate this as comparison
estimate_contrasts(model1, "tx", comparison = "((b2+b3)/2) = b1")

# marginaleffects equivalent
marginaleffects::avg_predictions(
  model1,
  variables = "tx",
  hypothesis = "((b2+b3)/2) = b1"
)
220
Daniel 🕹️ @strengejacke.de · 06/05/2025
I think this is easily done using `hypothesis` (or in similar fashion with the {modelbased} package, which I will promote a bit here...). Footnote: To use these features in {modelbased}, run `easystats::install_latest()`
library(modelbased)

# sample data and model
data(contrast_example, package = "modelbased")
model <- lm(outcome ~ score * tx, data = contrast_example)

# custom contrasts for factor "tx"
cond_tx <- cbind("no treatment" = c(1, 0, 0), "treatment" = c(0, 0.5, 0.5))

# comparison of slopes for factor "tx", taking factor-contrasts into account
estimate_slopes(model, "score", by = "tx", comparison = cond_tx)
#> Estimated Marginal Effects
#> 
#> Parameter    | Slope |   SE |        95% CI | t(24) |     p
#> -----------------------------------------------------------
#> no treatment |  0.76 | 0.27 | [ 0.21, 1.31] |  2.86 | 0.009
#> treatment    |  0.30 | 0.22 | [-0.15, 0.75] |  1.37 | 0.184
#> 
#> Marginal effects estimated for score

# marginaleffects-equivalent
marginaleffects::avg_slopes(
  model,
  variables = "score",
  by = "tx",
  hypothesis = cond_tx
)
#> 
#>          Term Estimate Std. Error    z Pr(>|z|)   S  2.5 % 97.5 %
#>  no treatment    0.763      0.267 2.86  0.00429 7.9  0.239  1.287
#>  treatment       0.301      0.220 1.37  0.17119 2.5 -0.130  0.732
#> 
#> Type:  response
140
Daniel 🕹️ @strengejacke.de · 05/05/2025
You can use the {modelbased} package (easystats.github.io/modelbased/), which is a convenient wrapper around the {marginaleffects} and {emmeans} packages. See screenshot for a short example.
library(modelbased)

data("nhanes2", package = "mice")
imp <- mice::mice(nhanes2, printFlag = FALSE)

# estimated marginal means
predictions <- lapply(1:5, function(i) {
  m <- lm(bmi ~ age + hyp + chl, data = mice::complete(imp, action = i))
  estimate_means(m, "age")
})
out <- pool_predictions(predictions)
out

plot(out)
020
Daniel 🕹️ @strengejacke.de · 12/04/2025
I really reduced my R package installation script, which I run when new R version (like from 4.3 to 4.4, or now to 4.5) is released, because I usually like "clean" installations, but... I'm not sure I'm successful (these are mostly dependencies, not the packages I primary wanted to install).
020
Daniel 🕹️ @strengejacke.de · 10/03/2025
And my windows login screen is Polars!
010
Daniel 🕹️ @strengejacke.de · 03/03/2025
That looks great! Can you also add a neutral category, e.g. to the left plot area?
110