5  Import and Inspect Forestry Data with R and Quarto

NoteData for this chapter

This chapter uses the BC silviculture (bc_disturbance_reforestation.xlsx), Ontario forest statistics (on_forest_statistics_1.xlsx), and BC air-quality (airdata3.csv) files. Click each file name to download it directly, then save it in your data/ folder. For sources and details, see the Datasets Used in This Book page.

Chapter goals

By the end of this chapter you will be able to:

  • Explain what a vector is in R and recognize the four atomic types: logical, integer, double, character.
  • Use c() to combine values into a vector, and apply vectorized functions (sum, mean, length) directly to a vector.
  • Distinguish a tibble from a data.frame and explain why the tidyverse uses tibbles.
  • Identify column types from glimpse() output (<int>, <dbl>, <chr>, <lgl>, <date>).
  • Import an Excel file with readxl::read_excel() and a CSV file with readr::read_csv().
  • Inspect a tibble with dim(), names(), glimpse(), summary(), head(), and tail().
  • Count missing values per column with colSums(is.na(...)) and distinguish NA from 0.
  • Write a small data dictionary in Quarto next to the code that produced it.
  • Render a complete Quarto HTML report that another reader could open without R.

Expected output for this chapter

TipWhat you will hand in
  1. A rendered Quarto HTML report named frst232_ch05_learner.html, containing:
    • The result of importing both bc_disturbance_reforestation.xlsx and on_forest_statistics_1.xlsx.
    • The inspection output — dim(), names(), glimpse(), summary(), head(), and tail() — for each file (the inspection moves taught in this chapter).
    • A missing-value summary for each file (one cell per column showing how many NA values it has).
    • A data dictionary table for the BC file, written by you in Markdown.
    • A short prose paragraph (two or three sentences) describing what is in each file.
  2. The corresponding .qmd source file.
  3. Notification that you have completed the Excel midterm in the lab session for this chapter (see Lab session — Excel midterm below).
  4. A short group peer-review worksheet of another learner’s rendered HTML.

This list is the same as the rubric for the chapter assignment. Keep it in view while you work.

5.1 Where we are in the course

In Chapter 4 you:

  • Installed R, RStudio, and Quarto (or set up Posit Cloud).
  • Created the frst232/ project with data/, outputs/, R/, notes/.
  • Imported bc_disturbance_reforestation.xlsx with readxl::read_excel().
  • Computed five statistics (sum, mean, min, max, median) on bc$Reforestation_ha.
  • Confirmed those numbers match the values you saw in Excel for Chapter 1.

Chapter 5 deepens the R fundamentals: vectors, tibbles, column types, missing values, and your first full Quarto report.

5.2 Excel-to-R bridge — the inspection moves

From Chapter 4 you already know:

Excel inspection R command Returns
Status-bar Count dim(bc) / nrow(bc) Row and column count
Status-bar Sum sum(bc$ColumnName) Sum of one column
Status-bar Average mean(bc$ColumnName) Mean of one column

This chapter adds three more — the missing-value and type moves that Excel cannot do cleanly:

Excel pattern R command Returns
“Cells: blank or text? Hard to tell.” glimpse(bc) Each column with type and first values
“Count = 35 but 37 rows? Two cells are text.” colSums(is.na(bc)) Number of missing values per column
“Format Cells → Number / Text” class(bc$ColumnName) The type (“numeric”, “character”, etc.)

By the end of this chapter, every move you used to do by looking at Excel is now a scripted R call.

5.3 Vectors — the building block

Almost everything in R is a vector: an ordered sequence of values, all of the same type.

# A vector of numbers
years <- c(1987, 1988, 1989, 1990, 1991)
years
[1] 1987 1988 1989 1990 1991
# A vector of text
species <- c("Pl", "Fd", "Sx")
species
[1] "Pl" "Fd" "Sx"
# A vector of TRUE/FALSE values
is_old <- c(TRUE, FALSE, TRUE, TRUE, FALSE)
is_old
[1]  TRUE FALSE  TRUE  TRUE FALSE

The c() function (c for combine) is how you build a vector. Type the values, comma-separated, inside c(...).

5.3.1 The four atomic types

Every value in R has a type. The four most common:

Type Examples R shorthand
logical TRUE, FALSE, NA <lgl>
integer 1L, 2L, 2026L <int>
double 1, 3.14, -2.5 <dbl>
character "Vancouver", "Pl" <chr>

Numbers without an L suffix are <dbl> (double-precision) by default. Most numeric R work uses doubles.

5.3.2 Vectorized operations

Most R functions work on a whole vector at once, not one element at a time:

ha <- c(220000, 180000, 250000, 190000)

# Each of these acts on the whole vector
sum(ha)
[1] 840000
mean(ha)
[1] 210000
min(ha)
[1] 180000
max(ha)
[1] 250000
length(ha)
[1] 4
# Arithmetic is vectorized too — element-by-element
ha / 1000      # convert each value to thousands of hectares
[1] 220 180 250 190
ha + 1000      # add 1000 to every value
[1] 221000 181000 251000 191000
ha > 200000    # comparison returns a logical vector
[1]  TRUE FALSE  TRUE FALSE

That last result — ha > 200000 returns a vector of TRUE/FALSE — is the bedrock of every filter and condition you will write in R.

