+ here and skimr

Author

Steen Flammild Harsted

Published

January 10, 2024



1 Getting started

Load the tidyverse and the here package

Code
library(tidyverse)
library(here)

 

1.1 here()

Try out the here() function

  • What happens if you write here() ?
  • What happens if you write here("raw_data") ?
  • Compare your output with your neighbors.
  • Compare your output with someone who has a different operating system (Windows, Mac, Linux) than you. (Hint: look for \ /)
  • Discuss if and how the here()function could ever be of any use to anybody


Load the soldiers dataset

Use the function read_csv2()
The file argument should be here("raw_data", "soldiers.csv")

Code
read_csv2(here("raw_data", "soldiers.csv"))


Assign the soldiers dataset to an object called soldiers

Great, now read the data again and assign it (remember <-) to an object called soldiers

Code
soldiers <- read_csv2(here("raw_data", "soldiers.csv"))


1.2 skim()

There are many ways to explore a dataframe. In this course we will take a shortcut and use the skim() function from the skimr package. The skim() function provides an exellent overview of a dataframe.

Load the skimr package and use the skim() function to get an impression of the soldiers dataset.

Discuss with your neighbor:

  • Nr of rows?
  • Nr of columns?
  • Missing values?
  • Types of variables?
  • How many unique values does the different character variables have?
  • Any fake data? (hint: Yes, for educational purposes I have added some fake data)
Code
library(skimr)
skim(soldiers)









2 dplyr

The main functions of dplyr that you will practice today are:

  • select()
  • filter()
  • summarise()
  • group_by()
  • arrange()
  • mutate()
  • count()



2.1 select()

select the columns subjectid, sex, age

Code
soldiers %>% 
  select(subjectid, sex, age)


select the columns 1, 3, 5:7

Code
soldiers %>% 
  select(1,3,5:7)


remove the columns 3:5

Code
soldiers %>% 
  select(-(3:5))


select all columns that contains the word “circumference”

Use one of the tidyselect helper functions.

Code
soldiers %>% 
  select(contains("circumference"))


remove all columns containing the letter “c”

Use one of the tidyselect helper functions.
Use a minus sign.

Code
soldiers %>% 
  select(-contains("c"))


select all columns that contains a “c” OR “x” OR “y” OR “z”

In R(and many other programming languages) the | sign is used as a logical operator for OR.

Code
soldiers %>% 
  select(contains("c") | contains("x") | contains("y") | contains("z"))


select all columns that contains a “c” OR “x” OR “y” OR “z”

This time use the tidyselect helper function called matches()
matches() allows you to use logical operators inside the function. E.g., matches("this|that")

Code
soldiers %>% 
  select(matches("c|x|y|z"))


Challenge: Why not always use matches()?

Use the preloaded iris data-set. (just write iris)
Try to use matches() to select all columns containing a “.”

Code
iris %>% 
  select(matches(".")) %>% 
  head() # This line is just to prevent a very long output.


Why did we select the Species column?
What happens if we use contains() instead

Code
iris %>% 
  select(contains(".")) %>% 
  head() # This line is just to prevent a very long output.


What is the difference in the output? Why is it different?

ANSWER

contains(): Contains a literal string.
matches(): Matches a regular expression.
In regular expressions . is a wildcard.

The wildcard . matches any character. For example, a.b matches any string that contains an “a”, and then any character and then “b”; and a.*b matches any string that contains an “a”, and then the character “b” at some later point.
https://en.wikipedia.org/wiki/Regular_expression









2.2 filter()

Restart R. (Click Session -> Restart R)
Load the tidyverse and the here package

Code
library(tidyverse)
library(here)


Load the soldiersdata and assign it to an object called soldiers

Code
soldiers <- read_csv2(here("raw_data", "soldiers.csv"))


Keep all rows where sex is Female:

???? == "Female"

Code
soldiers %>% filter(sex == "Female")


Keep all rows where weightkg is missing (NA value)

use the is.na() function

Code
soldiers %>% 
  filter(is.na(weightkg))


Keep all rows where WritingPreference is “Left hand” AND sex is “Female”

Code
soldiers %>% 
  filter(WritingPreference == "Left hand" ,  sex == "Female")  # you can use & instead of a ,


Keep all rows where WritingPreference is “Left hand” OR sex is “Female”

Code
soldiers %>% 
  filter(WritingPreference == "Left hand" |  sex == "Female")  


What is going wrong in this code?

soldiers %>% 
  select(1:5) %>% 
  filter(WritingPreference == "Left hand" |  sex == "Female")  

The error message is:

Error in `filter()`:
ℹ In argument: `WritingPreference == "Left hand" | sex == "Female"`.
Caused by error:
! object 'WritingPreference' not found
Run `rlang::last_error()` to see where the error occurred.
ANSWER
The variable WritingPreference was not selected in the first line.


Keep all rows where age is above 30 and the weightkg is below 600

