# Set Moderator Levels

#### Shu Fai Cheung & Sing-Hang Cheung

#### 2026-03-26

# 1 Introduction

This article is a brief illustration of how to use `mod_levels()`, `mod_levels_list()`, and `merge_mod_levels()` from the package manymome (Cheung & Cheung, 2024) to generate a table of moderator levels for use by `cond_indirect_effects()`. No need to use these functions if the default levels generated by `cond_indirect_effects()` is appropriated, as illustrated in `vignette("manymome")`.

Use these functions only when users want to use levels of the moderators other than those default levels.

# 2 Numeric Moderators

We first use the sample data set `data_med_mod_ab`:

    library(manymome)
    dat <- data_med_mod_ab
    print(head(dat), digits = 3)
    #>       x   w1   w2    m    y    c1   c2
    #> 1  9.27 4.97 2.66 3.46 8.80  9.26 3.14
    #> 2 10.79 4.13 3.33 4.05 7.37 10.71 5.80
    #> 3 11.10 5.91 3.32 4.04 8.24 10.60 5.45
    #> 4  9.53 4.78 2.32 3.54 8.37  9.22 3.83
    #> 5 10.00 4.38 2.95 4.65 8.39  9.58 4.26
    #> 6 12.25 5.81 4.04 4.73 9.65  9.51 4.01

Suppose this is the model being fitted:

    library(lavaan)
    #> This is lavaan 0.6-21
    #> lavaan is FREE software! Please report any bugs.
    dat$w1x <- dat$w1 * dat$x
    dat$w2m <- dat$w2 * dat$m
    mod <-
    "
    m ~ x + w1 + w1x
    y ~ m + w2 + w2m
    m ~~ w2 + w2m
    w2  ~~ w2m + x + w1 + w1x
    w2m ~~ x + w1 + w1x
    x   ~~ w1 + w1x
    w1  ~~ w1x
    "
    fit <- sem(model = mod, data = dat)

## 2.1 SD and Mean

It has two numeric moderators, `w1` and `w2`. To generate these three levels for `w1`, one standard deviation (SD) below mean, mean, and one SD above mean, just call `mod_levels()`:

    w1levels <- mod_levels(w = "w1", fit = fit)
    w1levels
    #>               w1
    #> M+1.0SD 6.173157
    #> Mean    5.105602
    #> M-1.0SD 4.038047

This is not necessary in calling `cond_indirect_effects()` because it will automatically generate these default levels.

Suppose we want to use only two levels, one SD below and one SD above mean, we can set the argument `sd_from_mean` to a vector of distances from means, which are `-1` and `1` in this example:

    w1levels <- mod_levels(w = "w1", fit = fit,
                           sd_from_mean = c(-1, 1))
    w1levels
    #>               w1
    #> M+1.0SD 6.173157
    #> M-1.0SD 4.038047

## 2.2 Percentiles

To generate the levels based on percentiles, set the argument `w_method` to `"percentile"`:

    w1levels <- mod_levels(w = "w1", fit = fit,
                           w_method = "percentile")
    w1levels
    #>           w1
    #> 84% 6.207972
    #> 50% 5.215974
    #> 16% 3.932444

The default percentiles are 16th, 50th, and 84th, corresponding to one SD below mean, mean, and one SD above mean in a normal distribution.

Suppose we want to change the percentiles to be used, for example, 25th and 75th, set the argument `percentiles` as shown below:

    w1levels <- mod_levels(w = "w1", fit = fit,
                           w_method = "percentile",
                           percentiles = c(.25, .75))
    w1levels
    #>           w1
    #> 75% 5.808579
    #> 25% 4.196759

## 2.3 Specific Values

If there are values that are meaningful for a moderator, they can be used by setting `values` to a vector of values:

    w1levels <- mod_levels(w = "w1", fit = fit,
                           values = c(2, 4, 8))
    w1levels
    #>   w1
    #> 8  8
    #> 4  4
    #> 2  2

