Table manipulation

Table Manipulation with dplyr and tidyr: preparing data for analysis

For some analyses, data has to be provided in a specific format, and sometimes data just needs to be reorganized or cleaned up.

The tidyr package was developed with the aim to make it easier to “tidy” data, dplyr is the associated data management package.

NOTE: the code for R base differs from the code for tidyr. If tidyr is activated, its code needs to be used, base code won’t work

There are four main functions to organize data:

  • Gather: groups multiple columns (wide to long)

  • Spread: converts key-value pairs to separate columns

  • Separate: splits column into multiple ones

  • Unite: does the reverse

In this example we’ll use the built-in DNase dataset in R.

library(tidyr)
library(dplyr)

Attaching package: 'dplyr'
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
data(iris)
head(iris)
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1          5.1         3.5          1.4         0.2  setosa
2          4.9         3.0          1.4         0.2  setosa
3          4.7         3.2          1.3         0.2  setosa
4          4.6         3.1          1.5         0.2  setosa
5          5.0         3.6          1.4         0.2  setosa
6          5.4         3.9          1.7         0.4  setosa

Adding and ID number and renaming columns

In the code below, the column ID is added to the table, by adding a sequence of +1 (the last 1), from the first sample until the last row (nrow).

iris$ID <- seq(1, nrow(iris), 1)
head(iris)
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species ID
1          5.1         3.5          1.4         0.2  setosa  1
2          4.9         3.0          1.4         0.2  setosa  2
3          4.7         3.2          1.3         0.2  setosa  3
4          4.6         3.1          1.5         0.2  setosa  4
5          5.0         3.6          1.4         0.2  setosa  5
6          5.4         3.9          1.7         0.4  setosa  6
tail(iris)
    Sepal.Length Sepal.Width Petal.Length Petal.Width   Species  ID
145          6.7         3.3          5.7         2.5 virginica 145
146          6.7         3.0          5.2         2.3 virginica 146
147          6.3         2.5          5.0         1.9 virginica 147
148          6.5         3.0          5.2         2.0 virginica 148
149          6.2         3.4          5.4         2.3 virginica 149
150          5.9         3.0          5.1         1.8 virginica 150

An ID column is added at the end.

To rename the columns:

iris1 <- rename(iris, SL = Sepal.Length, SW=Sepal.Width, PL=Petal.Length, PW=Petal.Width)
head(iris1)
   SL  SW  PL  PW Species ID
1 5.1 3.5 1.4 0.2  setosa  1
2 4.9 3.0 1.4 0.2  setosa  2
3 4.7 3.2 1.3 0.2  setosa  3
4 4.6 3.1 1.5 0.2  setosa  4
5 5.0 3.6 1.4 0.2  setosa  5
6 5.4 3.9 1.7 0.4  setosa  6

Gather and Spread

For the gathering and spreading functions, the data needs to have an ID, so that R knows which information comes from the same sample. Gather the data we want to keep:

iris1_long <-gather(iris1, key=flowerattr, value=measurement, SL, SW, PL, PW,)
head(iris1_long)
  Species ID flowerattr measurement
1  setosa  1         SL         5.1
2  setosa  2         SL         4.9
3  setosa  3         SL         4.7
4  setosa  4         SL         4.6
5  setosa  5         SL         5.0
6  setosa  6         SL         5.4

We now have a list of 4 variables wide and 600 objects long. Note that the order of the variables has changed.

To convert it back to its original form, this codes separates the attribute columns (SL, SW, PL, PW)

iris1_wide <-spread(iris1_long, key=flowerattr, value=measurement) 

Separating and uniting columns

In this step, we’ll separate the values in millimeters and centimeters into separate columns with the separate() and into() functions. This allows you to create new columns.

The sep function allows to specify where or how you want the data to be separated. In this case, we will split the mm values (position 2 of the number) from the cm values.

If the original data is character, is interpreted as a regular expression. However, if the original data is numeric the sep argument is interpreted as positions to split at.

By specifying the convert=TRUE argument, R will run the type.convert() function internally and in this case convert the output to numeric, removing the decimal place.

iris1.sep<-separate(iris1,col=SL, into=c("SepalCM","SepalMM"), sep=2, convert = TRUE)

