library(tidyverse)
recordTable <- read_csv(
"data/recordTable.csv"
)Session 3
Today we will connect three skills:
visualize data
↓
recognize and solve problems
↓
manipulate data with dplyr
We will not cover every detail from Modules 5, 6, and 7.
The website contains the full explanations.
Today we will focus on the most important tools you need to keep working with your own data.
Open your intro-r-course project.
Create a script called:
session-03.R
inside the scripts/ folder.
Then run:
Before plotting or manipulating data:
Ask:
Most plots follow the same structure:
In R:
There are many more options depending on what you want to explore or communicate with your data. If you want to go further, I recommend visiting these two websites: R Charts & R Graph Gallery | 
Question:
How many records do we have for each species?
ggplot(
data = recordTable,
aes(x = n_images)
) +
geom_histogram(
binwidth = 1,
fill = "steelblue",
color = "white"
) +
coord_cartesian(
xlim = c(1, 20)
) +
theme_bw()coord_cartesian() changes what we display, not the original data.
Setting:
Every point has the same color.
Mapping:
Color represents a variable.
We can map or set:
color
fill
shape
size
alpha
Example:
ggplot(
recordTable,
aes(
x = N_individuals,
y = n_images,
color = Species
)
) +
geom_point(alpha = 0.4)Sometimes a plot is easier to read if we focus on fewer groups.
Question:
Are records with more individuals associated with more images?
ggplot(
data = common_records,
aes(
x = N_individuals,
y = n_images,
color = Species
)
) +
geom_point(
alpha = 0.4,
size = 2
) +
theme_bw()Sometimes we need to summarize before plotting.
For example:
sp_counts <- recordTable |>
count(
Station,
Species
) |>
filter(
Species %in% common_species
)
sp_counts# A tibble: 62 × 3
Station Species n
<chr> <chr> <int>
1 Control 0 White-tailed deer 1
2 Control 1 Armadillo 6
3 Control 1 Hog 7
4 Control 1 Racoon 12
5 Control 1 Virginia opossum 2
6 Control 1 White-tailed deer 29
7 Control 2 Armadillo 182
8 Control 2 Hog 210
9 Control 2 Racoon 99
10 Control 2 Virginia opossum 73
# ℹ 52 more rows
Now each row represents:
one station × one species
ggplot(
data = sp_counts,
aes(
x = Species,
y = n,
fill = Species
)
) +
geom_boxplot(
show.legend = FALSE
) +
scale_y_log10() +
coord_flip() +
theme_bw()Here, the data are still counts.
Only the axis is displayed on a log scale.
#| eval: true
ggplot(
data = sp_counts,
aes(
x = Species,
y = n,
fill = Species
)
) +
geom_violin(alpha = 0.5) +
geom_boxplot(width = 0.15) +
geom_jitter(width = 0.15, alpha = 0.6) +
scale_y_log10() +
coord_flip() +
theme_bw()Each + adds a new layer.
| Variables | Possible plot |
|---|---|
| one categorical | bar plot |
| one numeric | histogram |
| two numeric | scatterplot |
| categorical + numeric | boxplot or violin plot |
Start with the question:
What do I want to show?
Now we move from:
plotting raw data
to:
preparing data for questions
We will use dplyr.
Many dplyr functions are verbs.
| Function | Action |
|---|---|
select() |
choose columns |
filter() |
choose rows |
arrange() |
sort rows |
rename() |
rename columns |
mutate() |
create columns |
count() |
count observations |
group_by() |
create groups |
summarise() |
calculate summaries |
select() works with columns.
selected_records <- recordTable |>
select(
Station,
Species,
N_individuals,
Age,
n_images
)
selected_records# A tibble: 3,898 × 5
Station Species N_individuals Age n_images
<chr> <chr> <dbl> <chr> <dbl>
1 Control 0 White-tailed deer 0 Unknown 1
2 Control 1 Armadillo 1 Adult 2
3 Control 1 Armadillo 2 Unknown 2
4 Control 1 Armadillo 1 Unknown 1
5 Control 1 Armadillo 1 Unknown 1
6 Control 1 Armadillo 1 Adult 2
7 Control 1 Armadillo 1 Adult 1
8 Control 1 Bug 1 Adult 1
9 Control 1 Bug 1 Adult 2
10 Control 1 Bug 1 Adult 1
# ℹ 3,888 more rows
# A tibble: 1,266 × 25
RootFolder File RelativePath Station Camera DateTime Species
<chr> <chr> <chr> <chr> <chr> <dttm> <chr>
1 Field Station … DSCF… "Control 0\… Contro… Contr… 2025-05-15 14:53:20 White-…
2 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-04 21:02:19 White-…
3 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-09 23:34:22 White-…
4 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-10 00:34:24 White-…
5 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-16 09:39:38 White-…
6 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-16 10:39:40 White-…
7 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-18 08:46:21 White-…
8 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-18 09:46:22 White-…
9 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-20 06:56:25 White-…
10 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-20 07:56:26 White-…
# ℹ 1,256 more rows
# ℹ 18 more variables: N_individuals <dbl>, Sex <chr>, Age <chr>,
# Identifier <chr>, Comments <chr>, Favorite <lgl>, DateTime_original <dttm>,
# year <dbl>, date_shift <dbl>, n_images <dbl>, Date <date>, Time <time>,
# delta.time.secs <dbl>, delta.time.mins <dbl>, delta.time.hours <dbl>,
# delta.time.days <dbl>, DateTime2 <dttm>, Date2 <date>
filter() works with rows.
# A tibble: 86 × 25
RootFolder File RelativePath Station Camera DateTime Species
<chr> <chr> <chr> <chr> <chr> <dttm> <chr>
1 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-20 06:56:25 White-…
2 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-20 07:56:26 White-…
3 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-28 12:48:34 White-…
4 Field Station … DSCF… "Control 2\… Contro… Contr… 2025-04-24 15:32:46 White-…
5 Field Station … DSCF… "Control 2\… Contro… Contr… 2025-04-24 16:55:22 White-…
6 Field Station … DSCF… "Control 2\… Contro… Contr… 2025-05-05 14:58:46 White-…
7 Field Station … DSCF… "Control 2\… Contro… contr… 2025-06-06 20:44:02 White-…
8 Field Station … DSCF… "Control 2\… Contro… contr… 2025-06-06 21:44:04 White-…
9 Field Station … DSCF… "Control 2\… Contro… contr… 2025-06-07 02:31:38 White-…
10 Field Station … DSCF… "Control 2\… Contro… contr… 2025-06-07 16:02:24 White-…
# ℹ 76 more rows
# ℹ 18 more variables: N_individuals <dbl>, Sex <chr>, Age <chr>,
# Identifier <chr>, Comments <chr>, Favorite <lgl>, DateTime_original <dttm>,
# year <dbl>, date_shift <dbl>, n_images <dbl>, Date <date>, Time <time>,
# delta.time.secs <dbl>, delta.time.mins <dbl>, delta.time.hours <dbl>,
# delta.time.days <dbl>, DateTime2 <dttm>, Date2 <date>
Both conditions must be true.
This is like using:
AND
# A tibble: 2,342 × 25
RootFolder File RelativePath Station Camera DateTime Species
<chr> <chr> <chr> <chr> <chr> <dttm> <chr>
1 Field Station … DSCF… "Control 0\… Contro… Contr… 2025-05-15 14:53:20 White-…
2 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-05 04:25:19 Armadi…
3 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-26 07:39:44 Armadi…
4 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-26 08:07:20 Armadi…
5 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-26 08:39:46 Armadi…
6 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-07-20 01:11:11 Armadi…
7 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-07-20 02:11:12 Armadi…
8 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-07 23:35:55 Racoon
9 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-07-05 06:20:42 Racoon
10 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-07-09 20:38:20 Racoon
# ℹ 2,332 more rows
# ℹ 18 more variables: N_individuals <dbl>, Sex <chr>, Age <chr>,
# Identifier <chr>, Comments <chr>, Favorite <lgl>, DateTime_original <dttm>,
# year <dbl>, date_shift <dbl>, n_images <dbl>, Date <date>, Time <time>,
# delta.time.secs <dbl>, delta.time.mins <dbl>, delta.time.hours <dbl>,
# delta.time.days <dbl>, DateTime2 <dttm>, Date2 <date>
%in% asks:
Is this value inside this group of possible values?
record_data <- recordTable |>
rename(
station = Station,
species = Species,
individuals = N_individuals,
age = Age,
images = n_images,
date = Date,
time = Time
)
record_data# A tibble: 3,898 × 25
RootFolder File RelativePath station Camera DateTime species
<chr> <chr> <chr> <chr> <chr> <dttm> <chr>
1 Field Station … DSCF… "Control 0\… Contro… Contr… 2025-05-15 14:53:20 White-…
2 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-05 04:25:19 Armadi…
3 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-26 07:39:44 Armadi…
4 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-26 08:07:20 Armadi…
5 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-26 08:39:46 Armadi…
6 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-07-20 01:11:11 Armadi…
7 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-07-20 02:11:12 Armadi…
8 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-09 13:26:30 Bug
9 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-07-06 13:27:56 Bug
10 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-07-06 14:24:44 Bug
# ℹ 3,888 more rows
# ℹ 18 more variables: individuals <dbl>, Sex <chr>, age <chr>,
# Identifier <chr>, Comments <chr>, Favorite <lgl>, DateTime_original <dttm>,
# year <dbl>, date_shift <dbl>, images <dbl>, date <date>, time <time>,
# delta.time.secs <dbl>, delta.time.mins <dbl>, delta.time.hours <dbl>,
# delta.time.days <dbl>, DateTime2 <dttm>, Date2 <date>
Now the column names are easier to type.
# A tibble: 3,898 × 26
RootFolder File RelativePath station Camera DateTime species
<chr> <chr> <chr> <chr> <chr> <dttm> <chr>
1 Field Station … DSCF… "Control 0\… Contro… Contr… 2025-05-15 14:53:20 White-…
2 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-05 04:25:19 Armadi…
3 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-26 07:39:44 Armadi…
4 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-26 08:07:20 Armadi…
5 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-26 08:39:46 Armadi…
6 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-07-20 01:11:11 Armadi…
7 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-07-20 02:11:12 Armadi…
8 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-06-09 13:26:30 Bug
9 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-07-06 13:27:56 Bug
10 Field Station … DSCF… "Control 1\… Contro… Contr… 2025-07-06 14:24:44 Bug
# ℹ 3,888 more rows
# ℹ 19 more variables: individuals <dbl>, Sex <chr>, age <chr>,
# Identifier <chr>, Comments <chr>, Favorite <lgl>, DateTime_original <dttm>,
# year <dbl>, date_shift <dbl>, images <dbl>, date <date>, time <time>,
# delta.time.secs <dbl>, delta.time.mins <dbl>, delta.time.hours <dbl>,
# delta.time.days <dbl>, DateTime2 <dttm>, Date2 <date>,
# multiple_individuals <lgl>
The new column is logical:
TRUE / FALSE
# A tibble: 22 × 2
species n
<chr> <int>
1 White-tailed deer 1266
2 Armadillo 554
3 Racoon 522
4 Hog 454
5 Virginia opossum 295
6 Eastern gray squirrel 244
7 Bug 99
8 Human 80
9 Bird 78
10 Coyote 75
# ℹ 12 more rows
The result has one row per species.
The column n contains the number of records.
# A tibble: 154 × 3
station species n
<chr> <chr> <int>
1 Control 0 White-tailed deer 1
2 Control 1 Armadillo 6
3 Control 1 Bug 30
4 Control 1 Coyote 1
5 Control 1 Eastern Cottontail 5
6 Control 1 Hog 7
7 Control 1 Human 1
8 Control 1 Racoon 12
9 Control 1 Virginia opossum 2
10 Control 1 White-tailed deer 29
# ℹ 144 more rows
Now each row represents:
one station × one species
This is a different unit than the original data.
species_summary <- record_data |>
group_by(species) |>
summarise(
records = n(),
stations = n_distinct(station),
total_individuals = sum(
individuals,
na.rm = TRUE
),
.groups = "drop"
)
species_summary # A tibble: 22 × 4
species records stations total_individuals
<chr> <int> <int> <dbl>
1 Armadillo 554 15 576
2 Bird 78 9 83
3 Black vulture 3 1 4
4 Bobcat 22 6 22
5 Bug 99 8 113
6 Coyote 75 11 79
7 Domestic cat 2 1 2
8 Domestic dog 5 2 6
9 Eastern Cottontail 61 8 60
10 Eastern gray squirrel 244 9 252
# ℹ 12 more rows
Now each row represents one species.
What does one row represent?
Before summarizing:
1 row = 1 camera-trap record
After summarizing by species:
1 row = 1 species
After summarizing by station:
1 row = 1 station
The pipe:
passes the result of one step into the next step.
Read it as:
then
Example:
common_species <- c(
"White-tailed deer",
"Armadillo",
"Racoon",
"Hog",
"Virginia opossum"
)
common_summary <- recordTable |>
filter(
Species %in% common_species
) |>
group_by(Species) |>
summarise(
records = n(),
stations = n_distinct(Station),
.groups = "drop"
) |>
arrange(
desc(records)
)
common_summary # A tibble: 5 × 3
Species records stations
<chr> <int> <int>
1 White-tailed deer 1266 16
2 Armadillo 554 15
3 Racoon 522 11
4 Hog 454 11
5 Virginia opossum 295 9
ggplot(
common_summary,
aes(
x = Species,
y = records,
fill = Species
)
) +
geom_col(
show.legend = FALSE
) +
coord_flip() +
theme_bw()Use geom_col() when the counts already exist in the data.
import data
↓
inspect
↓
visualize
↓
debug when needed
↓
select / filter
↓
group / summarize
↓
visualize summaries
↓
export results
You will see:
That is normal.
The goal is not to avoid every error.
The goal is to learn how to understand and fix them.
R is telling you something.
R completed the operation, but something may need attention.
R stopped because something went wrong.
Example:
R may say:
Error: object 'grafics' not found
Start with the message.
What is R telling you?
When code fails, check the object.
Do not only inspect the code.
Inspect the data being used by the code.
Most errors have happened to someone else before.
Useful search:
R ggplot2 object not found
Less useful search:
object not found
Include:
R
package name
function name
important part of the error
Not very useful:
My code does not work.
Much more useful:
I am trying to make a bar plot of Species.
This is my code:
ggplot(recordTable, aes(x = Species))
geom_bar()
This is the error:
Error: unexpected symbol
What should I check?
AI tools can help you:
But use them as a learning tool.
Not as something that does your thinking for you.
Always:

Important
Do not copy and run code that you do not understand.
Before the next session:
filter()
select()
count()
group_by()
summarise()