The output of `mod_levels` can be used when calling `cond_indirect_effects()`:

    out <- cond_indirect_effects(wlevels = w1levels,
                                 x = "x", y = "m",
                                 fit = fit)
    out
    #>
    #> == Conditional effects ==
    #>
    #>  Path: x -> m
    #>  Conditional on moderator(s): w1
    #>  Moderator(s) represented by: w1
    #>
    #>   [w1] (w1)    ind    SE   Stat pvalue Sig  CI.lo CI.hi
    #> 1    8    8  0.964 0.308  3.130  0.002  **  0.360 1.568
    #> 2    4    4  0.165 0.164  1.007  0.314     -0.156 0.486
    #> 3    2    2 -0.235 0.314 -0.747  0.455     -0.850 0.381
    #>
    #>  - [SE] are 'lavaan' standard errors.
    #>  - [Stat] are the z statistics used to test the effects.
    #>  - [pvalue] are p-values computed from 'Stat'.
    #>  - [Sig]: 0 '***' 0.001 '**' 0.01 '*' 0.05 ' ' 1.
    #>  - [CI.lo to CI.hi] are 95.0% confidence interval computed from 'lavaan'
    #>    standard errors.
    #>  - IMPORTANT: For a model fitted by structural equation model, the
    #>    p-values and confidence intervals for the conditional effects
    #>    computed from standard errors can only be trusted if all covariances
    #>    involving the product terms are free. Otherwise, the model may not
    #>    be invariant to linear transformation of the variables.
    #>  - The 'ind' column shows the conditional effects.
    #>

`cond_indirect_effects()` will determine the moderators automatically from the object assigned to `wlevels`.

## 2.4 Merging the Levels of Two Or More Moderators

In the previous example, there are two moderators. We can call `mod_levels()` once for each of them, or call `mod_levels_list()`:

    wlevels_list <- mod_levels_list("w1", "w2", fit = fit)
    wlevels_list
    #> [[1]]
    #>               w1
    #> M+1.0SD 6.173157
    #> M-1.0SD 4.038047
    #>
    #> [[2]]
    #>               w2
    #> M+1.0SD 4.040487
    #> M-1.0SD 2.055091

The output is a list of the output of `mod_levels()`. With two or more moderators, the default levels are two: one SD below mean and one SD above mean.

The function `mod_levels_list()` can merge the output into one table by setting `merge` to `TRUE`:

    wlevels_list <- mod_levels_list("w1", "w2", fit = fit,
                                    merge = TRUE)
    wlevels_list
    #>                                w1       w2
    #> w1: M+1.0SD; w2: M+1.0SD 6.173157 4.040487
    #> w1: M+1.0SD; w2: M-1.0SD 6.173157 2.055091
    #> w1: M-1.0SD; w2: M+1.0SD 4.038047 4.040487
    #> w1: M-1.0SD; w2: M-1.0SD 4.038047 2.055091

Calling `mod_levels_list()` is useful when the same settings will be used for all moderators. Most arguments of `mod_levels()` can be used in `mod_levels_list()`. For example, if we want to use 25th and 75th percentiles for both `w1` and `w2`, use `w_method` and `percentiles` as before:

    wlevels_list <- mod_levels_list("w1", "w2", fit = fit,
                                    w_method = "percentile",
                                    percentiles = c(.25, .75),
                                    merge = TRUE)
    wlevels_list
    #>                        w1       w2
    #> w1: 75%; w2: 75% 5.808579 3.692675
    #> w1: 75%; w2: 25% 5.808579 2.430643
    #> w1: 25%; w2: 75% 4.196759 3.692675
    #> w1: 25%; w2: 25% 4.196759 2.430643

## 2.5 Different Settings For Moderators

