reshape()
reshape(data, varying = NULL, v.names = NULL, timevar = "time", idvar = "id", ids = 1:NROW(data), times = seq_along(varying[[1]]), drop = NULL, direction, new.row.names = NULL, sep = ".", split = if (sep == "") { list(regexp = "[A-Za-z][0-9]", include = TRUE) } else { list(regexp = sep, include = FALSE, fixed = TRUE) }) What reshape() does
reshape() is a base R function in the stats package that moves a data frame between wide and long layouts. In a wide frame each row is a unit (a subject, a sample, a product) and repeated measurements sit in separate columns (x.1, x.2, x.3). In a long frame each row is one measurement, with a column that says which unit it belongs to and a column that says which time point it came from. Most longitudinal and repeated-measures work in R ends up needing one of these conversions at some point.
The function has shipped with R for decades and is not deprecated, but for new code the tidyverse equivalents tidyr::pivot_longer() and tidyr::pivot_wider() are usually a better fit. reshape() still earns its place when you want a self-contained base R solution, or when you need the round-trip behaviour described below.
A one-line preview using R’s built-in Indometh pharmacokinetic dataset: this call turns the long-form frame into a wide one with one column per time point.
reshape(Indometh, direction = "wide",
idvar = "Subject", timevar = "time")
The rest of this article walks through every argument and direction in detail.
Arguments
| Argument | Default | Purpose |
|---|---|---|
data | — | Input data frame. |
varying | NULL | Names (or numeric indices) of wide-format columns that collapse into one variable in long form. |
v.names | NULL | Names of the long-format variables that come from varying. If NULL, reshape() guesses them from the wide names using sep. |
timevar | "time" | Name of the column that records which measurement (time point, condition) each long row corresponds to. |
idvar | "id" | Name(s) of the column(s) identifying each unit. Pass a vector for hierarchical IDs. |
ids | 1:NROW(data) | Values used when creating a new idvar column going long to wide. |
times | seq_along(varying[[1]]) | Values used for the new timevar column going wide to long. |
drop | NULL | Variable names to drop before reshaping. |
direction | — | Either "wide" or "long". Mandatory unless you are reversing a prior reshape. |
new.row.names | NULL | Optional row names for the result. |
sep | "." | Separator used to guess v.names and times from wide column names. |
split | derived from sep | Advanced control over the splitting rule. Rarely needed. |
Reshape wide to long
When the wide columns already encode variable and time point through a separator, reshape() does the splitting for you. With sep = "" the rule is “split before the first digit after a letter,” so dose1, dose2, dose4 become v.names = "dose" and times = c(1, 2, 4):
df3 <- data.frame(
id = 1:4,
age = c(40, 50, 60, 50),
dose1 = c(1, 2, 1, 2),
dose2 = c(2, 1, 2, 1),
dose4 = c(3, 3, 3, 3)
)
reshape(df3, direction = "long", varying = 3:5, sep = "")
# id age dose time
# 1.1 1 40 1 1
# 1.2 1 40 2 2
# 1.4 1 40 3 4
# 2.1 2 50 2 1
# 2.2 2 50 1 2
# 2.4 2 50 3 4
# 3.1 3 60 1 1
# 3.2 3 60 2 2
# 3.4 3 60 3 4
# 4.1 4 50 2 1
# 4.2 4 50 1 2
# 4.4 4 50 3 4
If you would rather spell things out, pass v.names and times directly. Supplying both arguments explicitly turns off the name-splitting logic entirely, which is handy when your wide column names do not follow the var.time pattern, or when you want the same call to keep working if the source data grows new columns later. The result is identical to the auto-split version above:
reshape(df3, direction = "long",
varying = c("dose1", "dose2", "dose4"),
v.names = "dose", times = c(1, 2, 4),
idvar = "id")
Reshape long to wide
The built-in Indometh dataset is pharmacokinetic data in long form, with one row per subject per time point. Going wide puts every time point into its own column:
reshape(Indometh, direction = "wide",
idvar = "Subject", timevar = "time",
v.names = "conc", sep = "_")
# Subject conc_0.25 conc_0.5 conc_1 conc_2 conc_4 conc_8 conc_12
# 1 1 1.50 0.94 0.78 0.49 0.30 0.24 0.20
# 2 2 2.03 1.63 0.71 0.40 0.36 0.32 0.20
# 3 3 2.72 1.49 0.70 0.51 0.37 0.36 0.32
# ... (one row per Subject)
If your timevar already uses non-numeric labels such as "Before" and "After", that is fine. times accepts character vectors and treats them like factor levels, which makes pre/post or condition-style measurements straightforward. The wide output keeps your original labels in the column names:
df_b <- data.frame(id = rep(1:3, each = 2),
visit = rep(c("Before", "After"), 3),
score = c(80, 88, 75, 91, 82, 79))
reshape(df_b, idvar = "id", timevar = "visit",
v.names = "score", direction = "wide")
# id score.Before score.After
# 1 1 80 88
# 2 2 75 91
# 3 3 82 79
The result has one column per label, exactly as if those labels had been numeric time points. Use sep to control the join character between variable name and time label.
Round-tripping
reshape() attaches a reshapeWide or reshapeLong attribute to the result, so reversing the operation needs no arguments at all:
wide <- reshape(Indometh, direction = "wide",
idvar = "Subject", timevar = "time",
v.names = "conc", sep = "_")
reshape(wide) # back to long, identical to the input
This only works on frames that reshape() itself produced. If you strip the attributes manually (for example by re-wrapping with data.frame() or by piping through another tibble coercion), the bare reshape() call no longer knows which direction to use and you will have to pass direction again.
Common mistakes
- Forgetting
direction. It is the single most common error. Without it,reshape()stops with a missing-argument error. - Wrong column order in
varying. When you pass a vector with multiple time-varying variables,reshape()fills a matrix by column:c("x1", "y1", "x2", "y2")means “x at time 1, y at time 1, x at time 2, y at time 2.” Pass a list or matrix if you want a different order. - The default
sep = "."surprises people. If your wide column names contain a dot for some other reason (for examplemax.score), the auto-split will misbehave. Overridesepor supplyv.namesandtimesexplicitly. - Silent
NAfills. Long data with missing combinations becomesNAin the wide frame with no warning. Always check the row count after reshaping. - Implicit grouping warning. Any variable not listed in
v.namesis treated as time-varying. If those columns actually vary within anidvar,reshape()raises a warning. Passv.namesexplicitly to silence it and to make the reshape intent obvious. - Confusion with
reshape2.reshape2::melt()andreshape2::dcast()are a different, now-superseded package. Usetidyr::pivot_longer()andpivot_wider()for new tidyverse-style code.
See also
tidyr::pivot_longer()— modern tidyverse alternative for wide to long.tidyr::pivot_wider()— modern tidyverse alternative for long to wide.- How to convert wide to long in R — a worked example using base R.