This creates new NAs in the table, we will assign zero to those.

iris1.sep[is.na(iris1.sep)] <-0

The opposite of separate() function is the unite() function. In this step we will unite multiple columns into one, and reassign a numeric using the type.comvert() function

In this code we will create a new column, LS, from SepalCM and SepalMM, and add a decimal point.

iris1.unite<-unite(iris1.sep, col=SL, c("SepalCM","SepalMM"),sep=".") 
head(iris1.unite) 
   SL  SW  PL  PW Species ID
1 5.1 3.5 1.4 0.2  setosa  1
2 4.9 3.0 1.4 0.2  setosa  2
3 4.7 3.2 1.3 0.2  setosa  3
4 4.6 3.1 1.5 0.2  setosa  4
5 5.0 3.6 1.4 0.2  setosa  5
6 5.4 3.9 1.7 0.4  setosa  6

The SL objects are now characters, not numbers. R can convert this to numbers

iris1.unite$SL <- type.convert(iris1.unite$SL)
Warning in type.convert.default(iris1.unite$SL): 'as.is' should be specified by
the caller; using TRUE
head(iris1.unite)
   SL  SW  PL  PW Species ID
1 5.1 3.5 1.4 0.2  setosa  1
2 4.9 3.0 1.4 0.2  setosa  2
3 4.7 3.2 1.3 0.2  setosa  3
4 4.6 3.1 1.5 0.2  setosa  4
5 5.0 3.6 1.4 0.2  setosa  5
6 5.4 3.9 1.7 0.4  setosa  6

Cleaning up data

Three key functions to organize or clean up data are select(), filter() and distinct(). These functions pick out objects based on their name (select), on their value (filter) or removes duplicates (distinct).

Select data

To select data based on their name:

Petal <- select(iris1, c("Species", "PL", "PW"))
head(Petal)
  Species  PL  PW
1  setosa 1.4 0.2
2  setosa 1.4 0.2
3  setosa 1.3 0.2
4  setosa 1.5 0.2
5  setosa 1.4 0.2
6  setosa 1.7 0.4

Filter data

To filter data based on value, R uses logical statements.

Filter

To filter data based on value we can use the filter() function in dplyr:

iris1.large <- filter(iris1, PL >= 5, PW >= 1)
head(iris1.large)
   SL  SW  PL  PW    Species  ID
1 6.7 3.0 5.0 1.7 versicolor  78
2 6.0 2.7 5.1 1.6 versicolor  84
3 6.3 3.3 6.0 2.5  virginica 101
4 5.8 2.7 5.1 1.9  virginica 102
5 7.1 3.0 5.9 2.1  virginica 103
6 6.3 2.9 5.6 1.8  virginica 104

This results in 46 objects.
We can also use OR, to get more results (100 in this scenario)

iris1.large2 <- filter(iris1, PL >= 5 | PW >= 1)
head(iris1.large2)
   SL  SW  PL  PW    Species ID
1 7.0 3.2 4.7 1.4 versicolor 51
2 6.4 3.2 4.5 1.5 versicolor 52
3 6.9 3.1 4.9 1.5 versicolor 53
4 5.5 2.3 4.0 1.3 versicolor 54
5 6.5 2.8 4.6 1.5 versicolor 55
6 5.7 2.8 4.5 1.3 versicolor 56

Remove duplicates

To remove duplicates distinct() can be used (however, there are not duplicates in the iris data frame)

iris2 <- distinct(iris1)

To remove duplicates based on a specified variable the following code can be used. .keep_all =TRUE indicates all other information needs to be kept, otherwise you would only get the 3 species names.

iris3 <- distinct(iris1, Species, .keep_all = TRUE)

head(iris3)
   SL  SW  PL  PW    Species  ID
1 5.1 3.5 1.4 0.2     setosa   1
2 7.0 3.2 4.7 1.4 versicolor  51
3 6.3 3.3 6.0 2.5  virginica 101

Order

It may be useful to order data with the arrange() function. By default this will create an ascending list.

iris.ordered <- arrange(iris1, PL)
head(iris.ordered)
   SL  SW  PL  PW Species ID
