14  Exercise Solutions

14.1 Introduction

Try every Your Turn before reading its solution — the struggle is the lesson. Worked solutions below cover @dplyr-basics, @tidying-data-with-tidyr, @date-and-time-in-r, @date-arithmetic-time-zones, @categorical-data-in-r and @forcats-manipulation. Remaining chapters are open stubs — to contribute one, open an issue or pull request with the chapter, the question, and a runnable chunk in this book’s style (|>-first, same data as the chapter).

We will use the following R packages:

library(dplyr)
library(readr)

14.2 dplyr Basics

ecom <-
  read_csv('https://raw.githubusercontent.com/rsquaredacademy/datasets/master/web.csv',
    col_types = cols_only(device = col_factor(levels = c("laptop", "tablet", "mobile")),
      referrer = col_factor(levels = c("bing", "direct", "social", "yahoo", "google")),
      purchase = col_logical(), n_pages = col_double(), n_visit = col_double(),
      duration = col_double(), order_value = col_double(), order_items = col_double()
    )
  )

Q1. What is the average number of pages visited by purchasers and non-purchasers?

ecom |>
  summarise(mean_pages = mean(n_pages), .by = purchase)
# A tibble: 2 × 2
  purchase mean_pages
  <lgl>         <dbl>
1 FALSE          4.79
2 TRUE          15.8 

Purchasers browse far more pages (~16 vs ~5) — the signal behind the AOV case study.

Q2. What is the average time on site for purchasers vs non-purchasers?

ecom |>
  summarise(mean_time = mean(duration), .by = purchase)
# A tibble: 2 × 2
  purchase mean_time
  <lgl>        <dbl>
1 FALSE         355.
2 TRUE          359.

Time on site barely differs (~355 vs ~359 seconds): page depth, not duration, separates buyers here.

Q3. What is the average number of pages visited by purchasers and non-purchasers using mobile?

ecom |>
  filter(device == "mobile") |>
  summarise(mean_pages = mean(n_pages), .by = purchase)
# A tibble: 2 × 2
  purchase mean_pages
  <lgl>         <dbl>
1 FALSE          4.99
2 TRUE          16.2 

14.3 Tidying Data with tidyr

library(tidyr)
mockstring <-
  read_csv('https://raw.githubusercontent.com/rsquaredacademy/datasets/master/mock_strings.csv',
    show_col_types = FALSE)

Q1. Reshape the mean of n_visit and order_items by referrer from wide to long.

ecom |>
  summarise(across(c(n_visit, order_items), \(x) mean(x, na.rm = TRUE)),
    .by = referrer) |>
  pivot_longer(-referrer, names_to = "metric", values_to = "mean_value")
# A tibble: 10 × 3
   referrer metric      mean_value
   <fct>    <chr>            <dbl>
 1 google   n_visit           5.05
 2 google   order_items       1.11
 3 yahoo    n_visit           5.05
 4 yahoo    order_items       1.71
 5 direct   n_visit           4.90
 6 direct   order_items       1.51
 7 bing     n_visit           4.77
 8 bing     order_items       1.22
 9 social   n_visit           5.07
10 social   order_items       1.36

Q2. Split the email column of mockstring at "@" and reunite it — does the round-trip hold?

split <- mockstring |>
  select(email) |>
  separate(email, into = c("user", "domain"), sep = "@", remove = FALSE)
reunited <- split |>
  unite("rejoined", user, domain, sep = "@", remove = TRUE)
identical(reunited$rejoined, mockstring$email)
[1] TRUE

Yes — every row round-trips (TRUE), since each address holds exactly one "@".

Q3. Run the complete() example without fill: what does the missing count become, and why?

ecom |>
  filter(purchase) |>
  count(device, purchase) |>
  complete(device, purchase = c(TRUE, FALSE))
# A tibble: 6 × 3
  device purchase     n
  <fct>  <lgl>    <int>
1 laptop FALSE       NA
2 laptop TRUE        31
3 tablet FALSE       NA
4 tablet TRUE        36
5 mobile FALSE       NA
6 mobile TRUE        36

