This lesson is being piloted (Beta version)

Starting with Data

Overview

Teaching: 50 min
Exercises: 30 min
Questions
  • What is a data.frame?

  • How can I read a complete csv file into R?

  • How can I get basic summary information about my dataset?

  • How can I change the way R treats strings in my dataset?

  • Why would I want strings to be treated differently?

  • How are dates represented in R and how can I change the format?

Objectives
  • Describe what a data frame is.

  • Load external data from a .csv file into a data frame.

  • Summarize the contents of a data frame.

  • Describe the difference between a factor and a string.

  • Convert between strings and factors.

  • Reorder and rename factors.

  • Change how character strings are handled in a data frame.

  • Examine and change date formats.

Presentation of the SAFI Data

SAFI (Studying African Farmer-Led Irrigation) is a study looking at farming and irrigation methods in Tanzania and Mozambique. The survey data was collected through interviews conducted between November 2016 and June 2017. For this lesson, we will be using a subset of the available data. For information about the full teaching dataset used in other lessons in this workshop, see the dataset description.

We will be using a subset of the cleaned version of the dataset that was produced through cleaning in OpenRefine. Each row holds information for a single interview respondent, and the columns represent:

column_name description
key_id Added to provide a unique Id for each observation. (The InstanceID field does this as well but it is not as convenient to use)
village Village name
interview_date Date of interview
no_membrs How many members in the household?
years_liv How many years have you been living in this village or neighboring village?
respondent_wall_type What type of walls does their house have (from list)
rooms How many rooms in the main house are used for sleeping?
memb_assoc Are you a member of an irrigation association?
affect_conflicts Have you been affected by conflicts with other irrigators in the area?
liv_count Number of livestock owned.
items_owned Which of the following items are owned by the household? (list)
no_meals How many meals do people in your household normally eat in a day?
months_lack_food Indicate which months, In the last 12 months have you faced a situation when you did not have enough food to feed the household?
instanceID Unique identifier for the form data submission

Let’s create a new script for our analysis of the SAFI data. Create a new script by clicking File > New File > R Script Before proceeding, be sure to save your new script File > Save As. Let’s name our script SAFI_analysis.R and save it in our scripts folder. We will be using the tidyverse package to work with the SAFI dataset, so before we read in the data, we need to make sure that the package is loaded.

# install.packages("tidyverse")
library(tidyverse)

You are going load the data in R’s memory using the function read_csv() from the readr package which is part of the tidyverse. So, before we can use the read_csv() function, we need to load the package. Also, if you recall, the missing data is encoded as “NULL” in the dataset. We’ll tell it to the function, so R will automatically convert all the “NULL” entries in the dataset into NA.

interviews <- read_csv("data/SAFI_clean.csv", na = "NULL")

This statement doesn’t produce any output because, as you might recall, assignments don’t display anything. If we want to check that our data has been loaded, we can see the contents of the data frame by typing its name: interviews.

interviews
Warning in as.POSIXlt.POSIXct(x, tz): unknown timezone 'zone/tz/2019a.1.0/
zoneinfo/Australia/Sydney'
## Try also
## View(interviews)
# A tibble: 131 x 14
   key_ID village interview_date      no_membrs years_liv respondent_wall…
    <int> <chr>   <dttm>                  <int>     <int> <chr>           
 1      1 God     2016-11-17 00:00:00         3         4 muddaub         
 2      1 God     2016-11-17 00:00:00         7         9 muddaub         
 3      3 God     2016-11-17 00:00:00        10        15 burntbricks     
 4      4 God     2016-11-17 00:00:00         7         6 burntbricks     
 5      5 God     2016-11-17 00:00:00         7        40 burntbricks     
 6      6 God     2016-11-17 00:00:00         3         3 muddaub         
 7      7 God     2016-11-17 00:00:00         6        38 muddaub         
 8      8 Chirod… 2016-11-16 00:00:00        12        70 burntbricks     
 9      9 Chirod… 2016-11-16 00:00:00         8         6 burntbricks     
10     10 Chirod… 2016-12-16 00:00:00        12        23 burntbricks     
# … with 121 more rows, and 8 more variables: rooms <int>,
#   memb_assoc <chr>, affect_conflicts <chr>, liv_count <int>,
#   items_owned <chr>, no_meals <int>, months_lack_food <chr>,
#   instanceID <chr>

Note