1 4.6 3.6 1.0 0.2  setosa 23
2 4.3 3.0 1.1 0.1  setosa 14
3 5.8 4.0 1.2 0.2  setosa 15
4 5.0 3.2 1.2 0.2  setosa 36
5 4.7 3.2 1.3 0.2  setosa  3
6 5.4 3.9 1.3 0.4  setosa 17

To create a descending list:

iris.desc <- arrange(iris1, desc(PL))
head(iris.desc)
   SL  SW  PL  PW   Species  ID
1 7.7 2.6 6.9 2.3 virginica 119
2 7.7 3.8 6.7 2.2 virginica 118
3 7.7 2.8 6.7 2.0 virginica 123
4 7.6 3.0 6.6 2.1 virginica 106
5 7.9 3.8 6.4 2.0 virginica 132
6 7.3 2.9 6.3 1.8 virginica 108

You an also order within groups if you first assign groups to the data set and include the .by_group=TRUE argument in the arrange() function. If you do not “turn on” the .by_group argument than the groups are ignored in the sorting.

iris1.group <- group_by(iris1, Species)
iris1.order <- arrange (iris1.group, PL,.by_group=TRUE) 
head(iris1.order)
# A tibble: 6 × 6
# Groups:   Species [1]
     SL    SW    PL    PW Species    ID
  <dbl> <dbl> <dbl> <dbl> <fct>   <dbl>
1   4.6   3.6   1     0.2 setosa     23
2   4.3   3     1.1   0.1 setosa     14
3   5.8   4     1.2   0.2 setosa     15
4   5     3.2   1.2   0.2 setosa     36
5   4.7   3.2   1.3   0.2 setosa      3
6   5.4   3.9   1.3   0.4 setosa     17

Create new variables:

mutate() adds new variables that are functions of equations and existing variables.

summarize() reduces multiple values down to a single summary output.

For example, we can add a columns with the ratio of PL/PW, SL/SW, and the ratio of these two using mutate()

iris2 <- mutate (iris1, Petal.Ratio=PL/PW)
iris2 <- mutate (iris2, Sepal.Ratio=SL/SW)
iris2 <- mutate (iris2, Ratio=Petal.Ratio/Sepal.Ratio)
head(iris2)
   SL  SW  PL  PW Species ID Petal.Ratio Sepal.Ratio    Ratio
1 5.1 3.5 1.4 0.2  setosa  1        7.00    1.457143 4.803922
2 4.9 3.0 1.4 0.2  setosa  2        7.00    1.633333 4.285714
3 4.7 3.2 1.3 0.2  setosa  3        6.50    1.468750 4.425532
4 4.6 3.1 1.5 0.2  setosa  4        7.50    1.483871 5.054348
5 5.0 3.6 1.4 0.2  setosa  5        7.00    1.388889 5.040000
6 5.4 3.9 1.7 0.4  setosa  6        4.25    1.384615 3.069444

We can use summarize to create a separate data frame with the mean ratios for each iris species by

  • first telling R how to group the dataset with the group_by() function

  • then using he summarize() function.

Note that here we use the na.rm=T argument within the mean() function to ignore any missing values (in this case, there were none)

iris2<-group_by(iris2,Species) 
iris3 <-summarize (iris2, PetalRatio.avg=mean (Petal.Ratio,na.rm=T), SepalRatio.avg=mean (Sepal.Ratio, na.rm=T), Ratio.avg=mean (Ratio, na.rm=T))
head(iris3)
# A tibble: 3 × 4
  Species    PetalRatio.avg SepalRatio.avg Ratio.avg
  <fct>               <dbl>          <dbl>     <dbl>
1 setosa               6.91           1.47      4.73
2 versicolor           3.24           2.16      1.51
3 virginica            2.78           2.23      1.25

With separate(), any of the following functions can be applied:

For example, iris3 can be updated to include a standard error (sd/sqrt(n)) of all 3 values.
* The SE for petal ratio is coded as follows: Petal.RatioSE=sd(Petal.Ratio, na.rm=T)/sqrt(n())

