How to group-center / standardize variables in R?

Functions I'm familiar with include scale from base R, rescale from ARM.

Perhaps the best way would be to use some variant of apply, specifying one or more variables to use as grouping variables.

• Try: aggregate(state.x77, list(Region = state.region, Cold = state.x77[,"Frost"] > 130)), function(x) ((x - mean(x))/sd(x))) Sep 8 '11 at 0:18

Here is a possible plyr solution. Note that it relies on the base transform() function.

my.df <- data.frame(x=rnorm(100, mean=10),
sex=sample(c("M","F"), 100, rep=T),
group=gl(5, 20, labels=LETTERS[1:5]))
library(plyr)
ddply(my.df, c("sex", "group"), transform, x.std = scale(x))

(We can check whether it works as expected with e.g., with(subset(my.df, sex=="F" & group=="A"), scale(x)))

Basically, the 2nd argument describes how to "split" the data, the 3rd argument what function to apply to each chunk. The above will append a variable x.std to the data.frame. Use x if you want to replace your original variable by the scaled one.

group.center <- function(var,grp) {
return(var-tapply(var,grp,mean,na.rm=T)[grp])
}

Here is a data.table solution. It is definitely faster than plyr (relevant only for large data sets). Maybe later I'll do up a dplyr example.

# generate example data
raw.data <- data.frame( outcome = c(rnorm(500, 100, 15), rnorm(500, 110, 12)),
group = c(rep("a", 500), rep("b", 500)))

library(data.table)
# convert dataframe to data.table
raw.data <- data.table(raw.data, key = "group")

# create group standardized outcome variable
raw.data[ , group_std_outcome := (outcome - mean(outcome, na.rm = TRUE)) /
sd(outcome, na.rm = TRUE), "group"]

(Yes, I rediscovered a question I asked years ago when I was an R noob ;)

You can use (among others) tapply for this (the plyr package contains lots of other options that may be better suited for your specific situation):

tapply(variabletoscale, list(groupvar1, groupvar2), scale)
• With two factors, it won't return a data.frame. You'll need to post-process the result for that.
– chl
Sep 8 '11 at 9:38

This answer is from a white paper by Mahmood Arai. It has the convenient side effect of labeling the centered results with the prefix "C.":

gcenter <- function(df1,group) {
variables <- paste(
rep("C", ncol(df1)),  colnames(df1), sep=".")
copydf <- df1
for (i in 1:ncol(df1)) {
copydf[,i] <- df1[,i] - ave(df1[,i], group, FUN=mean)}
colnames(copydf) <- variables
return(cbind(df1,copydf))}

Here is an updated implementation using dplyr from tidyverse.

library(tidyverse)

my.df <- data.frame(x=rnorm(100, mean=10), sex=sample(c("M","F"), 100, rep=T))
my.df <- group_by(my.df, sex) %>% mutate(x.sd = as.numeric(scale(x)))