Data types and useful comparisons

DATA 121 | Friday, September 25, 2026

  • Practice core tidyverse functions
  • Data Day 1
  • Data visualization with ggplot!

Open your RStudio project

Practice: market segments

With a peer | 6 minutes

How many bookings are in each market segment, and what is their mean booked stay length?

Predict the table’s structure, then run the code and inspect the result.

  • Locate the grouped pattern in IDS 4.4.3 or R4DS 3.5.2 for reference
  • Reference and adapt the code from the previous example to complete the task.

Write code:

  • Start with the hotel_stays object

  • Group by market_segment.

  • Keep both summaries.

  • Summarize the number of bookings per market segment and the mean total_stay for each market segment.

  • Save as segment_summary, order by bookings, largest to smallest.

Check your results


Your result should have eight rows, with these three first:

market_segment bookings mean_nights
Online TA 56477 3.57
Offline TA/TO 24219 3.90
Groups 19811 2.99
  • A finding: Online TA has the most bookings, its mean booked stay is about 3.57 nights.

The full code and all eight rows are in the reference slides at the end of the slides

Park observations example

What do these columns mean?

Park observations

Park ID Access Condition Trees Temp. °C
001 Public Good 12 29.4
002 Private Fair 0 31.2
003 Public Poor NA NA

One row represents one park at the time of observation.

Which columns identify or describe categories? Which measure quantities?

Categories can have an order

Nominal: categories without a meaningful order or ranking.

  • Park ID: 001, 002, 003.
  • Access: Public, Private.

Ordinal: categories with an order, without established equal gaps.

  • Condition: Poor < Fair < Good.
  • Coding those ratings 1, 2, 3 preserves order; it does not establish equal distances.

Can we say that the change from Poor to Fair necessarily the same as Fair to Good?

Quantities can be counts or measurements

Variable Possible values Numerical kind
Number of trees 0, 1, 2, … Discrete
Recorded year opened Whole years Discrete
Temperature Values along a scale Continuous
Area Values along a scale Continuous
  • Discrete quantities have separated possible values.

  • Continuous quantities can take values between recorded measurements.

Note: Rounding a temperature to a whole degree does not turn it into a count.

Which comparisons make sense?

Measurement level What it supports Example
Nominal Same or different category Public / Private
Ordinal Category and order Poor < Fair < Good
Interval Order and equal differences Temperature in °C
Ratio Equal differences and meaningful ratios Tree count or area
  • These describe the meaning of a measurement

  • They are separate from discrete/continuous and from R’s storage types (num, chr, dbl)

Is 40°C twice as hot as 20°C?

Celsius Fahrenheit
20°C 68°F
40°C 104°F
  • 40 / 20 = 2, but 104 / 68 ≈ 1.53.
  • The temperatures did not change when we changed units.
  • The difference is 20°C, equivalent to a difference of 36°F.

Celsius and Fahrenheit have reference zeros. Ratios of their values do not measure “times as hot.”

What does zero mean?

Variable A useful comparison
Tree count 40 trees is twice 20 trees.
Area 2,000 m² is twice 1,000 m².
Calendar year 2010 is 10 years after 2000.
Elapsed time 20 minutes is twice 10 minutes.

Zero trees means none counted. Year zero is a calendar reference.

Calendar date and elapsed time support different comparisons.

R’s representation and a variable’s meaning

park_id <- c("001", "002", "003")
condition_code <- c(3, 2, 1)  # Good, Fair, Poor

class(park_id)         # "character"
class(condition_code)  # "numeric"
  • Quoted IDs preserve leading zeros.
  • Numerical condition codes still represent ordered categories.
  • class() helps explain R’s behavior; documentation explains the values.

Zero and missing are different

trees <- c(12, 0, NA)

mean(trees)                 # NA
mean(trees, na.rm = TRUE)   # 6
  • 0: a recorded count of no trees.
  • NA: the count is missing.
  • The mean of 6 uses two observed counts, including the zero.

Report what was missing and which observations entered the summary.

Housing inspection data

Invented observations · one row per building

ID Use Condition Built Vacant units Area (m²)
001 Residential Good 1920 0 240.5
002 Mixed use Poor 1960 4 481.0
003 Residential Fair 2000 NA 360.0
004 Mixed use Good 1985 2 600.8

Built is the recorded calendar year. Vacant units is a count. NA means the count is missing.

Classify and explain

With a partner | 4 minutes

For each housing variable:

  1. Decide whether it is categorical or numerical.
  2. Identify the measurement level and explain your choice.
  3. For numerical variables, distinguish discrete from continuous.

Then choose one variable. Propose a useful summary and one misleading calculation.

Use the examples in these slides. Record which example helped you decide.

Check your classifications

Variable Kind Measurement level
ID Categorical identifier Nominal
Use Categorical Nominal
Condition Categorical, ordered Ordinal
Built Numerical, discrete whole years Interval
Vacant units Numerical, discrete count Ratio
Area Numerical, continuous Ratio

The explanation matters: what does a difference, order, or ratio mean here?

Choose a summary and a plot

Question Variable Useful starting point
How many bookings at each hotel? Hotel category Counts and a bar chart
How far ahead are bookings entered? Lead time in days Distribution and a histogram
What is the mean booked stay by hotel? Category + nights Grouped counts and means
  • A mean gives one summary.
  • A distribution shows how values are spread out.
  • Keep the observational unit in the answer: one row is one booking.

Set up today’s document

Save a new Quarto document in activities in your course project.

library(tidyverse)
hotels <- read_csv("data/hotels.csv")

hotel_stays <- hotels |>
  mutate(total_nights = stays_in_weekend_nights +
                       stays_in_week_nights)

Run these chunks before the plot examples. If R is running from activities, use ../data/hotels.csv.