The missing cells become NA, not 0: with no fill, complete() adds the absent combinations but has no value to put in n, so the count is unknown (NA). Pass fill = list(n = 0L) when a missing combination genuinely means “zero events”.

14.4 Date & Time: Parsing and Components

library(lubridate)
transact <-
  read_csv('https://raw.githubusercontent.com/rsquaredacademy/datasets/master/transact.csv',
    show_col_types = FALSE)

Q1. Current date, current time, AM or PM, and leap years.

today()
[1] "2026-10-17"
now()
[1] "2026-10-17 14:21:25 UTC"
pm(now())
[1] TRUE
leap_year(c(2018, 2016))
[1] FALSE  TRUE

2018 was not a leap year (FALSE); 2016 was (TRUE).

Q2. R 1.0.0 was released on 2000-02-29 08:55:23 UTC. Save it as a Date from character, a Date from origin + number, POSIXct, POSIXlt (extracting month day, day of year, month, zone) and ISODate.

as.Date("2000-02-29")
[1] "2000-02-29"
as.Date(11016, origin = "1970-01-01")
[1] "2000-02-29"
as.POSIXct("2000-02-29 08:55:23", tz = "UTC")
[1] "2000-02-29 08:55:23 UTC"
lt <- as.POSIXlt("2000-02-29 08:55:23", tz = "UTC")
c(month_day = lt$mday, day_of_year = lt$yday, month = lt$mon + 1L, zone = lt$zone)
  month_day day_of_year       month        zone 
       "29"        "59"         "2"       "UTC" 
ISOdate(2000, 2, 29, 8, 55, 23, tz = "UTC")
[1] "2000-02-29 08:55:23 UTC"

$mon is zero-indexed, hence + 1L; $yday is 59 (zero-indexed — the 60th day of the leap year).

Q3. Create July 5th, 2019 from each spelling with as.Date() and the correct format.

as.Date("July-05-19", format = "%B-%d-%y")
[1] "2019-07-05"
as.Date("JUL-05-19", format = "%b-%d-%y")
[1] "2019-07-05"
as.Date("05.07.19", format = "%d.%m.%y")
[1] "2019-07-05"
as.Date("5-July 2019", format = "%d-%B %Y")
[1] "2019-07-05"
as.Date("July 5th, 2019", format = "July %dth, %Y")
[1] "2019-10-05"
as.Date("July 05, 2019", format = "%B %d, %Y")
[1] "2019-07-05"
as.Date("2019-July- 05", format = "%Y-%B- %d")
[1] "2019-07-05"
as.Date("05/07/2019", format = "%d/%m/%Y")
[1] "2019-07-05"
as.Date("07/05/2019", format = "%m/%d/%Y")
[1] "2019-07-05"
as.Date("7/5/2019", format = "%m/%d/%Y")
[1] "2019-07-05"
as.Date("07/5/19", format = "%m/%d/%y")
[1] "2019-07-05"
as.Date("2019-07-05")
[1] "2019-07-05"

Note the ambiguous pairs: 05/07/2019 vs 07/05/2019 is day-first vs month-first — the format string, not the data, decides. as.Date() matches literals ("July ", "th", ", ") character-for-character.

Q4. Parse the same spellings with strptime() / parse_date_time() / helpers.

dates <- c("July-05-19", "JUL-05-19", "05.07.19", "5-July 2019",
  "July 5th, 2019", "July 05, 2019", "2019-July- 05", "05/07/2019",
  "07/05/2019", "7/5/2019", "07/5/19", "2019-07-05")
parse_date_time(dates, orders = c("Bdy", "bdy", "dmy", "dBY", "BYd",
  "dmy", "mdy", "mdy", "mdy", "mdy", "ymd"))
 [1] "2019-07-05 UTC" "2019-07-05 UTC" "2019-07-05 UTC" "2019-07-05 UTC"
 [5] "2019-07-05 UTC" "2019-07-05 UTC" "2019-07-05 UTC" "2019-07-05 UTC"
 [9] "2019-05-07 UTC" "2019-05-07 UTC" "2019-05-07 UTC" "2019-07-05 UTC"
mdy("July 05, 2019")
[1] "2019-07-05"
dmy("05.07.19")
[1] "2019-07-05"
ymd("2019-07-05")
[1] "2019-07-05"

