Divide whole dataframe by mean of control group for each of several sub-groups

Viewed 99

Starting data

I'm working in R and I have a set of data generated from groups (cohorts) of animals treated with different doses of different drugs. A simplified reproducible example of my dataset follows:

# set starting values for simulation of animal cohorts across doses of various drugs with a few numeric endpoints
cohort_size <- 3
animals <- letters[1:cohort_size]
drugs <- factor(c("A", "B", "C"))
doses <- factor(c(0, 10, 100))
total_size <- cohort_size * length(drugs) * length(doses)

# simulate data based on above parameters
df <- cbind(expand.grid(drug = drugs, dose = doses, animal = animals),
            data.frame(
              other_metadata = sample(LETTERS[24:26], size = total_size, replace = TRUE),
              num1 = rnorm(total_size, mean = 10, sd = 3), 
              num2 = rnorm(total_size, mean = 60, sd = 9),
              num3 = runif(total_size, min = 1, max = 5)))

This produces something like:

##   drug dose animal other_metadata      num1     num2     num3
## 1    A    0      a              X  6.448411 54.49473 4.111368
## 2    B    0      a              Y  9.439396 67.39118 4.917354
## 3    C    0      a              Y  8.519773 67.11086 3.969524
## 4    A   10      a              Z  6.286326 69.25982 2.194252
## 5    B   10      a              Y 12.428265 70.32093 1.679301
## 6    C   10      a              X 13.278707 68.37053 1.746217

My goal

For each drug treatment, I consider the dose == 0 animals as my control group for that drug (let's say each was run at a different time and has it's own control group). I wish to calculate the mean for each numeric endpoint (columns 5:7 in this example) of the control group. Next I want to normalize (divide) every numeric endpoint (columns 5:7) for every animal by the mean of it's respective control group.

In other words num1 for all animals where drug == "A" should be divided by the mean of num1 for all animals where drug == "A" AND dose == 0 and so on for each endpoint.

The final output should be the same size as the original data.frame with all of the non-numeric metadata columns remaining unchanged on the left side and all the numeric data columns now with the normalized values.

Naturally I'd like to find the simplest solution possible - minimizing creation of new variables and ideally in a single dplyr pipeline if possible.

What I've tried so far

I should say that I have technically solved this but the solution is super ugly with a ton of steps so I'm hoping to get help to find a more elegant solution.

I know I can easily get the averages for the control groups into a new data.frame using:

df %>% 
  filter(dose == 0) %>%
  group_by(drug, dose) %>%
  summarise_all(mean) 

I've looked into several things but can't figure out how to implement them. In order of what seems most promising to me:

  1. dplyr::group_modify()
  2. dplyr::rowwise()
  3. sweep() in some type of loop

Thanks in advance for any help you can offer!

1 Answers

If the intention is to divide the numeric columns by the mean of the control group values, grouped by 'drug', after grouping by 'drug', use mutate with across (from dplyr 1.0.0), divide the column values (. with mean of the values where the 'dose' is 0

library(dplyr) # 1.0.0
df %>% 
   group_by(drug) %>% 
   mutate(across(where(is.numeric), ~ ./mean(.[dose == 0])))

If we have a dplyr version is < 1.0.0, use mutate_if

df %>%
    group_by(drug) %>%
    mutate_if(is.numeric, ~ ./mean(.[dose == 0]))
Related