Grammar of Data Wrangling

A grammar of data wrangling…

… based on the concepts of functions as verbs that manipulate data frames

Functions that manipulate existing data:

  • select: pick columns by name
  • arrange: reorder rows
  • slice: pick rows using index(es)
  • filter: pick rows matching criteria
  • distinct: filter for unique rows

A grammar of data wrangling…

… based on the concepts of functions as verbs that manipulate data frames

Functions that generate new data:

  • mutate: add new variables
  • summarize: reduce variables to values
  • group_by: for grouped operations
  • count: to create frequency tables

Rules of dplyr functions

  • First argument: always a data frame
  • Second argument: what to do with the data
  • Always return a data frame
  • Don’t modify in place, without creating a separate copy of the data structure

dplyr functions expect a data frame as input, produce a data frame as output

Data: Hotel bookings1

  • Data from two hotels: one resort and one city hotel
  • Observations: Each row represents a hotel booking
  • Goal for original data collection: Development of prediction models to classify likelihood that a hotel’s booking’s would be cancelled (Antonia et al., 2019)
hotels <- read_csv("data/hotels.csv")

First look: Variables

names(hotels)
 [1] "hotel"                          "is_canceled"                   
 [3] "lead_time"                      "arrival_date_year"             
 [5] "arrival_date_month"             "arrival_date_week_number"      
 [7] "arrival_date_day_of_month"      "stays_in_weekend_nights"       
 [9] "stays_in_week_nights"           "adults"                        
[11] "children"                       "babies"                        
[13] "meal"                           "country"                       
[15] "market_segment"                 "distribution_channel"          
[17] "is_repeated_guest"              "previous_cancellations"        
[19] "previous_bookings_not_canceled" "reserved_room_type"            
[21] "assigned_room_type"             "booking_changes"               
[23] "deposit_type"                   "agent"                         
[25] "company"                        "days_in_waiting_list"          
[27] "customer_type"                  "adr"                           
[29] "required_car_parking_spaces"    "total_of_special_requests"     
[31] "reservation_status"             "reservation_status_date"       

Second look: Overview

glimpse(hotels)
Rows: 119,390
Columns: 32
$ hotel                          <chr> "Resort Hotel", "Resort Hotel", "Resort…
$ is_canceled                    <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, …
$ lead_time                      <dbl> 342, 737, 7, 13, 14, 14, 0, 9, 85, 75, …
$ arrival_date_year              <dbl> 2015, 2015, 2015, 2015, 2015, 2015, 201…
$ arrival_date_month             <chr> "July", "July", "July", "July", "July",…
$ arrival_date_week_number       <dbl> 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,…
$ arrival_date_day_of_month      <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
$ stays_in_weekend_nights        <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ stays_in_week_nights           <dbl> 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 4, 4, 4, …
$ adults                         <dbl> 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, …
$ children                       <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ babies                         <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ meal                           <chr> "BB", "BB", "BB", "BB", "BB", "BB", "BB…
$ country                        <chr> "PRT", "PRT", "GBR", "GBR", "GBR", "GBR…
$ market_segment                 <chr> "Direct", "Direct", "Direct", "Corporat…
$ distribution_channel           <chr> "Direct", "Direct", "Direct", "Corporat…
$ is_repeated_guest              <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ previous_cancellations         <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ previous_bookings_not_canceled <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ reserved_room_type             <chr> "C", "C", "A", "A", "A", "A", "C", "C",…
$ assigned_room_type             <chr> "C", "C", "C", "A", "A", "A", "C", "C",…
$ booking_changes                <dbl> 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ deposit_type                   <chr> "No Deposit", "No Deposit", "No Deposit…
$ agent                          <chr> "NULL", "NULL", "NULL", "304", "240", "…
$ company                        <chr> "NULL", "NULL", "NULL", "NULL", "NULL",…
$ days_in_waiting_list           <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ customer_type                  <chr> "Transient", "Transient", "Transient", …
$ adr                            <dbl> 0.00, 0.00, 75.00, 75.00, 98.00, 98.00,…
$ required_car_parking_spaces    <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ total_of_special_requests      <dbl> 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 3, …
$ reservation_status             <chr> "Check-Out", "Check-Out", "Check-Out", …
$ reservation_status_date        <date> 2015-07-01, 2015-07-01, 2015-07-02, 20…

Changing data frames

Adding a new variable (column)

mutate(hotels, little_ones = children + babies)
names(hotels)
 [1] "hotel"                          "is_canceled"                   
 [3] "lead_time"                      "arrival_date_year"             
 [5] "arrival_date_month"             "arrival_date_week_number"      
 [7] "arrival_date_day_of_month"      "stays_in_weekend_nights"       
 [9] "stays_in_week_nights"           "adults"                        
[11] "children"                       "babies"                        
[13] "meal"                           "country"                       
[15] "market_segment"                 "distribution_channel"          
[17] "is_repeated_guest"              "previous_cancellations"        
[19] "previous_bookings_not_canceled" "reserved_room_type"            
[21] "assigned_room_type"             "booking_changes"               
[23] "deposit_type"                   "agent"                         
[25] "company"                        "days_in_waiting_list"          
[27] "customer_type"                  "adr"                           
[29] "required_car_parking_spaces"    "total_of_special_requests"     
[31] "reservation_status"             "reservation_status_date"       

Adding a new variable (column) - 2nd attempt

hotels <- mutate(hotels, little_ones = children + babies)
names(hotels)
 [1] "hotel"                          "is_canceled"                   
 [3] "lead_time"                      "arrival_date_year"             
 [5] "arrival_date_month"             "arrival_date_week_number"      
 [7] "arrival_date_day_of_month"      "stays_in_weekend_nights"       
 [9] "stays_in_week_nights"           "adults"                        
