Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft McDonald's Omega #669

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ S3method(logLik,iv_robust)
S3method(logLik,ivreg)
S3method(logLik,plm)
S3method(logLik,svycoxph)
S3method(mcdonalds_omega,data.frame)
S3method(mcdonalds_omega,matrix)
S3method(mcdonalds_omega,parameters_pca)
S3method(model_performance,Arima)
S3method(model_performance,BFBayesFactor)
S3method(model_performance,DirichletRegModel)
Expand Down Expand Up @@ -294,6 +297,7 @@ S3method(print,icc_decomposed)
S3method(print,item_difficulty)
S3method(print,item_discrimination)
S3method(print,looic)
S3method(print,mcdonalds_omega)
S3method(print,performance_accuracy)
S3method(print,performance_cv)
S3method(print,performance_hosmer)
Expand Down Expand Up @@ -547,6 +551,7 @@ export(item_reliability)
export(item_split_half)
export(looic)
export(mae)
export(mcdonalds_omega)
export(model_performance)
export(mse)
export(multicollinearity)
Expand Down
9 changes: 9 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# performance 0.10.9

## New functions

* `mcdonalds_omega()` to calculate McDonald's Omega for a scale.

## Changes

* `r2()` for models of class `glmmTMB` without random effects now returns the
Expand All @@ -12,6 +16,11 @@

* `check_itemscale()` gets a `print_html()` method.

* `check_itemscale()` and `item_reliability()` gain a `type` argument, to
specify the type of reliability to be computed. The default is `"alpha"`,
which computes Cronbach's alpha, the other option is `"omega"` (McDonald's
Omega).

* Clarification in the documentation of the `estimator` argument for
`performance_aic()`.

Expand Down
59 changes: 43 additions & 16 deletions R/check_itemscale.R
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@
#' @param factor_index If `x` is a data frame, `factor_index` must be specified.
#' It must be a numeric vector of same length as number of columns in `x`, where
#' each element is the index of the factor to which the respective column in `x`.
#' @inheritParams item_reliability
#'
#' @return A list of data frames, with related measures of internal
#' consistencies of each subscale.
#'
#' @details
#'
#' `check_itemscale()` calculates various measures of internal
#' consistencies, such as Cronbach's alpha, item difficulty or discrimination
#' `check_itemscale()` calculates various measures of internal consistencies,
#' such as Cronbach's Alpha (or McDonald's Omega), item difficulty or discrimination
#' etc. on subscales which were built from several items. Subscales are
#' retrieved from the results of [`parameters::principal_components()`], i.e.
#' based on how many components were extracted from the PCA,
#' `check_itemscale()` retrieves those variables that belong to a component
#' and calculates the above mentioned measures.
#' based on how many components were extracted from the PCA, `check_itemscale()`
#' retrieves those variables that belong to a component and calculates the above
#' mentioned measures.
#'
#' @note
#' - *Item difficulty* should range between 0.2 and 0.8. Ideal value
Expand Down Expand Up @@ -65,7 +65,7 @@
#' factor_index = parameters::closest_component(pca)
#' )
#' @export
check_itemscale <- function(x, factor_index = NULL) {
check_itemscale <- function(x, factor_index = NULL, type = "alpha") {
# check for valid input
if (!inherits(x, c("parameters_pca", "data.frame"))) {
insight::format_error(
Expand All @@ -89,6 +89,9 @@
}
}

# alpha or omega?
type <- match.arg(type, c("alpha", "omega"))

# assign data and factor index
if (inherits(x, "parameters_pca")) {
insight::check_if_installed("parameters")
Expand All @@ -102,12 +105,15 @@
out <- lapply(sort(unique(subscales)), function(.subscale) {
columns <- names(subscales)[subscales == .subscale]
items <- dataset[columns]
reliability <- item_reliability(items)
reliability <- item_reliability(items, type = type)

.item_discr <- reliability$item_discrimination
if (is.null(.item_discr)) .item_discr <- NA
.item_alpha <- reliability$alpha_if_deleted
if (is.null(.item_alpha)) .item_alpha <- NA
.item_rel_estimate <- switch(type,
alpha = reliability$alpha_if_deleted,
omega = reliability$omega_if_deleted
)
if (is.null(.item_rel_estimate)) .item_rel_estimate <- NA

s_out <- data.frame(
Item = columns,
Expand All @@ -117,13 +123,22 @@
Skewness = vapply(items, function(i) as.numeric(datawizard::skewness(i)), numeric(1)),
Difficulty = item_difficulty(items)$Difficulty,
Discrimination = .item_discr,
`alpha if deleted` = .item_alpha,
reliability_if_deleted = .item_rel_estimate,
stringsAsFactors = FALSE,
check.names = FALSE
)

# fix column name
colnames(s_out)[8] <- switch(type,
alpha = "alpha if deleted",
omega = "omega if deleted"
)

attr(s_out, "item_intercorrelation") <- item_intercor(items)
attr(s_out, "cronbachs_alpha") <- cronbachs_alpha(items)
if (type == "omega") {
attr(s_out, "mcdonalds_omega") <- mcdonalds_omega(items, ci = NULL)
}

s_out
})
Expand All @@ -144,31 +159,43 @@
lapply(seq_along(x), function(i) {
out <- x[[i]]
attr(out, "table_caption") <- c(sprintf("\nComponent %i", i), "red")
if (!is.null(attributes(out)$mcdonalds_omega)) {

Check warning on line 162 in R/check_itemscale.R

View workflow job for this annotation

GitHub Actions / lint-changed-files / lint-changed-files

file=R/check_itemscale.R,line=162,col=11,[if_not_else_linter] Prefer `if (A) x else y` to the less-readable `if (!A) y else x` in a simple if/else statement.
omega <- sprintf(" McDonald's omega = %.3f", attributes(out)$mcdonalds_omega)
} else {
omega <- ""
}
attr(out, "table_footer") <- c(sprintf(
"\nMean inter-item-correlation = %.3f Cronbach's alpha = %.3f",
"\nMean inter-item-correlation = %.3f Cronbach's alpha = %.3f%s",
attributes(out)$item_intercorrelation,
attributes(out)$cronbachs_alpha
attributes(out)$cronbachs_alpha,
omega
), "yellow")

out
}),
digits = digits,
format = "text",
missing = "<NA>",
zap_small = TRUE
))
cat("\n")
}