read_csv() assumes that fields are delineated by commas, however, in several countries, the comma is used as a decimal separator and the semicolon (;) is used as a field delineator. If you want to read in this type of files in R, you can use the read_csv2 function. It behaves exactly like read_csv but uses different parameters for the decimal and the field separators. If you are working with another format, they can be both specified by the user. Check out the help for read_csv() by typing ?read_csv to learn more. There is also the read_tsv() for tab-separated data files, and read_delim() allows you to specify more details about the structure of your file.

Commit and push your new script with the commit message, “Add script to in SAFI data using the read_csv() function in the readr package.”

What are data frames?

Data frames are the de facto data structure for tabular data, and what we use for statistics and plotting.

A data frame can be created by hand, but most commonly they are generated by the functions read_csv() or read_table(); in other words, when importing spreadsheets from your hard drive (or the web).

A data frame is the representation of data in the format of a table where the columns are vectors that all have the same length. Because columns are vectors, each column must contain a single type of data (e.g., characters, integers, factors). For example, here is a figure depicting a data frame comprising a numeric, a character, and a logical vector.

When using read_csv(), the data frame created is of class tbl_df (called “tibble”). The type of data included in each column is listed in an abbreviated fashion below the column names. For instance, here key_ID is a column of integers (abbreviated <int>), village is a column of characters (<chr>) and the interview_date is a column in the “date and time” format (<dttm>).

Inspecting data frames

When calling a tbl_df object (like interviews here), there is already a lot of information about our data frame being displayed such as the number of rows, the number of columns, the names of the columns, and as we just saw the class of data stored in each column. However, there are functions to extract this information from data frames. Here is a non-exhaustive list of some of these functions. Let’s try them out!

Note

Most of these functions are “generic”, they can be used on other types of objects besides data frames.

Indexing and subsetting data frames

Our interviews data frame has rows and columns (it has 2 dimensions), if we want to extract some specific data from it, we need to specify the “coordinates” we want from it. Row numbers come first, followed by column numbers. However, note that different ways of specifying these coordinates lead to results with different classes.

## first element in the first column of the data frame (as a vector)
interviews[1, 1]
# A tibble: 1 x 1
  key_ID
   <int>
1      1
## first element in the 6th column (as a vector)
interviews[1, 6]
# A tibble: 1 x 1
  respondent_wall_type
  <chr>               
1 muddaub             
## first column of the data frame (as a vector)
interviews[, 1]
# A tibble: 131 x 1
   key_ID
    <int>
 1      1
 2      1
 3      3
 4      4
 5      5
 6      6
 7      7
 8      8
 9      9
10     10
# … with 121 more rows
## first column of the data frame (as a data.frame)
interviews[1]
# A tibble: 131 x 1
   key_ID
    <int>
 1      1
 2      1
 3      3
 4      4
 5      5
 6      6
 7      7
 8      8
 9      9
10     10
# … with 121 more rows
## first three elements in the 7th column (as a vector)
interviews[1:3, 7]
# A tibble: 3 x 1
  rooms
  <int>
1     1
2     1
3     1
## the 3rd row of the data frame (as a data.frame)
interviews[3, ]
# A tibble: 1 x 14
  key_ID village interview_date      no_membrs years_liv respondent_wall…
   <int> <chr>   <dttm>                  <int>     <int> <chr>           
1      3 God     2016-11-17 00:00:00        10        15 burntbricks     
# … with 8 more variables: rooms <int>, memb_assoc <chr>,
#   affect_conflicts <chr>, liv_count <int>, items_owned <chr>,
#   no_meals <int>, months_lack_food <chr>, instanceID <chr>
## equivalent to head_interviews <- head(interviews)
head_interviews <- interviews[1:6, ]

: is a special function that creates numeric vectors of integers in increasing or decreasing order, test 1:10 and 10:1 for instance.

You can also exclude certain indices of a data frame using the “-” sign:

interviews[, -1]          # The whole data frame, except the first column
# A tibble: 131 x 13
   village interview_date      no_membrs years_liv respondent_wall… rooms
   <chr>   <dttm>                  <int>     <int> <chr>            <int>
 1 God     2016-11-17 00:00:00         3         4 muddaub              1
 2 God     2016-11-17 00:00:00         7         9 muddaub              1
 3 God     2016-11-17 00:00:00        10        15 burntbricks          1
 4 God     2016-11-17 00:00:00         7         6 burntbricks          1
 5 God     2016-11-17 00:00:00         7        40 burntbricks          1
 6 God     2016-11-17 00:00:00         3         3 muddaub              1
 7 God     2016-11-17 00:00:00         6        38 muddaub              1
 8 Chirod… 2016-11-16 00:00:00        12        70 burntbricks          3
 9 Chirod… 2016-11-16 00:00:00         8         6 burntbricks          1