[11] "children"                       "babies"                        
[13] "meal"                           "country"                       
[15] "market_segment"                 "distribution_channel"          
[17] "is_repeated_guest"              "previous_cancellations"        
[19] "previous_bookings_not_canceled" "reserved_room_type"            
[21] "assigned_room_type"             "booking_changes"               
[23] "deposit_type"                   "agent"                         
[25] "company"                        "days_in_waiting_list"          
[27] "customer_type"                  "adr"                           
[29] "required_car_parking_spaces"    "total_of_special_requests"     
[31] "reservation_status"             "reservation_status_date"       
[33] "little_ones"                   

Pipes

What is a pipe?

In programming, a pipe is a technique for passing information from one process to another.

  • Start with the data frame hotels, and pass it to the select() function,
hotels |> 
  select(hotel, lead_time) |>
  arrange(desc(lead_time))
# A tibble: 119,390 × 2
   hotel        lead_time
   <chr>            <dbl>
 1 Resort Hotel       737
 2 Resort Hotel       709
 3 City Hotel         629
 4 City Hotel         629
 5 City Hotel         629
 6 City Hotel         629
 7 City Hotel         629
 8 City Hotel         629
 9 City Hotel         629
10 City Hotel         629
# ℹ 119,380 more rows

What is a pipe?

In programming, a pipe is a technique for passing information from one process to another.

  • Start with the data frame hotels, and pass it to the select() function,
  • then we select the variables hotel and lead_time,
hotels |>
  select(hotel, lead_time) |> 
  arrange(desc(lead_time))
# A tibble: 119,390 × 2
   hotel        lead_time
   <chr>            <dbl>
 1 Resort Hotel       737
 2 Resort Hotel       709
 3 City Hotel         629
 4 City Hotel         629
 5 City Hotel         629
 6 City Hotel         629
 7 City Hotel         629
 8 City Hotel         629
 9 City Hotel         629
10 City Hotel         629
# ℹ 119,380 more rows

What is a pipe?

In programming, a pipe is a technique for passing information from one process to another.

  • Start with the data frame hotels, and pass it to the select() function,
  • then we select the variables hotel and lead_time,
  • and then we arrange the data frame by lead_time in descending order.
hotels |>
  select(hotel, lead_time) |> 
  arrange(desc(lead_time)) 
# A tibble: 119,390 × 2
   hotel        lead_time
   <chr>            <dbl>
 1 Resort Hotel       737
 2 Resort Hotel       709
 3 City Hotel         629
 4 City Hotel         629
 5 City Hotel         629
 6 City Hotel         629
 7 City Hotel         629
 8 City Hotel         629
 9 City Hotel         629
10 City Hotel         629
# ℹ 119,380 more rows

Adding a new variable (column) - 3rd attempt

hotels <- hotels |> mutate(little_ones = children + babies)

This becomes useful when we want to chain together several operations.

filter

filter to select a subset of rows

# bookings in City Hotels
hotels |>
  filter(hotel == "City Hotel") 
# A tibble: 79,330 × 33
   hotel      is_canceled lead_time arrival_date_year arrival_date_month
   <chr>            <dbl>     <dbl>             <dbl> <chr>             
 1 City Hotel           0         6              2015 July              
 2 City Hotel           1        88              2015 July              
 3 City Hotel           1        65              2015 July              
 4 City Hotel           1        92              2015 July              
 5 City Hotel           1       100              2015 July              
 6 City Hotel           1        79              2015 July              
 7 City Hotel           0         3              2015 July              
 8 City Hotel           1        63              2015 July              
 9 City Hotel           1        62              2015 July              
10 City Hotel           1        62              2015 July              
# ℹ 79,320 more rows
# ℹ 28 more variables: arrival_date_week_number <dbl>,
#   arrival_date_day_of_month <dbl>, stays_in_weekend_nights <dbl>,
#   stays_in_week_nights <dbl>, adults <dbl>, children <dbl>, babies <dbl>,
#   meal <chr>, country <chr>, market_segment <chr>,
#   distribution_channel <chr>, is_repeated_guest <dbl>,
#   previous_cancellations <dbl>, previous_bookings_not_canceled <dbl>, …

Logical operators in R


operator definition operator definition
< less than x | y x OR y
<= less than or equal to is.na(x) test if x is NA
> greater than !is.na(x) test if x is not NA
>= greater than or equal to x %in% y test if x is in y
== exactly equal to !(x %in% y) test if x is not in y
!= not equal to !x not x
x & y x AND y

filter for many conditions at once

hotels |>
  filter( 
    adults == 0,     
    children >= 1    
    ) |> 
  select(adults, babies, children)
# A tibble: 223 × 3
   adults babies children
    <dbl>  <dbl>    <dbl>
 1      0      0        3
 2      0      0        2
 3      0      0        2
 4      0      0        2
 5      0      0        2
 6      0      0        3
 7      0      1        2
 8      0      0        2
 9      0      0        2
10      0      0        2
# ℹ 213 more rows

select to select variables (columns)

Select a single column

View only lead_time (number of days between booking and arrival date):

select(hotels, lead_time)
# A tibble: 119,390 × 1
   lead_time
       <dbl>
 1       342
 2       737
 3         7
 4        13
 5        14
 6        14
 7         0
 8         9
 9        85
10        75
# ℹ 119,380 more rows

Select a single column

select(
  hotels, 
  lead_time
  )
  • Start with the function (a verb): select()

Select a single column

select(
  hotels,    
  lead_time   
  )
  • Start with the function (a verb): select()
  • First argument: data frame we’re working with , hotels

Select a single column

select( 
  hotels, 
  lead_time 
  )
  • Start with the function (a verb): select()
  • First argument: data frame we’re working with , hotels
  • Second argument: variable we want to select, lead_time