5.3.3 Subsetting with [

You can pull out specific positions from a vector with square brackets:

ha[1]                 # first value
[1] 220000
ha[c(1, 3)]           # first and third values
[1] 220000 250000
ha[ha > 200000]       # only values greater than 200,000
[1] 220000 250000

That third example reads: “return the elements of ha where ha > 200000 is TRUE”. This is the logical-vector subsetting pattern — the conceptual ancestor of dplyr::filter() which you will meet in Chapter 6.

5.4 Tibbles — vectors arranged in columns

A tibble is a tidyverse-flavored data.frame: a rectangular table where every column is a vector and all columns have the same length.

When you ran:

bc <- read_excel(here("data", "bc_disturbance_reforestation.xlsx"),
                 sheet = "Data")

…in Chapter 4, R built a tibble called bc. Each column of bc is a vector. The whole tibble is a list of vectors that R prints as a rectangle.

# A tibble: 37 × 8
  Fiscal_Year Clearcutting_ha Clearcutting_with_reserves_ha Partial_cutting_ha
        <dbl>           <dbl>                         <dbl>              <dbl>
1        1987         216305.                         5800.             20077.
2        1988         217836.                         1843.             31878.
3        1989         190636.                         1457.             23786.
4        1990         172361.                         1400.             18931.
5        1991         192005.                         1515.             19697.
6        1992         209444.                         2951.             21691.
# ℹ 31 more rows
# ℹ 4 more variables: Harvested_ha <dbl>, Natural_Disturbance_ha <dbl>,
#   Total_Disturbance_ha <dbl>, Reforestation_ha <dbl>

Look at the top of the printout. Just below the column names, you see things like <dbl>, <dbl>, … — the column types. A tibble always shows its column types when printed. That alone is reason enough to use tibbles over base R data.frames.

5.4.1 Tibble vs data.frame

For most purposes, a tibble is a data.frame. The differences that matter for this course:

Feature data.frame tibble
Prints All rows by default (slow on big data) First 10 rows only
Column types Hidden until you ask Always shown
Subset with [, "col"] Returns a vector Returns a tibble
Created by read.csv(), data.frame() read_excel(), read_csv(), tibble()

You can always convert in either direction: as_tibble(df) turns a data.frame into a tibble, and as.data.frame(tib) turns a tibble back into a data.frame. For tidyverse work, stay with tibbles.

5.5 Importing files

Use this after your own attempt. It should check your reasoning, not hand you the answer:

“I imported [file] and got a tibble of [rows] x [cols]. Check whether the column types look right, whether any column that should be numeric came in as text (or a date as character), and which inspection commands I should run next. Do not hand me a finished import script.”

Two import functions cover almost every real-world file:

5.5.1 readxl::read_excel()

bc <- read_excel(here("data", "bc_disturbance_reforestation.xlsx"),
                 sheet = "Data")

Arguments to remember:

  • The first argument is the file path — always build it with here().
  • sheet = names the sheet to read. If you skip it, read_excel reads the first sheet.
  • skip = 3 skips the first 3 rows (useful when the file has a banner or title above the headers).
  • na = c("", "NA", "n/a") tells read_excel which strings to treat as missing.

5.5.2 readr::read_csv()

# Example (you do not need to run this; we use it in Chapter 6).
airdata <- read_csv(here("data", "airdata3.csv"))

Arguments to remember:

  • The first argument is the file path.
  • col_types = can lock in column types if read_csv guesses wrong (advanced — leave it off for now).
  • na = c("", "NA", "-9999") is useful for fields that use sentinel values for missing.

5.5.3 Other readr siblings (briefly)

  • read_tsv() — tab-separated.
  • read_delim() — generic, any delimiter.

All readr functions return a tibble. All accept a file path as the first argument.

5.6 Inspecting a tibble — the eight moves

Whenever you load a new file, run these eight commands. They take seconds and catch most problems.

dim(bc)         # rows and columns
[1] 37  8
nrow(bc)        # rows only
[1] 37
ncol(bc)        # columns only
[1] 8
names(bc)       # column names
[1] "Fiscal_Year"                   "Clearcutting_ha"              
[3] "Clearcutting_with_reserves_ha" "Partial_cutting_ha"           
[5] "Harvested_ha"                  "Natural_Disturbance_ha"       
[7] "Total_Disturbance_ha"          "Reforestation_ha"             
head(bc, 3)     # first three rows
# A tibble: 3 × 8
  Fiscal_Year Clearcutting_ha Clearcutting_with_reserves_ha Partial_cutting_ha
        <dbl>           <dbl>                         <dbl>              <dbl>
1        1987         216305.                         5800.             20077.
2        1988         217836.                         1843.             31878.
3        1989         190636.                         1457.             23786.
# ℹ 4 more variables: Harvested_ha <dbl>, Natural_Disturbance_ha <dbl>,
#   Total_Disturbance_ha <dbl>, Reforestation_ha <dbl>
tail(bc, 3)     # last three rows
# A tibble: 3 × 8
  Fiscal_Year Clearcutting_ha Clearcutting_with_reserves_ha Partial_cutting_ha
        <dbl>           <dbl>                         <dbl>              <dbl>
1        2021          13177.                       122562.              8295.
2        2022           9237.                       100909.              8953.
3        2023           7300.                        65369.              6292.
# ℹ 4 more variables: Harvested_ha <dbl>, Natural_Disturbance_ha <dbl>,
#   Total_Disturbance_ha <dbl>, Reforestation_ha <dbl>
glimpse(bc)     # every column, type, and first values
Rows: 37
Columns: 8
$ Fiscal_Year                   <dbl> 1987, 1988, 1989, 1990, 1991, 1992, 1993…
$ Clearcutting_ha               <dbl> 216304.92, 217836.07, 190635.52, 172360.…
$ Clearcutting_with_reserves_ha <dbl> 5800.400, 1842.952, 1456.666, 1400.013, …
$ Partial_cutting_ha            <dbl> 20076.681, 31878.294, 23785.954, 18930.8…
$ Harvested_ha                  <dbl> 242182.0, 251557.3, 215878.1, 192691.6, …
$ Natural_Disturbance_ha        <dbl> 20875.00, 9641.20, 10711.80, 28300.70, 1…
$ Total_Disturbance_ha          <dbl> 263057.0, 261198.5, 226589.9, 220992.3, …
$ Reforestation_ha              <dbl> 201972.5, 221296.3, 237275.9, 260370.8, …
summary(bc)     # min / quartiles / mean / max for each numeric column
  Fiscal_Year   Clearcutting_ha  Clearcutting_with_reserves_ha
 Min.   :1987   Min.   :  7300   Min.   :   923.9             
 1st Qu.:1996   1st Qu.: 28099   1st Qu.: 14044.9             
 Median :2005   Median : 76531   Median :121658.2             
 Mean   :2005   Mean   : 89774   Mean   : 97152.6             
 3rd Qu.:2014   3rd Qu.:166369   3rd Qu.:155763.9             
 Max.   :2023   Max.   :217836   Max.   :200699.1             
 Partial_cutting_ha  Harvested_ha    Natural_Disturbance_ha
 Min.   : 2502      Min.   : 78961   Min.   :  1448        
 1st Qu.: 5603      1st Qu.:190393   1st Qu.:  7562        
 Median : 9273      Median :204791   Median : 13908        
 Mean   :12127      Mean   :199053   Mean   : 29035        
 3rd Qu.:18931      3rd Qu.:216302   3rd Qu.: 28733        
 Max.   :31878      Max.   :251557   Max.   :218275        
 Total_Disturbance_ha Reforestation_ha
 Min.   :116942       Min.   :189394  
 1st Qu.:207574       1st Qu.:207668  
 Median :224147       Median :219925  
 Mean   :228089       Mean   :223353  
 3rd Qu.:254544       3rd Qu.:237276  
 Max.   :422092       Max.   :274616  

Each one answers a different question:

What you want to know Use
How many rows × columns? dim()
Just rows nrow()
Just columns ncol()
The column names names()
What the first / last rows look like head() / tail()
Column types + a preview of values glimpse()
Distributions of numeric columns summary()

glimpse() is the single most valuable function in this list. Make it your default first move on any tibble.

5.7 Column types — and why they matter

In the glimpse(bc) output above, you see types like <dbl> next to each column name. The type tells you what R thinks this column contains.

Type Means Example
<int> integer (whole numbers, no decimals) 1987L, 2L
<dbl> double (any real number) 216304.919
<chr> character (text) "Vancouver"
<lgl> logical TRUE / FALSE
<fct> factor (categorical with a fixed set of levels) rare on imported files
<date> date 2024-09-15

If read_excel reads Fiscal_Year as <dbl> but you want it as <int>, you can coerce after import:

bc$Fiscal_Year <- as.integer(bc$Fiscal_Year)
class(bc$Fiscal_Year)
[1] "integer"

Most of the time the inferred types are fine. The only case where it matters is when something looks numeric but arrives as text (often because of an extra space or a stray character).

5.7.1 How to spot a type bug

If you compute sum(bc$Reforestation_ha) and get the wrong answer — or an error message like “non-numeric argument to binary operator” — the column is probably character. Check:

class(bc$Reforestation_ha)
[1] "numeric"

If it returns "character", fix it:

bc$Reforestation_ha <- as.numeric(bc$Reforestation_ha)

5.8 Missing values — NA is its own thing

Use this after your own attempt. It should check your reasoning, not hand you the answer:

colSums(is.na(df)) shows [which columns] have missing values. Check whether my plan to handle them ([drop / keep / na.rm = TRUE]) is appropriate for this dataset, and explain what could go wrong if I treated NA as zero. Do not decide the whole cleaning strategy for me.”

In R, missing values are represented by NA. NA is not zero. It is not an empty string. It is its own thing: a flag meaning “we do not know this value”. If that distinction is new to you, skim NA vs 0 (below) first — treating an unknown value as 0 silently corrupts every sum and mean.

5.8.1 Counting NAs in one column

is.na(bc$Reforestation_ha)            # logical vector — TRUE for each NA
 [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[13] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[25] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[37] FALSE
sum(is.na(bc$Reforestation_ha))       # how many NAs in this column
[1] 0

is.na() returns a logical vector of the same length as its input — TRUE for each NA, FALSE for each real value. Sum that vector to count the missing values (TRUE is treated as 1, FALSE as 0).

5.8.2 Counting NAs in every column at once

colSums(is.na(bc))
                  Fiscal_Year               Clearcutting_ha 
                            0                             0 
Clearcutting_with_reserves_ha            Partial_cutting_ha 
                            0                             0 
                 Harvested_ha        Natural_Disturbance_ha 
                            0                             0 
         Total_Disturbance_ha              Reforestation_ha 
                            0                             0 

This is the fastest missing-value audit in R. One line. Every column. Always run it after import.

column n_missing
Fiscal_Year 0
Clearcutting_ha 0
Clearcutting_with_reserves_ha 0
Partial_cutting_ha 0
Harvested_ha 0
Natural_Disturbance_ha 0
Total_Disturbance_ha 0
Reforestation_ha 0

For the BC silviculture file, every column shows 0 — there are no missing values, which matches what you saw in Excel for Chapter 1. (The Ontario file is the same — we will check in the exercises.)

5.8.3 NA vs 0 — a habit worth forming

Situation What to record
The variable was measured and the value really is zero 0
The variable was not measured NA
The plot did not exist that year NA
The measurement was below the detection limit depends — see your data dictionary

Mixing these up is a silent bug class. “Mean reforestation” including 12 unmeasured years coded as 0 will be 12 zeros pulled into the average. Coded as NA, they would be skipped (if you use na.rm = TRUE).

5.9 Skipping NA in calculations

# If a column had NAs, the default behaviour is:
mean(c(1, 2, NA, 4))    # returns NA
[1] NA
# Add na.rm = TRUE to skip missing values:
mean(c(1, 2, NA, 4), na.rm = TRUE)   # returns 7/3 = 2.33
[1] 2.333333

na.rm = TRUE is available on every aggregation function (sum, mean, min, max, median, sd). Add it whenever your data might have missing values. Forgetting it is the most common silent bug in first-year R.

5.10 A worked inspection — the BC file end-to-end

Here is the canonical inspection sequence on a freshly imported file:

# 1. Import
bc <- read_excel(here("data", "bc_disturbance_reforestation.xlsx"),
                 sheet = "Data")

# 2. Dimensions and column names
dim(bc)
[1] 37  8
names(bc)
[1] "Fiscal_Year"                   "Clearcutting_ha"              
[3] "Clearcutting_with_reserves_ha" "Partial_cutting_ha"           
[5] "Harvested_ha"                  "Natural_Disturbance_ha"       
[7] "Total_Disturbance_ha"          "Reforestation_ha"             
# 3. Type + preview
glimpse(bc)
Rows: 37
Columns: 8
$ Fiscal_Year                   <dbl> 1987, 1988, 1989, 1990, 1991, 1992, 1993…
$ Clearcutting_ha               <dbl> 216304.92, 217836.07, 190635.52, 172360.…
$ Clearcutting_with_reserves_ha <dbl> 5800.400, 1842.952, 1456.666, 1400.013, …
$ Partial_cutting_ha            <dbl> 20076.681, 31878.294, 23785.954, 18930.8…
$ Harvested_ha                  <dbl> 242182.0, 251557.3, 215878.1, 192691.6, …
$ Natural_Disturbance_ha        <dbl> 20875.00, 9641.20, 10711.80, 28300.70, 1…
$ Total_Disturbance_ha          <dbl> 263057.0, 261198.5, 226589.9, 220992.3, …
$ Reforestation_ha              <dbl> 201972.5, 221296.3, 237275.9, 260370.8, …
# 4. Numeric summaries
summary(bc)
  Fiscal_Year   Clearcutting_ha  Clearcutting_with_reserves_ha
 Min.   :1987   Min.   :  7300   Min.   :   923.9             
 1st Qu.:1996   1st Qu.: 28099   1st Qu.: 14044.9             
 Median :2005   Median : 76531   Median :121658.2             
 Mean   :2005   Mean   : 89774   Mean   : 97152.6             
 3rd Qu.:2014   3rd Qu.:166369   3rd Qu.:155763.9             
 Max.   :2023   Max.   :217836   Max.   :200699.1             
 Partial_cutting_ha  Harvested_ha    Natural_Disturbance_ha
 Min.   : 2502      Min.   : 78961   Min.   :  1448        
 1st Qu.: 5603      1st Qu.:190393   1st Qu.:  7562        
 Median : 9273      Median :204791   Median : 13908        
 Mean   :12127      Mean   :199053   Mean   : 29035        
 3rd Qu.:18931      3rd Qu.:216302   3rd Qu.: 28733        
 Max.   :31878      Max.   :251557   Max.   :218275        
 Total_Disturbance_ha Reforestation_ha
 Min.   :116942       Min.   :189394  
 1st Qu.:207574       1st Qu.:207668  
 Median :224147       Median :219925  
 Mean   :228089       Mean   :223353  
 3rd Qu.:254544       3rd Qu.:237276  
 Max.   :422092       Max.   :274616  
# 5. Missing-value audit
colSums(is.na(bc))
                  Fiscal_Year               Clearcutting_ha 
                            0                             0 
Clearcutting_with_reserves_ha            Partial_cutting_ha 
                            0                             0 
                 Harvested_ha        Natural_Disturbance_ha 
                            0                             0 
         Total_Disturbance_ha              Reforestation_ha 
                            0                             0 
# 6. Spot-check the verification identity from Chapter 1
identical_check <- all.equal(
  bc$Harvested_ha + bc$Natural_Disturbance_ha,
  bc$Total_Disturbance_ha
)
identical_check
[1] TRUE

Six lines. Six confirmations. You now know:

  • The file has 37 rows and 8 columns (matches Chapter 1).
  • Column names match the ReadMe (you can compare them by eye).
  • Every column is <dbl> — no surprise text-as-number issues.
  • The summary() table tells you each column’s range.
  • No missing values anywhere.
  • The arithmetic identity from Chapter 1 still holds: Harvested + Natural = Total for every row.

That sequence is what professional R analysts do every time they open a new file. Do it on every dataset you touch.

5.11 First Quarto report

You have been reading Quarto documents through this whole book. Now you write one.

5.11.1 The four parts of a .qmd file

A Quarto file has four ingredients:

Part Looks like What it does
YAML header --- block at the top Tells Quarto how to render
Markdown text Plain prose, headings, bullets The reading content
Code chunks ```{r} ... ``` R code that gets executed
Inline code `r expression` One-line R inside prose

A minimal .qmd file:

---
title: "My first Quarto report"
format: html
---

# Introduction

This report inspects the BC silviculture file.


::: {.cell}

```{.r .cell-code}
library(readxl)
library(here)
bc <- read_excel(here("data", "bc_disturbance_reforestation.xlsx"),
                 sheet = "Data")
dim(bc)
```

::: {.cell-output .cell-output-stdout}

```
[1] 37  8
```


:::
:::


The file has 37 rows.

Save as my_first_report.qmd. Click Render (or Ctrl+Shift+K / Cmd+Shift+K). You get a single self-contained HTML file.

5.11.2 Common chunk options

Chunk options go right after the ```{r} line, prefixed with #|:


::: {.cell}

:::
Option What it does
label: Names the chunk (helpful when one errors)
echo: false Hide the code in the rendered output (just show the result)
include: false Run the code but hide both code and output (useful for setup)
message: false Hide messages from R (e.g., the tidyverse load message)
warning: false Hide warnings
eval: false Show the code but do not run it
fig.width: 6 Plot width in inches (Chapter 9)
fig.cap: "..." Caption for figures

5.11.3 What rendering does

When you click Render, Quarto:

  1. Reads your .qmd file.
  2. Runs every R chunk in order, from top to bottom, in a fresh R session.
  3. Captures the output (text, tables, charts).
  4. Stitches code + text + output into a single HTML file.
  5. Saves the HTML alongside the .qmd.

The fresh session part is important: rendering does not use your interactive console. If a chunk depends on a variable that was only created in your console (and not in an earlier chunk), rendering will fail.

TipA reproducibility test

If your .qmd renders cleanly from a fresh R session, your work is reproducible. If it does not, your work depends on state that no one else can see — and that is the bug.

Restart R often. In RStudio: Session → Restart R (Ctrl+Shift+F10 / Cmd+Shift+F10). Then render. If it still works, you are good.

5.12 Writing a data dictionary in Quarto

A data dictionary lives next to your code. In Quarto, it is just a Markdown table:

| Column | Type | Unit | Description |
|---|---|---|---|
| `Fiscal_Year` | integer | year | Fiscal year (April–March) |
| `Harvested_ha` | double | hectares | Total area harvested |
| ... | | | |

That renders as:

Column Type Unit Description
Fiscal_Year integer year Fiscal year (April–March)
Harvested_ha double hectares Total area harvested

The point: the dictionary travels with the analysis. When you hand your .qmd to a colleague, they see the table and the code that built the numbers, in the same file.

TipStarter for the BC file

The BC silviculture file has these eight columns. Copy this table into your report and fill in the Type, Unit, and Description yourself (use glimpse(bc) for the types):

Column Type Unit Description
Fiscal_Year
Clearcutting_ha
Clearcutting_with_reserves_ha
Partial_cutting_ha
Harvested_ha
Natural_Disturbance_ha
Total_Disturbance_ha
Reforestation_ha

File-summary template. After the dictionary, add two or three sentences that turn your inspection output into prose, e.g.:

“The BC silviculture file has 37 rows and 8 columns. The variables are numeric areas in hectares plus a fiscal-year integer, and the missing-value audit shows no missing values. The main variables describe harvested, natural-disturbance, total-disturbance, and reforested area by fiscal year.”

5.13 Active learning

5.13.1 Activity 1 — Vectors

ha <- c(220000, 180000, 250000, 190000, 210000)

In the console, compute:

  • the sum,
  • the mean,
  • the count of values greater than 200,000 (hint: sum(ha > 200000)),
  • the values greater than 200,000 themselves.

5.13.2 Activity 2 — Inspect two files

Run the six-move inspection on the BC silviculture file, then on the Ontario forest stats file. Note the differences:

ontario <- read_excel(here("data", "on_forest_statistics_1.xlsx"),
                      sheet = "Data")
dim(ontario); glimpse(ontario)

How are the column types different from the BC file? Which file has any character columns?

5.13.3 Activity 3 — Missing-value audit

Run colSums(is.na(bc)) and colSums(is.na(ontario)). Are there any missing values? Confirm against what you saw in Excel for Chapters 1 and 2.

5.13.4 Activity 4 — Render your first Quarto report

Open the chapter learner file frst232_ch05_learner.qmd. Render it. Confirm the HTML opens and shows all expected output.

5.13.5 Activity 5 — Break and fix

In frst232_ch05_learner.qmd, deliberately misspell Reforestation_ha somewhere. Render. Read the error message carefully. Fix it. Re-render. The error message taught you something useful — what?

5.14 Lab session — Excel midterm

ImportantThe lab session for Chapter 5 is the Excel midterm

There is no separate practice lab this chapter. The lab session is given over to the Excel midterm, covering material from Chapters 1–3:

  • Importing and inspecting an Excel workbook (Chapter 1).
  • Cleaning, summarizing, and writing formulas (Chapter 2).
  • Visualization and PivotTables (Chapter 3).

The midterm is closed-AI but open-notes. Bring a laptop with Excel installed (or use the lab computers). The teaching team will provide a midterm-specific workbook at the start of the session.

What to do before the lab:

  • Re-read the Chapter 1, 2, and 3 Expected output callouts.
  • Make sure your laptop has Excel working.
  • Bring your bc_disturbance_reforestation.xlsx and on_forest_statistics_1.xlsx files in case the midterm references them.

What is NOT on the midterm:

  • R code.
  • Quarto rendering.
  • Anything from Chapter 4 or Chapter 5.

The R material is assessed through homework exercises, the chapter quizzes, and the final exam at the end of the term.

5.15 Group activity — Peer review of Quarto reports

Outside the midterm session, run this peer-review activity at any time during the chapter (e.g., during a lecture session).

NoteGroup: Peer review a rendered HTML report

Work in pairs (or threes). Each member should have a rendered frst232_ch05_learner.html to share.

Task 1 — Swap. Each person sends their rendered HTML to the person on their left. Open it in a browser.

Task 2 — Score each other’s report using this rubric:

Criterion 0 1 2
Renders without error Did not render Renders with warnings Renders cleanly
dim(), names(), glimpse() visible Missing Present but unclear Clearly visible
Missing-value summary present Missing Present but unexplained Present with note
Data dictionary table Missing Incomplete Complete and readable
Two-sentence prose summary Missing Vague Specific to the file

Task 3 — Give one piece of constructive feedback in writing. What is the strongest part of the report? What would you change?

Task 4 — Submit. Submit your peer-review rubric (with your own name and the author’s name) to the course site.

This activity is designed to fit inside a single lecture session.

5.16 Exercises

These are the take-home exercises that go with this chapter.

  1. Run the six-move inspection sequence on the BC silviculture file. Copy the output of each function into your .qmd file inside R chunks.

  2. Run the same six-move inspection on the Ontario file. Compare the two outputs. Which file has character columns? How many?

  3. Compute the missing-value summary for both files using colSums(is.na(...)). Are there any missing values?

  4. Build a data dictionary table for the BC silviculture file in your .qmd, in the format shown in this chapter. Each row should have Column, Type, Unit, Description.

  5. Use vectorized arithmetic to compute the ratio Reforestation_ha / Total_Disturbance_ha for every year. Save the result as a new vector. How many years had a ratio greater than 1?

  6. Use summary(bc) to find the median of Total_Disturbance_ha. Confirm it matches the value you would compute with median(bc$Total_Disturbance_ha).

  7. Restart R (Session → Restart R). Re-render your frst232_ch05_learner.qmd from scratch. Confirm it renders without errors. (If it does not, fix what depends on hidden console state.)

  8. Optional, harder. Write a tiny R function called inspect_file() that takes a tibble and prints dim, names, glimpse, and colSums(is.na(...)) all at once. Apply it to bc and ontario.

5.17 Optional, advanced

5.17.1 Lists — the catch-all data structure

A list in R is like a vector except its elements can be of different types. A tibble is technically a list of equal-length vectors.

my_list <- list(name = "BC silviculture",
                rows = 37,
                cols = 8,
                tibble = bc)
my_list$name
my_list$tibble

You will use lists in the optional spatial-data material, where each “row” is a complex geometry.

5.17.2 Factors — categorical variables with order

A factor is a vector with a fixed set of allowed values (levels). Useful when you want ordered categories (small < medium < large) or when a categorical variable has a known set of valid values.

size <- factor(c("small", "large", "medium", "large"),
               levels = c("small", "medium", "large"))
size

We will use factors more in Chapter 9 (ggplot2 axis ordering).

5.17.3 Locking column types at import time

If read_excel or read_csv guesses a column type wrong, you can override it:

read_csv("file.csv",
         col_types = cols(
           plot_id = col_character(),
           dbh_cm = col_double(),
           date_measured = col_date(format = "%Y-%m-%d")
         ))

This is overkill for clean BC and Ontario files. It becomes useful when you import messy real-world CSVs in Chapter 6.

5.18 A glimpse ahead: From inspection to cleaning

In Chapter 6 you move from inspecting to cleaning. The core moves of dplyr:

What you want to do Excel (Ch 2) dplyr verb (Ch 6)
Keep some rows AutoFilter filter()
Add a new column =A2+B2 in a new column mutate()
Sort Data → Sort arrange()
Keep only some columns Delete columns select()
Group and summarise PivotTable group_by() |> summarise()
Look up from a reference VLOOKUP left_join()
Recode values IFS case_when()

Every Excel cleaning move you learned in Chapter 2 has a one-line dplyr equivalent. The names are short, the pattern is consistent, and once you know the six verbs above, you can clean almost any real-world dataset.

Chapter 6 walks through each verb on the BC air-quality dataset (airdata3.csv) — a messier file than what you have seen so far, with actual missing values, mixed date formats, and the kind of problems that real forestry data presents.

5.19 Self-assessment quiz

Click the answer you think is correct. The right answer turns green; wrong answers turn red and the correct one is revealed. Explanations appear below each question.

1. What is a vector in R?
  • A graphical chart
  • An ordered sequence of values, all of the same type
  • A file format
  • The same as a list
Vectors are the building block of R. A vector has a single type — logical, integer, double, or character — and a length.
2. c(1987, 1988, 1989) is a vector of which type?
  • integer ()
  • double ()
  • character ()
  • logical ()
Numbers without an L suffix are doubles by default. c(1987L, 1988L, 1989L) would be integer.
3. A tibble is —
  • A new programming language
  • A tidyverse-flavored data.frame — a rectangular table where each column is a vector
  • A type of chart
  • A statistical model
Tibbles are the tidyverse's default rectangular structure. They print better than data.frames and show column types.
4. In a tibble printout, you see <dbl> under a column name. What does that mean?
  • The column has no values
  • The column is a double (any real number)
  • The column is text
  • The column has been deleted
<dbl> = double-precision number. <int> = integer. <chr> = character (text). <lgl> = logical.
5. To import a CSV file in R, you would use —
  • read_excel()
  • read_csv()
  • read_pdf()
  • open()
read_csv() from the readr package (part of tidyverse) reads comma-separated files into a tibble. read_excel() is for .xlsx files only.
6. colSums(is.na(bc)) returns —
  • The sum of every column
  • A named vector giving the number of missing (NA) values in each column
  • Whether the file is complete
  • An error
is.na(bc) returns a logical matrix (TRUE for each NA). colSums() adds the TRUEs per column. One line, complete missing-value audit.
7. NA in R means —
  • Zero
  • An empty string
  • Missing — the value is unknown
  • North America
NA is its own value type. Mixing it up with 0 is a silent bug: an unmeasured year recorded as 0 will be pulled into averages; recorded as NA, it will be skipped if you use na.rm = TRUE.
8. mean(c(1, 2, NA, 4)) returns —
  • 2.33
  • NA
  • An error
  • 7
By default, R refuses to silently ignore missing values. Add na.rm = TRUE to skip them: mean(c(1,2,NA,4), na.rm = TRUE) returns 2.33.
9. Which function gives the most useful one-line preview of every column in a tibble (name, type, and first values)?
  • dim()
  • summary()
  • glimpse()
  • head()
glimpse() shows every column, its type, and a preview of the first values — like a richer version of Excel's status bar.
10. summary(bc) on a tibble of numeric columns returns —
  • The R version
  • For each column: Min, 1st Quartile, Median, Mean, 3rd Quartile, Max (and NA count if any)
  • Only the column names
  • A chart
summary() produces the five-number summary plus the mean for each numeric column. For character columns it just notes the type.
11. A Quarto .qmd file has four main parts. Which is NOT one of them?
  • YAML header
  • Markdown text
  • Code chunks
  • A standalone CSS file
The four parts: YAML header, Markdown text, code chunks (```` ```{r} ... ``` ````), and inline R code (`r ...`). CSS can be customized but is not a required part.
12. Inside a Quarto code chunk, what does #| echo: false do?
  • Skips the chunk entirely
  • Hides the code in the rendered HTML; still shows the output
  • Hides the output; still shows the code
  • Stops the chunk after one second
echo: false shows the output but hides the source code. Use it for the setup chunk and for any code your reader does not need to see.
13. When you click Render in RStudio, Quarto —
  • Just exports the .qmd file as text
  • Runs every code chunk in order in a fresh R session and produces a self-contained HTML (or PDF) file
  • Asks an AI to render the document
  • Saves the file to the cloud
Render = run every chunk top-to-bottom in a fresh session + assemble code, text, and output into one HTML. The fresh session is what makes rendering a reproducibility test.
14. bc$Fiscal_Year is —
  • A tibble
  • A vector — the Fiscal_Year column of the bc tibble
  • An error
  • A function
Using $ on a tibble returns a single column as a vector. bc[, "Fiscal_Year"] on a tibble returns a one-column tibble, not a vector.
15. ha > 200000 returns —
  • A single number
  • A logical vector — TRUE or FALSE for each element of ha
  • An error
  • The number of values greater than 200,000
Comparison in R is vectorized: each element of ha is compared to 200000, returning a parallel logical vector. To count how many are TRUE, wrap with sum().
16. Inside a Quarto chunk, you write code that uses a variable created only in your interactive console. When you Render, what happens?
  • It works fine — Quarto sees the console
  • It fails — Render runs in a fresh R session that does not see console state
  • It works only on Posit Cloud
  • It runs the chunk twice
Render uses a fresh session. Everything the document depends on must be created inside a chunk. This is the whole point of Quarto being reproducible.
17. The Excel midterm is held —
  • Online, asynchronously
  • During the Chapter 5 lab session
  • At the end of the term
  • There is no midterm
The Chapter 5 lab session is given over to the midterm. The midterm covers Chapter 1, 2, and 3 (Excel) material.
18. Which is the closest equivalent of Excel's status-bar "Count = 35 but I selected 37 cells" warning?
  • summary(bc)
  • dim(bc)
  • colSums(is.na(bc)) — a non-zero entry means that column has missing or non-numeric cells
  • head(bc)
In Excel, status-bar Count < selection size meant some cells were text-not-number. In R, colSums(is.na(...)) per column tells you the same thing more precisely.
19. What is the main difference between a tibble and a data.frame in this course?
  • Tibbles cannot hold text
  • Tibbles print better (first 10 rows, column types shown) and are the default in tidyverse imports
  • data.frames are faster
  • There is no difference
For most operations, a tibble IS a data.frame (it inherits from it). The differences matter for printing, subsetting (tibble[, "col"] returns a tibble, not a vector), and consistency with tidyverse functions.
20. Reflection. Now that you have rendered a Quarto report, how does the experience compare to submitting an Excel workbook? What is easier? What is harder?
Common reflections: easier — every number in the report is traceable to the code that produced it · harder — getting the YAML header right the first time · easier — re-running with a new dataset doesn't require redoing every chart and table · harder — error messages on render are scary at first · aha moment — the Quarto report carries its own data dictionary AND its own analysis, in one file.

5.20 AI as a debugging companion

TipUseful prompts for this chapter
  • “My Quarto document fails to render with the error object 'bc' not found. The variable works fine in my console. What might be wrong?” (Hint: fresh-session rendering.)

  • “Explain this output of glimpse(bc) line by line:” (paste the glimpse output)

  • “I imported a CSV with read_csv and one of my numeric columns was read as character. What’s the most common cause and how do I fix it?”

5.20.1 A prompt template for the rest of the course

When you ask AI for help with R code, include four things:

  1. Your R version (R.version.string).
  2. The packages you have loaded (the top of your .qmd).
  3. The exact code you ran.
  4. The exact error message (copy-paste, do not paraphrase).

Without these four, the answer will be too generic to use.

5.20.2 Verifying AI’s answers

After AI suggests a fix, check:

  1. Run the suggested code in your console. Does it work?
  2. Does the result match what you expect from glimpse(), summary(), or a manual count?
  3. If AI rewrites a chunk for you, render the whole document afterwards to confirm nothing else broke.

5.20.3 When not to use AI

  • Do not let AI write your data dictionary. The dictionary describes your analysis decisions.
  • Do not let AI hide an unexplained behaviour with a try() or suppressWarnings(). Warnings exist for a reason — read them.

5.21 Reading

Instructor notes

  • The chapter has two big new ideas: vectors / tibbles (the R type system) and Quarto (the new deliverable format). Spend time on both; do not rush past the YAML header.
  • The missing-value audit (colSums(is.na(bc))) is the single most useful one-line R command in this chapter. Use it in your live demo.
  • Quarto rendering must succeed before submission. Many learners will hand in a .qmd that does not render. Build the “render from a fresh session” check into the grading rubric.

5.21.1 Excel midterm logistics

  • The lab session for this chapter is the midterm.
  • Material is Chapter 1, 2, 3 Excel only.
  • Recommended midterm structure: one 90-minute Excel deliverable (single workbook) with sections on importing, cleaning, summarizing, and a PivotTable. Provide a starter file at the start of the session.
  • Closed-AI, open-notes. Learners may use the chapter materials but not generative-AI tools.
  • TAs should be prepared for “my Excel won’t open” support questions at the start of the session — have a backup computer ready.

5.21.2 Expected output checklist (matches the top-of-chapter callout)

  1. frst232_ch05_learner.html (rendered) with imports for both BC and Ontario files.
  2. frst232_ch05_learner.qmd (source).
  3. Excel midterm completed in the lab session.
  4. Peer-review worksheet (during the lecture peer-review activity).

5.21.3 Materials provided alongside this chapter

File Purpose
bc_disturbance_reforestation.xlsx Raw BC silviculture (from Ch 1, unchanged).
on_forest_statistics_1.xlsx Raw Ontario forest stats (from Ch 2, unchanged).
frst232_ch05_learner.qmd Quarto starter with TODO chunks for each exercise.
frst232_ch05_solutions.qmd Completed Quarto document. Do not distribute to learners.
quiz_ch05.html Standalone interactive quiz.

5.21.4 Common stumbling points

  • “Object not found on render.” Almost always: the variable was created in the console, not in a chunk. Solution: Restart R, then re-render.
  • YAML errors. Quarto is fussy about the YAML header. Indentation must use spaces (no tabs); the --- fences must be on their own lines.
  • library(tidyverse) not run in the setup chunk. Then the first chunk that uses a tidyverse function errors. Make sure the setup chunk has include: false (runs but is hidden) and loads every package the document needs.
  • NA confusion. Some learners conflate NA with 0 when they see a “missing value count of 0” — they think “so there are zero values?”. Walk through the distinction explicitly.
  • Excel-to-R bridge: The most important sentence in this chapter is “every move you used to do by looking at Excel is now a scripted R call”. Refer back to it whenever a learner asks why a particular function exists.

Working with AI on this chapter

Use AI as a verification and debugging partner, not an answer machine. The goal is not to make AI do the work — it is to use AI to help you understand, check, and improve your own work.

Fill in the brackets and paste this into your AI assistant. It should explain and check, not hand you a final assignment answer:

“I am working on FRST 232, this chapter. My dataset is [file name] with columns [columns]. My task is [what you are trying to do]. I tried [paste your code or steps] and got [paste the exact output or error]. Explain what this does line by line, tell me whether it answers the question, and list the checks I should run to verify it. Do not give me the final answer.”

Verify it yourself — before trusting any result (yours or an AI’s):

  • the rows, columns, and units are what you expect;
  • the row count changes sensibly after a filter() or a join;
  • group sizes and summary values are plausible;
  • the figure answers the question and axis labels include units.

Common AI mistakes to watch for: inventing a column, function, or value that is not in your data; choosing a tool before reading the question; and (in Excel) suggesting the wrong cell range or forgetting to refresh a PivotTable.

When a reference solution is available, do not just copy it — use it to check your own work:

  • Expected result — does your output match the target shape and values?
  • Required components — did you include every step the task asked for?
  • If your result differs — say where it matched, where it differed, what caused the difference, how you fixed it, and what you learned.

Write a prompt that would guide an AI toward the reference solution without handing over the final answer. Include the dataset, the columns, the task goal, the output format you need, and the verification checks. Then paste (or summarise) the AI’s response and answer: did its approach match the book’s? did it miss a verification step? did it invent a column, function, or value? what did your group change after checking it?