10 Chirod… 2016-12-16 00:00:00        12        23 burntbricks          5
# … with 121 more rows, and 7 more variables: memb_assoc <chr>,
#   affect_conflicts <chr>, liv_count <int>, items_owned <chr>,
#   no_meals <int>, months_lack_food <chr>, instanceID <chr>
interviews[-c(7:131), ]   # Equivalent to head(interviews)
# A tibble: 6 x 14
  key_ID village interview_date      no_membrs years_liv respondent_wall…
   <int> <chr>   <dttm>                  <int>     <int> <chr>           
1      1 God     2016-11-17 00:00:00         3         4 muddaub         
2      1 God     2016-11-17 00:00:00         7         9 muddaub         
3      3 God     2016-11-17 00:00:00        10        15 burntbricks     
4      4 God     2016-11-17 00:00:00         7         6 burntbricks     
5      5 God     2016-11-17 00:00:00         7        40 burntbricks     
6      6 God     2016-11-17 00:00:00         3         3 muddaub         
# … with 8 more variables: rooms <int>, memb_assoc <chr>,
#   affect_conflicts <chr>, liv_count <int>, items_owned <chr>,
#   no_meals <int>, months_lack_food <chr>, instanceID <chr>

Data frames can be subset by calling indices (as shown previously), but also by calling their column names directly:

interviews["village"]       # Result is a data frame
interviews[, "village"]     # Result is a data frame
interviews[["village"]]     # Result is a vector
interviews$village          # Result is a vector

In RStudio, you can use the autocompletion feature to get the full and correct names of the columns.

Exercise

  1. Create a data frame (interviews_100) containing only the data in row 100 of the interviews dataset.

  2. Notice how nrow() gave you the number of rows in a data frame?

    • Use that number to pull out just that last row in the data frame.
    • Compare that with what you see as the last row using tail() to make sure it’s meeting expectations.
    • Pull out that last row using nrow() instead of the row number.
    • Create a new data frame (interviews_last) from that last row.
  3. Use nrow() to extract the row that is in the middle of the data frame. Store the content of this row in an object named interviews_middle.

  4. Combine nrow() with the - notation above to reproduce the behavior of head(interviews), keeping just the first through 6th rows of the interviews dataset.

Solution

## 1.
interviews_100 <- interviews[100, ]
## 2.
# Saving `n_rows` to improve readability and reduce duplication
n_rows <- nrow(interviews)
interviews_last <- interviews[n_rows, ]
# An alternative solution using tail()
interviews_last <- tail(interviews, 1)
## 3.
# Simple solution
interviews_middle <- interviews[n_rows / 2, ]
# but is this really the entry in the middle?
# This would be more accurate:
interviews_middle <- interviews[ceiling(n_rows / 2), ]
## 4.
interviews_head <- interviews[-(7:n_rows), ]

Take a moment to save, commit and push your changes to your script.

Factors

R has a special data class, called factor, to deal with categorical data that you may encounter when creating plots or doing statistical analyses. Factors are very useful and actually contribute to making R particularly well suited to working with data. So we are going to spend a little time introducing them.

Factors represent categorical data. They are stored as integers associated with labels and they can be ordered or unordered. While factors look (and often behave) like character vectors, they are actually treated as integer vectors by R. So you need to be very careful when treating them as strings.

Once created, factors can only contain a pre-defined set of values, known as levels. By default, R always sorts levels in alphabetical order. For instance, if you have a factor with 2 levels:

respondent_floor_type <- factor(c("earth", "cement", "cement", "earth"))

R will assign 1 to the level "cement" and 2 to the level "earth" (because c comes before e, even though the first element in this vector is "earth"). You can see this by using the function levels() and you can find the number of levels using nlevels():

levels(respondent_floor_type)
[1] "cement" "earth" 
nlevels(respondent_floor_type)
[1] 2

Sometimes, the order of the factors does not matter, other times you might want to specify the order because it is meaningful (e.g., “low”, “medium”, “high”), it improves your visualization, or it is required by a particular type of analysis. Here, one way to reorder our levels in the respondent_floor_type vector would be:

respondent_floor_type # current order
[1] earth  cement cement earth 
Levels: cement earth
respondent_floor_type <- factor(respondent_floor_type, levels = c("earth", "cement"))
respondent_floor_type # after re-ordering
[1] earth  cement cement earth 
Levels: earth cement

In R’s memory, these factors are represented by integers (1, 2), but are more informative than integers because factors are self describing: "cement", "earth" is more descriptive than 1, and 2. Which one is “earth”? You wouldn’t be able to tell just from the integer data. Factors, on the other hand, have this information built in. It is particularly helpful when there are many levels. It also makes renaming levels easier. Let’s say we made a mistake and need to recode “cement” to “brick”.

