Modifying big data is dangerous!

Finding example data

There is already a lot of data in the R library. The data() command displays all available data sets.
We are going to use the airquality data here:

# data()
str(airquality)
## 'data.frame':    153 obs. of  6 variables:
##  $ Ozone  : int  41 36 12 18 NA 28 23 19 8 NA ...
##  $ Solar.R: int  190 118 149 313 NA NA 299 99 19 194 ...
##  $ Wind   : num  7.4 8 12.6 11.5 14.3 14.9 8.6 13.8 20.1 8.6 ...
##  $ Temp   : int  67 72 74 62 56 66 65 59 61 69 ...
##  $ Month  : int  5 5 5 5 5 5 5 5 5 5 ...
##  $ Day    : int  1 2 3 4 5 6 7 8 9 10 ...
dim(airquality)
## [1] 153   6
head(airquality)
##   Ozone Solar.R Wind Temp Month Day
## 1    41     190  7.4   67     5   1
## 2    36     118  8.0   72     5   2
## 3    12     149 12.6   74     5   3
## 4    18     313 11.5   62     5   4
## 5    NA      NA 14.3   56     5   5
## 6    28      NA 14.9   66     5   6

The subset command

The subset function returns subsets of vectors, matrices or data frames which meet specified conditions. The following examples are from the subset function description (slightly modified).

In the following example, we extract data rows with temperature above 70 degrees, and keep only two columns (Ozone and Temp) of the original data frame:

sub1 = subset(airquality, Temp > 70, select = c(Ozone, Temp))
head(sub1)
##    Ozone Temp
## 2     36   72
## 3     12   74
## 11     7   74
## 22    11   73
## 29    45   81
## 30   115   79

Next, we keep only day1 rows, and skip the Temp column:

sub2 = subset(airquality, Day == 1, select = -Temp)
head(sub2)
##     Ozone Solar.R Wind Month Day
## 1      41     190  7.4     5   1
## 32     NA     286  8.6     6   1
## 62    135     269  4.1     7   1
## 93     39      83  6.9     8   1
## 124    96     167  6.9     9   1

We select all columns from Ozone to Wind (using the colon):

sub3 = subset(airquality, select = Ozone:Wind)
head(sub3)
##   Ozone Solar.R Wind
## 1    41     190  7.4
## 2    36     118  8.0
## 3    12     149 12.6
## 4    18     313 11.5
## 5    NA      NA 14.3
## 6    28      NA 14.9

The logical drop parameter tells the function if the original data structure should be preserved or not.
The default value is FALSE. We give an example where only one column is selected:

sub4 = subset(airquality, Day == 1, select = Temp, drop = FALSE)  # drop = FALSE must not be stated explicitely 
str(sub4)
## 'data.frame':    5 obs. of  1 variable:
##  $ Temp: int  67 78 84 81 91

We see that sub4 is still a data frame, even though it consists of one vector only (Temp).

Now, different from above, we declare drop = TRUE:

sub5 = subset(airquality, Day == 1, select = Temp, drop = TRUE)  # drop = TRUE ! 
str(sub5)
##  int [1:5] 67 78 84 81 91

As a result, the command keeps only a vector. The data frame structure has been dropped.

The merge command

The merge command can join two data frames by common columns or row names.

Example data

Let us create some example data sets:

df1 <- data.frame(  firstname = c("hans", "peter", "mary", "steve", "katie"),  
                      surname = c("lund", "mueller", "kaminski", "smith", "brendel"),
                      profession = c("dentist", "trucker", "midwife", "dentist", "physician"))
df1
##   firstname  surname profession
## 1      hans     lund    dentist
## 2     peter  mueller    trucker
## 3      mary kaminski    midwife
## 4     steve    smith    dentist
## 5     katie  brendel  physician

In an new data frame df2, we change the column name surname to name:

df2 = df1
colnames(df2)[2] = "name"
# alternative :  df2 <- within(df1, { name <- surname; rm(surname) })
df2
##   firstname     name profession
## 1      hans     lund    dentist
## 2     peter  mueller    trucker
## 3      mary kaminski    midwife
## 4     steve    smith    dentist
## 5     katie  brendel  physician

A third data frame:

cars <- data.frame(
    name = c("lund", "mueller", "kaminski", "smith", "brendel", "weller", "cooper"),
    type = c("kia", "vw", "ford", "bmw", "bmw", "fiat", "audi"),
    color = c(NA, "red", NA, "blue", "white", "black", "white"),
    miles = c(1200, 400, 650, 1590, 8420, 5000, 7200))
cars
##       name type color miles
## 1     lund  kia  <NA>  1200
## 2  mueller   vw   red   400
## 3 kaminski ford  <NA>   650
## 4    smith  bmw  blue  1590
## 5  brendel  bmw white  8420
## 6   weller fiat black  5000
## 7   cooper audi white  7200

Merging on columns with identical names

By default, the data frames are merged on columns with identical names. If we merge df2 and cars, the common column name is name. We have 5 common entries in the name column:

intersect(df2$name, cars$name)
## [1] "lund"     "mueller"  "kaminski" "smith"    "brendel"