If we need to use different settings for the two moderators, then we need to call `mod_levels()` once for each of them, and merge the results by `merge_mod_levels()`:

    w1levels <- mod_levels(w = "w1", fit = fit)
    w1levels
    #>               w1
    #> M+1.0SD 6.173157
    #> Mean    5.105602
    #> M-1.0SD 4.038047
    w2levels <- mod_levels(w = "w2", fit = fit, values = c(2, 5))
    w2levels
    #>   w2
    #> 5  5
    #> 2  2
    wlevels_all <- merge_mod_levels(w1levels, w2levels)
    wlevels_all
    #>                          w1 w2
    #> w1: M+1.0SD; w2: 5 6.173157  5
    #> w1: M+1.0SD; w2: 2 6.173157  2
    #> w1: Mean; w2: 5    5.105602  5
    #> w1: Mean; w2: 2    5.105602  2
    #> w1: M-1.0SD; w2: 5 4.038047  5
    #> w1: M-1.0SD; w2: 2 4.038047  2

# 3 Categorical Moderators

We use the dataset `data_med_mod_serial_cat` for illustration:

    dat <- data_med_mod_serial_cat
    print(head(dat), digits = 3)
    #>      x     w1    w2   m1   m2    y   c1   c2
    #> 1 8.09 group2 team1 7.70 5.26 3.17 4.31 5.03
    #> 2 6.16 group2 team2 5.71 4.36 3.12 4.69 3.19
    #> 3 6.03 group2 team2 5.40 5.22 4.23 4.07 4.25
    #> 4 6.92 group1 team1 7.26 5.98 3.65 5.87 5.35
    #> 5 7.76 group3 team1 4.49 4.83 4.01 4.35 4.16
    #> 6 6.84 group2 team2 5.72 6.31 3.38 4.32 4.48

It has two categorical moderators, `w1` with three categories and `w2` with two categories. We use only `w1` here for illustration.

## 3.1 Create Dummy Variables and Fit the Model

To fit a model using path analysis, two dummy variables need to be created for `w1`. This can be done by `factor2var()` or similar functions from other packages.

    w1dummies <- factor2var(dat$w1, prefix = "w1")
    head(w1dummies)
    #>        w1group2 w1group3
    #> group2        1        0
    #> group2        1        0
    #> group2        1        0
    #> group1        0        0
    #> group3        0        1
    #> group2        1        0
    # Add them to the dataset
    dat[, c("w1group2", "w1group3")] <- w1dummies
    print(head(dat), digits = 3)
    #>      x     w1    w2   m1   m2    y   c1   c2 w1group2 w1group3
    #> 1 8.09 group2 team1 7.70 5.26 3.17 4.31 5.03        1        0
    #> 2 6.16 group2 team2 5.71 4.36 3.12 4.69 3.19        1        0
    #> 3 6.03 group2 team2 5.40 5.22 4.23 4.07 4.25        1        0
    #> 4 6.92 group1 team1 7.26 5.98 3.65 5.87 5.35        0        0
    #> 5 7.76 group3 team1 4.49 4.83 4.01 4.35 4.16        0        1
    #> 6 6.84 group2 team2 5.72 6.31 3.38 4.32 4.48        1        0

This is the model:

    dat$w1group2x <- dat$w1group2 * dat$x
    dat$w1group3x <- dat$w1group3 * dat$x
    mod <-
    "
    m1 ~ x + w1group2 + w1group3 + w1group2x + w1group3x
    y ~ m1 + x
    "
    fit <- sem(model = mod, data = dat)

## 3.2 Default Levels

The levels of a categorical moderator are just the categories (unique combinations of the coding). This can be generated by `mod_levels()`. `w` should be a vector of the dummy variables:

    w1levels <- mod_levels(w = c("w1group2", "w1group3"), fit = fit)
    w1levels
    #>           w1group2 w1group3
    #> Reference        0        0
    #> 2                1        0
    #> 3                0        1

The names of `group2` and `group3` are `2` and `3` because they are inferred from the names of the dummy variables. The common part, `"w1group"`, is removed.

To tell `mod_levels` which part to be removed, set `prefix` to the part to be removed:

    w1levels <- mod_levels(w = c("w1group2", "w1group3"), fit = fit,
                           prefix = "w1")
    w1levels
    #>           w1group2 w1group3
    #> Reference        0        0
    #> group2           1        0
    #> group3           0        1

