When observations are nested within groups (repeated measurements per person, students within classrooms, etc.), the association between two variables has two faces: how they co-vary within groups over time, and whether groups that score higher on one variable also tend to score higher on the other. A naive linear regression cannot distinguish these two effects, and conflating them can produce severely misleading conclusions.
The Random Effects Within-Between (REWB) model (Bell
et al., 2019) solves this by including both components as separate
predictors. This vignette shows how to use
decompose_within_between() to prepare data for REWB models
and how to fit and interpret those models.
Example Data
We use media_diary, a simulated daily diary dataset
included with mlstats (100 participants over 14 days;
N = 100 persons, T = 1,400 daily observations). See
?media_diary for details.
data("media_diary")The dataset was generated to illustrate two processes that can operate simultaneously — and in opposite directions — at within- and between-person levels:
- Between persons: people who watch more entertainment media on average tend to have lower average wellbeing — perhaps because chronic heavy media use reflects lower trait self-control, which itself predicts lower wellbeing.
- Within persons: on days when someone watches more than usual, their wellbeing is slightly higher — consistent with short-term escapism or mood repair through media use.
Because these processes were built into the simulation, they are present by design — not empirical discoveries. The purpose of the example is to show how REWB models recover effects that point in opposite directions, and what happens when they are conflated. A naive regression conflates them and produces a near-zero coefficient, making it appear that screen time has no relationship with wellbeing, when in fact it has two real and opposing effects (in the simulation).
Decomposing Time-Varying Predictors
decompose_within_between() splits each specified
variable into up to three components:
-
_grand_mean_centered: grand-mean-centered value -
_between_{group}: group mean (stable between-group component) -
_within_{group}: deviation from the group mean (within-group fluctuation)
The vars argument names the variables to decompose.
group names the grouping variable.
media_diary |>
decompose_within_between(group = "person", vars = "screen_time") |>
select(starts_with("screen_time_"))
#> # A tibble: 1,400 × 3
#> screen_time_grand_mean_centered screen_time_between_…¹ screen_time_within_p…²
#> <dbl> <dbl> <dbl>
#> 1 -48.7 69.9 10.1
#> 2 -8.66 69.9 50.1
#> 3 -30.7 69.9 28.1
#> 4 -16.7 69.9 42.1
#> 5 -108. 69.9 -48.9
#> 6 -44.7 69.9 14.1
#> 7 -80.7 69.9 -21.9
#> 8 -70.7 69.9 -11.9
#> 9 -89.7 69.9 -30.9
#> 10 -46.7 69.9 12.1
#> # ℹ 1,390 more rows
#> # ℹ abbreviated names: ¹screen_time_between_person, ²screen_time_within_personscreen_time_within_person is the group-mean-centred
score: how many more (or fewer) minutes this person watched today
compared to their own average. screen_time_between_person
is the person’s average screen time, repeated for every row belonging to
that person. screen_time_grand_mean_centered is the
grand-mean-centred value, which shows each observation’s deviation from
the overall mean.
Selecting Components
By default all three components are returned. Use the
components argument to select a subset. For REWB models,
the within and between components are the predictors you need.
media_diary |>
decompose_within_between(
group = "person",
vars = "screen_time",
components = c("within", "between")
) |>
select(starts_with("screen_time"))
#> # A tibble: 1,400 × 3
#> screen_time screen_time_between_person screen_time_within_person
#> <dbl> <dbl> <dbl>
#> 1 80 69.9 10.1
#> 2 120 69.9 50.1
#> 3 98 69.9 28.1
#> 4 112 69.9 42.1
#> 5 21 69.9 -48.9
#> 6 84 69.9 14.1
#> 7 48 69.9 -21.9
#> 8 58 69.9 -11.9
#> 9 39 69.9 -30.9
#> 10 82 69.9 12.1
#> # ℹ 1,390 more rowsValid values for components are any non-empty subset of
c("within", "between", "gmc").
Customising Column Names
The within_pattern, between_pattern, and
gmc_pattern arguments control the naming of the new
columns. Each pattern is a glue-style string where {col} is
replaced by the variable name and {group} is replaced by
the grouping variable name. For example, the default
{col}_within_{group} produces
screen_time_within_person. Here, we use
{col}_wg and {col}_bg to produce shorter
names:
media_diary |>
decompose_within_between(
group = "person",
vars = c("screen_time"),
components = c("within", "between"),
within_pattern = "{col}_wg",
between_pattern = "{col}_bg"
) |>
select(starts_with("screen_time"))
#> # A tibble: 1,400 × 3
#> screen_time screen_time_bg screen_time_wg
#> <dbl> <dbl> <dbl>
#> 1 80 69.9 10.1
#> 2 120 69.9 50.1
#> 3 98 69.9 28.1
#> 4 112 69.9 42.1
#> 5 21 69.9 -48.9
#> 6 84 69.9 14.1
#> 7 48 69.9 -21.9
#> 8 58 69.9 -11.9
#> 9 39 69.9 -30.9
#> 10 82 69.9 12.1
#> # ℹ 1,390 more rowsDecomposing Multiple Variables at Once
Pass a character vector to vars to decompose several
variables in a single call. The same components and naming
patterns apply to all variables:
media_diary |>
decompose_within_between(
group = "person",
vars = c("screen_time", "stress"),
components = c("within", "between")
) |>
glimpse()
#> Rows: 1,400
#> Columns: 10
#> $ person <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2…
#> $ self_control <dbl> 5.1, 5.1, 5.1, 5.1, 5.1, 5.1, 5.1, 5.1, 5.1…
#> $ wellbeing <dbl> 4.9, 5.4, 5.8, 6.5, 5.5, 5.9, 6.4, 5.7, 6.5…
#> $ screen_time <dbl> 80, 120, 98, 112, 21, 84, 48, 58, 39, 82, 8…
#> $ stress <dbl> 4.1, 4.0, 3.4, 3.5, 2.8, 3.2, 2.6, 2.6, 2.4…
#> $ enjoyment <dbl> 5.7, 5.6, 5.0, 6.2, 4.4, 6.0, 5.1, 4.5, 5.3…
#> $ screen_time_between_person <dbl> 69.92857, 69.92857, 69.92857, 69.92857, 69.…
#> $ stress_between_person <dbl> 3.257143, 3.257143, 3.257143, 3.257143, 3.2…
#> $ screen_time_within_person <dbl> 10.071429, 50.071429, 28.071429, 42.071429,…
#> $ stress_within_person <dbl> 0.84285714, 0.74285714, 0.14285714, 0.24285…Fitting the REWB Model
Step 1 — Within and between effects
We start with a model that includes only the within- and
between-person components of screen_time and a random
intercept for person. This is the core REWB specification:
diary_decomp <- decompose_within_between(
data = media_diary,
group = "person",
vars = "screen_time",
components = c("within", "between"),
within_pattern = "{col}_within",
between_pattern = "{col}_between"
)
fit_rewb <- lmer(
wellbeing ~ screen_time_within + screen_time_between + (1 | person),
data = diary_decomp
)
summary(fit_rewb, correlation = FALSE)
#> Linear mixed model fit by REML. t-tests use Satterthwaite's method [
#> lmerModLmerTest]
#> Formula: wellbeing ~ screen_time_within + screen_time_between + (1 | person)
#> Data: diary_decomp
#>
#> REML criterion at convergence: 2747.7
#>
#> Scaled residuals:
#> Min 1Q Median 3Q Max
#> -2.9465 -0.6343 -0.0225 0.6429 3.3618
#>
#> Random effects:
#> Groups Name Variance Std.Dev.
#> person (Intercept) 0.3126 0.5591
#> Residual 0.3393 0.5825
#> Number of obs: 1400, groups: person, 100
#>
#> Fixed effects:
#> Estimate Std. Error df t value Pr(>|t|)
#> (Intercept) 5.364e+00 2.596e-01 9.800e+01 20.659 < 2e-16 ***
#> screen_time_within 8.585e-03 5.143e-04 1.299e+03 16.694 < 2e-16 ***
#> screen_time_between -7.095e-03 1.967e-03 9.800e+01 -3.607 0.00049 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1Interpreting the coefficients:
In this simulated dataset, the within-person coefficient (0.0086) is positive and highly significant. To illustrate how such an effect would be interpreted: on days when someone watches one minute more than their own average, their wellbeing is 0.0086 points higher. For a person watching 60 minutes more than usual, the expected gain would be 0.52 wellbeing points.
The between-person coefficient (-0.0071) is negative and significant. Illustrating interpretation: people who watch one minute more per day on average show 0.0071 lower wellbeing. For someone who watches 60 minutes more per day on average than another person, the expected wellbeing gap would be 0.43 points.
The two effects point in opposite directions — exactly the pattern built into the simulation. A naive regression conflates them:
fit_naive <- lm(wellbeing ~ screen_time, data = diary_decomp)
summary(fit_naive)
#>
#> Call:
#> lm(formula = wellbeing ~ screen_time, data = diary_decomp)
#>
#> Residuals:
#> Min 1Q Median 3Q Max
#> -2.95423 -0.61285 0.01078 0.60269 2.42402
#>
#> Coefficients:
#> Estimate Std. Error t value Pr(>|t|)
#> (Intercept) 4.3293929 0.0744348 58.164 <2e-16 ***
#> screen_time 0.0009457 0.0005496 1.721 0.0855 .
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> Residual standard error: 0.8693 on 1398 degrees of freedom
#> Multiple R-squared: 0.002113, Adjusted R-squared: 0.0014
#> F-statistic: 2.961 on 1 and 1398 DF, p-value: 0.08553The naive coefficient is near zero because the positive within-person and negative between-person effects cancel each other out — an entirely uninformative result that hides two simulated effects pointing in opposite directions. This illustrates why a naive regression can be misleading when within- and between-group processes operate simultaneously.
Step 2 — Accounting for confounding
self_control was identified above as a confounder of the
between-person effect: people with lower trait self-control may
watch more media on average and have lower wellbeing, making it
appear as though heavy media use causes worse wellbeing at the
between-person level. Adding self_control as a covariate
lets us test whether the between-person association with screen time
persists after removing this alternative explanation.
fit_rewb_conf <- lmer(
wellbeing ~ screen_time_within + screen_time_between + self_control +
(1 | person),
data = diary_decomp
)
summary(fit_rewb_conf, correlation = FALSE)
#> Linear mixed model fit by REML. t-tests use Satterthwaite's method [
#> lmerModLmerTest]
#> Formula: wellbeing ~ screen_time_within + screen_time_between + self_control +
#> (1 | person)
#> Data: diary_decomp
#>
#> REML criterion at convergence: 2715.5
#>
#> Scaled residuals:
#> Min 1Q Median 3Q Max
#> -2.9677 -0.6221 -0.0331 0.6453 3.3345
#>
#> Random effects:
#> Groups Name Variance Std.Dev.
#> person (Intercept) 0.2129 0.4614
#> Residual 0.3393 0.5825
#> Number of obs: 1400, groups: person, 100
#>
#> Fixed effects:
#> Estimate Std. Error df t value Pr(>|t|)
#> (Intercept) 2.026e+00 5.582e-01 9.700e+01 3.629 0.000457 ***
#> screen_time_within 8.585e-03 5.143e-04 1.299e+03 16.694 < 2e-16 ***
#> screen_time_between 2.673e-03 2.233e-03 9.700e+01 1.197 0.234218
#> self_control 5.170e-01 7.960e-02 9.700e+01 6.495 3.56e-09 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1Interpreting the coefficients:
In this simulated dataset, the within-person coefficient is unchanged
(0.0086): self_control is a stable trait measured once per
person, so it carries no within-person variation and cannot alter the
within-person estimate. This is a general property of between-person
covariates in REWB models, not specific to these simulated data.
The between-person coefficient changes substantially — from -0.0071
(significant) in the unadjusted model to 0.0027 (p = .23,
non-significant) after adjusting for self_control. This
illustrates confounding: the simulation was designed so that the
apparent between-person harm of screen time is driven by self-control.
In a real study, a similar pattern would suggest that people with lower
self-control watch more TV on average and have lower wellbeing,
and that the self-control deficit — not screen time — explains the
wellbeing gap.
Adding More Predictors
When you have several time-varying predictors, decompose all of them at once:
diary_decomp2 <- decompose_within_between(
data = media_diary,
group = "person",
vars = c("screen_time", "stress"),
components = c("within", "between"),
within_pattern = "{col}_within",
between_pattern = "{col}_between"
)
fit_multi <- lmer(
wellbeing ~ screen_time_within + screen_time_between +
stress_within + stress_between +
self_control + (1 | person),
data = diary_decomp2
)Here stress_within captures whether more stressful days
than usual predict lower wellbeing on those days (within-person), while
stress_between captures whether chronically more stressed
people have lower wellbeing overall (between-person).
Adding Random Slopes
The REWB model above assumes the within-person effect of screen time on wellbeing is the same for all persons. You can allow this effect to vary by adding a random slope:
fit_slopes <- lmer(
wellbeing ~ screen_time_within + screen_time_between + self_control +
(screen_time_within | person),
data = diary_decomp
)A significant random slope variance indicates that the within-person association between screen time and wellbeing differs across persons — for some, extra media use lifts their mood more than for others.
Further Reading
This vignette covers the data-preparation and basic modelling side of
REWB analysis. For thorough treatments of model specification,
assumption checking, and interpretation — including cross-level
interactions — see Bell et al. (2019) and Enders & Tofighi (2007).
For descriptive statistics and correlation matrices that can inform REWB
model specification, see
vignette("multilevel-descriptives").
References
Bell, A., Fairbrother, M., & Jones, K. (2019). Fixed and random effects models: Making an informed choice. Quality & Quantity, 53(2), 1051–1074. https://doi.org/10.1007/s11135-018-0802-x
Enders, C. K., & Tofighi, D. (2007). Centering predictor variables in cross-sectional multilevel models: A new look at an old issue. Psychological Methods, 12(2), 121–138. https://doi.org/10.1037/1082-989X.12.2.121
