0
$\begingroup$

In my work I've stumbled upon an interesting result when a classifier is applied to within-subjects data. My question is whether this is a known result, and if so, does it have a name? I can't find anything quite like it my searches.

Say we have some within-subjects data with multiple variables. Participants completed both phases of a manipulation (two conditions, A/B, test/control, etc), and we have measured at least two variables during both phases of the manipulation. So we have their scores for, say, both Test 1 and Test 2, during both the test and control conditions. Now, we have some dependent variable of interest. Our question is, is there relationship between how participant's test responses changed during the manipulation and the dependent variable? This is frequently operationalized as a difference score - e.g., cor(test1_visit1 - test1_visit2,outcome).

Now, the interesting result is as follows: instead of calculating these difference scores, let's instead train a classifier to distinguish the two conditions of the manipulation, using the responses to both Test 1 and Test 2. We will of course create test and train splits. We now have a probability for each the two conditions, for each participant. We then create a difference score using the probabilities for each of the two conditions. It turns out, that this difference between the condition probabilities will also be correlated with our dependent variable, almost always more strongly than either of the individual difference scores. Yes, there is some 'ML' happening, but it is completely without reference to our outcome variable of interest. In some ways it feels like a weighted factor analysis, but in a way I've never encountered before. Is this a known analytic approach that I've just never heard of before?

Some example code from a simulation I've been working on. All variables are multivariate normal, and the classifier is just a logistic regression. I've systematically varied all the parameters across several million simulations, and the result holds 99.8% of the time, as long as the classifier performs reasonably well (auc > 0.6).

library(dplyr)
library(faux) 
library(tidyr) 

##################

sim_dat = function(train_n,
                          test_n,
                          var.num,  
                          main.effect.condition ,
                          within.obs.correl, 
                          total.var ,
                          total_n ){
  
  ##################################

  #### generate within-subjects correlation matrix
  var.num2 = var.num*2
  means = rep(main.effect.condition,var.num)
  mean.vec = matrix(c(rep(0,var.num),means),nrow = 2,byrow = T) %>%  
  c()
  
  cor.vec = rep(0, (var.num2^2-var.num2)/2)
  
  index = 1
  nextindex = var.num2-1
  while(nextindex > 0){
    index = c(index,max(index)+nextindex) 
    nextindex = nextindex - 1
  }
  
  index = index[(seq_len(length(index)) %% 2 ) == 1]
  cor.vec[index] =  rep(within.obs.correl,var.num)
  
  #### simulate data
  dat <- faux::rnorm_multi(
    n = total_n, 
    vars = var.num2, 
    r =  cor.vec, 
    mu = mean.vec, 
    sd = rep(1,var.num2), 
    varnames = letters[c(1:var.num2)]
  ) 
  
  ### make difference scores
  
  make_diff = function(dat,var.num2){
    
    odd_col =  seq_len(var.num2) %% 2 
    odd_col = c(1:var.num2)[odd_col == 1]
    
    diff.mat = matrix(data = NA,nrow =total_n,var.num2/2 ) %>% 
      as.data.frame()
    
    for(i  in 1:length(odd_col)){
      diff.mat[,i] =  dat[,odd_col[i]+1] - dat[,odd_col[i]]
    }
    return(diff.mat)
  }
  
  diff.mat = make_diff(dat,var.num2)
  
  #### simulate dependent variable
  yeffect = sqrt(total.var/var.num)
  betas = rep(yeffect,var.num)
  
  M <- t(as.matrix(betas)) * diff.mat[1:length(betas)]
  
  var.epsilon <- sum(cov(M))*((1 - total.var)/total.var)
  
  epsilon <- rnorm(total_n, sd=sqrt(var.epsilon))
  
  diff.mat$y <-  apply(cbind(M,epsilon),1,sum)
  summary(lm(y ~., data = diff.mat))
  
  diff.mat$ID = c(1:total_n)
  
#### transform data with both conditions to long-format
  make_long = function(tempdat, var.num){
    var.num2 = var.num * 2
    odd_col =  seq_len(var.num2) %% 2 
    odd_col = c(1:var.num2)[odd_col == 1]
    
    con0 = tempdat[,odd_col] %>% as.data.frame()
    con1 = tempdat[,-odd_col] %>% as.data.frame()
    
    colnames(con0) = paste("Var",c(1:var.num),sep="")
    colnames(con1) = paste("Var",c(1:var.num),sep="")
    
    con0$con = 0
con1$con = 1
    
    con0$ID = c(1:total_n)
con1$ID = c(1:total_n)
    
    tempdat_out = rbind(con0,con1)
    return(tempdat_out)
  }
  dat2 = make_long(dat, var.num)

  #############################
  #### simple classifier model
  
  IDs = unique(dat2$ID)
  g1 = sample(IDs,size = train_n, replace = FALSE)
  
  test_pool = IDs[!IDs %in% g1]
  g2 = sample(test_pool,size = test_n, replace = FALSE)
  
  train = dat2[dat2$ID  %in%  g1 ,]
  
  test = dat2[dat2$ID  %in%  g2 ,]

  standardize = function(tempdat){
    tempdat[,grep("Var",colnames(tempdat))] = 
      tempdat[, grep("Var", colnames(tempdat))] %>% 
      apply(2,function(X){
        X = scale(X, center = TRUE, scale=TRUE)
        attributes(X) = NULL
        return(X)
        
      })
    return(tempdat)
  }
  
  train = standardize(train)
  test = standardize(test)

  log_mod = glm(con ~., data = train[,-grep("ID", colnames(train))])
    
  test$pred = predict(log_mod, test, type="response")

  return_diff_sig = function(test_temp){
    
    test0 = test_temp %>% dplyr::filter(con == 0) %>% 
     dplyr::select(c("ID","pred"))
    test1 = test_temp %>% dplyr::filter(con == 1) %>% 
     dplyr::select(c("ID","pred"))
    
    colnames(test0)[2] = "pred0"
    colnames(test1)[2] = "pred1"
    
    testdiff = merge(test1,test0)
    testdiff$diff = testdiff$pred1 - testdiff$pred0
    return(testdiff)
  }
  
  testdiff = return_diff_sig(test)

  testdiff = merge(testdiff,diff.mat)

## return data
    
  out = list()
  
  out$orig = test
  out$differencescores = testdiff
  
  return(out)
}   

