How to mean center variables based on binary condition in r

Viewed 52

I have a dataframe ("md") containing several variables, of which one is binary ("adopter"). I would like to mean center three of the other (continous) variables, let's say X, Y, and Z, but only for the ones where adopter = 1. The others, for which adopter = 0, should remain unchanged. In the end I would like to end up with a new dataframe containing all variables as before, but with the X, Y, and Z for which adopter = 1 being mean centered, while leaving the X, Y, and Z for which adopter = 0 being unchanged.

My dataframe looks like this (117 observations in total):

adopter X Y Z A B
0 0.5 2.3 4.5 3 4.7
1 1.5 6.5 -2.3 69.3 -2.5
... ... ... ...

So the new dataframe should contain the center means of X, Y, and Z of the second row in this example, as adopter=1, and leave the rest unchanged.

I know how to mean center all X, Y, and Z:

md_cen <- md

covs_to_center <- c("X", "Y", "Z")
md_cen[covs_to_center] <- scale(md_cen[covs_to_center], 
                                scale = FALSE)

But I cannot figure out how to get the "only if adopter == "1" " into it. I also tried applying a function:

center_apply <- function(x) {
  apply(x, 2, function(y) y - mean(y))}

However, this leaves me again with the mean centered versions for all X, Y, Z, of course, and on top the new dataset contains only those three variables.

Can anyone help me out here, please?

1 Answers

The basic way to accomplish what you're trying to do is to use the split-apply-combine workflow. That is:

  1. Split your data frame up into coherent and useful sub-parts.
  2. Do the thing you want to each sub-part.
  3. Reconstitute the parts into the whole.

First, here's a toy dataset:

covs_to_center <- c("X", "Y", "Z")

set.seed(123)

md <- data.frame(
  adopter = sample(0:1, 10, replace = T),
  X = rnorm(10, 2, 1),
  Y = rnorm(10, 3, 2),
  Z = rnorm(10, 5, 10),
  A = rnorm(10, 40, 50),
  B = rnorm(10, 0, 2)
)

md

##    adopter         X          Y          Z          A           B
## 1        0 3.7150650  6.5738263 -11.866933  74.432013 -2.24621717
## 2        0 2.4609162  3.9957010  13.377870  67.695883 -0.80576967
## 3        0 0.7349388 -0.9332343   6.533731  36.904414 -0.93331071
## 4        1 1.3131471  4.4027118  -6.381369  24.701867  1.55993024
## 5        0 1.5543380  2.0544172  17.538149  20.976450 -0.16673813
## 6        1 3.2240818  0.8643526   9.264642   5.264651  0.50663703
## 7        1 2.3598138  2.5640502   2.049285  29.604136 -0.05709351
## 8        1 2.4007715  0.9479911  13.951257 -23.269818 -0.08574091
## 9        0 2.1106827  1.5422175  13.781335 148.447798  2.73720457
## 10       0 1.4441589  1.7499215  13.215811 100.398100 -0.45154197

A base R solution:

md_base <- data.frame(row_num = 1:nrow(md), md)
  # append column of row numbers to make it easier to recombine things later

md_split <- split(md_base, md_base$adopter)
  # this is a list of 2 data frames, corresponding to the 2 possible outcomes
  # of the adopter variable

md_split$`1`[, covs_to_center] <-
  apply(md_split$`1`[, covs_to_center], 2, function(y) y - mean(y))
  # grab the data frame that had a 1 in the response column; apply the centering
  # function to the correct variables in that data frame

md_new <- do.call(rbind, md_split)
  # glue the data frame back together; it will be ordered by adopter

rownames(md_new) <- NULL
  # remove row name artifact created by joining

md_new <- md_new[order(md_new$row_num), names(md_new) != "row_num"]
  # sort by the row_num column, then drop it

This is pretty clunky, and I'm sure it could be improved. Here's a tidyverse equivalent that produces the same output:

library(tidyverse)

md %>%
  group_by(adopter) %>%
  mutate(across(covs_to_center, function(y) y - adopter * mean(y))) %>%
  ungroup()

The idea behind this is: group by adopter (much like the split() approach), calculate the mean() of the relevant variables within each group, and then subtract the mean of the subgroup multiplied by the adopter variable (meaning when adopter == 0, nothing will be subtracted).

Related