Quick summary
Summarize this blog with AI
Standardize numeric columns with scale
With its defaults, scale() subtracts each column mean and divides by its sample standard deviation. It returns a matrix, even for a numeric data frame. This tutorial shows the transformation, missing-value behavior and how to reuse training statistics on new observations. All blocks run in order with base R 4.5.1.
Inspect the center and scale
train <- data.frame(height = c(160, 170, 180), weight = c(50, 60, 70))
z_train <- scale(train)
print(z_train)
# Each column: -1, 0, 1
centers <- attr(z_train, "scaled:center")
spreads <- attr(z_train, "scaled:scale")
print(centers) # height 170; weight 60
print(spreads) # height 10; weight 10
stopifnot(all(z_train[, 1] == c(-1, 0, 1)))
stopifnot(all(z_train[, 2] == c(-1, 0, 1)))
Use scale(train, scale = FALSE) to center only. With center = FALSE, scale = TRUE, the divisor is a root-mean-square quantity with denominator n - 1, not generally the sample standard deviation. If a method requires a different transformation, supply the explicit numeric divisors.
Preserve missing values
incomplete <- data.frame(a = c(1, NA, 3), b = c(10, 20, 30))
z_incomplete <- scale(incomplete)
print(round(z_incomplete, 6))
# a: -0.707107, NA, 0.707107; b: -1, 0, 1
stopifnot(is.na(z_incomplete[2, "a"]))
stopifnot(isTRUE(all.equal(attr(z_incomplete, "scaled:center"),
c(a = 2, b = 20))))
The default method omits missing observations when estimating each column center and divisor, while leaving their transformed positions missing. It does not accept a na.rm argument and it does not impute missing data. A downstream model may still reject the missing values. Decide the missing-data policy separately and fit any imputation using training data only.
Apply the training transformation to new rows
new_rows <- data.frame(height = c(175, 190), weight = c(65, 80))
stopifnot(identical(names(new_rows), names(train)))
z_new <- scale(new_rows, center = centers, scale = spreads)
print(z_new)
# Each column: 0.5, 2.0
stopifnot(all(z_new[, 1] == c(0.5, 2)))
# The same statistics work for separate batches.
batches <- lapply(seq_len(nrow(new_rows)), function(i) {
scale(new_rows[i, , drop = FALSE], center = centers, scale = spreads)
})
stopifnot(isTRUE(all.equal(unname(do.call(rbind, batches)),
unname(z_new), check.attributes = FALSE)))
Independently calling scale() on each batch estimates a different transformation for every batch. The stored centers and spreads keep batches comparable. A training/test split has the same requirement: do not let test observations determine preprocessing parameters. Keep the same predictor names and order; a numeric vector of centers is applied positionally.
Reject unusable columns explicitly
candidate <- data.frame(varying = c(1, 2, 3),
constant = c(7, 7, 7),
absent = c(NA_real_, NA_real_, NA_real_))
candidate_sd <- vapply(candidate, sd, numeric(1), na.rm = TRUE)
usable <- is.finite(candidate_sd) & candidate_sd > 0
print(usable) # TRUE FALSE FALSE
stopifnot(identical(unname(usable), c(TRUE, FALSE, FALSE)))
stopifnot(all(is.nan(scale(candidate["constant"]))))
A constant column cannot be standardized by its zero standard deviation. An all-missing column, or one with only one observed value, also lacks a usable sample standard deviation. Investigate these columns and explicitly choose removal or another model-specific treatment. Do not replace their transformed values with zero merely to suppress an error.
Reverse the transformation when needed
recovered <- sweep(sweep(z_train, 2, spreads, "*"), 2, centers, "+")
stopifnot(isTRUE(all.equal(unname(recovered), unname(as.matrix(train)),
check.attributes = FALSE)))
Scaling does not remove outliers or make a distribution normal. It changes the units. Select only intended numeric predictors—never a numeric identifier or a target column just because it has numeric storage. For an example that uses this preprocessing in a classifier, see nearest-neighbor classification in R.