Code
soldiers %>% 
  filter(age > 30, weightkg < 600)


Keep all rows where Ethnicity is either “Mexican” OR “Filipino” OR “Caribbean Islander”

you need to use %in% and c()

Code
soldiers %>% 
  filter(Ethnicity %in% c("Mexican", "Filipino", "Caribbean Islander"))

tldr - Use %in% instead of == when you want to filter for multiple values.

Read on if you want to understand why. (You don’t have to)

The code filter(Ethnicity == c("Mexican", "Filipino")) is likely not doing what you expect. The ‘==’ operator does an element-wise comparison, which means it compares the first element of ‘Ethnicity’ to the first element of the vector (“Mexican”), the second element of ‘Ethnicity’ to the second element of the vector (“Filipino”). The short vector is then recycled so now the third element of ‘Ethnicity’ is compared to the first element of the vector (“Mexican”), the fourth element of ‘Ethnicity’ to the second element of the vector (“Filipino”), and so on.

Inspect the differences in how may rows these lines of code produce

soldiers %>% 
  filter(Ethnicity %in% c("Mexican", "Filipino"))

soldiers %>% 
  filter(Ethnicity == c("Mexican", "Filipino"))

Run this code chunk line by line. Inspect the differences.

# Create a data frame
df <- data.frame(
  Ethnicity = c("Mexican", "Filipino", "Italian", "Mexican", "Italian", "Filipino"),
  Name = c("John", "Maria", "Luigi", "Carlos", "Francesco", "Jose"),
  stringsAsFactors = FALSE
)

# Investigate the data frame
df

# Filter using %in%
df %>% filter(Ethnicity %in% c("Mexican", "Filipino"))

# Filter using ==
df %>% filter(Ethnicity == c("Mexican", "Filipino"))









2.3 summarise()

Restart R. (Click Session -> Restart R)
Load the tidyverse and the here package

Code
library(tidyverse)
library(here)


Load the soldiersdata and assign it to an object called soldiers

Code
soldiers <- read_csv2(here("raw_data", "soldiers.csv"))


Calculate the mean and standard deviation of footlength

Code
soldiers %>% summarise(
  footlength_avg = mean(footlength),
  footlength_sd = sd(footlength))


Calculate the median and interquartile range of earlength

HINT use the IQR() function
Code
soldiers %>% 
  summarise(
    earlength_median = median(earlength),
    earlength_iqr = IQR(earlength))


Count the number of rows where WritingPreference is equal to “Right hand”

Code
soldiers %>%  
  summarise(
    n_righthanded = sum(WritingPreference == "Right hand"))


How old is the oldest soldier?

HINT if you can’t work out why get an NA value

Many Base R functions, including mean(), does not ignore NA values by default. This means that if the vector contains an NA value the result will be NA. Is this a good or bad thing?
You can set the argument na.rm = TRUE, to ignore missing values.

Code
soldiers %>% 
  summarise(
    age_max = max(age, na.rm = TRUE))


Calculate the mean weight of the Females

HINT if you can’t work out why get an NA value

Many Base R functions, including mean(), does not ignore NA values by default. This means that if the vector contains an NA value the result will be NA. Is this a good or bad thing?
You can set the argument na.rm = TRUE, to ignore missing values.

Code
soldiers %>% 
  filter(sex == "Female") %>% 
  summarise(
    weight_avg = mean(weightkg, na.rm = TRUE))


Calculate the range in weight (max-min) within Males

Code
soldiers %>% 
  filter(sex == "Male") %>% 
  summarise(
    weight_range = max(weightkg, na.rm = TRUE)-min(weightkg, na.rm = TRUE))









2.4 group_by() and arrange()

Restart R. (Click Session -> Restart R)
Load the tidyverse and the here package

Code
library(tidyverse)
library(here)


Load the soldiersdata and assign it to an object called soldiers

Code
soldiers <- read_csv2(here("raw_data", "soldiers.csv"))


Calculate the mean and sd of weightkg and age for all Installations

Code
soldiers %>% 
  group_by(Installation) %>% 
  summarise(weight_avg = mean(weightkg, na.rm = TRUE),
            weight_sd = sd(weightkg, na.rm = TRUE),
            age_avg = mean(age, na.rm = TRUE),
            age_sd = sd(age, na.rm = TRUE))


Calculate the mean and sd of weightkg and age for all Installations for both sexes

Code
soldiers %>% 
  group_by(Installation, sex) %>% 
  summarise(weight_avg = mean(weightkg, na.rm = TRUE),
            weight_sd = sd(weightkg, na.rm = TRUE),
            age_avg = mean(age, na.rm = TRUE),
            age_sd = sd(age, na.rm = TRUE))


Calcualate the average height for each Installation and count the number of missing values within each Installation

To count missings, use the functions sum() and is.na()

