library(dplyr)
x <- c(10, 12, 11, 15, 14) # five values in order
print(c(NA, head(x, -1))) # -> [1] NA 10 12 11 15[1] NA 10 12 11 15
print(c(x[-1], NA)) # -> [1] 12 11 15 14 NA[1] 12 11 15 14 NA
lag(x) shifts a column down one row. Row 2 gets what was in row 1, row 3 gets what was in row 2, and row 1 gets NA because nothing came before it. lead(x) shifts the other way.
In this lesson I put x, lag(x), lag(x, 2) and lead(x) side by side in one small table so you can see the shift, then use lag() on the price file to give every row yesterday’s close.
Shifting down means dropping the last element and putting NA in front. Shifting up means dropping the first element and putting NA at the end. Both are one line of base R, using the head() and negative indexing from Lesson 5.
[1] NA 10 12 11 15
[1] 12 11 15 14 NA
lag() and lead() from dplyr do exactly that, and keep the length the same.
[1] NA 10 12 11 15
[1] 12 11 15 14 NA
Same numbers. From here I use the named function, because lag(Close) says what it means inside a mutate().
Put all four columns in one data frame and the alignment is on the page.
x lag1 lag2 lead1
1 10 NA NA 12
2 12 10 NA 11
3 11 12 10 15
4 15 11 12 14
5 14 15 11 NA
Read row 3. x is 11, lag1 is 12 which is row 2, lag2 is 10 which is row 1, and lead1 is 15 which is row 4. lag(x, 2) costs two NA rows at the top instead of one, and lead() puts its NA at the bottom.
default = says what to put in the row that has no neighbour.
[1] 0 10 12 11 15
[1] NA 2 -1 4 -1
[1] 10 2 -1 4 -1
Without a default, the first difference is NA, because NA propagates through arithmetic. With default = 0 the first row reports 10, which is 10 - 0. For a price change that is wrong: the stock did not rise by its whole price on day one. Leave it as NA unless zero is genuinely the right answer.
Now the same move on real columns. prices.csv holds simulated daily closes for three tickers over 40 business days. read.csv() gives Date as text, so I convert it with as.Date() and sort on it, since a lag is only meaningful once the rows are in date order.
I take one ticker. lag() runs straight down a column and does not know where AAA stops and CCC starts.
[1] 40
Date Ticker Close Volume
1 2026-01-02 AAA 186.66 2183494
2 2026-01-05 AAA 186.38 8463199
3 2026-01-06 AAA 188.51 5299573
Add yesterday’s close as a column, then subtract.
Date Close Close_prev Change
1 2026-01-02 186.66 NA NA
2 2026-01-05 186.38 186.66 -0.28
3 2026-01-06 188.51 186.38 2.13
4 2026-01-07 187.24 188.51 -1.27
5 2026-01-08 189.27 187.24 2.03
Two prices from different rows now sit on the same row, so ordinary column arithmetic can compare them. Row 3 holds both 188.51 and 186.38 and their difference of 2.13.
Divide instead of subtract and you have the return from Lesson 15.
Date Close Return
1 2026-01-02 186.66 NA
2 2026-01-05 186.38 -0.001500054
3 2026-01-06 188.51 0.011428265
4 2026-01-07 187.24 -0.006737043
5 2026-01-08 189.27 0.010841700
[1] 1
Lesson 8 built returns on a bare vector with diff(x) / head(x, -1), which returns 39 numbers from 40 prices. Stick an NA on the front of those 39 and you have the column above.
[1] NA -0.001500 0.011428 -0.006737
[1] TRUE
Same numbers. diff() shortens the vector, so it will not fit next to a 40-row table without padding. lag() keeps the length, which is why it is the version that works in a data frame.
Run the same lag() on all 120 rows sorted by ticker and then date, and look at the seam.
Date Ticker Close Close_prev
39 2026-02-25 AAA 171.42 172.64
40 2026-02-26 AAA 169.42 171.42
41 2026-01-02 CCC 416.98 169.42
42 2026-01-05 CCC 423.19 416.98
Row 41 is the first CCC day, and its “previous close” is 169.42, the last AAA price. lag() counts rows, not tickers. Filter to one ticker, or lag inside group_by(Ticker), which is Lesson 19.
Take CCC, sort by date, and add Close_next = lead(Close) and Gap = Close_next - Close. Print the first five rows and the last two. Which end holds the NA?
msft <- prices |>
filter(Ticker == "CCC") |> # one ticker
arrange(Date) |> # oldest first
mutate(
Close_next = lead(Close), # tomorrow's close on today's row
Gap = Close_next - Close # tomorrow minus today
)
print(head(msft[, c("Date", "Close", "Close_next", "Gap")], 5))
# -> Date Close Close_next Gap
# -> 1 2026-01-02 416.98 423.19 6.21
# -> 2 2026-01-05 423.19 423.95 0.76
# -> 3 2026-01-06 423.95 435.34 11.39
# -> 4 2026-01-07 435.34 432.41 -2.93
# -> 5 2026-01-08 432.41 437.84 5.43
print(tail(msft[, c("Date", "Close", "Close_next", "Gap")], 2))
# -> Date Close Close_next Gap
# -> 39 2026-02-25 435.51 435.94 0.43
# -> 40 2026-02-26 435.94 NA NAThe NA sits at the bottom. The last day has no tomorrow.
lag() is how a value known at the previous row becomes available on the current row. Lesson 18 uses it to run a cumulative product down a column and build an equity curve.