rguides

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

ArgumentDefaultPurpose
dataInput data frame.
varyingNULLNames (or numeric indices) of wide-format columns that collapse into one variable in long form.
v.namesNULLNames 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.
ids1:NROW(data)Values used when creating a new idvar column going long to wide.
timesseq_along(varying[[1]])Values used for the new timevar column going wide to long.
dropNULLVariable names to drop before reshaping.
directionEither "wide" or "long". Mandatory unless you are reversing a prior reshape.
new.row.namesNULLOptional row names for the result.
sep"."Separator used to guess v.names and times from wide column names.
splitderived from sepAdvanced 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 example max.score), the auto-split will misbehave. Override sep or supply v.names and times explicitly.
  • Silent NA fills. Long data with missing combinations becomes NA in the wide frame with no warning. Always check the row count after reshaping.
  • Implicit grouping warning. Any variable not listed in v.names is treated as time-varying. If those columns actually vary within an idvar, reshape() raises a warning. Pass v.names explicitly to silence it and to make the reshape intent obvious.
  • Confusion with reshape2. reshape2::melt() and reshape2::dcast() are a different, now-superseded package. Use tidyr::pivot_longer() and pivot_wider() for new tidyverse-style code.

See also