Some everyday data tasks: a few hints with R

R
R-bloggers
Data management
Author

Andrea Onofri

Published

August 14, 2026

It is important to know how to reshape a dataframe into a form that might be more suitable for the analyses we intend to perform. For me, it is also important to know how to do this with base R, without using any other packages. Yes, I know that other packages, such as dplyr and tidyr, are much better… Still, having the skills to perform most of our everyday tasks with base R may be a good idea. In particular, there are at least four routine tasks that we need to be able to perform:

  1. subsetting
  2. sorting
  3. casting
  4. melting

Subsetting the data

Subsetting means selecting the records (rows) or the variables (columns) that satisfy certain criteria. In base R, we can use the subset() function.

Let’s consider the students dataset, which is available in the statforbiology package. It represents a collection of exams taken by students at my university in different subjects. Let’s load it using the getAgroData() function from the statforbiology package.

library(statforbiology)
students <- getAgroData("students")
head(students)
##   Id  Subject       Date Mark Year  HighSchool
## 1  1 AGRONOMY 10/06/2002   30 2001  HUMANITIES
## 2  2 AGRONOMY 08/07/2002   24 2001 AGRICULTURE
## 3  3 AGRONOMY 24/06/2002   30 2001 AGRICULTURE
## 4  4 AGRONOMY 24/06/2002   26 2001  HUMANITIES
## 5  5 AGRONOMY 23/01/2003   30 2001  HUMANITIES
## 6  6 AGRONOMY 09/09/2002   28 2001 AGRICULTURE

Let’s say that we want a new dataset that contains only the records where the mark was equal to or above 28 (please note that, in Italy, an exam is passed with a minimum mark of 18, while the maximum mark is 30).

subData <- subset(students, Mark >= 28)
head(subData)
##    Id  Subject       Date Mark Year  HighSchool
## 1   1 AGRONOMY 10/06/2002   30 2001  HUMANITIES
## 3   3 AGRONOMY 24/06/2002   30 2001 AGRICULTURE
## 5   5 AGRONOMY 23/01/2003   30 2001  HUMANITIES
## 6   6 AGRONOMY 09/09/2002   28 2001 AGRICULTURE
## 11 11 AGRONOMY 09/09/2002   28 2001  SCIENTIFIC
## 17 17 AGRONOMY 10/06/2002   30 2001  HUMANITIES

Let’s make it more difficult and extract the records where the mark ranges from 26 to 28 (margins included). Look at the AND clause, which is expressed by using the & operator:

subData <- subset(students, Mark <= 28 & Mark >= 26)
head(subData)
##    Id  Subject       Date Mark Year  HighSchool
## 4   4 AGRONOMY 24/06/2002   26 2001  HUMANITIES
## 6   6 AGRONOMY 09/09/2002   28 2001 AGRICULTURE
## 7   7 AGRONOMY 24/02/2003   26 2001  HUMANITIES
## 8   8 AGRONOMY 09/09/2002   26 2001  SCIENTIFIC
## 10 10 AGRONOMY 08/07/2002   27 2001  HUMANITIES
## 11 11 AGRONOMY 09/09/2002   28 2001  SCIENTIFIC

Now we are interested in those students who got a mark ranging from 26 to 28 in MATHS (please note the equality operator, written as ==):

subData <- subset(students, Mark <= 28 & Mark >= 26 & 
                    Subject == "MATHS")
head(subData)
##      Id Subject       Date Mark Year  HighSchool
## 115 115   MATHS 15/07/2002   26 2001 AGRICULTURE
## 124 124   MATHS 16/09/2002   26 2001  SCIENTIFIC
## 138 138   MATHS 04/02/2002   27 2001  HUMANITIES
## 144 144   MATHS 10/02/2003   27 2001  HUMANITIES
## 145 145   MATHS 04/07/2003   27 2002  HUMANITIES
## 146 146   MATHS 28/02/2002   28 2001 AGRICULTURE

Let’s look for good students who got a mark ranging from 26 to 28 either in MATHS or in CHEMISTRY (OR clause; note the | operator):

subData <- subset(students, Mark <= 28 & Mark >= 26 & 
                    (Subject == "MATHS" | 
                     Subject == "CHEMISTRY"))
head(subData)
##    Id   Subject       Date Mark Year   HighSchool
## 68 68 CHEMISTRY 04/06/2002   28 2001 OTHER SCHOOL
## 70 70 CHEMISTRY 04/06/2002   26 2001   ACCOUNTING
## 71 71 CHEMISTRY 04/06/2002   27 2001  AGRICULTURE
## 72 72 CHEMISTRY 23/01/2003   27 2001   SCIENTIFIC
## 75 75 CHEMISTRY 10/07/2002   27 2001  AGRICULTURE
## 81 81 CHEMISTRY 23/01/2003   28 2001  AGRICULTURE