levels(respondent_floor_type)
[1] "earth"  "cement"
levels(respondent_floor_type)[2] <- "brick"
levels(respondent_floor_type)
[1] "earth" "brick"
respondent_floor_type
[1] earth brick brick earth
Levels: earth brick

Converting factors

If you need to convert a factor to a character vector, you use as.character(x).

as.character(respondent_floor_type)
[1] "earth" "brick" "brick" "earth"

Converting factors where the levels appear as numbers (such as concentration levels, or years) to a numeric vector is a little trickier. The as.numeric() function returns the index values of the factor, not its levels, so it will result in an entirely new (and unwanted in this case) set of numbers. One method to avoid this is to convert factors to characters, and then to numbers. Another method is to use the levels() function. Compare:

year_fct <- factor(c(1990, 1983, 1977, 1998, 1990))
as.numeric(year_fct)                     # Wrong! And there is no warning...
[1] 3 2 1 4 3
as.numeric(as.character(year_fct))       # Works...
[1] 1990 1983 1977 1998 1990
as.numeric(levels(year_fct))[year_fct]   # The recommended way.
[1] 1990 1983 1977 1998 1990

Notice that in the recommended levels() approach, three important steps occur:

Renaming factors

When your data is stored as a factor, you can use the plot() function to get a quick glance at the number of observations represented by each factor level. Let’s extract the affect_conflicts column from our data frame, convert it into a factor, and use it to look at the number of interview respondents who were or were not members of an irrigation association:

## create a vector from the data frame column "affect_conflicts"
affect_conflicts <- interviews$affect_conflicts
## convert it into a factor
affect_conflicts <- as.factor(affect_conflicts)
## let's see what it looks like
affect_conflicts
  [1] <NA>       once       <NA>       <NA>       <NA>       <NA>      
  [7] never      never      never      never      <NA>       never     
 [13] never      <NA>       once       <NA>       <NA>       <NA>      
 [19] <NA>       <NA>       never      <NA>       <NA>       never     
 [25] never      never      <NA>       more_once  frequently <NA>      
 [31] <NA>       more_once  more_once  more_once  more_once  once      
 [37] <NA>       never      <NA>       never      <NA>       never     
 [43] never      <NA>       never      once       once       <NA>      
 [49] <NA>       never      <NA>       never      frequently never     
 [55] <NA>       never      never      never      <NA>       never     
 [61] more_once  <NA>       <NA>       <NA>       once       frequently
 [67] more_once  more_once  more_once  more_once  more_once  <NA>      
 [73] never      once       <NA>       <NA>       frequently more_once 
 [79] never      more_once  more_once  more_once  more_once  never     
 [85] more_once  more_once  never      <NA>       more_once  more_once 
 [91] more_once  more_once  never      never      never      never     
 [97] never      frequently more_once  more_once  never      frequently
[103] never      <NA>       never      never      <NA>       more_once 
[109] never      <NA>       never      <NA>       <NA>       frequently
[115] frequently never      never      frequently never      never     
[121] never      never      never      never      never      once      
[127] more_once  more_once  more_once  more_once  <NA>      
Levels: frequently more_once never once
## bar plot of the number of interview respondents who were
## affected by conflicts with different frequency:
plot(affect_conflicts)

plot of chunk factor-plot-default-order

Looking at the plot compared to the output of the vector, we can see that in addition to “never”, “once”, “more_once” and “frequently”, there are some respondents for which the information about how much they were affected by conflicts hasn’t been recorded, and encoded as missing data. They do not appear on the plot. Let’s encode them differently so they can counted and visualized in our plot.

## Let's recreate the vector from the data frame column "affect_conflicts"
affect_conflicts <- interviews$affect_conflicts
## replace the missing data with "undetermined"
affect_conflicts[is.na(affect_conflicts)] <- "undetermined"
## convert it into a factor
affect_conflicts <- as.factor(affect_conflicts)
## let's see what it looks like
affect_conflicts
  [1] undetermined once         undetermined undetermined undetermined
  [6] undetermined never        never        never        never       
 [11] undetermined never        never        undetermined once        
 [16] undetermined undetermined undetermined undetermined undetermined
 [21] never        undetermined undetermined never        never       
 [26] never        undetermined more_once    frequently   undetermined
 [31] undetermined more_once    more_once    more_once    more_once   
 [36] once         undetermined never        undetermined never       
 [41] undetermined never        never        undetermined never       
 [46] once         once         undetermined undetermined never       
 [51] undetermined never        frequently   never        undetermined
 [56] never        never        never        undetermined never       
 [61] more_once    undetermined undetermined undetermined once        
 [66] frequently   more_once    more_once    more_once    more_once   
 [71] more_once    undetermined never        once         undetermined
 [76] undetermined frequently   more_once    never        more_once   
 [81] more_once    more_once    more_once    never        more_once   
 [86] more_once    never        undetermined more_once    more_once   
 [91] more_once    more_once    never        never        never       
 [96] never        never        frequently   more_once    more_once   