Select a single column

select( 
  hotels, 
  lead_time
  )
# A tibble: 119,390 × 1
   lead_time
       <dbl>
 1       342
 2       737
 3         7
 4        13
 5        14
 6        14
 7         0
 8         9
 9        85
10        75
# ℹ 119,380 more rows

  • Start with the function (a verb): select()
  • First argument: data frame we’re working with , hotels
  • Second argument: variable we want to select, lead_time
  • Result: data frame with 119390 rows and 1 column

Tip

dplyr functions always expect a data frame and always yield a data frame.

select(hotels, lead_time)
# A tibble: 119,390 × 1
   lead_time
       <dbl>
 1       342
 2       737
 3         7
 4        13
 5        14
 6        14
 7         0
 8         9
 9        85
10        75
# ℹ 119,380 more rows

select to keep variables (columns)

hotels |>
  select(hotel, lead_time) 
# A tibble: 119,390 × 2
   hotel        lead_time
   <chr>            <dbl>
 1 Resort Hotel       342
 2 Resort Hotel       737
 3 Resort Hotel         7
 4 Resort Hotel        13
 5 Resort Hotel        14
 6 Resort Hotel        14
 7 Resort Hotel         0
 8 Resort Hotel         9
 9 Resort Hotel        85
10 Resort Hotel        75
# ℹ 119,380 more rows

select to exclude variables

hotels |>
  select(!agent) 
# A tibble: 119,390 × 32
   hotel        is_canceled lead_time arrival_date_year arrival_date_month
   <chr>              <dbl>     <dbl>             <dbl> <chr>             
 1 Resort Hotel           0       342              2015 July              
 2 Resort Hotel           0       737              2015 July              
 3 Resort Hotel           0         7              2015 July              
 4 Resort Hotel           0        13              2015 July              
 5 Resort Hotel           0        14              2015 July              
 6 Resort Hotel           0        14              2015 July              
 7 Resort Hotel           0         0              2015 July              
 8 Resort Hotel           0         9              2015 July              
 9 Resort Hotel           1        85              2015 July              
10 Resort Hotel           1        75              2015 July              
# ℹ 119,380 more rows
# ℹ 27 more variables: arrival_date_week_number <dbl>,
#   arrival_date_day_of_month <dbl>, stays_in_weekend_nights <dbl>,
#   stays_in_week_nights <dbl>, adults <dbl>, children <dbl>, babies <dbl>,
#   meal <chr>, country <chr>, market_segment <chr>,
#   distribution_channel <chr>, is_repeated_guest <dbl>,
#   previous_cancellations <dbl>, previous_bookings_not_canceled <dbl>, …

select a range of variables

hotels |>
  select(hotel:arrival_date_month) 
# A tibble: 119,390 × 5
   hotel        is_canceled lead_time arrival_date_year arrival_date_month
   <chr>              <dbl>     <dbl>             <dbl> <chr>             
 1 Resort Hotel           0       342              2015 July              
 2 Resort Hotel           0       737              2015 July              
 3 Resort Hotel           0         7              2015 July              
 4 Resort Hotel           0        13              2015 July              
 5 Resort Hotel           0        14              2015 July              
 6 Resort Hotel           0        14              2015 July              
 7 Resort Hotel           0         0              2015 July              
 8 Resort Hotel           0         9              2015 July              
 9 Resort Hotel           1        85              2015 July              
10 Resort Hotel           1        75              2015 July              
# ℹ 119,380 more rows

select variables with certain characteristics

hotels |>
  select(starts_with("arrival")) 
# A tibble: 119,390 × 4
   arrival_date_year arrival_date_month arrival_date_week_number
               <dbl> <chr>                                 <dbl>
 1              2015 July                                     27
 2              2015 July                                     27
 3              2015 July                                     27
 4              2015 July                                     27
 5              2015 July                                     27
 6              2015 July                                     27
 7              2015 July                                     27
 8              2015 July                                     27
 9              2015 July                                     27
10              2015 July                                     27
# ℹ 119,380 more rows
# ℹ 1 more variable: arrival_date_day_of_month <dbl>

select variables with certain characteristics

hotels |>
  select(ends_with("type")) 
# A tibble: 119,390 × 4
   reserved_room_type assigned_room_type deposit_type customer_type
   <chr>              <chr>              <chr>        <chr>        
 1 C                  C                  No Deposit   Transient    
 2 C                  C                  No Deposit   Transient    
 3 A                  C                  No Deposit   Transient    
 4 A                  A                  No Deposit   Transient    
 5 A                  A                  No Deposit   Transient    
 6 A                  A                  No Deposit   Transient    
 7 C                  C                  No Deposit   Transient    
 8 C                  C                  No Deposit   Transient    
 9 A                  A                  No Deposit   Transient    
10 D                  D                  No Deposit   Transient    
# ℹ 119,380 more rows

Select helpers

  • starts_with(): Starts with a prefix
  • ends_with(): Ends with a suffix
  • contains(): Contains a literal string
  • num_range(): Matches a numerical range like x01, x02, x03
  • one_of(): Matches variable names in a character vector
  • everything(): Matches all variables
  • last_col(): Select last variable, possibly with an offset
  • matches(): Matches a regular expression (a sequence of symbols/characters expressing a string/pattern to be searched for within text)

See help for any of these functions for more info, e.g. ?everything.

Select multiple columns

View only the hotel type and lead_time:

select(hotels, hotel, lead_time)
# A tibble: 119,390 × 2
   hotel        lead_time
   <chr>            <dbl>
 1 Resort Hotel       342
 2 Resort Hotel       737
 3 Resort Hotel         7
 4 Resort Hotel        13
 5 Resort Hotel        14
 6 Resort Hotel        14
 7 Resort Hotel         0
 8 Resort Hotel         9
 9 Resort Hotel        85