One parse_date_time() call with several orders handles the whole mixed vector — prefer helpers (mdy()/dmy()/ymd()) when a column uses a single convention.

Q5. From the Due column of transact, tabulate year, labelled month, labelled weekday, hour and quarter.

transact |>
  mutate(due_year = year(Due),
    due_month = month(Due, label = TRUE),
    due_wday = wday(Due, label = TRUE),
    due_hour = hour(Due),
    due_quarter = quarter(Due)) |>
  count(due_year, due_month, due_wday, due_hour, due_quarter)
# A tibble: 162 × 6
   due_year due_month due_wday due_hour due_quarter     n
      <dbl> <ord>     <ord>       <int>       <int> <int>
 1     2012 Feb       Sun             0           1    17
 2     2012 Feb       Mon             0           1    12
 3     2012 Feb       Tue             0           1    12
 4     2012 Feb       Wed             0           1    16
 5     2012 Feb       Thu             0           1    13
 6     2012 Feb       Fri             0           1    11
 7     2012 Feb       Sat             0           1     6
 8     2012 Mar       Sun             0           1    12
 9     2012 Mar       Mon             0           1    17
10     2012 Mar       Tue             0           1     6
# ℹ 152 more rows

due_hour is 0 everywhere: Due is a Date (midnight, no time component) — hour() on dates is always 0.

Q6. R 2.0.0 (2004-10-04 14:24:38) with make_date() and make_datetime(); R 3.0.0 (2013-04-03 07:12:36) via update().

r200 <- make_datetime(2004, 10, 4, 14, 24, 38)
r200_date <- make_date(2004, 10, 4)
update(r200, year = 2013, month = 4, mday = 3, hour = 7, min = 12, sec = 36)
[1] "2013-04-03 07:12:36 UTC"

14.5 Date & Time: Arithmetic and Time Zones

Q1. Vacation length, shifted vacation, invoice settlement and employment length.

vac_start <- ymd("2020-04-19")
vac_end <- ymd("2020-04-25")
vac_start %--% vac_end
[1] 2020-04-19 UTC--2020-04-25 UTC
as.period(vac_start %--% vac_end)
[1] "6d 0H 0M 0S"
# shift the start by 10 days and the end by 2 weeks, then recompute
vac_start2 <- vac_start + days(10)
vac_end2 <- vac_end + weeks(2)
as.period(vac_start2 %--% vac_end2)
[1] "10d 0H 0M 0S"
receivables <-
  read_csv('https://raw.githubusercontent.com/rsquaredacademy/datasets/master/receivables.csv',
    show_col_types = FALSE)
receivables |>
  mutate(invoice_date = mdy(invoice_date), due_date = mdy(due_date),
    settled_date = mdy(settled_date),
    days_to_settle = settled_date - invoice_date,
    days_overdue = settled_date - due_date) |>
  select(invoice_number, days_to_settle, days_overdue) |>
  head()
# A tibble: 6 × 3
  invoice_number days_to_settle days_overdue
           <dbl> <drtn>         <drtn>      
1         611365 13 days        -17 days    
2        7900770 36 days          6 days    
3        9231909  5 days        -25 days    
4        9888306 35 days          5 days    
5       15752855 34 days          4 days    
6       18104516 26 days         -4 days    
hr <-
  read_csv('https://raw.githubusercontent.com/rsquaredacademy/datasets/master/hr-data.csv',
    show_col_types = FALSE)
hr |>
  filter(Termd == 1, !is.na(DateofTermination)) |>
  mutate(hire = mdy(DateofHire), term = mdy(DateofTermination),
    employed = term - hire) |>
  select(Employee_Name, hire, term, employed) |>
  head()
# A tibble: 6 × 4
  Employee_Name    hire       term       employed 
  <chr>            <date>     <date>     <drtn>   
1 Howard, Estelle  2015-02-16 2015-04-15   58 days
2 Smith, Leigh Ann 2011-09-26 2013-09-25  730 days
3 Bunbury, Jessica 2011-08-15 2014-08-02 1083 days
4 Gill, Whitney    2014-07-07 2015-09-05  425 days
5 Guilianno, Mike  2011-03-07 2014-10-31 1334 days
6 Becker, Renee    2014-07-07 2015-09-12  432 days