Generate the simulated data:

dat = sim_dat(train_n = 1000, #training sample size
test_n = 1000, # test sample size
total_n = 10000, # total population size
var.num = 2, # number of independent tests measured
main.effect.condition = 1, # mean difference in test scores between 
                           # conditions (Cohen's D), same for all tests
within.obs.correl = .8, 
    # correlation between test scores for a given test, across the two 
    # conditions, same for all tests
total.var = .2 # total variance of the outcome variable explained by 
               # the test difference scores
)

Both tests show an effect of the manipulation in the simulation

a1 = dat$orig %>% tidyr::pivot_longer(cols = 
       dplyr::starts_with("Var"), names_to = "Variable")

ggplot(a1,aes(x = Variable, y = value, fill = as.factor(con))) +
  scale_x_discrete(labels = c("Test 1", "Test 2")) +
  scale_fill_discrete(labels = c("Control","Manipulation")) +
  geom_boxplot() +
  theme_classic() +
  labs(fill = "Condition") +
  ylab(label = "Test score") + xlab(label = "")

Simulation results

Both difference scores are correlated with the outcome variable of interest (by design).

dat$differencescores[,-c(1:4)] %>% cor()
           V1         V2         y
V1 1.00000000 0.02977112 0.2948523
V2 0.02977112 1.00000000 0.3871282
y  0.29485230 0.38712816 1.0000000

Interestingly, if we compute a difference score from the predicted condition probabilities, it is also correlated with our outcome variable, even more strongly than either of the individual difference scores.

cor(dat$differencescores$y,dat$differencescores$diff)
[1] 0.4749716
$\endgroup$
4
  • 1
    $\begingroup$ As I understand it, you've taken the raw test scores, transformed them into a probability space, and found some correlations improved. It's not clear to me why that transformation would inherently be "better" than any other - depending on the data you might need a logistic, or logarithmic, or square, or any number of other transforms. Might there be some type of "match" between your probability estimator and underlying data? Does this work with other probability models or data distributions? $\endgroup$ Commented Nov 8 at 17:29
  • $\begingroup$ I don't know if I fully understand your comment. Yes, the data are transformed into a probability space (one in which all the variables are collapsed/combined). In practice, this works on a data set of biological variables I have that are all approximately normally distributed. In that data, correlations with the transformed/combined variable are all universally stronger that the correlations with each of the individual difference scores, for a wide set of outcome variables. I also explored other classification algorithms (LDA, SVM, RF), with similar results. $\endgroup$
    – David B
    Commented Nov 8 at 18:09
  • 1
    $\begingroup$ Basically, I'm a bit surprised this probability transformation improved the correlation and wonder if this is generalizable to other data. There are presumably many other transformations which would not have improved the correlation, although you could imagine datasets where those transforms might work and the one used here wouldn't. I'm not sure this transform universally improves correlation, or if it only works for certain types of underlying data-generating functions. $\endgroup$ Commented Nov 8 at 18:56
  • $\begingroup$ I am also surprised. But it only works when multiple independent variables are used to train the classifier. Simply performing a probability transformation on a single variable has no effect, as you would expect. Which is why I'm trying to figure out if this is something that others have already looked at. The fact that it works with a logistic regression classifier trained on multivariate normal data suggests some degree of generalizability, at least in my mind. $\endgroup$
    – David B
    Commented Nov 8 at 19:25

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.