10 Resort Hotel        75
# ℹ 119,380 more rows

What if we wanted to select these columns, and then arrange the data in descending order of lead time?

Data wrangling, step-by-step

Select:

hotels |>
  select(hotel, lead_time)
# A tibble: 119,390 × 2
   hotel        lead_time
   <chr>            <dbl>
 1 Resort Hotel       342
 2 Resort Hotel       737
 3 Resort Hotel         7
 4 Resort Hotel        13
 5 Resort Hotel        14
 6 Resort Hotel        14
 7 Resort Hotel         0
 8 Resort Hotel         9
 9 Resort Hotel        85
10 Resort Hotel        75
# ℹ 119,380 more rows

Select, then arrange:

hotels |>
  select(hotel, lead_time) |>
  arrange(desc(lead_time))
# A tibble: 119,390 × 2
   hotel        lead_time
   <chr>            <dbl>
 1 Resort Hotel       737
 2 Resort Hotel       709
 3 City Hotel         629
 4 City Hotel         629
 5 City Hotel         629
 6 City Hotel         629
 7 City Hotel         629
 8 City Hotel         629
 9 City Hotel         629
10 City Hotel         629
# ℹ 119,380 more rows

arrange in ascending / descending order

hotels |>
  select(adults, children, babies) |>
  arrange(babies)
# A tibble: 119,390 × 3
   adults children babies
    <dbl>    <dbl>  <dbl>
 1      2        0      0
 2      2        0      0
 3      1        0      0
 4      1        0      0
 5      2        0      0
 6      2        0      0
 7      2        0      0
 8      2        0      0
 9      2        0      0
10      2        0      0
# ℹ 119,380 more rows
hotels |>
  select(adults, children, babies) |>
  arrange(desc(babies)) 
# A tibble: 119,390 × 3
   adults children babies
    <dbl>    <dbl>  <dbl>
 1      2        0     10
 2      1        0      9
 3      2        0      2
 4      2        0      2
 5      2        0      2
 6      2        0      2
 7      2        0      2
 8      2        0      2
 9      2        0      2
10      2        0      2
# ℹ 119,380 more rows

slice for certain row numbers

# first five
hotels |>
  slice(1:5) 
# A tibble: 5 × 33
  hotel        is_canceled lead_time arrival_date_year arrival_date_month
  <chr>              <dbl>     <dbl>             <dbl> <chr>             
1 Resort Hotel           0       342              2015 July              
2 Resort Hotel           0       737              2015 July              
3 Resort Hotel           0         7              2015 July              
4 Resort Hotel           0        13              2015 July              
5 Resort Hotel           0        14              2015 July              
# ℹ 28 more variables: arrival_date_week_number <dbl>,
#   arrival_date_day_of_month <dbl>, stays_in_weekend_nights <dbl>,
#   stays_in_week_nights <dbl>, adults <dbl>, children <dbl>, babies <dbl>,
#   meal <chr>, country <chr>, market_segment <chr>,
#   distribution_channel <chr>, is_repeated_guest <dbl>,
#   previous_cancellations <dbl>, previous_bookings_not_canceled <dbl>,
#   reserved_room_type <chr>, assigned_room_type <chr>, …

Tip

In R, you can use the # for adding comments to your code. Any text following # will be printed as is, and won’t be run as R code. This is useful for leaving comments in your code and for temporarily disabling certain lines of code while debugging.

hotels |>
  # slice the first five rows  # this line is a comment
  #select(hotel) |>           # this one doesn't run
  slice(1:5)                   # this line runs
# A tibble: 5 × 33
  hotel        is_canceled lead_time arrival_date_year arrival_date_month
  <chr>              <dbl>     <dbl>             <dbl> <chr>             
1 Resort Hotel           0       342              2015 July              
2 Resort Hotel           0       737              2015 July              
3 Resort Hotel           0         7              2015 July              
4 Resort Hotel           0        13              2015 July              
5 Resort Hotel           0        14              2015 July              
# ℹ 28 more variables: arrival_date_week_number <dbl>,
#   arrival_date_day_of_month <dbl>, stays_in_weekend_nights <dbl>,
#   stays_in_week_nights <dbl>, adults <dbl>, children <dbl>, babies <dbl>,
#   meal <chr>, country <chr>, market_segment <chr>,
#   distribution_channel <chr>, is_repeated_guest <dbl>,
#   previous_cancellations <dbl>, previous_bookings_not_canceled <dbl>,
#   reserved_room_type <chr>, assigned_room_type <chr>, …

distinct and count

distinct to filter for unique rows

And arrange to order alphabetically

hotels |> 
  distinct(market_segment) |> 
  arrange(market_segment)
# A tibble: 8 × 1
  market_segment
  <chr>         
1 Aviation      
2 Complementary 
3 Corporate     
4 Direct        
5 Groups        
6 Offline TA/TO 
7 Online TA     
8 Undefined     
hotels |> 
  distinct(hotel, market_segment) |> 
  arrange(hotel, market_segment)
# A tibble: 14 × 2
   hotel        market_segment
   <chr>        <chr>         
 1 City Hotel   Aviation      
 2 City Hotel   Complementary 
 3 City Hotel   Corporate     
 4 City Hotel   Direct        
 5 City Hotel   Groups        
 6 City Hotel   Offline TA/TO 
 7 City Hotel   Online TA     
 8 City Hotel   Undefined     
 9 Resort Hotel Complementary 
10 Resort Hotel Corporate     
11 Resort Hotel Direct        
12 Resort Hotel Groups        
13 Resort Hotel Offline TA/TO 
14 Resort Hotel Online TA     

count to create frequency tables

# alphabetical order by default
hotels |>
  count(market_segment) 