The base vacation is 6 days; after shifting it spans 10 days. Positive days_overdue means the customer paid late; filter Termd == 1 (plus a non-missing termination date) so only terminated employees count.

Q2. Your timezone, daylight savings and the current UTC time.

Sys.timezone()
[1] "UTC"
dst(now())
[1] FALSE
now("UTC")
[1] "2026-10-17 14:21:26 UTC"

Q3. Round transact due dates: up to weeks, down to days, back to the start of the month.

transact |>
  mutate(due_week_up = ceiling_date(Due, "week"),
    due_day_down = floor_date(Due, "day"),
    due_month_start = rollback(Due, roll_to_first = TRUE)) |>
  select(Due, due_week_up, due_day_down, due_month_start) |>
  head()
# A tibble: 6 × 4
  Due        due_week_up due_day_down due_month_start
  <date>     <date>      <date>       <date>         
1 2013-02-01 2013-02-03  2013-02-01   2013-02-01     
2 2013-02-25 2013-03-03  2013-02-25   2013-02-01     
3 2013-08-02 2013-08-04  2013-08-02   2013-08-01     
4 2013-03-12 2013-03-17  2013-03-12   2013-03-01     
5 2012-11-24 2012-11-25  2012-11-24   2012-11-01     
6 2012-02-26 2012-03-04  2012-02-26   2012-02-01     

Due values are whole dates, so rounding down to days is a no-op (midnight already); ceiling_date() moves each due date forward to its week boundary.

14.6 Categorical Data Fundamentals

analytics_raw <- readRDS("analytics_raw.rds")

Q1. Quantitative or qualitative? Quantitative: Age, Annual Sales, Weight, Height, Job/Work Experience. Qualitative: Gender, Education Level, Vehicle Type, Ice Cream Flavor, Blood Group, Color, Mode of Payment. (Education Level is qualitative but ordered — see ordered factors below.)

Q2. Dichotomous or polychotomous? Dichotomous (exactly two outcomes): Heads / Tails, Rich / Poor, Democrat / Republican. Polychotomous (three or more): Blood Group, Transportation Mode, Education Level.

Q3–Q9. Factor checks, coercion and level control on analytics_raw.

# Q3: which of these are already factors? (raw file: none — all character)
sapply(analytics_raw[c("device", "page_depth", "landing_page")], is.factor)
      device   page_depth landing_page 
       FALSE        FALSE        FALSE 
# Q4: coerce the categorical columns (page_depth stays numeric;
# `city` is not in this file, so it is skipped)
cat_vars <- c("device", "os", "browser", "user_type", "channel",
  "gender", "landing_page", "exit_page", "country")
analytics_raw[cat_vars] <- lapply(analytics_raw[cat_vars], factor)

# Q5: keep only male/female levels in gender
gender <- factor(analytics_raw$gender, levels = c("male", "female"))
levels(gender)
[1] "male"   "female"
# Q6: admit NA as a level (no-op here — this file has no missing gender,
# but the call is what makes unseen NAs explicit downstream)
library(forcats)
gender <- fct_na_value_to_level(gender, level = NA)
levels(gender)
[1] "male"   "female" NA      
# Q7: label that NA level "missing"
gender <- fct_na_value_to_level(gender, level = "(Missing)")
levels(gender)
[1] "male"      "female"    "(Missing)"
# Q8: relabel user_type levels
user_type <- fct_recode(factor(analytics_raw$user_type),
  "New" = "New Visitor", "Returning" = "Returning Visitor")
levels(user_type)
[1] "New"       "Returning"
# Q9: is user_rating ordered? coerce if not
is.ordered(analytics_raw$user_rating)
[1] FALSE
user_rating <- ordered(analytics_raw$user_rating)
is.ordered(user_rating)
[1] TRUE

Q10–Q13. Level counts, categories, frequencies and a two-way summary.

# Q10: number of levels
sapply(analytics_raw[c("browser", "channel", "landing_page", "exit_page")],
  \(x) nlevels(factor(x)))
     browser      channel landing_page    exit_page 
          26            8           33           33 