Code
soldiers %>% 
  group_by(Installation) %>% 
  summarise(height_avg = mean(Heightin, na.rm = TRUE),
            height_n_miss = sum(is.na(Heightin)))


As before, but now also add the number of observations (rows) within each Installation

Use n()

Code
soldiers %>% 
  group_by(Installation) %>% 
  summarise(height_avg = mean(Heightin, na.rm = TRUE),
            height_n_miss = sum(is.na(Heightin)),
            n = n())


As before, but now arrange the output after number of soldiers at each Installation in descending order.

Code
soldiers %>% 
  group_by(Installation) %>% 
  summarise(height_avg = mean(Heightin, na.rm = TRUE),
            height_n_miss = sum(is.na(Heightin)),
            n = n()) %>% 
  arrange(desc(n))









2.5 mutate()

Restart R. (Click Session -> Restart R)
Load the tidyverse and the here package

Code
library(tidyverse)
library(here)


Load the soldiersdata and assign it to an object called soldiers

Code
soldiers <- read_csv2(here("raw_data", "soldiers.csv"))


Add a column called heightcm with the height of the soldiers in cm

  • Update the soldiers dataset with the new variable
  • place the new variable after Heightin
Code
soldiers <- soldiers %>% 
  mutate(
    heightcm = Heightin * 2.54,
    .after = Heightin)


Update the weightkg column to kg instead of kg*10

  • Update the soldiers dataset with the new weightkg column
Code
soldiers <- soldiers %>% 
  mutate(
    weightkg = weightkg/10
    )


Add a column called BMI with the Body mass index (BMI) of the soldiers

BMI

  • Update the soldiers dataset to with the new variable
  • place the new variable after weightkg
Code
soldiers <- soldiers %>% 
  mutate(BMI = weightkg/(heightcm/100)^2,
         .after = weightkg)


Add a column called obese that contains the value TRUE if BMI is > 30

Code
soldiers %>% 
  mutate(
    obese = if_else(BMI > 30, TRUE, FALSE),
    .before = 1 # This line code just places the variable at the front
  )

# OR

soldiers %>% 
  mutate(
    obese = BMI > 30,
    .before = 1 # This line code just places the variable at the front
  )


Inspect the below table from Wikipedia

Category BMI (kg/m2)
Underweight (Severe thinness) < 16.0
Underweight (Moderate thinness) 16.0 – 16.9
Underweight (Mild thinness) 17.0 – 18.4
Normal range 18.5 – 24.9
Overweight (Pre-obese) 25.0 – 29.9
Obese (Class I) 30.0 – 34.9
Obese (Class II) 35.0 – 39.9
Obese (Class III) >= 40.0


Create the variable category that tells us whether the soldiers are “Underweight”, “Normal range”, “Overweight”, or “Obese”

  • Update the soldiers dataset with the new variable
  • place the new variable after BMI

Use case_when()

soldiers %>% 
  mutate(
    category = ????
    )
soldiers %>% 
  mutate(
    category = case_when(
      #TEST HERE ~ OUTPUT, 
      #TEST HERE ~ OUTPUT,
      #TEST HERE ~ OUTPUT,
      #.default = OUTPUT
    )
    )
Code
soldiers <- soldiers %>% 
  mutate(
    category = case_when(
      BMI < 18.5 ~ "Underweight",
      BMI < 25   ~ "Normal range",
      BMI < 30   ~ "Overweight",
      BMI >= 30  ~ "Obese",
      .default = NA),
    .after = BMI
    )









2.6 count()

For simple counting count() is faster than summarise(n = n()) or mutate(n = n())



What is this code equivalent to?

diamonds %>% 
  count()
ANSWER count() works like summarise(n = n())



What is this code equivalent to?

diamonds %>% 
  count(cut)
Answer
# The code is equivalent to:
diamonds %>% group_by(cut) %>% summarise(n = n())

# OR

diamonds %>% summarise(n = n(), .by = cut)



What is this code equivalent to?

diamonds %>% 
  count(cut, color)
Answer
# The code is equivalent to:
diamonds %>% group_by(cut, color) %>% summarise(n = n())

# OR

diamonds %>% summarise(n = n(), .by = c(cut, color))


# However, notice that the first solution returns a grouped tibble



What is this code equivalent to?

diamonds %>% 
  add_count()
ANSWER add_count() works like mutate(n = n())



Count the number of diamonds within each type of cut and calculate the percentage of each cut

Code
diamonds %>% 
  count(cut) %>% 
  mutate(percent = n/sum(n)*100)



Inspect the output of this code. What is going wrong?

diamonds %>% 
  group_by(cut) %>% 
  count() %>% 
  mutate(percent = n/sum(n)*100)
# A tibble: 5 × 3
# Groups:   cut [5]
  cut           n percent
  <ord>     <int>   <dbl>
1 Fair       1610     100
2 Good       4906     100
3 Very Good 12082     100
4 Premium   13791     100
5 Ideal     21551     100