# A tibble: 8 × 2
  market_segment     n
  <chr>          <int>
1 Aviation         237
2 Complementary    743
3 Corporate       5295
4 Direct         12606
5 Groups         19811
6 Offline TA/TO  24219
7 Online TA      56477
8 Undefined          2
# descending frequency order
hotels |>
  count(market_segment, 
        sort = TRUE) 
# A tibble: 8 × 2
  market_segment     n
  <chr>          <int>
1 Online TA      56477
2 Offline TA/TO  24219
3 Groups         19811
4 Direct         12606
5 Corporate       5295
6 Complementary    743
7 Aviation         237
8 Undefined          2

count and arrange

# ascending frequency order
hotels |>
  count(market_segment) |>
  arrange(n) 
# A tibble: 8 × 2
  market_segment     n
  <chr>          <int>
1 Undefined          2
2 Aviation         237
3 Complementary    743
4 Corporate       5295
5 Direct         12606
6 Groups         19811
7 Offline TA/TO  24219
8 Online TA      56477
# descending frequency order
# just like adding sort = TRUE
hotels |>
  count(market_segment) |>
  arrange(desc(n)) 
# A tibble: 8 × 2
  market_segment     n
  <chr>          <int>
1 Online TA      56477
2 Offline TA/TO  24219
3 Groups         19811
4 Direct         12606
5 Corporate       5295
6 Complementary    743
7 Aviation         237
8 Undefined          2

count for multiple variables

hotels |>
  count(hotel, market_segment) 
# A tibble: 14 × 3
   hotel        market_segment     n
   <chr>        <chr>          <int>
 1 City Hotel   Aviation         237
 2 City Hotel   Complementary    542
 3 City Hotel   Corporate       2986
 4 City Hotel   Direct          6093
 5 City Hotel   Groups         13975
 6 City Hotel   Offline TA/TO  16747
 7 City Hotel   Online TA      38748
 8 City Hotel   Undefined          2
 9 Resort Hotel Complementary    201
10 Resort Hotel Corporate       2309
11 Resort Hotel Direct          6513
12 Resort Hotel Groups          5836
13 Resort Hotel Offline TA/TO   7472
14 Resort Hotel Online TA      17729

order matters when you count

# hotel type first
hotels |>
  count(hotel, market_segment) 
# A tibble: 14 × 3
   hotel        market_segment     n
   <chr>        <chr>          <int>
 1 City Hotel   Aviation         237
 2 City Hotel   Complementary    542
 3 City Hotel   Corporate       2986
 4 City Hotel   Direct          6093
 5 City Hotel   Groups         13975
 6 City Hotel   Offline TA/TO  16747
 7 City Hotel   Online TA      38748
 8 City Hotel   Undefined          2
 9 Resort Hotel Complementary    201
10 Resort Hotel Corporate       2309
11 Resort Hotel Direct          6513
12 Resort Hotel Groups          5836
13 Resort Hotel Offline TA/TO   7472
14 Resort Hotel Online TA      17729
# market segment first
hotels |>
  count(market_segment, hotel) 
# A tibble: 14 × 3
   market_segment hotel            n
   <chr>          <chr>        <int>
 1 Aviation       City Hotel     237
 2 Complementary  City Hotel     542
 3 Complementary  Resort Hotel   201
 4 Corporate      City Hotel    2986
 5 Corporate      Resort Hotel  2309
 6 Direct         City Hotel    6093
 7 Direct         Resort Hotel  6513
 8 Groups         City Hotel   13975
 9 Groups         Resort Hotel  5836
10 Offline TA/TO  City Hotel   16747
11 Offline TA/TO  Resort Hotel  7472
12 Online TA      City Hotel   38748
13 Online TA      Resort Hotel 17729
14 Undefined      City Hotel       2

What is happening in the following chunk?

hotels |>
  mutate(little_ones = children + babies) |>
  count(hotel, little_ones) |>
  mutate(prop = n / sum(n))
# A tibble: 12 × 4
   hotel        little_ones     n       prop
   <chr>              <dbl> <int>      <dbl>
 1 City Hotel             0 73923 0.619     
 2 City Hotel             1  3263 0.0273    
 3 City Hotel             2  2056 0.0172    
 4 City Hotel             3    82 0.000687  
 5 City Hotel             9     1 0.00000838
 6 City Hotel            10     1 0.00000838
 7 City Hotel            NA     4 0.0000335 
 8 Resort Hotel           0 36131 0.303     
 9 Resort Hotel           1  2183 0.0183    
10 Resort Hotel           2  1716 0.0144    
11 Resort Hotel           3    29 0.000243  
12 Resort Hotel          10     1 0.00000838

summarize and group_by

summarize for summary stats

# mean average daily rate for all bookings
hotels |>
  summarize(mean_adr = mean(adr)) 
# A tibble: 1 × 1
  mean_adr
     <dbl>
1     102.

Tip

summarize() changes entirely the kind of info we get from the data frame. It collapses rows down to a single summary statistic, and removes all columns that are irrelevant to the calculation.

Tip

summarize() also lets you get away with being sloppy and not naming your new column, but that’s not recommended!

hotels |>
  summarize(mean(adr))
# A tibble: 1 × 1
  `mean(adr)`
        <dbl>
1        102.
hotels |>
  summarize(mean_adr = mean(adr))
# A tibble: 1 × 1
  mean_adr
     <dbl>
1     102.

group_by for grouped operations

# mean average daily rate for all booking at city and resort hotels
hotels |>
  group_by(hotel) |> 
  summarize(mean_adr = mean(adr))
# A tibble: 2 × 2
  hotel        mean_adr
  <chr>           <dbl>
1 City Hotel      105. 
2 Resort Hotel     95.0

Calculating frequencies

