@@ -120,6 +120,153 @@ test_that("do_cor should skip group with only one row.", {
120120 expect_equal(nrow(res %> % filter(z == F )), 0 )
121121})
122122
123+ test_that(" do_cor with polychoric method" , {
124+ skip_if_not_installed(" polycor" )
125+
126+ # Polychoric correlation is for ordinal variables. The latent variables behind x and y
127+ # are correlated at 0.7, while z is independent of them.
128+ set.seed(123 )
129+ n <- 200
130+ cut5 <- function (z ) as.integer(cut(z , breaks = c(- Inf , - 0.84 , - 0.25 , 0.25 , 0.84 , Inf )))
131+ z1 <- rnorm(n ); z2 <- 0.7 * z1 + sqrt(1 - 0.49 ) * rnorm(n ); z3 <- rnorm(n )
132+ df <- data.frame (x = cut5(z1 ), y = cut5(z2 ), z = cut5(z3 ))
133+ model_df <- df %> % do_cor(`x` , `y` , `z` , method = " polychoric" , distinct = FALSE , diag = TRUE , return_type = " model" )
134+ res <- model_df %> % tidy_rowwise(model , type = ' cor' )
135+ expect_equal(nrow(res ), 9 ) # All 9 combinations.
136+ xy <- res %> % filter(pair.name.x == " x" , pair.name.y == " y" )
137+ expect_true(xy $ correlation > 0.5 && xy $ correlation < 0.9 ) # Recovers the true latent 0.7, not inflated.
138+ expect_true(xy $ p_value < 0.05 ) # Statistically significant.
139+ expect_true(is.finite(xy $ statistic )) # z value is populated.
140+ xz <- res %> % filter(pair.name.x == " x" , pair.name.y == " z" )
141+ expect_true(abs(xz $ correlation ) < 0.3 ) # z is independent of x: near-zero polychoric correlation.
142+ expect_true(xz $ p_value > 0.05 ) # The independent pair is not statistically significant.
143+ diag_row <- res %> % filter(pair.name.x == " x" , pair.name.y == " x" )
144+ expect_equal(diag_row $ correlation , 1 ) # Diagonal correlation is 1.
145+ expect_equal(diag_row $ p_value , 0 ) # Diagonal P value is 0.
146+ })
147+
148+ test_that(" do_cor automatic method selects the specified correlation family" , {
149+ ordinal <- ordered(rep(1 : 5 , each = 20 ))
150+ expect_identical(resolve_correlation_method(data.frame (x = 1 : 100 , y = 101 : 200 ), " auto" ), " pearson" )
151+ expect_identical(resolve_correlation_method(data.frame (x = ordinal , y = rev(ordinal )), " auto" ), " polychoric" )
152+ expect_identical(resolve_correlation_method(data.frame (x = 1 : 100 , y = ordinal ), " auto" ), " mixed" )
153+ })
154+
155+ test_that(" do_cor supports automatic and mixed correlations with factor inputs" , {
156+ skip_if_not_installed(" polycor" )
157+
158+ set.seed(456 )
159+ n <- 100
160+ ordinal <- ordered(cut(rnorm(n ), breaks = c(- Inf , - 0.84 , - 0.25 , 0.25 , 0.84 , Inf )))
161+ df <- data.frame (continuous = rnorm(n ), ordinal = ordinal )
162+
163+ auto <- df %> % do_cor(continuous , ordinal , method = " auto" , distinct = FALSE , diag = TRUE , return_type = " model" ) %> % tidy_rowwise(model , type = " cor" )
164+ mixed <- df %> % do_cor(continuous , ordinal , method = " mixed" , distinct = FALSE , diag = TRUE , return_type = " model" ) %> % tidy_rowwise(model , type = " cor" )
165+
166+ expect_equal(nrow(auto ), 4 )
167+ expect_equal(nrow(mixed ), 4 )
168+ expect_true(is.finite(auto $ correlation [auto $ pair.name.x != auto $ pair.name.y ][1 ]))
169+ expect_true(is.finite(mixed $ correlation [mixed $ pair.name.x != mixed $ pair.name.y ][1 ]))
170+ })
171+
172+ test_that(" do_cor with polychoric method handles a constant column without error" , {
173+ skip_if_not_installed(" polycor" )
174+
175+ set.seed(123 )
176+ n <- 100
177+ cut5 <- function (z ) as.integer(cut(z , breaks = c(- Inf , - 0.84 , - 0.25 , 0.25 , 0.84 , Inf )))
178+ z1 <- rnorm(n ); z2 <- 0.7 * z1 + sqrt(1 - 0.49 ) * rnorm(n )
179+ df <- data.frame (x = cut5(z1 ), y = cut5(z2 ), w = rep(3L , n )) # w is constant.
180+ # hetcor warns for the non-estimable pairs involving the constant column; that is expected.
181+ model_df <- suppressWarnings(df %> % do_cor(`x` , `y` , `w` , method = " polychoric" , distinct = FALSE , diag = TRUE , return_type = " model" ))
182+ res <- model_df %> % tidy_rowwise(model , type = ' cor' )
183+ xy <- res %> % filter(pair.name.x == " x" , pair.name.y == " y" )
184+ expect_equal(nrow(xy ), 1 ) # The estimable pair is still computed.
185+ expect_true(xy $ correlation > 0.5 )
186+ # The constant-column pair has NA correlation and is dropped by na.rm in mat_to_df.
187+ expect_equal(nrow(res %> % filter(pair.name.x == " x" , pair.name.y == " w" )), 0 )
188+ })
189+
190+ test_that(" do_cor with polychoric method handles complex column names" , {
191+ skip_if_not_installed(" polycor" )
192+
193+ set.seed(123 )
194+ n <- 100
195+ cut5 <- function (z ) as.integer(cut(z , breaks = c(- Inf , - 0.84 , - 0.25 , 0.25 , 0.84 , Inf )))
196+ z1 <- rnorm(n ); z2 <- 0.7 * z1 + sqrt(1 - 0.49 ) * rnorm(n )
197+ sname <- " 航空 会社 !\" #$%&'()*+, -./:;<=>?@[]^_'{|}~ 表"
198+ df <- data.frame (a = cut5(z1 ), b = cut5(z2 ), check.names = FALSE )
199+ names(df ) <- c(sname , " plain" )
200+ model_df <- df %> % do_cor(tidyselect :: everything(), method = " polychoric" , distinct = FALSE , diag = TRUE , return_type = " model" )
201+ res <- model_df %> % tidy_rowwise(model , type = ' cor' )
202+ expect_true(sname %in% as.character(res $ pair.name.x )) # Complex name survives the round trip.
203+ pair <- res %> % filter(as.character(pair.name.x ) == sname , pair.name.y == " plain" )
204+ expect_equal(nrow(pair ), 1 )
205+ expect_true(is.finite(pair $ correlation ))
206+ })
207+
208+ test_that(" do_cor with polychoric method accepts use values hetcor does not support" , {
209+ skip_if_not_installed(" polycor" )
210+
211+ # hetcor only accepts "complete.obs" and "pairwise.complete.obs", but the public
212+ # do_cor API (and the other methods via cor()/cor.test()) also accept "everything",
213+ # "all.obs", and "na.or.complete". Those must be mapped, not passed through as an error.
214+ set.seed(123 )
215+ n <- 100
216+ cut5 <- function (z ) as.integer(cut(z , breaks = c(- Inf , - 0.84 , - 0.25 , 0.25 , 0.84 , Inf )))
217+ z1 <- rnorm(n ); z2 <- 0.7 * z1 + sqrt(1 - 0.49 ) * rnorm(n )
218+ df <- data.frame (x = cut5(z1 ), y = cut5(z2 ))
219+ for (u in c(" everything" , " all.obs" , " na.or.complete" , " complete.obs" )) {
220+ model_df <- df %> % do_cor(`x` , `y` , method = " polychoric" , use = u , distinct = FALSE , diag = TRUE , return_type = " model" )
221+ res <- model_df %> % tidy_rowwise(model , type = ' cor' )
222+ xy <- res %> % filter(pair.name.x == " x" , pair.name.y == " y" )
223+ expect_equal(nrow(xy ), 1 , info = u ) # The pair is computed regardless of the use value.
224+ expect_true(xy $ correlation > 0.5 , info = u )
225+ }
226+ })
227+
228+ test_that(" do_cor with polychoric method for grouped (repeat-by) data" , {
229+ skip_if_not_installed(" polycor" )
230+
231+ # Repeat By on Analytics View maps to group_by(). Each group must get its own
232+ # polychoric correlation. Group A has a positive relationship, group B a negative one.
233+ set.seed(123 )
234+ n <- 100
235+ cut5 <- function (z ) as.integer(cut(z , breaks = c(- Inf , - 0.84 , - 0.25 , 0.25 , 0.84 , Inf )))
236+ mk <- function (rho ) {
237+ z1 <- rnorm(n ); z2 <- rho * z1 + sqrt(1 - rho ^ 2 ) * rnorm(n )
238+ data.frame (x = cut5(z1 ), y = cut5(z2 ))
239+ }
240+ df <- dplyr :: bind_rows(cbind(mk(0.7 ), grp = " A" ), cbind(mk(- 0.7 ), grp = " B" ))
241+ model_df <- df %> % group_by(grp ) %> % do_cor(`x` , `y` , method = " polychoric" , distinct = FALSE , diag = TRUE , return_type = " model" )
242+ res <- model_df %> % tidy_rowwise(model , type = ' cor' )
243+ expect_setequal(unique(as.character(res $ grp )), c(" A" , " B" )) # Both groups produced results.
244+ a_xy <- res %> % filter(grp == " A" , pair.name.x == " x" , pair.name.y == " y" )
245+ b_xy <- res %> % filter(grp == " B" , pair.name.x == " x" , pair.name.y == " y" )
246+ expect_true(a_xy $ correlation > 0.4 ) # Positive correlation in group A.
247+ expect_true(b_xy $ correlation < - 0.4 ) # Negative correlation in group B, computed independently.
248+ })
249+
250+ test_that(" do_cor with polychoric method handles NA values via pairwise complete obs" , {
251+ skip_if_not_installed(" polycor" )
252+
253+ # Survey data routinely has missing responses (NA). The default use="pairwise.complete.obs"
254+ # must drop NAs pairwise rather than error, so every pair is still estimated.
255+ set.seed(123 )
256+ n <- 200
257+ cut5 <- function (z ) as.integer(cut(z , breaks = c(- Inf , - 0.84 , - 0.25 , 0.25 , 0.84 , Inf )))
258+ z1 <- rnorm(n ); z2 <- 0.7 * z1 + sqrt(1 - 0.49 ) * rnorm(n ); z3 <- rnorm(n )
259+ x <- cut5(z1 ); y <- cut5(z2 ); z <- cut5(z3 )
260+ x [1 : 20 ] <- NA ; y [15 : 30 ] <- NA ; z [40 : 60 ] <- NA # Missing responses at different rows per column.
261+ df <- data.frame (x = x , y = y , z = z )
262+ model_df <- df %> % do_cor(`x` , `y` , `z` , method = " polychoric" , distinct = FALSE , diag = TRUE , return_type = " model" )
263+ res <- model_df %> % tidy_rowwise(model , type = ' cor' )
264+ expect_equal(nrow(res ), 9 ) # Every pair is still estimated from the pairwise-complete rows.
265+ xy <- res %> % filter(pair.name.x == " x" , pair.name.y == " y" )
266+ expect_true(is.finite(xy $ correlation )) # NAs did not break the estimate.
267+ expect_true(xy $ correlation > 0.4 ) # The x-y relationship is still recovered despite the NAs.
268+ })
269+
123270test_that(" test do_svd.kv with fill" , {
124271 test_df <- data.frame (
125272 rand = runif(20 , min = 0 , max = 10 ),
0 commit comments