Consequently, we should see 5 rows in the merged data set:

merge1 <- merge(df2, cars)
merge1
##       name firstname profession type color miles
## 1  brendel     katie  physician  bmw white  8420
## 2 kaminski      mary    midwife ford  <NA>   650
## 3     lund      hans    dentist  kia  <NA>  1200
## 4  mueller     peter    trucker   vw   red   400
## 5    smith     steve    dentist  bmw  blue  1590

Specifying the column for joining explicitely is better

However, I suggest to specify the column for joining explicitely, because it feels safer. The parameter by declares the column to be used for joining:

merge(df2, cars, by = "name")
##       name firstname profession type color miles
## 1  brendel     katie  physician  bmw white  8420
## 2 kaminski      mary    midwife ford  <NA>   650
## 3     lund      hans    dentist  kia  <NA>  1200
## 4  mueller     peter    trucker   vw   red   400
## 5    smith     steve    dentist  bmw  blue  1590

This presupposes that there is a column with the same name in both data frames.

Columns with the same names but different entries

In the cases above, the profession columns had identical entries, so that we need only one profession column in the output. What if we have profession columns in the datasets which are not identical?

df3 = df2
df3$profession[3] = "director"
df3$profession
## [1] "dentist"   "trucker"   "director"  "dentist"   "physician"

The frames df1 and df3 are once more displayed:

df1
##   firstname  surname profession
## 1      hans     lund    dentist
## 2     peter  mueller    trucker
## 3      mary kaminski    midwife
## 4     steve    smith    dentist
## 5     katie  brendel  physician
df3
##   firstname     name profession
## 1      hans     lund    dentist
## 2     peter  mueller    trucker
## 3      mary kaminski   director
## 4     steve    smith    dentist
## 5     katie  brendel  physician
merge(df1, df3, by = "firstname")
##   firstname  surname profession.x     name profession.y
## 1      hans     lund      dentist     lund      dentist
## 2     katie  brendel    physician  brendel    physician
## 3      mary kaminski      midwife kaminski     director
## 4     peter  mueller      trucker  mueller      trucker
## 5     steve    smith      dentist    smith      dentist

Now, merge has created two profession columns with the suffixes .x and .y, respectively.

Joining on columns with different names

What if we want to join on columns with different names in the two data sets? For instance, df1 and cars have no common column names:

intersect(colnames(df1), colnames(cars))
## character(0)

…. but the entries in name and surname are of the same nature, so we could join based on them:

df1$surname
## [1] "lund"     "mueller"  "kaminski" "smith"    "brendel"
cars$name
## [1] "lund"     "mueller"  "kaminski" "smith"    "brendel"  "weller"   "cooper"

Using the parameters by.x and by.y, we can specify which columns to use in the two datasets:

merge(cars, df1, by.x = "name", by.y = "surname")
##       name type color miles firstname profession
## 1  brendel  bmw white  8420     katie  physician
## 2 kaminski ford  <NA>   650      mary    midwife
## 3     lund  kia  <NA>  1200      hans    dentist
## 4  mueller   vw   red   400     peter    trucker
## 5    smith  bmw  blue  1590     steve    dentist

Keeping all entries when joining, instead of common ones only

When using surname in df1 and name in cars for joining, we have only 5 entries in the output, the common entries for these columns.

If we want all names/surnames in the merged dataset, we can specify all = TRUE:

m2 = merge(cars, df1, by.x = "name", by.y = "surname", all = TRUE)
m2
##       name type color miles firstname profession
## 1  brendel  bmw white  8420     katie  physician
## 2   cooper audi white  7200      <NA>       <NA>
## 3 kaminski ford  <NA>   650      mary    midwife
## 4     lund  kia  <NA>  1200      hans    dentist
## 5  mueller   vw   red   400     peter    trucker
## 6    smith  bmw  blue  1590     steve    dentist
## 7   weller fiat black  5000      <NA>       <NA>

If we do so, we have to tolerate unassigned fields (NA) in the output, because there is no cooper and weller in the surname column of df1.

Skipping rows with unassigned fields

We have seen that the frame m2 contains unassigned fields, marked by NA. If we have to remove rows with such fields, we can use the complete.cases command:

complete.cases(m2)
## [1]  TRUE FALSE FALSE FALSE  TRUE  TRUE FALSE

The result of complete.cases() is a logical vector indicating rows with complete data (TRUE), and rows containing NA (FALSE). We can utilize this function to clean a data frame from rows with unassigned fields:

m3 = m2[complete.cases(m2), ]
m3
##      name type color miles firstname profession
## 1 brendel  bmw white  8420     katie  physician
## 5 mueller   vw   red   400     peter    trucker
## 6   smith  bmw  blue  1590     steve    dentist

We have used the []-operator to select rows with TRUE-values.

Sorting of data frames

In some situations, it might be necessary to reorder the rows (or columns) of a data frame. This can be accomplished by using the match function. Let us create a small data frame for explanation:

df <- data.frame(name = letters[1:4], value = c(rep(TRUE, 2), rep(FALSE, 2)))
df
##   name value
## 1    a  TRUE
## 2    b  TRUE
## 3    c FALSE
## 4    d FALSE