By default, the group with 0s on all dummy variables is labelled `Reference` because its name cannot be determined from the names of the dummy variables. The label can be changed by setting `reference_group_label`:

    w1levels <- mod_levels(w = c("w1group2", "w1group3"), fit = fit,
                           prefix = "w1",
                           reference_group_label = "group1")
    w1levels
    #>        w1group2 w1group3
    #> group1        0        0
    #> group2        1        0
    #> group3        0        1

The output can then be used in `cond_indirect_effects()`:

    out <- cond_indirect_effects(wlevels = w1levels,
                                 x = "x", y = "y", m = "m1",
                                 fit = fit)
    out
    #>
    #> == Conditional indirect effects ==
    #>
    #>  Path: x -> m1 -> y
    #>  Conditional on moderator(s): w1
    #>  Moderator(s) represented by: w1group2, w1group3
    #>
    #>     [w1] (w1group2) (w1group3)   ind  m1~x  y~m1
    #> 1 group1          0          0 0.263 1.002 0.262
    #> 2 group2          1          0 0.249 0.950 0.262
    #> 3 group3          0          1 0.122 0.467 0.262
    #>
    #>  - The 'ind' column shows the conditional indirect effects.
    #>  - 'm1~x','y~m1' is/are the path coefficient(s) along the path
    #>    conditional on the moderator(s).

## 3.3 User-Supplied Levels

To have full control on the labels and coding, use `values` and supply a named list of numeric vectors: the name is the group label and the vector is the coding:

    w1levels <- mod_levels(w = c("w1group2", "w1group3"), fit = fit,
                           values = list(group1 = c(0, 0),
                                         group2 = c(1, 0),
                                         group3 = c(0, 1)))
    w1levels
    #>        w1group2 w1group3
    #> group1        0        0
    #> group2        1        0
    #> group3        0        1

# 4 Mixing Numeric and Categorical Moderators

Numeric and categorical moderators can be mixed but they need to be generated separately and then merged by `merge_mod_levels()`.

Using the example in the previous section, pretending that `x` is a numeric moderator:

    xlevels <- mod_levels(w = "x", fit = fit,
                          sd_from_mean = c(-1, 1))
    xlevels
    #>                x
    #> M+1.0SD 7.149362
    #> M-1.0SD 5.087437
    w1levels <- mod_levels(w = c("w1group2", "w1group3"), fit = fit,
                           prefix = "w1",
                           reference_group_label = "group1")
    w1levels
    #>        w1group2 w1group3
    #> group1        0        0
    #> group2        1        0
    #> group3        0        1
    wlevels_all <- merge_mod_levels(xlevels, w1levels)
    wlevels_all
    #>                               x w1group2 w1group3
    #> x: M+1.0SD; w1: group1 7.149362        0        0
    #> x: M+1.0SD; w1: group2 7.149362        1        0
    #> x: M+1.0SD; w1: group3 7.149362        0        1
    #> x: M-1.0SD; w1: group1 5.087437        0        0
    #> x: M-1.0SD; w1: group2 5.087437        1        0
    #> x: M-1.0SD; w1: group3 5.087437        0        1

# 5 Determining The Type of a Moderator

The function `mod_levels()` and `mod_levels_list()` usually can determine the type correctly. However, if the detected type is wrong or users want to specify explicitly the type, set the argument `w_type` to either `"numeric"` or `"categorical"`.

# 6 Further Information

For further information on `mod_levels()`, `mod_levels_list()`, and `merge_mod_levels()`, please refer to their help pages.

# 7 Reference

Cheung, S. F., & Cheung, S.-H. (2024). manymome: An R package for computing the indirect effects, conditional effects, and conditional indirect effects, standardized or unstandardized, and their bootstrap confidence intervals, in many (though not all) models. Behavior Research Methods, 56(5), 4862–4882. https://doi.org/10.3758/s13428-023-02224-z