Quick summary
Summarize this blog with AI
Introduction
diff() subtracts an earlier observation from a later one and returns a shorter result. The lag argument chooses their position gap; differences repeats the subtraction operation. Neither argument automatically measures calendar time, and na.rm is not a missing-value removal option for the default method.
Tested with R 4.5.1. Run the following code blocks in order. All inputs are included.
Separate lag from repeated differences
Use a fixed sequence with visibly changing increments.
x <- c(10, 13, 19, 28)
print(diff(x)) # 3 6 9
print(diff(x, lag = 2)) # 9 15
print(diff(x, differences = 2)) # 3 3
stopifnot(identical(diff(x), c(3, 6, 9)))
stopifnot(identical(diff(x, lag = 2), c(9, 15)))
stopifnot(identical(diff(x, differences = 2), c(3, 3)))
With lag 2, each result subtracts the value two positions earlier. A second difference instead differences the first-difference sequence. These are not interchangeable seasonal adjustments.
Align differences to their ending row
A difference belongs to a pair of observations. Retain timestamps so the interval remains visible.
daily <- data.frame(day = as.Date(c("2026-01-01", "2026-01-02", "2026-01-04", "2026-01-05")),
value = x)
daily$change <- c(NA_real_, diff(daily$value))
daily$elapsed_days <- c(NA_integer_, as.integer(diff(daily$day)))
print(daily)
stopifnot(identical(daily$elapsed_days, c(NA_integer_, 1L, 2L, 1L)))
stopifnot(identical(daily$change, c(NA_real_, 3, 6, 9)))
The six-unit change ending January 4 spans two days. Dividing by elapsed time produces an average rate over that interval, not an observed value for the missing date. Sort and separate independent entities before differencing.
Leave unknown differences visible
Removing a missing observation connects neighbors that were not originally adjacent. That is a different calculation, not a harmless cleanup.
with_na <- c(10, NA_real_, 19, 28)
print(diff(with_na)) # NA NA 9
stopifnot(identical(diff(with_na), c(NA_real_, NA_real_, 9)))
stopifnot(identical(diff(with_na, na.rm = TRUE), diff(with_na)))
print(diff(with_na[!is.na(with_na)])) # 9 9: different adjacency
changes <- c(3, 0, -2, NA_real_)
direction <- ifelse(is.na(changes), NA_character_,
ifelse(changes > 0, "increase", ifelse(changes < 0, "decrease", "unchanged")))
stopifnot(identical(direction, c("increase", "unchanged", "decrease", NA_character_)))
The tested default method accepts extra arguments through ... but does not use na.rm to remove missing values. Use elementwise ifelse for a vector of direction labels; scalar if cannot classify several changes at once.
Test short vectors and matrix axes
For a numeric matrix, default diff works down rows within each column.
stopifnot(length(diff(numeric(0))) == 0, length(diff(10)) == 0)
stopifnot(length(diff(1:3, lag = 3)) == 0)
m <- cbind(a = c(1, 3, 6), b = c(10, 15, 21))
d <- diff(m)
print(d) # rows: 2 5; 3 6
stopifnot(identical(dim(d), c(2L, 2L)), all(d[, "a"] == c(2, 3)))
err <- tryCatch(diff(c("a", "b")), error = function(e) e)
stopifnot(inherits(err, "error"))
For a time series object, method dispatch also preserves relevant time-series structure. State the class you are using and verify the output’s index and shape. See lagging without changing row count for a different alignment operation.