Nothing
#' Prepare dyadic data for multilevel models
#'
#' Validates dyadic data, records the structural variables, and adds metadata
#' and model-ready columns for dyadic multilevel model parameterizations.
#'
#' Data must be in long format. Cross-sectional dyadic data may contain at most
#' one row per member within dyad. Intensive longitudinal dyadic data may
#' contain at most one row per member and observed measurement occasion within
#' dyad. Measured variables may contain missing values. Numeric `dyad`, `member`,
#' `time`, and selected predictor columns must not contain infinite values.
#' Structural completeness is assessed across all rows. `incomplete_dyads`
#' controls dyads with fewer than two members; dyads with more than two members
#' always cause an error.
#' When `role` is supplied, stable member roles are resolved across repeated
#' rows before `missing_role` is applied.
#'
#' Dyad composition labels are canonical: role labels are sorted alphabetically
#' before being combined, so labels do not depend on row or member order.
#'
#' @param data A data frame or tibble. Data must be in long format. For
#' cross-sectional dyadic data, each observed member of each dyad has one row.
#' For intensive longitudinal dyadic data, each observed member of each dyad has
#' one row per observed time point.
#' @param dyad Column identifying the dyad.
#' @param member Column identifying a person or the member within dyad.
#' @param role Optional column identifying a stable member role, such as gender.
#' Non-missing values must be consistent within each `dyad` x `member` and
#' must not contain `_x_`. In repeated-measures data, an observed role is
#' propagated to missing rows for the same member within a dyad.
#' `missing_role` controls dyads in which a member has no non-missing role on
#' any row. If no role is supplied, all dyads are treated as the same type of
#' exchangeable dyads.
#' @param time Optional column identifying time or measurement order of repeated
#' measures.
#' @param predictors Optional variables to use for temporal predictor
#' decomposition and model-ready predictor construction.
#' @param lag1_predictors Optional subset of `predictors` for which lag-1
#' model-ready columns should be created. Requires `time` to be a finite,
#' integer-valued numeric measurement index. Lagging respects the dyad and
#' member structure, matches observations at exactly `time - 1`, and does not
#' bridge missing occasions. If occasions are unequally spaced, for example
#' waves in 2008, 2010, and 2013, number the study's occasions (1, 2, 3) and
#' supply that index as `time`. The lag then refers to the previous occasion,
#' regardless of how much time lies between them. Eligible raw,
#' within-person, and APIM GMC variables are lagged, but stable
#' between-person variables are not.
#' @param model_types Model-ready column families to construct. Can contain one
#' or more of `"apim"`, `"dim"`, and `"dsm"`. `"apim"` creates
#' actor and partner predictors. `"dim"` creates dyad-mean and
#' within-dyad member-deviation predictors. DIM requires one final
#' exchangeable dyad composition, which may result from pooling
#' exchangeable compositions. `"dsm"` creates dyadic-score model
#' predictor columns. `"none"` skips model-specific predictor
#' construction after validation, composition inference, and optional
#' temporal predictor decomposition, and must be used alone. `"dim"` and
#' `"dsm"` must be requested in separate calls.
#' @param dsm_role_order For `model_types = "dsm"`, a character vector giving
#' the two distinguishable roles in the order used for directional
#' differences. For example, `c("female", "male")` defines predictor
#' differences as female minus male and assigns the DSM role contrast `+0.5`
#' to female partners and `-0.5` to male partners. Required when DSM columns
#' are requested and must be `NULL` otherwise.
#' @param temporal_decomposition Temporal decomposition strategy for
#' `predictors`.
#' `"none"` leaves predictors undecomposed before model-specific columns are
#' constructed. `"2l"` indicates a two-level temporal predictor
#' decomposition into within-person and between-person components. `"auto"`
#' resolves to `"2l"` when both `time` and `predictors` are supplied, and
#' to `"none"` otherwise. `"2l"` retains raw model-ready predictors in
#' addition to their within-person and between-person components. For
#' longitudinal DIM and DSM construction, raw and within-person dyadic scores
#' are computed within each dyad occasion, while between-person scores are
#' computed within dyads. Raw DIM and DSM dyad means are grand-mean centered.
#' Do not include the raw, within-person, and between-person versions of the
#' same contemporaneous predictor in one model because they are linearly
#' dependent.
#' @param set_exchangeable_compositions Optionally specify dyad compositions
#' to treat as exchangeable, when their roles would otherwise imply distinguishability.
#' Requires `role`. Compositions that are already
#' exchangeable should not be listed. Each composition must be supplied as one
#' string, using `_x_`, `-`, `_`, or whitespace (` `) between the two role labels,
#' for example `"female_x_male"`, `"female-male"`, `"female_male"`, or
#' `"female male"`, in arbitrary order.
#' To set multiple compositions, use a character vector of such strings.
#' Unlike `include_arbitrary_member_contrast`, this reclassifies the selected
#' compositions as exchangeable and changes their composition-role coding and
#' indicators, which is required for composition pooling and DIM
#' compatibility.
#' @param keep_compositions Optional observed dyad compositions to keep
#' before exchangeability overrides and pooling. Requires `role`. Composition
#' references use the same format as `set_exchangeable_compositions`. `NULL`
#' keeps all observed compositions.
#' @param pool_compositions Optionally pool exchangeable dyad compositions
#' into a shared final composition label. Must be a named list where each name
#' is the final composition label and each value is a character vector of
#' composition references, for example
#' `list(same_sex_couples = c("female-female", "male-male"))`. Only
#' exchangeable compositions can be pooled. Each pool must contain at least
#' two distinct observed compositions after composition references are
#' resolved.
#' @param incomplete_dyads How to handle dyads with fewer than two unique
#' members across all rows in `data`. `"error"` stops with an error and
#' `"drop"` removes the entire dyad. A dyad with more than two unique members
#' is invalid and always causes an error, regardless of this setting.
#' @param missing_role How to handle dyads in which at least one member has no
#' non-missing `role` value on any row. A consistent non-missing role observed
#' for a member is propagated to that member's other rows before this policy
#' is applied. `"error"` stops with an error and `"drop"` removes the entire
#' dyad. Conflicting non-missing roles always cause an error. Ignored when no
#' `role` column is supplied.
#' @param seed Optional seed for random `.member_contrast_*` sign assignment.
#' If `NULL`, the current R session's RNG state is used.
#' @param short_colnames Whether to use shorter composition-dependent generated
#' column names when the final data contain one composition. The default `TRUE`
#' omits the redundant composition label from `.is_*` and
#' `.member_contrast_*` names. `FALSE` always retains composition-qualified
#' names. Other generated column names are unaffected.
#' @param include_arbitrary_member_contrast Whether to also generate arbitrary
#' `.member_contrast_*` columns for distinguishable compositions. The default
#' `FALSE` generates these columns only for exchangeable compositions.
#' `TRUE` retains distinguishable composition metadata, composition roles,
#' and role indicators. It only adds the contrast needed to fit an
#' exchangeability-constrained random-effects model to the same prepared
#' data. Use `set_exchangeable_compositions` instead to reclassify
#' compositions for pooling or DIM compatibility.
#' @param add_apim_gmc_predictors Whether to add APIM GMC variants for numeric
#' predictors. `TRUE` retains the raw columns and adds `.{pred}_gmc` plus its
#' actor and partner versions, centered over all retained non-missing values
#' after filtering. Lagged variants use the same mean. Mixed non-numeric
#' predictors remain raw and are listed in one warning. In longitudinal data,
#' the mean is observation-weighted. Requires `"apim"` in `model_types`, at
#' least one numeric predictor, and resolved `temporal_decomposition = "none"`.
#' Do not use raw and GMC variants together in a model with an intercept.
#'
#' @return The original data as a tibble with class `dyadMLM_data`,
#' `.composition` and `.composition_role` factor columns,
#' `.is_*` numeric indicator columns, and numeric
#' `.member_contrast_*` columns coded `-1` and `1` for the two members of
#' each matching composition and `0` otherwise. With one final composition,
#' their default names omit the composition label. The `dyadMLM` attribute
#' contains structural metadata, `dyad_compositions`, and
#' predictor metadata such as `temporal_decompositions`, `lag1_predictors`,
#' `apim_predictors`, and `dim_predictors`, as well as `dsm_predictors` and
#' `dsm_role_order` when applicable. The `generated_columns` table records each
#' package-generated column retained in the returned data. Modifying prepared
#' data does not automatically regenerate package-generated columns.
#'
#' @examples
#' data <- data.frame(
#' dyad_id = c(1, 1, 2, 2, 3, 3),
#' person_id = c(1, 2, 3, 4, 5, 6),
#' role = c("female", "male", "female", "female", "male", "male"),
#' x = c(4, 7, 5, 6, 3, 8)
#' )
#'
#' prepared <- prepare_dyad_data(
#' data,
#' dyad = dyad_id,
#' member = person_id,
#' role = role,
#' predictors = x,
#' model_types = "apim"
#' )
#'
#' print(prepared)
#'
#' pooled <- prepare_dyad_data(
#' data,
#' dyad = dyad_id,
#' member = person_id,
#' role = role,
#' predictors = x,
#' model_types = "apim",
#' set_exchangeable_compositions = "female-male",
#' pool_compositions = list(
#' romantic_couples = c("female-female", "male-male", "female-male")
#' )
#' )
#'
#' print(pooled)
#'
#' ild_data <- data.frame(
#' dyad_id = rep(c(1, 2), each = 4),
#' person_id = rep(c(1, 2), times = 4),
#' time = rep(c(1, 1, 2, 2), times = 2),
#' x = c(4, 7, 5, 8, 3, 6, 4, 7)
#' )
#'
#' ild_prepared <- prepare_dyad_data(
#' ild_data,
#' dyad = dyad_id,
#' member = person_id,
#' time = time,
#' predictors = x,
#' lag1_predictors = x,
#' model_types = "apim",
#' seed = 123
#' )
#'
#' print(ild_prepared)
#'
#' @export
prepare_dyad_data <- function(
data,
dyad,
member,
role = NULL,
time = NULL,
predictors = NULL,
lag1_predictors = NULL,
model_types = "apim",
dsm_role_order = NULL,
temporal_decomposition = c("auto", "2l", "none"),
set_exchangeable_compositions = NULL,
keep_compositions = NULL,
pool_compositions = NULL,
incomplete_dyads = c("error", "drop"),
missing_role = c("error", "drop"),
seed = NULL,
short_colnames = TRUE,
include_arbitrary_member_contrast = FALSE,
add_apim_gmc_predictors = FALSE
) {
model_types <- normalize_model_types(model_types)
temporal_decomposition <- rlang::arg_match(temporal_decomposition)
incomplete_dyads <- rlang::arg_match(incomplete_dyads)
missing_role <- rlang::arg_match(missing_role)
if (!is.logical(add_apim_gmc_predictors) ||
length(add_apim_gmc_predictors) != 1L ||
is.na(add_apim_gmc_predictors)) {
stop(
"`add_apim_gmc_predictors` must be `TRUE` or `FALSE`.",
call. = FALSE
)
}
if (add_apim_gmc_predictors && !"apim" %in% model_types) {
stop(
paste0(
"`add_apim_gmc_predictors = TRUE` requires that `model_types` ",
"includes \"apim\"."
),
call. = FALSE
)
}
out <- validate_dyad_data(
data = data,
dyad = {{ dyad }},
member = {{ member }},
role = {{ role }},
time = {{ time }},
predictors = {{ predictors }},
lag1_predictors = {{ lag1_predictors }},
model_types = model_types,
dsm_role_order = dsm_role_order,
temporal_decomposition = temporal_decomposition,
incomplete_dyads = incomplete_dyads,
missing_role = missing_role
)
if (add_apim_gmc_predictors) {
meta_data <- attr(out, "dyadMLM")
if (meta_data$temporal_decomposition != "none") {
stop(
paste0(
"`add_apim_gmc_predictors = TRUE` requires ",
"`temporal_decomposition = \"none\"` after `\"auto\"` is resolved."
),
call. = FALSE
)
}
predictors <- meta_data$predictors
predictor_is_numeric <- logical(length(predictors))
for (predictor_index in seq_along(predictors)) {
predictor <- predictors[[predictor_index]]
predictor_is_numeric[[predictor_index]] <- is.numeric(out[[predictor]])
}
numeric_predictors <- predictors[predictor_is_numeric]
if (length(numeric_predictors) == 0L) {
stop(
paste0(
"`add_apim_gmc_predictors = TRUE` requires at least one numeric ",
"variable selected by `predictors`."
),
call. = FALSE
)
}
non_numeric_predictors <- predictors[!predictor_is_numeric]
if (length(non_numeric_predictors) > 0L) {
warning(
paste0(
"`add_apim_gmc_predictors = TRUE` was not applied to non-numeric ",
"predictor(s): ",
paste0("`", non_numeric_predictors, "`", collapse = ", "),
". Their raw actor and partner columns were retained. Recode ",
"predictors explicitly as numeric only when grand-mean centering ",
"is substantively meaningful."
),
call. = FALSE
)
}
}
if (attr(out, "dyadMLM")$longitudinal) {
# Temporarily add an absent partner row at occasions observed for at least
# one member. This lets centering, lagging, and dyadic columns use complete
# structural pairs. The temporary rows are removed before returning.
out <- temporarily_complete_dyad_occasions(out)
}
out <- infer_dyad_compositions(
out,
seed = seed,
keep_compositions = keep_compositions,
set_exchangeable_compositions = set_exchangeable_compositions,
pool_compositions = pool_compositions,
short_colnames = short_colnames,
include_arbitrary_member_contrast = include_arbitrary_member_contrast
)
# Validate model compatibilities
if ("dsm" %in% model_types) {
validate_dsm_compatibility(out)
}
out <- center_predictors(
out,
add_apim_gmc_predictors = add_apim_gmc_predictors
)
out <- add_temporal_lag_columns(out)
if ("dim" %in% model_types) {
# Current DIM construction supports one exchangeable composition.
validate_dim_compatibility(out)
}
# Add model cols
if ("apim" %in% model_types) {
out <- add_actor_partner_columns(out)
}
if ("dim" %in% model_types) {
out <- add_dyad_individual_columns(out)
}
if ("dsm" %in% model_types) {
out <- add_dyadic_score_columns(out)
}
if (attr(out, "dyadMLM")$longitudinal) {
out <- restore_observed_dyad_rows(out)
}
warn_if_lags_missing(out)
return(out)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.