The following two give the same result, so count is simply short for group_by, then determine frequencies

hotels |>
  group_by(hotel) |>
  summarize(n = n())
# A tibble: 2 × 2
  hotel            n
  <chr>        <int>
1 City Hotel   79330
2 Resort Hotel 40060
hotels |>
  count(hotel)
# A tibble: 2 × 2
  hotel            n
  <chr>        <int>
1 City Hotel   79330
2 Resort Hotel 40060

Multiple summary statistics

summarize can be used for multiple summary statistics

hotels |>
  summarize(
    min_adr = min(adr),
    mean_adr = mean(adr),
    median_adr = median(adr),
    max_adr = max(adr)
    )
# A tibble: 1 × 4
  min_adr mean_adr median_adr max_adr
    <dbl>    <dbl>      <dbl>   <dbl>
1   -6.38     102.       94.6    5400

Which values or entries are in a column?

hotel_stays |>
  distinct(hotel) |>
  arrange(hotel)
hotel
City Hotel
Resort Hotel

distinct() keeps one row for each distinct combination of the selected columns. This result lists hotel categories; it does not count their bookings.

Count the bookings in each category

These return the same frequency table:

hotel_stays |> group_by(hotel) |> summarize(n = n())

hotel_stays |> count(hotel)
hotel n
City Hotel 79,330
Resort Hotel 40,060

count() is a shortcut for grouped row counts. Use count(hotel, sort = TRUE) to order by frequency, largest first.

The data behind the example

Open R4DS 1.2.1. Use section 1.1.1 to check the package setup.

In the reading In our hotel work
penguins: one row per penguin hotel_stays: one row per booking
species: a category hotel: a category
body_mass_g: mass in grams total_nights: booked nights

Before using a plot example, identify the input table, the meaning of a row, and the variables and their units.

Where does the chapter load the packages and introduce the data?

Build a ggplot

R4DS 1.2–1.3: data, mappings, then a geometry. Here is that structure using the hotel table.

ggplot(hotel_stays)

ggplot(hotel_stays, aes(x = hotel))

ggplot(hotel_stays, aes(x = hotel)) +
  geom_bar()

Read and predict: what does each version add?

Which part supplies the rows? Which maps a column to an axis? Which draws marks?

A bar counts a category

R4DS 1.4.1: use the plot structure to show counts for a category.

ggplot(hotel_stays,
       aes(x = hotel)) +
  geom_bar() +
  labs(x = "Hotel",
       y = "Bookings")

City and Resort are categories.

Each bar counts bookings in one category. Check its height against count(hotel).

A bar chart counts 79,330 City Hotel bookings and 40,060 Resort Hotel bookings.

Read the histogram example

R4DS 1.4.2: keep the plot structure; change the variable and geometry.

ggplot(penguins, aes(x = body_mass_g)) +
  geom_histogram(binwidth = 200)
  • Input: one row per penguin; x: body mass in grams.
  • binwidth = 200: count observations in intervals 200 grams wide.
  • Our replacements: hotel_stays, lead_time, and a width in days.

Predict: what will one bar count after those changes?

A histogram counts values in intervals

ggplot(hotel_stays,
       aes(x = lead_time)) +
  geom_histogram(
    binwidth = 30,
    boundary = 0,
    closed = "left"
  ) +
  labs(x = "Lead time (days)",
       y = "Bookings")

Histogram of booking lead time in 30-day intervals, beginning at zero days; lead time is measured in days before arrival.

The first interval is 0 ≤ lead time < 30 days. Each bar counts bookings.

Adapt, compare, explain

With a partner | 8 minutes

Question: How are booked stay lengths distributed?

  1. Adapt the histogram to total_nights.
  2. Start with binwidth = 1 and boundary = -0.5.
  3. Change the width to 3; keep the same boundary.
  4. Explain what one bar counts in each version and what detail changes.

Keep closed = "left". Update the x-axis label to “Booked nights.” Record your source, your changes, and one check.

Check the plot against the question

  • The input still has one row per booking.
  • The x-axis now measures booked nights, not lead time.
  • One-night bins separate the recorded whole-night values.
  • Three-night bins combine nearby values; they do not add observations.
  • Both versions describe the same bookings, including cancellations.

An average and a histogram answer different parts of the question.

Reference: more summary functions

hotel_stays |>
  summarize(
    shortest = min(total_nights),
    mean_nights = mean(total_nights),
    median_nights = median(total_nights),
    longest = max(total_nights)
  )

These data: minimum 0, mean about 3.43, median 3, maximum 69 nights.

median() gives the middle value after ordering; min() and max() give the extremes. See IDS 4.4.2.

Reference: row and column helpers

hotels |> select(hotel, starts_with("arrival"))
hotels |> select(!agent)
hotels |> filter(hotel == "City Hotel", lead_time <= 7)
hotels |> slice(1:5)
hotels |> arrange(desc(lead_time)) |> slice_head(n = 5)
  • Selection helpers choose columns by their names.
  • Conditions separated by commas in filter() must all hold.
  • slice() chooses row positions; their meaning depends on the order.
  • # begins a code comment; R does not run the rest of that line.

Reference: market-segment code


segment_summary <- hotel_stays |>
  group_by(market_segment) |>
  summarize(
    bookings = n(),
    mean_nights = mean(total_nights)
  ) |>
  arrange(desc(bookings))

segment_summary

Changed: grouping variable, output name, and ordering.
Kept: the input table, row count, and mean calculation.

Extension: Add filter(hotel == "Resort Hotel") |> before group_by().

Reference: all eight market segments

market_segment bookings mean_nights
Online TA 56477 3.57
Offline TA/TO 24219 3.90
Groups 19811 2.99
Direct 12606 3.21
Corporate 5295 2.09
Complementary 743 1.65
Aviation 237 3.61
Undefined 2 1.50

