To help you get a feel for the kind of advice tblcheck provides, we’ve collected a few example exercises that use grade_this_table()
or grade_this_vector()
from tblcheck to grade a learnr exercise.
In the examples below, we’ll show the specific R chunks you’d need to prepare the exercise. Globally, we assume learnr and gradethis are already loaded, and we’ve made a few adjustments to the default gradethis options using gradethis::gradethis_setup()
:
```{r setup}
library(learnr)
library(gradethis)
library(tblcheck)
gradethis_setup(
pass.praise = TRUE,
fail.hint = TRUE,
fail.encourage = TRUE,
maybe_code_feedback.before = "\n\n"
)
```
Note that in all examples below we’re using either grade_this_table()
or grade_this_vector()
without any additional customizations. We’ve also made sure that each exercise contains a -solution
chunk. (Read more about how to set up an exercise for use with tblcheck.)
The first example repeats the example presented in the Get started article. The goal is for the learner to use the tibble::tibble()
function to create a table, but it takes them a few tries.
Prompt
Create atibble
with three columns:food
,vegetable
, andcolor
.
- In
food
, store “lettuce” and “tomato”.- In
vegetable
, store whether the food is a vegetable.- In
color
, store the color of the food.
tibble(
food = c("lettuce", "tomato"),
vegetable = c(TRUE, FALSE),
color = c("green", "red")
)
#> # A tibble: 2 × 3
#> food vegetable color
#> <chr> <lgl> <chr>
#> 1 lettuce TRUE green
#> 2 tomato FALSE red
list(
food = "lettuce",
fruit = "TRUE",
color = "green"
)
#> $food
#> [1] "lettuce"
#>
#> $fruit
#> [1] "TRUE"
#>
#> $color
#> [1] "green"
Your table should be a tibble (class tbl_df
), but it is a list (class list
).
tibble()
where you called list()
. Try it again. I have a good feeling about this.
tibble(
food = "lettuce",
fruit = "TRUE",
color = "green"
)
#> # A tibble: 1 × 3
#> food fruit color
#> <chr> <chr> <chr>
#> 1 lettuce TRUE green
Your table should have a column named vegetable
. Your table should not have a column named fruit
.
tibble()
to include fruit = "TRUE"
. You may have included an unnecessary argument, or you may have left out or misspelled an important argument name. Let’s try it again.
tibble(
food = "lettuce",
vegetable = "TRUE",
color = "green"
)
#> # A tibble: 1 × 3
#> food vegetable color
#> <chr> <chr> <chr>
#> 1 lettuce TRUE green
Your table should have 2 rows, but it has 1 row.
Intibble(food = "lettuce", vegetable = "TRUE", color = "green")
, I expected you to call food = c()
where you wrote food = "lettuce"
. Try it again. You get better each time.
tibble(
food = c("lettuce", "tomato"),
vegetable = c("TRUE", "TRUE"),
color = c("green", "red")
)
#> # A tibble: 2 × 3
#> food vegetable color
#> <chr> <chr> <chr>
#> 1 lettuce TRUE green
#> 2 tomato TRUE red
Your vegetable
column should be a vector of TRUE/FALSE values (class logical
), but it is a vector of text (class character
).
c("TRUE", "TRUE")
, I expected TRUE
where you wrote "TRUE"
. Try it again. I have a good feeling about this.
tibble(
food = c("lettuce", "tomato"),
vegetable = c(TRUE, TRUE),
color = c("green", "red")
)
#> # A tibble: 2 × 3
#> food vegetable color
#> <chr> <lgl> <chr>
#> 1 lettuce TRUE green
#> 2 tomato TRUE red
The first 2 values of your vegetable
column should be TRUE
and FALSE
, not TRUE
and TRUE
.
c(TRUE, TRUE)
, I expected FALSE
where you wrote TRUE
. That’s okay: you learn more from mistakes than successes. Let’s do it one more time.
transmute()
The first example shows all of the problems that tblcheck will discover. This example, however, is more realistic.
In this problem students are asked to use the dplyr::transmute()
function to transform the starwars
dataset.
Prompt
Consider thestarwars
dataset. Usetransmute()
to turn convertheight
from centimeters into inches andmass
from kilgrams into pounds, keeping only those columns.
The correct answer uses dplyr::transmute()
to divide the height
column by 2.54
(to convert height from cm to in) and to multiply the mass
column by 2.205
(to convert kg to lb). Note that transmute()
keeps only the columns named in its calculations, so our final table has only two columns.
starwars %>%
transmute(
height = height / 2.54,
mass = mass * 2.205
)
#> # A tibble: 87 × 2
#> height mass
#> <dbl> <dbl>
#> 1 67.7 170.
#> 2 65.7 165.
#> 3 37.8 70.6
#> 4 79.5 300.
#> 5 59.1 108.
#> 6 70.1 265.
#> 7 65.0 165.
#> 8 38.2 70.6
#> 9 72.0 185.
#> 10 71.7 170.
#> # … with 77 more rows
In this example, the student uses dplyr::mutate()
, which is certainly a reasonable choice but not what was requested. As a result, their submission contains more columns than expected.
starwars %>%
mutate(
height = height / 2.54,
mass = mass * 2.205
)
#> # A tibble: 87 × 14
#> name height mass hair_color skin_color eye_color birth_year sex
#> <chr> <dbl> <dbl> <chr> <chr> <chr> <dbl> <chr>
#> 1 Luke… 67.7 170. blond fair blue 19 male
#> 2 C-3PO 65.7 165. NA gold yellow 112 none
#> 3 R2-D2 37.8 70.6 NA white, bl… red 33 none
#> 4 Dart… 79.5 300. none white yellow 41.9 male
#> 5 Leia… 59.1 108. brown light brown 19 fema…
#> 6 Owen… 70.1 265. brown, gr… light blue 52 male
#> 7 Beru… 65.0 165. brown light blue 47 fema…
#> 8 R5-D4 38.2 70.6 NA white, red red NA none
#> 9 Bigg… 72.0 185. black light brown 24 male
#> 10 Obi-… 71.7 170. auburn, w… fair blue-gray 57 male
#> # … with 77 more rows, and 6 more variables: gender <chr>,
#> # homeworld <chr>, species <chr>, films <list>, vehicles <list>,
#> # starships <list>
Your table should not have columns named name
, hair_color
, skin_color
, or 9 more.
transmute()
where you called mutate()
. But no need to fret, try it again.
Here the student has done more or less the right thing, but they specified the columns in the opposite order.
starwars %>%
transmute(
mass = mass * 2.205,
height = height / 2.54
)
#> # A tibble: 87 × 2
#> mass height
#> <dbl> <dbl>
#> 1 170. 67.7
#> 2 165. 65.7
#> 3 70.6 37.8
#> 4 300. 79.5
#> 5 108. 59.1
#> 6 265. 70.1
#> 7 165. 65.0
#> 8 70.6 38.2
#> 9 185. 72.0
#> 10 170. 71.7
#> # … with 77 more rows
height
and mass
. Try it again. Perseverence is the key to success.
If we decide we don’t mind if the columns aren’t in the right order, we could set check_column_order = FALSE
in the check code for this exercise.
If the student were to somehow create columns with an incorrect class, e.g. integer values where doubles were expected, tblcheck will alert them to their mistake.
starwars %>%
transmute(
mass = as.integer(mass * 2.205),
height = as.integer(height / 2.54)
)
#> # A tibble: 87 × 2
#> mass height
#> <int> <int>
#> 1 169 67
#> 2 165 65
#> 3 70 37
#> 4 299 79
#> 5 108 59
#> 6 264 70
#> 7 165 64
#> 8 70 38
#> 9 185 72
#> 10 169 71
#> # … with 77 more rows
Your table’s columns were not in the expected order. The first 2 columns of your table should be height
and mass
.
height = /
where you called height = as.integer()
. Give it another try.
If the values are completely wrong — here the student is multiplying height
by 2.54
rather than dividing — then tblcheck points out which column is incorrect and gives a hint about the expected values.
starwars %>%
transmute(
height = height * 2.54,
mass = mass * 2.205
)
#> # A tibble: 87 × 2
#> height mass
#> <dbl> <dbl>
#> 1 437. 170.
#> 2 424. 165.
#> 3 244. 70.6
#> 4 513. 300.
#> 5 381 108.
#> 6 452. 265.
#> 7 419. 165.
#> 8 246. 70.6
#> 9 465. 185.
#> 10 462. 170.
#> # … with 77 more rows
The first 3 values of your height
column should be 67.7165354330709
, 65.748031496063
, and 37.7952755905512
, not 436.88
, 424.18
, and 243.84
.
starwars %>% transmute(height = height * 2.54, mass = mass * 2.205)
, I expected you to call height = /
where you called height = *
. Try it again. You get better each time.
This example comes from a lesson on factors
in R. Students have just learned about functions from the forcats package, and are now asked to practice using forcats::fct_reorder()
.
Prompt
Consider themarital_age
dataset. Usefct_reorder()
to turn themarital
column into a factor with levels ordered based onavg_age
.
We created a new table specifically for this exercise in the factor-setup
chunk, drawing from the data provided in forcats::gss_cat
.
```{r factor-setup}
library(dplyr)
library(forcats)
marital_age <- gss_cat %>%
mutate(marital = as.character(marital)) %>%
group_by(marital) %>%
summarize(avg_age = mean(age, na.rm = TRUE)) %>%
ungroup()
```
```{r factor, exercise=TRUE}
marital_age
```
```{r factor-solution}
marital_age %>%
mutate(marital = fct_reorder(marital, avg_age))
```
```{r factor-check}
grade_this_table()
```
Since students aren’t familiar with the marital_age
table, we’ve included the starter code so they can see the initial state of the table when they start the exercise.
marital_age
#> # A tibble: 6 × 2
#> marital avg_age
#> <chr> <dbl>
#> 1 Divorced 51.1
#> 2 Married 48.7
#> 3 Never married 33.9
#> 4 No answer 52.4
#> 5 Separated 45.3
#> 6 Widowed 71.7
In the expected solution, the student will use the forecats::fct_reorder()
function to transform the marital
column, reordering its factor levels by increasing avg_age
.
marital_age %>%
mutate(marital = fct_reorder(marital, avg_age))
#> # A tibble: 6 × 2
#> marital avg_age
#> <fct> <dbl>
#> 1 Divorced 51.1
#> 2 Married 48.7
#> 3 Never married 33.9
#> 4 No answer 52.4
#> 5 Separated 45.3
#> 6 Widowed 71.7
Here the student forgets about the forcats::fct_reorder()
function and tries to use order()
to set the order of the factor levels in the martial
column.
marital_age %>%
mutate(marital = order(marital, avg_age))
#> # A tibble: 6 × 2
#> marital avg_age
#> <int> <dbl>
#> 1 1 51.1
#> 2 2 48.7
#> 3 3 33.9
#> 4 4 52.4
#> 5 5 45.3
#> 6 6 71.7
Your marital
column should be a vector of factors (class factor
), but it is a vector of integers (class integer
).
marital_age %>% mutate(marital = order(marital, avg_age))
, I expected you to call marital = fct_reorder()
where you called marital = order()
. Try it again. You get better each time.
In this case the student remembers to use forecats::fct_reorder()
but incorrectly assigns the result to an unexpected column.
marital_age %>%
mutate(marital_fct = fct_reorder(marital, avg_age))
#> # A tibble: 6 × 3
#> marital avg_age marital_fct
#> <chr> <dbl> <fct>
#> 1 Divorced 51.1 Divorced
#> 2 Married 48.7 Married
#> 3 Never married 33.9 Never married
#> 4 No answer 52.4 No answer
#> 5 Separated 45.3 Separated
#> 6 Widowed 71.7 Widowed
Your table should not have a column named marital_fct
.
mutate()
to include marital_fct = fct_reorder()
. You may have included an unnecessary argument, or you may have left out or misspelled an important argument name. Try it again. You get better each time.
In another case, the student uses forecats::fct_reorder()
but sets .desc = TRUE
, resulting in an unexpected ordering of the factor levels.
marital_age %>%
mutate(marital = fct_reorder(marital, avg_age, .desc = TRUE))
#> # A tibble: 6 × 2
#> marital avg_age
#> <fct> <dbl>
#> 1 Divorced 51.1
#> 2 Married 48.7
#> 3 Never married 33.9
#> 4 No answer 52.4
#> 5 Separated 45.3
#> 6 Widowed 71.7
The order of the levels in your marital
column are the reverse of the expected order.
fct_reorder(marital, avg_age, .desc = TRUE)
, I expected .desc = FALSE
where you wrote .desc = TRUE
. Try it again. You get better each time.
In addition to table-checking functions, tblcheck includes functions for checking vectors — after all, columns in a table are vectors!
Here’s an example exercise from a tutorial on string transformations with the stringr package. Students are asked to practice concepts they just discovered in the tutorial:
Prompt
Consider thefruit
data. Use a function from stringr to create a vector containing only the fruits with more than one word in their name.Hint: a fruit named with more than one word also has at least one space in its name.
In the exercise markdown, we call grade_this_vector()
in the string-check
chunk to automatically compare the result of the student’s submission with our string-solution
.
```{r string-setup}
library(dplyr)
library(stringr)
```
```{r string, exercise=TRUE}
```
```{r string-solution}
str_subset(fruit, " ")
```
```{r string-check}
grade_this_vector()
```
Note that fruit
is from stringr::fruit
, a vector containing 80 fruit names.
head(stringr::fruit, 20)
#> [1] "apple" "apricot" "avocado" "banana"
#> [5] "bell pepper" "bilberry" "blackberry" "blackcurrant"
#> [9] "blood orange" "blueberry" "boysenberry" "breadfruit"
#> [13] "canary melon" "cantaloupe" "cherimoya" "cherry"
#> [17] "chili pepper" "clementine" "cloudberry" "coconut"
11 of the 80 fruits have more than one word. What’s your favorite two-worded fruit name? (Mine is the purple mangosteen.)
str_subset(fruit, " ")
#> [1] "bell pepper" "blood orange" "canary melon"
#> [4] "chili pepper" "goji berry" "kiwi fruit"
#> [7] "purple mangosteen" "rock melon" "salal berry"
#> [10] "star fruit" "ugli fruit"
This student submission makes a potentially common mistake of hoping that stringr::str_which()
will return the fruit
which have pattern
. Instead, str_which()
follows the base R function which()
, and returns an integer
index indicating the items in fruit
where the pattern
is matched.
str_which(fruit, " ")
#> [1] 5 9 13 17 32 42 66 72 73 75 79
Your result should be a vector of text (class character
), but it is a vector of integers (class integer
).
str_subset()
where you called str_which()
. Try it again. You get better each time.
In another mistake example, this student probably expected stringr::str_extract()
to extract elements from the vector. However, str_extract()
instead extracts the pattern from each item, so it returns a vector with the same length as the input.
str_extract(fruit, " ")
#> [1] NA NA NA NA " " NA NA NA " " NA NA NA " " NA NA NA
#> [17] " " NA NA NA
#> [ reached getOption("max.print") -- omitted 60 entries ]
Your result should contain 11 values, but it has 80 values.
I expected you to callstr_subset()
where you called str_extract()
. But no need to fret, try it again.
Finally, a student might have chosen the right function, but they missed our hint and came up with a pattern that’s too clever. Here the student probably looked at the first 20 fruit names and took a guess at the answer.
str_subset(fruit, " (pepper|orange|melon|fruit)")
#> [1] "bell pepper" "blood orange" "canary melon" "chili pepper"
#> [5] "kiwi fruit" "rock melon" "star fruit" "ugli fruit"
Your result should contain 11 values, but it has 8 values. I expected your result to include the values goji berry
, purple mangosteen
, and salal berry
.
str_subset(fruit, " (pepper|orange|melon|fruit)")
, I expected " "
where you wrote " (pepper|orange|melon|fruit)"
. That’s okay: you learn more from mistakes than successes. Let’s do it one more time.