# Q11: the categories themselves
lapply(analytics_raw[c("os", "channel", "browser", "gender", "user_type")],
  \(x) levels(factor(x)))
$os
 [1] "(not set)"        "Android"          "BlackBerry"       "Chrome OS"       
 [5] "Firefox OS"       "iOS"              "Linux"            "Macintosh"       
 [9] "OS/2"             "Playstation 4"    "Playstation Vita" "Samsung"         
[13] "Tizen"            "Windows"          "Windows Phone"    "Xbox"            

$channel
[1] "(Other)"        "Affiliates"     "Direct"         "Display"       
[5] "Organic Search" "Paid Search"    "Referral"       "Social"        

$browser
 [1] "Amazon Silk"              "Android Browser"         
 [3] "Android Webview"          "APKPure"                 
 [5] "BlackBerry"               "Chrome"                  
 [7] "Coc Coc"                  "Edge"                    
 [9] "Firefox"                  "Internet Explorer"       
[11] "Maxthon"                  "Mozilla Compatible Agent"
[13] "MRCHROME"                 "Opera"                   
[15] "Opera Mini"               "Playstation 4"           
[17] "Playstation Vita Browser" "Puffin"                  
[19] "Safari"                   "Safari (in-app)"         
[21] "Samsung Internet"         "SeaMonkey"               
[23] "Seznam"                   "UC Browser"              
[25] "User-Agent:Mozilla"       "YaBrowser"               

$gender
[1] "female" "male"  

$user_type
[1] "New Visitor"       "Returning Visitor"
# Q12: frequencies
fct_count(factor(analytics_raw$channel))
# A tibble: 8 × 2
  f                   n
  <fct>           <int>
1 (Other)          6073
2 Affiliates       7388
3 Direct          39853
4 Display          3375
5 Organic Search 139668
6 Paid Search      4395
7 Referral        35615
8 Social           8031
fct_count(factor(analytics_raw$user_type))
# A tibble: 2 × 2
  f                      n
  <fct>              <int>
1 New Visitor       179045
2 Returning Visitor  65353
# Q13: channel by user_type — counts and column shares
table(factor(analytics_raw$channel), factor(analytics_raw$user_type))
                
                 New Visitor Returning Visitor
  (Other)               3718              2355
  Affiliates            5785              1603
  Direct               31576              8277
  Display               2631               744
  Organic Search      106197             33471
  Paid Search           2477              1918
  Referral             19511             16104
  Social                7150               881
proportions(table(factor(analytics_raw$channel),
  factor(analytics_raw$user_type)), margin = 2)
                
                 New Visitor Returning Visitor
  (Other)         0.02076573        0.03603507
  Affiliates      0.03231031        0.02452833
  Direct          0.17635790        0.12665065
  Display         0.01469463        0.01138433
  Organic Search  0.59313022        0.51215705
  Paid Search     0.01383451        0.02934831
  Referral        0.10897260        0.24641562
  Social          0.03993409        0.01348064

14.7 Factor Manipulation with forcats

library(forcats)
data <- readRDS("analytics.rds")

Q1–Q2. Frequencies in descending order; is laptop a device level?

data |> pull(device) |> fct_count()
# A tibble: 3 × 2
  f            n
  <fct>    <int>
1 Desktop 177282
2 Mobile   63482
3 Tablet    3634
data |> pull(landing_page) |> fct_count()
# A tibble: 33 × 2
   f               n
   <fct>       <int>
 1 Accessories  4529
 2 Apparel     32234
 3 Bags         3462
 4 Basket       3920
 5 Brands        128
 6 Drinkware    2806
 7 Eco            75
 8 Electronics   486
 9 FAQ          2626
10 Fun             8
# ℹ 23 more rows
data |> pull(exit_page) |> fct_count()
# A tibble: 34 × 2
   f               n
   <fct>       <int>
 1 Accessories  4856
 2 Apparel     38318
 3 Bags         4584
 4 Basket      12072
 5 Brands        129
 6 Drinkware    3227
 7 Eco            79
 8 Electronics   470
 9 FAQ          3455