We can also select columns; for example, we may want to display only the Subject, Mark, and HighSchool columns:

subData <- subset(students, Mark <= 28 & Mark >= 26 & 
                    (Subject == "MATHS" | 
                     Subject == "CHEMISTRY"),
                  select = c(Subject, Mark, HighSchool))
head(subData)
##      Subject Mark   HighSchool
## 68 CHEMISTRY   28 OTHER SCHOOL
## 70 CHEMISTRY   26   ACCOUNTING
## 71 CHEMISTRY   27  AGRICULTURE
## 72 CHEMISTRY   27   SCIENTIFIC
## 75 CHEMISTRY   27  AGRICULTURE
## 81 CHEMISTRY   28  AGRICULTURE

We can also drop unwanted columns:

subData <- subset(students, Mark <= 28 & Mark >= 26 & 
                    (Subject == "MATHS" | 
                     Subject == "CHEMISTRY"),
                  select = c(-Id, 
                             -Date,
                             -Year))
head(subData)
##      Subject Mark   HighSchool
## 68 CHEMISTRY   28 OTHER SCHOOL
## 70 CHEMISTRY   26   ACCOUNTING
## 71 CHEMISTRY   27  AGRICULTURE
## 72 CHEMISTRY   27   SCIENTIFIC
## 75 CHEMISTRY   27  AGRICULTURE
## 81 CHEMISTRY   28  AGRICULTURE

The subset() function is very easy to use. However, we might have greater flexibility by using indices for subsetting. We already know that the notation dataframe[i, j] returns the element in the i-th row and j-th column of a data frame. We can, of course, replace i and j with some subsetting rules. For example, selecting the exams where the mark is between 25 and 29 is done as follows:

subData <- students[(students$Mark <= 29 & students$Mark >= 25),]
head(subData)
##    Id  Subject       Date Mark Year  HighSchool
## 4   4 AGRONOMY 24/06/2002   26 2001  HUMANITIES
## 6   6 AGRONOMY 09/09/2002   28 2001 AGRICULTURE
## 7   7 AGRONOMY 24/02/2003   26 2001  HUMANITIES
## 8   8 AGRONOMY 09/09/2002   26 2001  SCIENTIFIC
## 10 10 AGRONOMY 08/07/2002   27 2001  HUMANITIES
## 11 11 AGRONOMY 09/09/2002   28 2001  SCIENTIFIC

This is useful for quickly editing the data. For example, if we want to replace all marks from 25 to 29 with NAs (missing values), we can simply do:

subData <- students
subData[(subData$Mark <= 29 & subData$Mark >= 25), "Mark"] <- NA
head(subData)
##   Id  Subject       Date Mark Year  HighSchool
## 1  1 AGRONOMY 10/06/2002   30 2001  HUMANITIES
## 2  2 AGRONOMY 08/07/2002   24 2001 AGRICULTURE
## 3  3 AGRONOMY 24/06/2002   30 2001 AGRICULTURE
## 4  4 AGRONOMY 24/06/2002   NA 2001  HUMANITIES
## 5  5 AGRONOMY 23/01/2003   30 2001  HUMANITIES
## 6  6 AGRONOMY 09/09/2002   NA 2001 AGRICULTURE

Please note that I created a new dataset to make the replacement, so as not to modify the original dataset. Of course, I can use the is.na() function to find missing values and edit them.

subData[is.na(subData$Mark), "Mark"] <- 0 
head(subData)
##   Id  Subject       Date Mark Year  HighSchool
## 1  1 AGRONOMY 10/06/2002   30 2001  HUMANITIES
## 2  2 AGRONOMY 08/07/2002   24 2001 AGRICULTURE
## 3  3 AGRONOMY 24/06/2002   30 2001 AGRICULTURE
## 4  4 AGRONOMY 24/06/2002    0 2001  HUMANITIES
## 5  5 AGRONOMY 23/01/2003   30 2001  HUMANITIES
## 6  6 AGRONOMY 09/09/2002    0 2001 AGRICULTURE

Sorting the data

Sorting is very similar to subsetting by indexing. We simply need to use the order() function. For example, let’s sort the students dataset by mark:

sortedData <- students[order(students$Mark), ]
head(sortedData)
##    Id   Subject       Date Mark Year   HighSchool
## 51 51   BIOLOGY 01/03/2002   18 2001   HUMANITIES
## 67 67 CHEMISTRY 20/02/2003   18 2002  AGRICULTURE
## 76 76 CHEMISTRY 24/02/2003   18 2002 OTHER SCHOOL
## 79 79 CHEMISTRY 18/06/2003   18 2002  AGRICULTURE
## 82 82 CHEMISTRY 18/07/2002   18 2001  AGRICULTURE
## 83 83 CHEMISTRY 23/01/2003   18 2001   SCIENTIFIC