#' @export
print_html.check_itemscale <- function(x, digits = 2, ...) {
x <- lapply(seq_along(x), function(i) {
out <- x[[i]]
if (!is.null(attributes(out)$mcdonalds_omega)) {

Check warning on line 188 in R/check_itemscale.R

View workflow job for this annotation

GitHub Actions / lint-changed-files / lint-changed-files

file=R/check_itemscale.R,line=188,col=9,[if_not_else_linter] Prefer `if (A) x else y` to the less-readable `if (!A) y else x` in a simple if/else statement.
omega <- sprintf(", McDonald's omega = %.3f", attributes(out)$mcdonalds_omega)
} else {
omega <- ""
}
attr(out, "table_caption") <- sprintf(
"Component %i: Mean inter-item-correlation = %.3f, Cronbach's alpha = %.3f",
"Component %i: Mean inter-item-correlation = %.3f, Cronbach's alpha = %.3f%s",
i,
attributes(out)$item_intercorrelation,
attributes(out)$cronbachs_alpha
attributes(out)$cronbachs_alpha,
omega
)
out
})
Expand Down
5 changes: 2 additions & 3 deletions R/cronbachs_alpha.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
#' @description Compute various measures of internal consistencies
#' for tests or item-scales of questionnaires.
#'
#' @param x A matrix or a data frame.
#' @param x A matrix or a data frame, or an object returned by
#' `[parameters::principal_components()]`.
#' @param ... Currently not used.
#'
#' @return The Cronbach's Alpha value for `x`.
Expand Down Expand Up @@ -50,14 +51,12 @@ cronbachs_alpha.data.frame <- function(x, verbose = TRUE, ...) {
}



#' @export
cronbachs_alpha.matrix <- function(x, verbose = TRUE, ...) {
cronbachs_alpha(as.data.frame(x), verbose = verbose, ...)
}



#' @export
cronbachs_alpha.parameters_pca <- function(x, verbose = TRUE, ...) {
# fetch data used for the PCA
Expand Down
58 changes: 40 additions & 18 deletions R/item_reliability.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,25 @@
#' for tests or item-scales of questionnaires.
#'
#' @param x A matrix or a data frame.
#' @param type Type of reliability estimate. Either `"alpha"` (default, see
#' [`cronbachs_alpha()`]) or `"omega"` (see [`mcdonalds_omega()`]). Note that
#' computing McDonald's Omega is more computationally intensive than Cronbach's
#' Alpha.
#' @param standardize Logical, if `TRUE`, the data frame's vectors will be
#' standardized. Recommended when the variables have different measures /
#' scales.
#' @param digits Amount of digits for returned values.
#'
#' @return A data frame with the corrected item-total correlations (*item
#' discrimination*, column `item_discrimination`) and Cronbach's Alpha
#' (if item deleted, column `alpha_if_deleted`) for each item
#' of the scale, or `NULL` if data frame had too less columns.
#' discrimination*, column `item_discrimination`) and Cronbach's Alpha
#' (if item deleted, column `alpha_if_deleted`) resp. McDonald's Omega for each
#' item of the scale, or `NULL` if data frame had too few columns.
#'
#' @details
#'
#' This function calculates the item discriminations (corrected item-total
#' correlations for each item of `x` with the remaining items) and the
#' Cronbach's alpha for each item, if it was deleted from the scale. The
#' Cronbach's alpha (when `type = "alpha"`) resp. McDonald's Omega (when
#' `type = "omega"`) for each item, if it was deleted from the scale. The
#' absolute value of the item discrimination indices should be above 0.2. An
#' index between 0.2 and 0.4 is considered as "fair", while an index above 0.4
#' (or below -0.4) is "good". The range of satisfactory values is from 0.4 to
Expand All @@ -28,18 +32,23 @@
#' determine why a negative value was obtained (e.g. reversed answer categories
#' regarding positive and negative poles).
#'
#' @examples
#' @examplesIf requireNamespace("lavaan", quietly = TRUE)
#' data(mtcars)
#' x <- mtcars[, c("cyl", "gear", "carb", "hp")]
#' item_reliability(x)
#' item_reliability(mtcars[, c("cyl", "gear", "carb", "hp")])
#'
#' data(iris)
#' item_reliability(iris[1:4], type = "omega")
#' @export
item_reliability <- function(x, standardize = FALSE, digits = 3) {
item_reliability <- function(x, type = "alpha", standardize = FALSE, digits = 3) {
# check param
if (!is.matrix(x) && !is.data.frame(x)) {
insight::format_alert("`x` needs to be a data frame or matrix.")
return(NULL)
}

# alpha or omega?
type <- match.arg(type, c("alpha", "omega"))

# remove missings, so correlation works
x <- stats::na.omit(x)

Expand All @@ -58,20 +67,33 @@ item_reliability <- function(x, standardize = FALSE, digits = 3) {
# when items have different measures / scales
if (standardize) x <- .std(x)

# calculate cronbach-if-deleted
cronbachDeleted <- vapply(seq_len(ncol(x)), function(i) cronbachs_alpha(x[, -i]), numeric(1L))

# calculate corrected total-item correlation
totalCorr <- vapply(seq_len(ncol(x)), function(i) {
stats::cor(x[, i], rowSums(x[, -i]), use = "pairwise.complete.obs")
}, numeric(1L))

ret.df <- data.frame(
term = df.names,
alpha_if_deleted = round(cronbachDeleted, digits),
item_discrimination = round(totalCorr, digits),
stringsAsFactors = FALSE
)
# reliability estimate alpha or omega?
if (type == "alpha") {
# calculate cronbach-if-deleted
cronbachDeleted <- vapply(seq_len(ncol(x)), function(i) cronbachs_alpha(x[, -i]), numeric(1L))

ret.df <- data.frame(
term = df.names,
alpha_if_deleted = round(cronbachDeleted, digits),
item_discrimination = round(totalCorr, digits),
stringsAsFactors = FALSE
)
} else {
# calculate omega-if-deleted
omegaDeleted <- vapply(seq_len(ncol(x)), function(i) mcdonalds_omega(x[, -i], ci = NULL), numeric(1L))

ret.df <- data.frame(
term = df.names,
omega_if_deleted = round(omegaDeleted, digits),
item_discrimination = round(totalCorr, digits),
stringsAsFactors = FALSE
)
}
} else {
insight::format_warning("Data frame needs at least three columns for reliability-test.")
}
Expand Down
Loading
Loading