-
Notifications
You must be signed in to change notification settings - Fork 18
Fixes some issues with reliability #301
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
base: master
Are you sure you want to change the base?
Changes from all commits
6e4fe2a
198fd4d
7abd7a5
8cf3581
953f5f2
bca6e5b
d538b54
c7d1341
477419d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
|
|
||
| jasp_dev_work_dir/ |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -446,13 +446,10 @@ unidimensionalReliabilityFrequentist <- function(jaspResults, dataset, options) | |||||||||||||||||||
|
|
||||||||||||||||||||
| nit <- ncol(dataset) | ||||||||||||||||||||
| splits <- split(seq_len(nit), 1:2) | ||||||||||||||||||||
| if (options[["coefficientType"]] == "unstandardized") { | ||||||||||||||||||||
| for (i in seq_len(options[["bootstrapSamples"]])) { | ||||||||||||||||||||
| out[["samp"]][i] <- .splithalfCor(model[["bootSamp"]][i, , ], splits, progressbarTick) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } else { # either we have the boostrapped cor samples from the standardized coefficients or we have them through | ||||||||||||||||||||
| # the splithalf method | ||||||||||||||||||||
| out[["samp"]] <- apply(model[["bootCor"]], 1, .splithalfCor, splits = splits) | ||||||||||||||||||||
| isStd <- options[["coefficientType"]] == "standardized" | ||||||||||||||||||||
| # split-half bootstrap always uses covariance matrices (bootSamp) | ||||||||||||||||||||
| for (i in seq_len(options[["bootstrapSamples"]])) { | ||||||||||||||||||||
| out[["samp"]][i] <- .splithalfCor(model[["bootSamp"]][i, , ], splits, standardized = isStd, callback = progressbarTick) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
@@ -476,18 +473,19 @@ unidimensionalReliabilityFrequentist <- function(jaspResults, dataset, options) | |||||||||||||||||||
|
|
||||||||||||||||||||
| if (options[["itemDeletedSplithalf"]] && is.null(model[["empty"]]) && options[["intervalMethod"]] == "bootstrapped") { | ||||||||||||||||||||
|
|
||||||||||||||||||||
| type <- ifelse(options[["coefficientType"]] == "unstandardized", "bootSamp", "bootCor") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| startProgressbar(options[["bootstrapSamples"]] * ncol(dataset)) | ||||||||||||||||||||
| jaspBase::.setSeedJASP(options) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| isStd <- options[["coefficientType"]] == "standardized" | ||||||||||||||||||||
| nit <- ncol(dataset) - 1 | ||||||||||||||||||||
| splits <- split(seq_len(nit), 1:2) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| out[["itemSamp"]] <- .frequentistItemDroppedStats(covSamp = model[[type]], | ||||||||||||||||||||
| # split-half bootstrap always uses covariance matrices (bootSamp) | ||||||||||||||||||||
| out[["itemSamp"]] <- .frequentistItemDroppedStats(covSamp = model[["bootSamp"]], | ||||||||||||||||||||
| f1 = .splithalfCor, | ||||||||||||||||||||
| callback = progressbarTick, | ||||||||||||||||||||
| splits = splits) | ||||||||||||||||||||
| splits = splits, | ||||||||||||||||||||
| standardized = isStd) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (options[["samplesSavingDisabled"]]) | ||||||||||||||||||||
| return(out) | ||||||||||||||||||||
|
|
@@ -947,16 +945,16 @@ unidimensionalReliabilityFrequentist <- function(jaspResults, dataset, options) | |||||||||||||||||||
| if (options[["scaleSplithalf"]]) { | ||||||||||||||||||||
| nit <- ncol(dataset) | ||||||||||||||||||||
| splits <- split(seq_len(nit), 1:2) | ||||||||||||||||||||
| out[["est"]][["scaleSplithalf"]] <- .splithalfData(dtUse, splits = splits, useCase = model[["use.cases"]]) | ||||||||||||||||||||
| isStd <- options[["coefficientType"]] == "standardized" | ||||||||||||||||||||
| # split-half always uses raw data; standardized = Spearman-Brown, unstandardized = Flanagan-Rulon | ||||||||||||||||||||
| out[["est"]][["scaleSplithalf"]] <- .splithalfData(dataset, splits = splits, useCase = model[["use.cases"]], standardized = isStd) | ||||||||||||||||||||
| if (options[["intervalMethod"]] == "bootstrapped") { | ||||||||||||||||||||
| samp <- model[["scaleSplithalf"]][["samp"]] | ||||||||||||||||||||
| out[["conf"]][["scaleSplithalf"]] <- quantile(samp, probs = c((1 - ciValue) / 2, 1 - (1 - ciValue) / 2), na.rm = TRUE) | ||||||||||||||||||||
| out[["se"]][["scaleSplithalf"]] <- sd(samp, na.rm = TRUE) | ||||||||||||||||||||
| } else { # interval analytic | ||||||||||||||||||||
| partSums1 <- rowSums(dtUse[, splits[[1]], drop = FALSE]) | ||||||||||||||||||||
| partSums2 <- rowSums(dtUse[, splits[[2]], drop = FALSE]) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| out[["se"]][["scaleSplithalf"]] <- .seSplithalf(partSums1, partSums2, model[["use.cases"]]) | ||||||||||||||||||||
| out[["se"]][["scaleSplithalf"]] <- .seSplithalf(dataset, splits, standardized = isStd, scaleThreshold = options[["hiddenScaleThreshold"]]) | ||||||||||||||||||||
| out[["conf"]][["scaleSplithalf"]] <- out[["est"]][["scaleSplithalf"]] + c(-1, 1) * out[["se"]][["scaleSplithalf"]] * qnorm(1 - (1 - ciValue) / 2) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
@@ -969,10 +967,13 @@ unidimensionalReliabilityFrequentist <- function(jaspResults, dataset, options) | |||||||||||||||||||
| out[["conf"]][["averageInterItemCorrelation"]] <- quantile(samp, probs = c((1 - ciValue) / 2, 1 - (1 - ciValue) / 2), na.rm = TRUE) | ||||||||||||||||||||
| out[["se"]][["averageInterItemCorrelation"]] <- sd(samp, na.rm = TRUE) | ||||||||||||||||||||
| } else { # interval analytic | ||||||||||||||||||||
| # TODO: what is the SE of the average interitem correlation? | ||||||||||||||||||||
| out[["se"]][["averageInterItemCorrelation"]] <- NA | ||||||||||||||||||||
| if (model[["pairwise"]]) { | ||||||||||||||||||||
| out[["se"]][["averageInterItemCorrelation"]] <- NA | ||||||||||||||||||||
| out[["error"]][["averageInterItemCorrelation"]] <- gettext("The analytic confidence interval is not available for the average interitem correlation when data contain missings and pairwise complete observations are used. Try changing to 'Delete listwise' within 'Advanced Options'.") | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| out[["se"]][["averageInterItemCorrelation"]] <- .seAverageInterItemCor(dtUse, scaleThreshold = options[["hiddenScaleThreshold"]]) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| out[["conf"]][["averageInterItemCorrelation"]] <- out[["est"]][["averageInterItemCorrelation"]] + c(-1, 1) * out[["se"]][["averageInterItemCorrelation"]] * qnorm(1 - (1 - ciValue) / 2) | ||||||||||||||||||||
| out[["error"]][["averageInterItemCorrelation"]] <- gettext("The standard error of the average interitem correlation is not available. ") | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -1170,19 +1171,17 @@ unidimensionalReliabilityFrequentist <- function(jaspResults, dataset, options) | |||||||||||||||||||
| out[["lower"]][["itemDeletedSplithalf"]] <- c(NA, NA) | ||||||||||||||||||||
| out[["upper"]][["itemDeletedSplithalf"]] <- c(NA, NA) | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| for (i in seq_len(ncol(dtUse))) { | ||||||||||||||||||||
| dtCut <- dtUse[, -i, drop = FALSE] | ||||||||||||||||||||
| isStd <- options[["coefficientType"]] == "standardized" | ||||||||||||||||||||
| for (i in seq_len(ncol(dataset))) { | ||||||||||||||||||||
| dtCut <- dataset[, -i, drop = FALSE] | ||||||||||||||||||||
| nit <- ncol(dtCut) | ||||||||||||||||||||
| splits <- split(seq_len(nit), 1:2) | ||||||||||||||||||||
| est <- .splithalfData(dtCut, splits = splits, useCase = model[["use.cases"]]) | ||||||||||||||||||||
| est <- .splithalfData(dtCut, splits = splits, useCase = model[["use.cases"]], standardized = isStd) | ||||||||||||||||||||
| out[["est"]][["itemDeletedSplithalf"]][i] <- est | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (options[["intervalMethod"]] == "analytic") { | ||||||||||||||||||||
|
|
||||||||||||||||||||
| partSums1 <- rowSums(dtCut[, splits[[1]]]) | ||||||||||||||||||||
| partSums2 <- rowSums(dtCut[, splits[[2]]]) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| se <- .seSplithalf(partSums1, partSums2, model[["use.cases"]]) | ||||||||||||||||||||
| se <- .seSplithalf(dtCut, splits, standardized = isStd, scaleThreshold = options[["hiddenScaleThreshold"]]) | ||||||||||||||||||||
| conf <- est + c(-1, 1) * se * qnorm(1 - (1 - ciValue) / 2) | ||||||||||||||||||||
| out[["lower"]][["itemDeletedSplithalf"]][i] <- conf[1] | ||||||||||||||||||||
| out[["upper"]][["itemDeletedSplithalf"]][i] <- conf[2] | ||||||||||||||||||||
|
|
@@ -1573,13 +1572,14 @@ unidimensionalReliabilityFrequentist <- function(jaspResults, dataset, options) | |||||||||||||||||||
| f1 = function(){}, | ||||||||||||||||||||
| callback = function(){}, | ||||||||||||||||||||
| missing = NULL, | ||||||||||||||||||||
| splits = NULL) { | ||||||||||||||||||||
| splits = NULL, | ||||||||||||||||||||
| standardized = FALSE) { | ||||||||||||||||||||
|
|
||||||||||||||||||||
| dd <- dim(covSamp) | ||||||||||||||||||||
| out <- matrix(0, dd[1], dd[3]) | ||||||||||||||||||||
| if (!is.null(splits)) { # split half | ||||||||||||||||||||
| for (i in seq_len(dd[3])) { | ||||||||||||||||||||
| out[, i] <- apply(covSamp[, -i, -i], c(1), f1, callback = callback, splits = splits) | ||||||||||||||||||||
| out[, i] <- apply(covSamp[, -i, -i], c(1), f1, callback = callback, splits = splits, standardized = standardized) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| if (!is.null(missing)) { # cfa | ||||||||||||||||||||
|
|
@@ -2040,17 +2040,69 @@ unidimensionalReliabilityFrequentist <- function(jaspResults, dataset, options) | |||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| .splithalfData <- function(X, splits, useCase) { | ||||||||||||||||||||
| # SE of average inter-item correlation via multivariate delta method on vec(Sigma). | ||||||||||||||||||||
| # The average inter-item correlation is r_bar = (1 / (J*(J-1))) * sum_{i!=j} C_ij / sqrt(C_ii * C_jj) | ||||||||||||||||||||
| # where C = Var(X). The gradient w.r.t. vec(C) is obtained by differentiating each | ||||||||||||||||||||
| # r_ij = C_ij / sqrt(C_ii * C_jj) w.r.t. C_ab and averaging. | ||||||||||||||||||||
| .seAverageInterItemCor <- function(X, VC = NULL, scaleThreshold = 10) { | ||||||||||||||||||||
| J <- ncol(X) | ||||||||||||||||||||
| if (is.null(VC)) { | ||||||||||||||||||||
| levs <- sapply(as.data.frame(X), function(col) length(unique(col[!is.na(col)]))) | ||||||||||||||||||||
| if (any(levs > scaleThreshold)) { | ||||||||||||||||||||
| VC <- .varVCwishart(stats::var(X), nrow(X)) | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| VC <- .varCM(X) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| C <- var(X) | ||||||||||||||||||||
| R <- cov2cor(C) | ||||||||||||||||||||
| m <- J * (J - 1) # number of off-diagonal pairs (both triangles) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Build J x J gradient matrix G_mat where entry (a, b) = d(r_bar) / d(C_ab). | ||||||||||||||||||||
| # Off-diagonal (a != b): d(r_bar)/d(C_ab) = 1 / (m * sqrt(C_aa * C_bb)) | ||||||||||||||||||||
| # because only r_ab depends on C_ab (as numerator), and d(r_ab)/d(C_ab) = 1/sqrt(C_aa*C_bb), | ||||||||||||||||||||
| # plus r_ba = r_ab so appears twice in the double sum, but m counts both triangles. | ||||||||||||||||||||
| # Diagonal (a = a): d(r_bar)/d(C_aa) = -sum_{j!=a} R_aj / (m * C_aa) | ||||||||||||||||||||
| # because d(r_aj)/d(C_aa) = -C_aj / (2 * C_aa * sqrt(C_aa*C_jj)) = -R_aj / (2*C_aa), | ||||||||||||||||||||
| # summed over all j != a in both triangles (each pair appears twice) gives -sum_{j!=a} R_aj / (m*C_aa). | ||||||||||||||||||||
| Gmat <- matrix(0, J, J) | ||||||||||||||||||||
| dC <- diag(C) | ||||||||||||||||||||
| for (a in seq_len(J)) { | ||||||||||||||||||||
| for (b in seq_len(J)) { | ||||||||||||||||||||
| if (a != b) { | ||||||||||||||||||||
| Gmat[a, b] <- 1 / (m * sqrt(dC[a] * dC[b])) | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| Gmat[a, a] <- -sum(R[a, -a]) / (m * dC[a]) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+2071
to
+2079
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| # Vectorize: vec(G_mat) as a 1 x J^2 row vector, column-major order matching vec(C) | ||||||||||||||||||||
| G <- matrix(as.vector(Gmat), nrow = 1) | ||||||||||||||||||||
| V <- G %*% VC %*% t(G) | ||||||||||||||||||||
| return(sqrt(as.numeric(V))) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| .splithalfData <- function(X, splits, useCase, standardized = FALSE) { | ||||||||||||||||||||
|
|
||||||||||||||||||||
| partSums1 <- rowSums(X[, splits[[1]], drop = FALSE]) | ||||||||||||||||||||
| partSums2 <- rowSums(X[, splits[[2]], drop = FALSE]) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| rsh_uncorrected <- cor(partSums1, partSums2, use = useCase) | ||||||||||||||||||||
| rsh <- (2 * rsh_uncorrected) / (1 + rsh_uncorrected) | ||||||||||||||||||||
| if (standardized) { | ||||||||||||||||||||
| # Spearman-Brown coefficient: 2r/(1+r) on raw data correlation | ||||||||||||||||||||
| r <- cor(partSums1, partSums2, use = useCase) | ||||||||||||||||||||
| rsh <- (2 * r) / (1 + r) | ||||||||||||||||||||
|
Comment on lines
+2093
to
+2096
|
||||||||||||||||||||
| } else { | ||||||||||||||||||||
| # Flanagan-Rulon / Guttman split-half: 4 * Cov(X1, X2) / Var(X) | ||||||||||||||||||||
| totalScore <- partSums1 + partSums2 | ||||||||||||||||||||
| rsh <- 4 * cov(partSums1, partSums2, use = useCase) / var(totalScore, na.rm = TRUE) | ||||||||||||||||||||
|
||||||||||||||||||||
| rsh <- 4 * cov(partSums1, partSums2, use = useCase) / var(totalScore, na.rm = TRUE) | |
| varTotal <- var(totalScore, na.rm = TRUE) | |
| if (is.na(varTotal) || varTotal <= .Machine$double.eps) { | |
| # Variance of total score is zero or effectively zero; reliability undefined | |
| rsh <- NA_real_ | |
| } else { | |
| rsh <- 4 * cov(partSums1, partSums2, use = useCase) / varTotal | |
| } |
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Spearman-Brown calculation could encounter division by zero when (1 + r) is zero, i.e., when r = -1. This is theoretically possible when the two halves have perfect negative correlation. Consider adding a check for this edge case and returning NA or an appropriate value.
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the standardized case, the gradient calculation could be problematic when (1 + r)^2 is zero or near-zero (i.e., when r approaches -1). This could lead to numerical instability or division by zero. Consider adding validation or numerical safeguards for this edge case.
| G <- matrix((2 / (1 + r)^2) * dr, nrow = 1) | |
| # Guard against (1 + r)^2 being zero or numerically too small | |
| denom <- (1 + r)^2 | |
| if (denom < .Machine$double.eps) { | |
| denom <- .Machine$double.eps | |
| } | |
| G <- matrix((2 / denom) * dr, nrow = 1) |
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the Flanagan-Rulon case, the gradient calculation involves division by S^2 (total variance squared). When S is zero or near-zero (constant data), this will cause numerical problems. Consider adding validation to check for this case and return NA for the standard error when appropriate.
| # dFR/d(vecSigma) = 4*(a_AB * S - C_AB) / S^2 | |
| # dFR/d(vecSigma) = 4*(a_AB * S - C_AB) / S^2 | |
| # Guard against S being zero or numerically near-zero, which would | |
| # cause division by S^2 to be unstable; in that case, the SE is undefined. | |
| if (abs(S) < sqrt(.Machine$double.eps)) { | |
| return(NA_real_) | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,9 @@ | ||
| # jaspReliability | ||
| <div align="right"> | ||
|
|
||
| [](https://github.com/jasp-stats/jaspReliability/actions/workflows/unittests.yml) | ||
| [](https://codecov.io/gh/jasp-stats/jaspReliability) | ||
| <br> | ||
| <b>Maintainer:</b> <a href="https://github.com/juliuspfadt/">Julius Pfadt</a> | ||
|
|
||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The split-half coefficient calculation uses raw dataset without considering model[["use.cases"]] for missing data handling in the analytic SE calculation (.seSplithalf), while the point estimate correctly uses model[["use.cases"]]. This inconsistency could lead to mismatched dimensions or incorrect SE when there are missing values. Consider ensuring both use the same data or use cases approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, thats not correct. The split-half coefficient for the standardized case operates on the raw data with a different formula, if we used dtUse it would use the standardized data.