We can also sort in decreasing order:

sortedData <- students[order(-students$Mark), ]
head(sortedData)
##    Id  Subject       Date Mark Year  HighSchool
## 1   1 AGRONOMY 10/06/2002   30 2001  HUMANITIES
## 3   3 AGRONOMY 24/06/2002   30 2001 AGRICULTURE
## 5   5 AGRONOMY 23/01/2003   30 2001  HUMANITIES
## 17 17 AGRONOMY 10/06/2002   30 2001  HUMANITIES
## 18 18 AGRONOMY 10/06/2002   30 2001 AGRICULTURE
## 19 19 AGRONOMY 09/09/2002   30 2001 AGRICULTURE

We can obviously use multiple keys. For example, let’s sort by mark and, within each mark, by subject:

sortedData <- students[order(-students$Mark, students$Subject), ]
head(sortedData)
##    Id  Subject       Date Mark Year  HighSchool
## 1   1 AGRONOMY 10/06/2002   30 2001  HUMANITIES
## 3   3 AGRONOMY 24/06/2002   30 2001 AGRICULTURE
## 5   5 AGRONOMY 23/01/2003   30 2001  HUMANITIES
## 17 17 AGRONOMY 10/06/2002   30 2001  HUMANITIES
## 18 18 AGRONOMY 10/06/2002   30 2001 AGRICULTURE
## 19 19 AGRONOMY 09/09/2002   30 2001 AGRICULTURE

If I want to sort a character variable (such as Subject) in decreasing order, I need to use the helper function xtfrm():

sortedData <- students[order(-students$Mark, -xtfrm(students$Subject)), ]
head(sortedData)
##      Id Subject       Date Mark Year   HighSchool
## 116 116   MATHS 01/07/2002   30 2001 OTHER SCHOOL
## 117 117   MATHS 18/06/2002   30 2001   ACCOUNTING
## 118 118   MATHS 09/07/2002   30 2001  AGRICULTURE
## 121 121   MATHS 18/06/2002   30 2001   ACCOUNTING
## 123 123   MATHS 09/07/2002   30 2001   HUMANITIES
## 130 130   MATHS 07/02/2002   30 2001   SCIENTIFIC

Casting and melting

These are two operations that we can perform on the entire dataframe, to reshape it from:

  1. LONG to WIDE format (casting)
  2. WIDE to LONG format (melting)

In base R, we use the same function, namely reshape(), which was originally tailored to the needs of longitudinal data management. Although the terminology comes from this original conceptual framework (which is quite confusing at the beginning), you can use this function for any type of data that needs to be reshaped from LONG to WIDE or vice versa.

Casting the data

We might be familiar with the ‘pivot table’ function in Excel, which reshapes a dataset from the LONG format to the WIDE format. For example, let’s take the rimsulfuron dataset in the statforbiology package, which contains the results of an experiment in blocks designed to compare 16 herbicides for weed control in maize. The dataset is in the LONG format, with one row for each plot and the observations made in each plot listed in different columns.

rimsulfuron <- getAgroData("rimsulfuron")
head(rimsulfuron)
##                      Herbicide Plot Code Block Column WeedCover Yield
## 1             Rimsulfuron (40)    1    1     1      1      27.8 85.91
## 2             Rimsulfuron (45)    2    2     1      1      27.8 93.03
## 3             Rimsulfuron (50)    3    3     1      1      23.0 86.93
## 4             Rimsulfuron (60)    4    4     1      1      42.8 52.99
## 5    Rimsulfuron (50+30 split)    5    5     1      1      15.1 71.36
## 6 Rimsulfuron + thyfensulfuron    6    6     1      1      22.9 75.28

Let’s put this data frame in the WIDE format, so that we have the 16 herbicides in different rows, and the observations for each herbicide are listed in different columns, with a separate column per each block (provided that we have only one observation per herbicide in each block, which is the case here). In base R, we can use the reshape() function with direction = "wide". Basically, we need to specify which variable should identify the rows (in our case, idvar = "Herbicide") and which variable identifies the different sets of observations (in our case, timevar = "Block"). The name timevar shows that this function was initially tailored to the needs of longitudinal data. Initially, we have to subset the data frame to retain only the columns we need to display in the final table, although we can also use the drop argument to exclude the variables we do not intend to use.