10 Fun            11
# ℹ 24 more rows
"laptop" %in% levels(data$device)
[1] FALSE

fct_count() sorts most-frequent-first. (device here holds Desktop/Mobile/Tablet, so "laptop" is not a level — it belongs to the ecom schema, not analytics.)

Q3–Q8. Collapsing, lumping, keeping and anonymizing levels.

# Q3: collapse account-related pages
data |> pull(landing_page) |>
  fct_collapse(Account = c("My Account", "Register", "Sign In", "Your Info")) |>
  fct_count() |> head(6)
# A tibble: 6 × 2
  f               n
  <fct>       <int>
1 Accessories  4529
2 Apparel     32234
3 Bags         3462
4 Basket       3920
5 Brands        128
6 Drinkware    2806
# Q4: lump landing pages behind fewer than 1000 visits into "Other"
data |> pull(landing_page) |> fct_lump_min(min = 1000) |> fct_count()
# A tibble: 16 × 2
   f                  n
   <fct>          <int>
 1 Accessories     4529
 2 Apparel        32234
 3 Bags            3462
 4 Basket          3920
 5 Drinkware       2806
 6 FAQ             2626
 7 Home          135485
 8 Lifestyle       5695
 9 My Account      1121
10 New             6340
11 Office          2454
12 Search          2066
13 Shop by Brand  26826
14 Sign In         3184
15 Store           8979
16 Other           2671
# Q5: top 10 landing and exit pages
data |> pull(landing_page) |> fct_lump_n(n = 10) |> fct_count()
# A tibble: 11 × 2
   f                  n
   <fct>          <int>
 1 Accessories     4529
 2 Apparel        32234
 3 Bags            3462
 4 Basket          3920
 5 Home          135485
 6 Lifestyle       5695
 7 New             6340
 8 Shop by Brand  26826
 9 Sign In         3184
10 Store           8979
11 Other          13744
data |> pull(exit_page) |> fct_lump_n(n = 10) |> fct_count()
# A tibble: 12 × 2
   f                 n
   <fct>         <int>
 1 Accessories    4856
 2 Apparel       38318
 3 Bags           4584
 4 Basket        12072
 5 Home          88051
 6 Lifestyle     13996
 7 New           10426
 8 Search         7185
 9 Shop by Brand 25231
10 Store         17016
11 Other         22095
12 <NA>            568
# Q6: landing pages with at least 5% of traffic
data |> pull(landing_page) |> fct_lump_prop(prop = 0.05) |> fct_count()
# A tibble: 4 × 2
  f                  n
  <fct>          <int>
1 Apparel        32234
2 Home          135485
3 Shop by Brand  26826
4 Other          49853
# Q7: keep only the big-four browsers
data |> pull(browser) |>
  fct_other(keep = c("Chrome", "Firefox", "Safari", "Edge")) |>
  fct_count()
# A tibble: 5 × 2
  f            n
  <fct>    <int>
1 Chrome  192176
2 Edge      3787
3 Firefox   6420
4 Safari   35323
5 Other     6692
# Q8: anonymize page levels (stable within a session via seed)
set.seed(42)
data |> pull(landing_page) |> fct_anon() |> head()
[1] 30 01 31 25 02 25
33 Levels: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 ... 33

Q9–Q15. Reordering, shuffling and reversing levels.

set.seed(42)
landing_page <- data$landing_page
browser <- data$browser

# Q9/Q10/Q11: Home first, Apparel second, Specials last
landing_page |> fct_relevel("Home") |> levels() |> head(4)
[1] "Home"        "Accessories" "Apparel"     "Bags"       
landing_page |> fct_relevel("Apparel", after = 1L) |> levels() |> head(4)
[1] "Accessories" "Apparel"     "Bags"        "Basket"     
landing_page |> fct_relevel("Specials", after = Inf) |> levels() |> tail(3)
[1] "Wishlist"  "Your Info" "Specials" 
# Q12: browsers by frequency
browser |> fct_infreq() |> levels()
 [1] "Chrome"                   "Safari"                  
 [3] "Firefox"                  "Edge"                    
 [5] "Samsung Internet"         "Android Webview"         
 [7] "Opera"                    "Internet Explorer"       
 [9] "Safari (in-app)"          "YaBrowser"               