As an example, we define a desired order of the rows and store it in the variable target. Then, we use match to find the positions of target in the name column of the data frame df:

target <- c("b", "c", "a", "d")  # desired order of the rows
order = match(target, df$name) 
order 
## [1] 2 3 1 4

The match function takes two vectors as arguments. It yields the positions of the first vector in the second vector (here saved in the integer vector order). We see that b comes second in df$name, c comes third in df$name, a comes first in df$name, and d comes forth in df$name.
Using the information stored in the integer vector order, we can rearrange the rows of the frame df:

df_ordered = df[order,]
df_ordered
##   name value
## 2    b  TRUE
## 3    c FALSE
## 1    a  TRUE
## 4    d FALSE

This is the desired order of the data frame.

We can of course shorten the procedure:

target <- c("b", "c", "a", "d")  # desired order of the rows
df_ordered = df[match(target, df$name),]
df_ordered
##   name value
## 2    b  TRUE
## 3    c FALSE
## 1    a  TRUE
## 4    d FALSE

This procedure can for instance be used to bring the rows (or columns) of two datasets into the same order, by choosing a column of one frame as target vector:

dfa <- data.frame(name = c("a", "b", "c", "d"), value = c(rep(TRUE, 2), rep(FALSE, 2)))
dfb <- data.frame(name = c("c", "d", "a", "b"), value = 1:4)
dfa
##   name value
## 1    a  TRUE
## 2    b  TRUE
## 3    c FALSE
## 4    d FALSE
dfb
##   name value
## 1    c     1
## 2    d     2
## 3    a     3
## 4    b     4

The data frames do not have the same order when we focus on the name column.

target = dfb$name
order = match(target, dfa$name)
new_dfb = dfb[order,]
new_dfb
##   name value
## 3    a     3
## 4    b     4
## 1    c     1
## 2    d     2

If we want to have this very concise, we can write:

new_dfb = dfb[match(dfb$name, dfa$name),]
new_dfb
##   name value
## 3    a     3
## 4    b     4
## 1    c     1
## 2    d     2

In a similar way, we can also change the order of the columns.

Searching in big data frames

The grep command (or one of its flavours) can be used to search words in character vectors.

v0 = c("A", "B", "C", "D", "A", "a", "B", "B", "D", "E", "E","X", "XX", "DDD", "A", "XX", "FFFF", "0000", "12", "000")
grep("B", v0)
## [1] 2 7 8

We see that the command outputs the indices of the matches. Using this information, we can create a new vector containing only the matches:

v1 = v0[grep("B", v0)]
v1
## [1] "B" "B" "B"

Apply functions to every row or column

The above grep command works only for vectors. If we want to work on every row or column of a frame or matrix, we can use the apply command.

Some example data:

v0 = c("A", "B", "C", "D", "W", "B")
v1 = c("X", "B", "C", "BB", "A", "B")
v2 = c("A", "B", "C", "D", "W", "B")
v3 = c("X", "T", "X", "CC", "S", "U")
Vall = data.frame(one = v0, two = v1, three = v2, four = v3)
rownames(Vall) = c("eins", "zwei", "drei", "vier", "fuenf", "sechs")
Vall
##       one two three four
## eins    A   X     A    X
## zwei    B   B     B    T
## drei    C   C     C    X
## vier    D  BB     D   CC
## fuenf   W   A     W    S
## sechs   B   B     B    U
grep("B", Vall)
## [1] 1 2 3

The output of grep shows only which columns contain the “B”. If we want the positions in each column of the frame, we can use the apply command:

apply(Vall, 2, grep, pattern = "B") # "pattern" is a formal parameter of the grep function 
## $one
## [1] 2 6
## 
## $two
## [1] 2 4 6
## 
## $three
## [1] 2 6
## 
## $four
## integer(0)

We can also search by row. Therefore, we invoke a 1 as second parameter of the command:

apply(Vall, 1, grep, pattern = "B") # "pattern" is a formal parameter of the grep function 
## $eins
## integer(0)
## 
## $zwei
## [1] 1 2 3
## 
## $drei
## integer(0)
## 
## $vier
## [1] 2
## 
## $fuenf
## integer(0)
## 
## $sechs
## [1] 1 2 3

There are more flavours of the apply function, all listed here.

A few words about regular expressions

Did you notice that the command also considered “BB” as a match? (See the row named vier.)

If we only want matches with a single “B”, we have to modify the search pattern:

apply(Vall, 1, grep, pattern = "^B$")
## $eins
## integer(0)
## 
## $zwei
## [1] 1 2 3
## 
## $drei
## integer(0)
## 
## $vier
## integer(0)
## 
## $fuenf
## integer(0)
## 
## $sechs
## [1] 1 2 3

Now, there isn’t any match in the row named vier.

Construction of such search patterns, called regular expressions, can be quite complicated. Explanation of regular expressions is out of the scope of this document. However, there are lots of manuals and cheatsheets on the web, for instance here or here.


Note that all code snippets are designed for comprehensibility, not for efficiency!


uwe.menzel@matstat.org