iris3 <-summarize (iris2, PetalRatio.avg=mean(Petal.Ratio,na.rm=T), Petal.RatioSE=sd(Petal.Ratio, na.rm=T)/sqrt(n()), SepalRatio.avg=mean (Sepal.Ratio, na.rm=T), Sepal.RatioSE=sd(Sepal.Ratio, na.rm=T)/sqrt(n()), Ratio.avg=mean (Ratio, na.rm=T), RatioSE=sd(Ratio, na.rm=T)/sqrt(n()))
head(iris3)
# A tibble: 3 × 7
  Species    PetalRatio.avg Petal.RatioSE SepalRatio.avg Sepal.RatioSE Ratio.avg
  <fct>               <dbl>         <dbl>          <dbl>         <dbl>     <dbl>
1 setosa               6.91        0.404            1.47        0.0168      4.73
2 versicolor           3.24        0.0442           2.16        0.0323      1.51
3 virginica            2.78        0.0576           2.23        0.0349      1.25
# ℹ 1 more variable: RatioSE <dbl>

Data 2 is grouped, to ungroup():

ungroup (iris2)
# A tibble: 150 × 9
      SL    SW    PL    PW Species    ID Petal.Ratio Sepal.Ratio Ratio
   <dbl> <dbl> <dbl> <dbl> <fct>   <dbl>       <dbl>       <dbl> <dbl>
 1   5.1   3.5   1.4   0.2 setosa      1        7           1.46  4.80
 2   4.9   3     1.4   0.2 setosa      2        7           1.63  4.29
 3   4.7   3.2   1.3   0.2 setosa      3        6.5         1.47  4.43
 4   4.6   3.1   1.5   0.2 setosa      4        7.5         1.48  5.05
 5   5     3.6   1.4   0.2 setosa      5        7           1.39  5.04
 6   5.4   3.9   1.7   0.4 setosa      6        4.25        1.38  3.07
 7   4.6   3.4   1.4   0.3 setosa      7        4.67        1.35  3.45
 8   5     3.4   1.5   0.2 setosa      8        7.5         1.47  5.1 
 9   4.4   2.9   1.4   0.2 setosa      9        7           1.52  4.61
10   4.9   3.1   1.5   0.1 setosa     10       15           1.58  9.49
# ℹ 140 more rows

Merge data tables:

There are 5 main functions in dplyr to merge tables. These functions assume you have two tables, (x) and (y) , note that (x) is positioned left of (y).
Joining these two tables adds columns from the right dataset (y) to the left dataset (x) based on matching key columns:

Join Type Function Result
Inner Join inner_join(x, y) Keeps only rows that match in both x and y.

Left Join

Most common.

left_join(x, y) Keeps all rows from x, matching rows from y (fills unmatched with NA).
Right Join right_join(x, y) Keeps all rows from y, matching rows from x (fills unmatched with NA).
Full Join full_join(x, y) Keeps all rows from both x and y, filling NA wherever there isn’t a match.

Let’s say another student in the lab gave you height data on the irises in data frame iris, however, plants with ID 5 to 10 died, so they measured 10 additional irises instead.

The new data can be found in the working directory, named irisbatch2.csv

irisbatch2 <- read.csv("irisbatch2.csv", header=T, na.strings="")

Some ways to join these tables:

Left-join: keep all the original rows, ignore the measurements for the last 10 plants, there will be NAs for height of plants 5 to 10.

irisleft <- left_join(iris1, irisbatch2)
Joining with `by = join_by(Species, ID)`
head(irisleft)
   SL  SW  PL  PW Species ID plant.height
1 5.1 3.5 1.4 0.2  setosa  1         55.1
2 4.9 3.0 1.4 0.2  setosa  2         52.9
3 4.7 3.2 1.3 0.2  setosa  3         55.7
4 4.6 3.1 1.5 0.2  setosa  4         64.6
5 5.0 3.6 1.4 0.2  setosa  5           NA
6 5.4 3.9 1.7 0.4  setosa  6           NA
tail(irisleft)
     SL  SW  PL  PW   Species  ID plant.height
145 6.7 3.3 5.7 2.5 virginica 145         48.7
146 6.7 3.0 5.2 2.3 virginica 146         57.7
147 6.3 2.5 5.0 1.9 virginica 147         54.3
148 6.5 3.0 5.2 2.0 virginica 148         57.5
149 6.2 3.4 5.4 2.3 virginica 149         66.2
150 5.9 3.0 5.1 1.8 virginica 150         47.9