[11] "UC Browser"               "Amazon Silk"             
[13] "Android Browser"          "Opera Mini"              
[15] "Coc Coc"                  "User-Agent:Mozilla"      
[17] "Puffin"                   "Playstation 4"           
[19] "Mozilla Compatible Agent" "BlackBerry"              
[21] "Maxthon"                  "SeaMonkey"               
[23] "APKPure"                  "MRCHROME"                
[25] "Playstation Vita Browser" "Seznam"                  
# Q13: landing pages in order of appearance
landing_page |> fct_inorder() |> levels() |> head(6)
[1] "Home"          "Accessories"   "Sign In"       "Apparel"      
[5] "Shop by Brand" "Lifestyle"    
# Q14: shuffled os levels
data |> pull(os) |> fct_shuffle() |> levels()
 [1] "(not set)"        "Firefox OS"       "Xbox"             "OS/2"            
 [5] "Playstation 4"    "Chrome OS"        "Android"          "Windows"         
 [9] "Macintosh"        "Linux"            "Playstation Vita" "Tizen"           
[13] "Windows Phone"    "Samsung"          "BlackBerry"       "iOS"             
# Q15: reversed browser levels
browser |> fct_rev() |> levels()
 [1] "YaBrowser"                "User-Agent:Mozilla"      
 [3] "UC Browser"               "Seznam"                  
 [5] "SeaMonkey"                "Samsung Internet"        
 [7] "Safari (in-app)"          "Safari"                  
 [9] "Puffin"                   "Playstation Vita Browser"
[11] "Playstation 4"            "Opera Mini"              
[13] "Opera"                    "MRCHROME"                
[15] "Mozilla Compatible Agent" "Maxthon"                 
[17] "Internet Explorer"        "Firefox"                 
[19] "Edge"                     "Coc Coc"                 
[21] "Chrome"                   "BlackBerry"              
[23] "APKPure"                  "Android Webview"         
[25] "Android Browser"          "Amazon Silk"             

Q16. The plots.

library(ggplot2)
# 1: bar plot of channel
ggplot(data) +
  geom_bar(aes(x = channel), fill = "blue") +
  ggtitle("Source of Traffic") + xlab("Channel") + ylab("Count")

# 2: grouped bars, user_type by channel
ggplot(data) +
  geom_bar(aes(x = channel, fill = user_type), position = "dodge") +
  ggtitle("User Type by Channel") + xlab("Channel") + ylab("User Type")

# 3: stacked bars, channel by gender
ggplot(data) +
  geom_bar(aes(x = gender, fill = channel)) +
  ggtitle("Channel by Gender") + xlab("Gender") + ylab("Channel")

# 4: proportional bars, channel by device
data |>
  select(device, channel) |>
  table() |>
  tibble::as_tibble() |>
  ggplot(aes(x = device, y = n, fill = channel)) +
  geom_bar(stat = "identity", position = "fill") +
  ggtitle("Channel by Device") + xlab("Device") + ylab("Channel")

# 5: count plot, device by channel
ggplot(data, aes(x = device, y = channel)) +
  geom_count() +
  ggtitle("Channel by Device") +
  xlab("Device") + ylab("Channel")

# 6: pie and donut of channel
data |> pull(channel) |> table() |> pie()

data |>
  pull(channel) |>
  fct_count() |>
  rename(channel = f, count = n) |>
  ggplot() +
  geom_bar(aes(x = "", y = count, fill = channel), width = 1,
    stat = "identity") +
  coord_polar("y", start = 0)

14.8 Open stubs

Chapters without Your Turn blocks still need proposed drills + solutions — contributions welcome (see above):

  • @joining-tables-in-r-dplyr: join puzzles on customer/order
  • @dplyr-helper-functions: sampling/slicing drills on ecom
  • @r-pipe-magrittr: rewrite-a-chain exercises (%>% → |>, _, \(x))
  • @tibbles-in-r: tibble vs data.frame edge cases
  • @strings-in-r: URL/email parsing variants on mockstring
  • @import-data-in-r-basics and @import-data-in-r-advanced: import-parameter drills