castData <- reshape(
  rimsulfuron[, c("Herbicide", "Block", "Yield")], 
  direction = "wide",
  idvar = "Herbicide",
  timevar = "Block"
)
castData
##                                     Herbicide Yield.1 Yield.2 Yield.3 Yield.4
## 1                            Rimsulfuron (40)   85.91   91.09  111.42   93.15
## 2                            Rimsulfuron (45)   93.03  105.00   89.19   79.04
## 3                            Rimsulfuron (50)   86.93  105.82  110.02   89.10
## 4                            Rimsulfuron (60)   52.99  102.86  100.62   97.04
## 5                   Rimsulfuron (50+30 split)   71.36   77.57  115.91   92.16
## 6                Rimsulfuron + thyfensulfuron   75.28   82.59   94.96   85.85
## 7                        Rimsulfuron + hoeing   73.22   86.06  118.01   98.32
## 8    Pendimethalin (pre) + rimsulfuron (post)   65.51   88.72   95.52   82.39
## 9  Pendimethalin (post) + rimsuulfuron (post)   94.82   87.72  102.05  101.94
## 10                        Rimsulfuron + Atred   94.11   89.86  104.34   99.63
## 11                             Thifensulfuron   78.47   42.32   62.52   24.34
## 12         Metolachlor + terbuthylazine (pre)   51.77   52.10   49.46   34.67
## 13                  Alachlor + terbuthylazine   12.06   49.58   41.34   16.37
## 14                                Hand-Weeded   77.58   92.08   86.59   99.63
## 15                                 Unweeded 1   10.88   31.77   23.92   20.85
## 16                                 Unweeded 2   27.58   51.55   25.13   38.61

Melting the data

The reshape() function can also be used to transform a dataset from WIDE to LONG format by setting the direction = "long" argument. For this task, let’s use the WeedPop dataset in the statforbiology package, which reports the results of a weed survey involving six species in nine conditions (the letters from A to I in the Code variable). The experimental unit is the condition and, for each condition, the ground cover of the six species is reported in different columns.

Now, we want to reshape this data frame so that we have one row for each combination of species and condition, with the ground cover listed in a single column. In order to use the reshape() function, we need to think of this table as if it represented longitudinal data, with repeated measurements taken at different time points on the same subject (id). Thus, the values that change with ‘time’ (the varying variables) are those contained in the original dataset in columns 2 to 7. These variables will be combined into a single column in the newly created dataset, which we will name WeedCover (v.names = "WeedCover").

Now, we have to add at least two other variables to this newly created dataset. The first one represents the subjects (idvar) and must contain the codes contained in the WeedPop$Code column (ids = WeedPop$Code). We will name this variable Code (idvar = "Code").

The second variable must contain the names of the original variables (times = colnames(WeedPop[2:7])) corresponding to the observations taken for each subject. We can assign a name to this newly created variable by using timevar = "Weed name".

WeedPop <- getAgroData("WeedPop")
WeedPop
##   Code POLLA CHEPO ECHCG AMARE XANST POLAV
## 1    A   0.1    33    11     0   0.1   0.1
## 2    B   0.1     3     3     0   0.1   0.0
## 3    C   7.0    19    19     4   7.0   1.0
## 4    D  18.0     3    28    19  12.0   6.0
## 5    E   5.0     7    28     3  10.0   1.0
## 6    F  11.0     9    33     7  10.0   6.0
## 7    G   8.0    13    33     6  15.0  15.0
## 8    H  18.0     5    33     4  19.0  12.0
## 9    I   6.0     6    38     3  10.0   6.0
mdati <- reshape(
  WeedPop,
  direction = "long",
  varying = colnames(WeedPop[2:7]),
  v.names = "WeedCover",
  times = colnames(WeedPop[2:7]),
  timevar = "Weed name",
  ids = WeedPop$Code,
  idvar = "Code",
)
head(mdati)
##         Code Weed name WeedCover
## A.POLLA    A     POLLA       0.1
## B.POLLA    B     POLLA       0.1
## C.POLLA    C     POLLA       7.0
## D.POLLA    D     POLLA      18.0
## E.POLLA    E     POLLA       5.0
## F.POLLA    F     POLLA      11.0

I must admit that these functions are not particularly intuitive to use and the corresponding functions in the packages ‘dplyr’ and ‘tidyr’ may be easier to use. However, I like my students to have a good command of base R before they move on to other dialects

Have fun working with these functions! Should you have comments, please, drop me a note at the address below.

And … don’t forget to check out my new book!

Prof. Andrea Onofri
Department of Agricultural, Food and Environmental Sciences
University of Perugia (Italy)
Send comments to: andrea.onofri@unipg.it

Book cover


This post was originally published on 2019-03-27