Full-join: keeps all 160 plants, but there will be NAs under the petal and sepal columns, as well as height for plants 5 to 10

irisfull <- full_join(iris1, irisbatch2)
Joining with `by = join_by(Species, ID)`
head(irisfull)
   SL  SW  PL  PW Species ID plant.height
1 5.1 3.5 1.4 0.2  setosa  1         55.1
2 4.9 3.0 1.4 0.2  setosa  2         52.9
3 4.7 3.2 1.3 0.2  setosa  3         55.7
4 4.6 3.1 1.5 0.2  setosa  4         64.6
5 5.0 3.6 1.4 0.2  setosa  5           NA
6 5.4 3.9 1.7 0.4  setosa  6           NA
tail(irisfull)
    SL SW PL PW   Species  ID plant.height
155 NA NA NA NA virginica 155         48.7
156 NA NA NA NA virginica 156         57.7
157 NA NA NA NA virginica 157         54.3
158 NA NA NA NA virginica 158         57.5
159 NA NA NA NA virginica 159         66.2
160 NA NA NA NA virginica 160         47.9

If we want to only keep the plant IDs that have values for all variables, we can choose inner join, and add %>% drop.na() . The resulting table has 144 rows.

R, %>% is called the pipe operator. It takes the output from the left side and passes it as the first argument into the function on the right side, it freely translates to ‘and then’.

irisinner <- inner_join(iris1, irisbatch2, by = "ID") %>% drop_na()
head(irisinner)
   SL  SW  PL  PW Species.x ID Species.y plant.height
1 5.1 3.5 1.4 0.2    setosa  1    setosa         55.1
2 4.9 3.0 1.4 0.2    setosa  2    setosa         52.9
3 4.7 3.2 1.3 0.2    setosa  3    setosa         55.7
4 4.6 3.1 1.5 0.2    setosa  4    setosa         64.6
5 5.4 3.7 1.5 0.2    setosa 11    setosa         56.4
6 4.8 3.4 1.6 0.2    setosa 12    setosa         52.8
tail(irisinner)
     SL  SW  PL  PW Species.x  ID Species.y plant.height
139 6.7 3.3 5.7 2.5 virginica 145 virginica         48.7
140 6.7 3.0 5.2 2.3 virginica 146 virginica         57.7
141 6.3 2.5 5.0 1.9 virginica 147 virginica         54.3
142 6.5 3.0 5.2 2.0 virginica 148 virginica         57.5
143 6.2 3.4 5.4 2.3 virginica 149 virginica         66.2
144 5.9 3.0 5.1 1.8 virginica 150 virginica         47.9

Appending tables

Tables can also be stacked on top of each other, if the columns are matching.

Row-bind, rbind(, is the easiest to use. To add iris1 to iris1.order use:

combined <- rbind(iris1, iris1.order)

This results in a table of 300 rows and 6 variables.

However, rbind will give an error when there are non-matching column names in the new file. The function rbind.fill() can get around this, it is available in the plyr package

library(plyr)
------------------------------------------------------------------------------
You have loaded plyr after dplyr - this is likely to cause problems.
If you need functions from both plyr and dplyr, please load plyr first, then dplyr:
library(plyr); library(dplyr)
------------------------------------------------------------------------------

Attaching package: 'plyr'
The following objects are masked from 'package:dplyr':

    arrange, count, desc, mutate, rename, summarise, summarize
combined2 <- rbind.fill(iris1, irisbatch2)

This results in a table of 310 rows and 7 variables.


Removing all data in the workspace:

By now you may have a large number of datasets open in the workspace. Closing them all at once can be achieved with this code:

rm(list = ls())
  • ls() lists all object names currently in your active environment.

  • rm(list = ...) deletes the objects provided in that list.

To also clear the cached memory, use Garbage Collector gc() .

gc()
          used (Mb) gc trigger (Mb) limit (Mb) max used (Mb)
Ncells  947899 50.7    1801824 96.3         NA  1454137 77.7
Vcells 1735965 13.3    8388608 64.0      32768  2652272 20.3