[101] never        frequently   never        undetermined never       
[106] never        undetermined more_once    never        undetermined
[111] never        undetermined undetermined frequently   frequently  
[116] never        never        frequently   never        never       
[121] never        never        never        never        never       
[126] once         more_once    more_once    more_once    more_once   
[131] undetermined
Levels: frequently more_once never once undetermined
## bar plot of the number of interview respondents who were
## affected by conflicts with different frequency:
plot(affect_conflicts)

plot of chunk factor-plot-reorder

Exercise

  • Rename the factor level “more_once” to “more than once”.

  • Can you recreate the barplot such that factor levels are ordered from least to most frequent (placing “undetermined” last)?

Solution

levels(affect_conflicts)[2] <- "more than once"
affect_conflicts <- factor(affect_conflicts, levels = c("never", "once", "more than once", "frequently", "undetermined"))
plot(affect_conflicts)

plot of chunk factor-plot-exercise

Now that you have created this plot, it may be a good time to commit the changes you made to the R script to your git repository.

Formatting Dates

One of the most common issues that new (and experienced!) R users have is converting date and time information into a variable that is appropriate and usable during analyses. As a reminder from earlier in this lesson, the best practice for dealing with date data is to ensure that each component of your date is stored as a separate variable. In our dataset, we have a column interview_date which contains information about the year, month, and day that the interview was conducted. Let’s convert those dates into three separate columns.

str(interviews)

We are going to use the package lubridate (which belongs to the tidyverse; learn more here) to work with dates. lubridate gets installed as part as the tidyverse installation. When you load the tidyverse (library(tidyverse)), the core packages (the packages used in most data analyses) get loaded. lubridate however does not belong to the core tidyverse, so you have to load it explicitly with library(lubridate)

Start by loading the required package:

library(lubridate)

The lubridate function ymd() takes a vector representing year, month, and day, and converts it to a Date vector. Date is a class of data recognized by R as being a date and can be manipulated as such. The argument that the function requires is flexible, but, as a best practice, is a character vector formatted as “YYYY-MM-DD”.

Let’s extract our interview_date column and inspect the structure:

dates <- interviews$interview_date
str(dates)
 POSIXct[1:131], format: "2016-11-17" "2016-11-17" "2016-11-17" "2016-11-17" "2016-11-17" ...

When we imported the data in R, read_csv() recognized that this column contained date information. We can now use the day(), month() and year() functions to extract this information from the date, and create new columns in our data frame to store it:

interviews$day <- day(dates)
interviews$month <- month(dates)
interviews$year <- year(dates)
interviews
# A tibble: 131 x 17
   key_ID village interview_date      no_membrs years_liv respondent_wall…
    <int> <chr>   <dttm>                  <int>     <int> <chr>           
 1      1 God     2016-11-17 00:00:00         3         4 muddaub         
 2      1 God     2016-11-17 00:00:00         7         9 muddaub         
 3      3 God     2016-11-17 00:00:00        10        15 burntbricks     
 4      4 God     2016-11-17 00:00:00         7         6 burntbricks     
 5      5 God     2016-11-17 00:00:00         7        40 burntbricks     
 6      6 God     2016-11-17 00:00:00         3         3 muddaub         
 7      7 God     2016-11-17 00:00:00         6        38 muddaub         
 8      8 Chirod… 2016-11-16 00:00:00        12        70 burntbricks     
 9      9 Chirod… 2016-11-16 00:00:00         8         6 burntbricks     
10     10 Chirod… 2016-12-16 00:00:00        12        23 burntbricks     
# … with 121 more rows, and 11 more variables: rooms <int>,
#   memb_assoc <chr>, affect_conflicts <chr>, liv_count <int>,
#   items_owned <chr>, no_meals <int>, months_lack_food <chr>,
#   instanceID <chr>, day <int>, month <dbl>, year <dbl>

Notice the three new columns at the end of our data frame.

Before you move on, commit and push if you haven’t done so already.

Key Points

  • Use read_csv to read tabular data in R.

  • Use factors to represent categorical data in R.