Check: the counts total 119,390 bookings. A mean based on two bookings deserves different scrutiny from one based on thousands.

Reference: the mpg adaptation


mpg_summary <- mpg |>
  group_by(class) |>
  summarize(
    configurations = n(),
    mean_hwy = mean(hwy)
  )

Check: seven rows; group counts total 234 configurations.

The compact group has 47 configurations and a mean of about 28.30 highway mpg.

These are selected vehicle configurations from 1999 and 2008, not all cars or sales counts.

Reference: checking an example

What you notice What to check
“Object not found” Did an earlier chunk create the table? Does that column exist?
%>% instead of |> Both pass the table onward in the simple pipelines used here.
summarise() instead of summarize() These are two spellings of the same dplyr function.
A mean returns NA Inspect missing values before deciding to exclude them.
mean(c(2, NA, 4))                # NA
mean(c(2, NA, 4), na.rm = TRUE)  # 3; based on two values

Record what was excluded. Removing missing values does not recover the missing information.

Reference: more grouping

R4DS 3.5.3: keep the original row with the largest value in each group.

hotel_stays |>
  group_by(hotel) |>
  slice_max(total_nights, n = 1, with_ties = FALSE)

R4DS 3.5.4: summarize each observed combination of two variables.

hotel_stays |>
  group_by(hotel, market_segment) |>
  summarize(bookings = n(), .groups = "drop")

slice_max() retains booking rows. This summarize() produces group counts. .groups = "drop" removes grouping from the resulting summary.

Reference: a first ggplot

ggplot(hotels,
       aes(x = hotel)) +
  geom_bar() +
  labs(x = "Hotel",
       y = "Number of bookings")

Input: one row per booking.

geom_bar() counts rows for each x value.

City Hotel has 79,330 bookings and Resort Hotel has 40,060 bookings. The vertical axis starts at zero.

Reference: parts of a ggplot

ggplot(hotels, aes(x = hotel)) +
  geom_bar() +
  labs(x = "Hotel", y = "Number of bookings")
  • hotels supplies the booking rows.
  • aes(x = hotel) puts hotel categories on the horizontal axis.
  • geom_bar() counts the rows in each category.
  • + adds a plot layer; labs() supplies readable labels.

Which part would you change to use a different table?

Reference: subset practice

With a partner · 4 minutes

First, use Friday’s tools to keep bookings for at least seven nights:

long_stays <- hotel_stays |>
  filter(total_nights >= 7)

Copy the first chart. Change only the input table to long_stays.

  • Keep aes(x = hotel) and geom_bar().
  • Predict whether either hotel’s count can exceed its count in the full table.
  • Run it. Explain what one bar counts and which hotel has more long bookings.

Reference: which bookings did we count?

ggplot(long_stays,
       aes(x = hotel)) +
  geom_bar() +
  labs(x = "Hotel",
       y = "Number of bookings")

3,307 City bookings and 10,605 Resort bookings are for at least seven nights.

Each bar counts bookings, not nights or guests.

Read the y-axis: this chart uses a different scale from the full-data chart.

Among bookings for at least seven nights, City Hotel has 3,307 bookings and Resort Hotel has 10,605.

Optional: plotting means

ggplot(hotel_summary,
       aes(x = hotel,
           y = mean_nights)) +
  geom_col() +
  labs(x = "Hotel",
       y = "Mean booked nights")

Input: one row per hotel group.

geom_col() uses the supplied y values.

Mean booked stay is 2.98 nights for City Hotel and 4.32 nights for Resort Hotel. The vertical axis starts at zero.

Optional: counts and means

Two bar charts. City has more bookings, 79,330 versus 40,060; Resort has a longer mean booked stay, 4.32 nights versus 2.98. Both axes start at zero.

Which chart supports “City has more bookings”? Which supports “Resort has longer stays on average”?

Compare the axis labels and units before comparing bar heights across charts.

Reference: the stay-length adaptation

ggplot(hotel_stays, aes(x = total_nights)) +
  geom_histogram(
    binwidth = 1,
    boundary = -0.5,
    closed = "left"
  ) +
  labs(x = "Booked nights", y = "Number of bookings")

For the second version, change only binwidth = 1 to binwidth = 3.

With width 3, the first interval is −0.5 ≤ nights < 2.5: it contains the recorded values 0, 1, and 2.

Reference: categories in R

condition <- factor(
  c("Good", "Fair", "Poor"),
  levels = c("Poor", "Fair", "Good"),
  ordered = TRUE
)

class(condition)  # "ordered" "factor"
levels(condition)

The levels record category order. They do not establish equal gaps.

Reference: inspect before summarizing

glimpse(hotel_stays)
class(hotel_stays$hotel)        # "character"
class(hotel_stays$total_nights) # "numeric"

hotel_stays |>
  summarize(
    bookings = n(),
    missing_nights = sum(is.na(total_nights))
  )

These data have 119,390 bookings and no missing total_nights values. Check again when the input changes.

Reference: reorganizing a table

Invented yearly booking totals:

Hotel 2024 2025
City 12 15
Resort 8 10

For an analysis across years, put year values in a year column:

Hotel year bookings
City 2024 12
City 2025 15
Resort 2024 8
Resort 2025 10

Here one row is a hotel-year summary. tidyr::pivot_longer() can make this change.

Reference: count combinations

hotel_stays |>
  count(hotel, market_segment, sort = TRUE)

hotel_stays |>
  distinct(hotel, market_segment)

Both results have one row per observed hotel–segment combination. Only count() adds the number of booking rows in each combination.

Changing the order of the variables changes column/default sorting order, not the count for a given combination.

Sources for these slides

Hotel results were calculated from the same hotels.csv used in the Week 3 lab.

Sources for these slides