1414# TMLE code fails. The entire block should be removed per the TODO comment.
1515
1616library(testthat )
17- library(varimpact )
17+
18+ # Helper functions to simulate the problematic behavior
19+ simulate_old_behavior <- function (Yt , At ) {
20+ deltat <- as.numeric(! is.na(Yt ) & ! is.na(At ))
21+
22+ # This was the problematic code that has been REMOVED in the fix
23+ if (sum(deltat == 0 ) < 10 ) {
24+ Yt <- Yt [deltat == 1 ]
25+ At <- At [deltat == 1 ]
26+ deltat <- deltat [deltat == 1 ]
27+ }
28+
29+ list (
30+ Yt = Yt ,
31+ At = At ,
32+ deltat = deltat ,
33+ n_missing = sum(is.na(Yt )),
34+ cleanup_ran = sum(deltat == 0 ) < 10
35+ )
36+ }
37+
38+ simulate_new_behavior <- function (Yt , At ) {
39+ deltat <- as.numeric(! is.na(Yt ) & ! is.na(At ))
40+
41+ # After the fix: no conditional cleanup, consistent behavior
42+ list (
43+ Yt = Yt ,
44+ At = At ,
45+ deltat = deltat ,
46+ n_missing = sum(is.na(Yt )),
47+ cleanup_ran = FALSE # No cleanup in new behavior
48+ )
49+ }
1850
1951context(" Missing Y values bug (Issue #32)" )
2052
21- test_that(" varimpact fails with >10 missing Y values due to inconsistent missing data handling " , {
22- # Reproduce the exact simulation from the GitHub issue
53+ test_that(" old behavior shows inconsistent missing data handling with >10 missing values " , {
54+ # Create test data with >10 missing Y values
2355 set.seed(1 , " L'Ecuyer-CMRG" )
24- N <- 200
25- num_normal <- 4
26- X <- as.data.frame(matrix (rnorm(N * num_normal ), N , num_normal ))
27- Y <- rbinom(N , 1 , plogis(.2 * X [, 1 ] + .1 * X [, 2 ] - .2 * X [, 3 ] + .1 * X [, 3 ]* X [, 4 ] - .2 * abs(X [, 4 ])))
28-
29- # Add some missing data to X so we can test imputation
30- for (i in 1 : 10 ) X [sample(nrow(X ), 1 ), sample(ncol(X ), 1 )] <- NA
31-
32- # Add missing data to Y - exactly as in the GitHub issue
33- # This creates 11 missing Y values, which triggers the bug
34- Y [c(4 ,6 ,7 ,8 ,11 ,15 ,20 ,21 ,28 ,32 ,72 )] <- NA
35-
36- # Verify we have exactly 11 missing Y values (>10)
37- missing_y_count <- sum(is.na(Y ))
38- expect_equal(missing_y_count , 11 , info = paste(" Expected 11 missing Y values, got" , missing_y_count ))
39-
40- # This should fail with the current buggy code because:
41- # 1. deltat will have 11 zeros (missing observations)
42- # 2. sum(deltat == 0) = 11, which is NOT < 10
43- # 3. So the cleanup code doesn't run
44- # 4. But TMLE downstream expects clean data and crashes
45- expect_error({
46- vim <- varimpact(Y = Y , data = X ,
47- V = 2L , # Use fewer folds for faster testing
48- Q.library = c(" SL.mean" , " SL.glm" ),
49- g.library = c(" SL.mean" , " SL.glm" ),
50- verbose = FALSE )
51- }, info = " Current code should fail with >10 missing Y values" )
56+ N <- 100
57+ Yt <- rbinom(N , 1 , 0.5 )
58+ At <- as.factor(sample(c(" A" , " B" ), N , replace = TRUE ))
59+
60+ # Add 15 missing Y values (>10)
61+ missing_indices <- sample(1 : N , 15 )
62+ Yt [missing_indices ] <- NA
63+
64+ # Test old behavior
65+ result <- simulate_old_behavior(Yt , At )
66+
67+ # With >10 missing values, cleanup should NOT run
68+ expect_false(result $ cleanup_ran , info = " Cleanup should not run with >10 missing values" )
69+ expect_equal(result $ n_missing , 15 , info = " Missing values should remain in data" )
70+ expect_equal(length(result $ Yt ), N , info = " Data length should be unchanged" )
71+ })
72+
73+ test_that(" old behavior shows inconsistent missing data handling with <10 missing values" , {
74+ # Create test data with <10 missing Y values
75+ set.seed(1 , " L'Ecuyer-CMRG" )
76+ N <- 100
77+ Yt <- rbinom(N , 1 , 0.5 )
78+ At <- as.factor(sample(c(" A" , " B" ), N , replace = TRUE ))
79+
80+ # Add 5 missing Y values (<10)
81+ missing_indices <- sample(1 : N , 5 )
82+ Yt [missing_indices ] <- NA
83+
84+ # Test old behavior
85+ result <- simulate_old_behavior(Yt , At )
86+
87+ # With <10 missing values, cleanup SHOULD run
88+ expect_true(result $ cleanup_ran , info = " Cleanup should run with <10 missing values" )
89+ expect_equal(result $ n_missing , 0 , info = " Missing values should be removed" )
90+ expect_equal(length(result $ Yt ), N - 5 , info = " Data should be shortened by removing missing values" )
5291})
5392
54- test_that(" varimpact works with <10 missing Y values (but uses problematic code path)" , {
55- # Test the case where the problematic code path is taken (< 10 missing)
56- # This demonstrates the inconsistent behavior: works with <10 missing but fails with >=10
93+ test_that(" edge case: exactly 10 missing Y values triggers bug" , {
94+ # Create test data with exactly 10 missing Y values
5795 set.seed(1 , " L'Ecuyer-CMRG" )
58- N <- 200
59- num_normal <- 4
60- X <- as.data.frame(matrix (rnorm(N * num_normal ), N , num_normal ))
61- Y <- rbinom(N , 1 , plogis(.2 * X [, 1 ] + .1 * X [, 2 ] - .2 * X [, 3 ] + .1 * X [, 3 ]* X [, 4 ] - .2 * abs(X [, 4 ])))
62-
63- # Add some missing data to X
64- for (i in 1 : 10 ) X [sample(nrow(X ), 1 ), sample(ncol(X ), 1 )] <- NA
65-
66- # Add only a few missing Y values (< 10)
67- Y [c(4 ,6 ,7 )] <- NA
68-
69- # Verify we have exactly 3 missing Y values (<10)
70- missing_y_count <- sum(is.na(Y ))
71- expect_equal(missing_y_count , 3 , info = paste(" Expected 3 missing Y values, got" , missing_y_count ))
72-
73- # This should work because sum(deltat == 0) = 3 < 10, so cleanup code runs
74- # But this demonstrates the inconsistent behavior that should be fixed
75- expect_silent({
76- vim <- varimpact(Y = Y , data = X ,
77- V = 2L , # Use fewer folds for faster testing
78- Q.library = c(" SL.mean" , " SL.glm" ),
79- g.library = c(" SL.mean" , " SL.glm" ),
80- verbose = FALSE )
81- })
96+ N <- 100
97+ Yt <- rbinom(N , 1 , 0.5 )
98+ At <- as.factor(sample(c(" A" , " B" ), N , replace = TRUE ))
99+
100+ # Add exactly 10 missing Y values
101+ missing_indices <- sample(1 : N , 10 )
102+ Yt [missing_indices ] <- NA
103+
104+ # Test old behavior
105+ result <- simulate_old_behavior(Yt , At )
106+
107+ # With exactly 10 missing values, cleanup should NOT run (10 is not < 10)
108+ expect_false(result $ cleanup_ran , info = " Cleanup should not run with exactly 10 missing values" )
109+ expect_equal(result $ n_missing , 10 , info = " Missing values should remain in data" )
110+ expect_equal(length(result $ Yt ), N , info = " Data length should be unchanged" )
111+ })
112+
113+ test_that(" new behavior shows consistent handling regardless of missing value count" , {
114+ # Test with various numbers of missing values
115+ test_cases <- c(5 , 10 , 15 , 20 )
116+
117+ for (n_missing in test_cases ) {
118+ set.seed(1 , " L'Ecuyer-CMRG" )
119+ N <- 100
120+ Yt <- rbinom(N , 1 , 0.5 )
121+ At <- as.factor(sample(c(" A" , " B" ), N , replace = TRUE ))
122+
123+ # Add missing Y values
124+ missing_indices <- sample(1 : N , n_missing )
125+ Yt [missing_indices ] <- NA
126+
127+ # Test new behavior
128+ result <- simulate_new_behavior(Yt , At )
129+
130+ # New behavior should be consistent regardless of missing count
131+ expect_false(result $ cleanup_ran , info = paste(" No cleanup should run with" , n_missing , " missing values" ))
132+ expect_equal(result $ n_missing , n_missing , info = paste(" All" , n_missing , " missing values should remain" ))
133+ expect_equal(length(result $ Yt ), N , info = paste(" Data length should be unchanged with" , n_missing , " missing values" ))
134+ }
82135})
83136
84- test_that(" edge case: exactly 10 missing Y values " , {
85- # Test the exact boundary condition
137+ test_that(" fix eliminates the arbitrary 10- missing-value threshold " , {
138+ # Test that the fix removes the inconsistent behavior
86139 set.seed(1 , " L'Ecuyer-CMRG" )
87- N <- 200
88- num_normal <- 4
89- X <- as.data.frame(matrix (rnorm(N * num_normal ), N , num_normal ))
90- Y <- rbinom(N , 1 , plogis(.2 * X [, 1 ] + .1 * X [, 2 ] - .2 * X [, 3 ] + .1 * X [, 3 ]* X [, 4 ] - .2 * abs(X [, 4 ])))
91-
92- # Add some missing data to X
93- for (i in 1 : 10 ) X [sample(nrow(X ), 1 ), sample(ncol(X ), 1 )] <- NA
94-
95- # Add exactly 10 missing Y values - this should trigger the bug
96- # because sum(deltat == 0) = 10, which is NOT < 10
97- Y [c(4 ,6 ,7 ,8 ,11 ,15 ,20 ,21 ,28 ,32 )] <- NA
98-
99- # Verify we have exactly 10 missing Y values
100- missing_y_count <- sum(is.na(Y ))
101- expect_equal(missing_y_count , 10 , info = paste(" Expected 10 missing Y values, got" , missing_y_count ))
102-
103- # This should fail because sum(deltat == 0) = 10 is NOT < 10
104- # So cleanup code doesn't run, but TMLE expects clean data
105- expect_error({
106- vim <- varimpact(Y = Y , data = X ,
107- V = 2L , # Use fewer folds for faster testing
108- Q.library = c(" SL.mean" , " SL.glm" ),
109- g.library = c(" SL.mean" , " SL.glm" ),
110- verbose = FALSE )
111- }, info = " Should fail with exactly 10 missing Y values due to >= 10 condition" )
140+ N <- 100
141+
142+ # Test case 1: 9 missing values
143+ Yt1 <- rbinom(N , 1 , 0.5 )
144+ At1 <- as.factor(sample(c(" A" , " B" ), N , replace = TRUE ))
145+ Yt1 [sample(1 : N , 9 )] <- NA
146+
147+ # Test case 2: 11 missing values
148+ Yt2 <- rbinom(N , 1 , 0.5 )
149+ At2 <- as.factor(sample(c(" A" , " B" ), N , replace = TRUE ))
150+ Yt2 [sample(1 : N , 11 )] <- NA
151+
152+ # Old behavior would be different
153+ old_result1 <- simulate_old_behavior(Yt1 , At1 )
154+ old_result2 <- simulate_old_behavior(Yt2 , At2 )
155+
156+ expect_true(old_result1 $ cleanup_ran , info = " Old behavior: cleanup runs with 9 missing" )
157+ expect_false(old_result2 $ cleanup_ran , info = " Old behavior: cleanup doesn't run with 11 missing" )
158+
159+ # New behavior should be consistent
160+ new_result1 <- simulate_new_behavior(Yt1 , At1 )
161+ new_result2 <- simulate_new_behavior(Yt2 , At2 )
162+
163+ expect_equal(new_result1 $ cleanup_ran , new_result2 $ cleanup_ran ,
164+ info = " New behavior should be consistent regardless of missing count" )
165+ expect_false(new_result1 $ cleanup_ran , info = " New behavior: no cleanup with 9 missing" )
166+ expect_false(new_result2 $ cleanup_ran , info = " New behavior: no cleanup with 11 missing" )
112167})
0 commit comments