--- title: "Get started with tblcheck" description: > An introduction to using the tblcheck package for automatic feedback for grading common problems in tables. output: rmarkdown::html_vignette: toc: true vignette: > %\VignetteIndexEntry{tblcheck} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- [learnr]: https://rstudio.github.io/learnr/ [gradethis-get-started]: https://pkgs.rstudio.com/gradethis/articles/gradethis.html [gradethis]: https://pkgs.rstudio.com/gradethis/ ```{css alert, echo = FALSE, eval = !identical(Sys.getenv("IN_PKGDOWN", "false"), "true")} /* alerts for standard vignettes (not pkgdown with BS5) */ .alert { padding: 0.5em 1em; margin: 1em 2em; border: 1px #eee solid; border-radius: 5px; } .alert.alert-success { background-color: #e4f2dc; border-color: #ddedcf; } .alert.alert-success, .alert.alert-success code, .alert.alert-success code a:not(.close):not(.btn) { color: #5c9653; } .alert.alert-danger { background-color: #f2e2e2; border-color: #eed9dc; } .alert.alert-danger, .alert.alert-danger code, .alert.alert-danger code a:not(.close):not(.btn) { color: #ad3532; } .alert code { background-color: #ffffff55; } ``` ```{css echo=FALSE, eval = identical(Sys.getenv("IN_PKGDOWN", "false"), "true")} /* tweak alert code colors for pkgdown */ .alert code { background-blend-mode: lighten; background-color: rgba(255, 255, 255, 0.5); background-image: none; color: inherit; } ``` ```{css echo=FALSE} /* tweak alerts for both pkgdown and standard vignettes */ .alert > :last-child { margin-bottom: 0; } ``` ```{css callout, echo=FALSE} .callout { padding: 1.5rem 1.5rem 1.5rem 80px; margin: 1em 0; background-size: 40px; background-repeat: no-repeat; background-position: 20px 1.5em; min-height: 80px; position: relative; border: 2px solid; } .callout::before { content: "!"; display: flex; position: absolute; top: 0; left: 0; background-repeat: no-repeat; background-size: contain; width: 40px; height: 40px; transform: translate(20px, 20px) scale(1); transition: transform 0.15s linear, background-color 0.2s linear; font-weight: bold; font-size: 2em; background-color: #486181; color: white; align-items: center; justify-content: center; border-radius: 50%; } .callout:first-child { margin-top: 0; } .callout p:last-child { margin-bottom: 0; } .callout { border-color: #b5c1d2; } .callout p, .callout ol, .callout ul { color: #486181; } .callout h3, .callout h4 { color: #294263; } .callout p code, .callout ol code, .callout ul code, .callout h3 code, .callout h4 code { background-color: #e9f2fd; color: #294263; } .callout blockquote { border-color: #b5c1d1; } @media only screen and (max-width: 400px) { .callout::before { transform: translate(-10px, -15px) scale(0.9); } .callout { padding-left: 1.5rem; padding-top: 1.5rem; margin-top: 1.5rem; } } ``` ```{r setup, include = FALSE} library(knitr) library(dplyr) library(gradethis) library(tblcheck) opts_chunk$set( collapse = TRUE, comment = "#>" ) set.seed(424341) options(max.print = 20, width = 70) source(system.file("internal", "fake-grading.R", package = "tblcheck"), local = TRUE) ``` ## Overview ### Introducing tblcheck tblcheck works with [gradethis] to help instructors compare students' exercise results with intended solutions in [learnr] tutorials. If you are new to grading learnr tutorials, we recommend that you [get comfortable with gradethis][gradethis-get-started] before incorporating tblcheck into your tutorials. tblcheck provides four levels of grading: 1. For fully automatic grading as a drop-in replacement for `gradethis::grade_this()`, use `grade_this_table()` or `grade_this_vector()`. 2. To integrate tblcheck into existing grading code, you can use `tbl_grade()` or `vec_grade()` for specific automated checks. 3. You can directly call the [specialized individual grading functions](#specific-grading-functions) that power the higher-level functions. 4. Finally, you can check for table and vector problems to [create your own custom feedback](#custom-mistake-handling). The most common use case for tblcheck is to provide automatic feedback for [common problems in tables](#grading-tables). Because the columns of data frames in R are vectors, tblcheck can also provide feedback for [common problems in vectors](#grading-vectors) with `vec_grade()`. These all-in-one grading functions check for a set of possible problems, or differences, between the exercise solution and the student's table or vector. You can [control which checks are applied](#skipping-tests) with the arguments of these functions, or you can [directly call individual grading functions](#specific-grading-functions). The table grading functions are prefixed with `tbl_grade_` and the vector grading functions prefixed with `vec_grade_`. For complete control over the feedback presented to users, each `tbl_grade_` or `vec_grade_` function is paired with a `tbl_check_` or `vec_check_` counterpart that finds problems and returns an object that you can use to [create custom feedback](#custom-mistake-handling). ### Usage To use tblcheck in a learnr tutorial, load tblcheck after learnr and gradethis in the `setup` chunk of your tutorial: ````markdown ```{r setup}`r ''` library(learnr) library(gradethis) library(tblcheck) ``` ```` Then, ensure your exercise has a `-solution` chunk and choose one of the following grading functions to grade your exercise: - If the solution is a **table**, use `grade_this_table()` ````markdown ```{r ex-check}`r ''` grade_this_table() ``` ```` or use `tbl_grade()` in existing grading code. ````markdown ```{r ex-check}`r ''` grade_this({ pass_if_equal() tbl_grade() pass() }) ``` ```` - If the solution is a **_column_ in a table**, use `grade_this_column()` ````markdown ```{r ex-check}`r ''` grade_this_column("eruptions") ``` ```` or use `tbl_grade_column()` in existing grading code. ````markdown ```{r ex-check}`r ''` grade_this({ pass_if_equal() tbl_grade_column() pass() }) ``` ```` - If the solution is a **vector**, use `grade_this_vector()` ````markdown ```{r ex-check}`r ''` grade_this_vector() ``` ```` or use `vec_grade()` in existing grading code. ````markdown ```{r ex-check}`r ''` grade_this({ pass_if_equal() vec_grade() pass() }) ``` ```` In each of the above cases, the fully automated first version is functionally equivalent to the second version that uses `gradethis::grade_this()`. ## Grading tables ### Automated table checking `grade_this_table()` uses `tbl_grade()` to compare the result of the user's input to the result of the `-solution` chunk, automatically returning targeted feedback to the user if any problems are discovered. `tbl_grade()` checks that the user's table 1. is the correct class, 1. has the correct column names, 1. has the correct number of rows and columns, 1. and that each column of the user's table 1. is the correct class and 1. contains the correct values. If any of these checks detect a problem in the submitted code, the student will see a single message with the first detected issue, based on the order described above. ### Usage To grade an exercise where the solution is a table, ensure you have a `-solution` chunk and call `grade_this_table()` in your `-check` chunk ````markdown ```{r example-check, chunk_label = "example-check"} grade_this_table() ``` ```` or add `tbl_grade()` to the grading code in your `-check` chunk. ````markdown ```{r example-check-gt, chunk_label = "example-check", grade_this = TRUE} pass_if_equal() tbl_grade() pass() ``` ```` By default, `tblcheck` functions compare the `gradethis` objects `.result` and `.solution`, just like `gradethis::pass_if_equal()` and `gradethis::fail_if_equal()`. ::: callout If you are using `tbl_grade()`, be sure to include a function like `pass()` or `pass_if_equal()` in your checking code to ensure students can get a passing grade! `tbl_grade()` only returns feedback to the student if it discovers a problem; if the student gives the correct answer, it produces no output. This lets you quickly check for simple problems, following up with more detailed checking with other `gradethis` functions. ::: ### Finding problems If the user's submitted table differs from the correct table, `grade_this_table()` returns a failing grade and a message with an explanation for what went wrong. If there are multiple problems with a student's submission, `grade_this_table()` tries to give the most actionable item first. We'll demonstrate how this works for a simple exercise that asks students to create the following table using `tibble()`. ```{r example-table-prompt, echo=FALSE} solution_table <- tibble( food = c("lettuce", "tomato"), vegetable = c(TRUE, FALSE), color = c("green", "red") ) knitr::kable(solution_table) ``` In the R Markdown source of the learnr tutorial, we use an exercise chunk labelled `food`, with a `food-solution` chunk with the expected solution and a `food-check` chunk with the exercise checking code using [gradethis] and tblcheck. ````markdown ```{r ex-food, chunk_label = "food", is_exercise = TRUE} ``` ```{r ex-food-solution, chunk_label = "food-solution"} tibble( food = c("lettuce", "tomato"), vegetable = c(TRUE, FALSE), color = c("green", "red") ) ``` ```{r ex-food-check, chunk_label = "food-check"} grade_this_table() ``` ```` We'll use this example throughout the sections that follow to demonstrate how tblcheck will respond to various types of errors that students may make. Keep in mind this is a contrived example designed for this vignette. In real-world usage, students are likely to only encounter one or two of the problems `grade_this_table()` is designed to find. #### Checking class First, `grade_this_table()` ensures that the class of the student's submission matches the class of the expected solution. Here, the student attempts to store the data in the table as a list rather than by using `tibble()`. ```{r exercise-2} list( food = "lettuce", fruit = "TRUE", color = "green" ) ``` ```{r echo=FALSE} fake_grade_chunks("exercise-2", solution = "ex-food-solution", check = "ex-food-check") ``` Based on this advice, the student revises their solution to use `tibble()` instead of `list()`. #### Checking column names Next, the code checks that the student used the correct column names, and they haven't missed any columns or included any unexpected columns. Here, `grade_this_table()` notices that the student has an unexpected column named `fruit`. ```{r exercise-3} tibble( food = "lettuce", fruit = "TRUE", color = "green" ) ``` ```{r echo=FALSE} fake_grade_chunks("exercise-3", solution = "ex-food-solution", check = "ex-food-check") ``` Based on this advice, the student revises their solution to name the second column `vegetable` instead of `fruit`. #### Checking length Next, `grade_this_table()` checks that the student has submitted the correct number of rows, and in this case notices that the student has only included one row. ```{r exercise-4} tibble( food = "lettuce", vegetable = "TRUE", color = "green" ) ``` ```{r echo=FALSE} fake_grade_chunks("exercise-4", solution = "ex-food-solution", check = "ex-food-check") ``` Based on this advice, the student realizes they've only entered the first row of the table. They go back to the example table and add the second row to their submission. #### Checking column classes Next, `grade_this_table()` checks that each individual column contains the correct type of data. Here, the student has stored the values of the `vegetable` column as a string, but we were expecting them to be logical values. ```{r exercise-5} tibble( food = c("lettuce", "tomato"), vegetable = c("TRUE", "TRUE"), color = c("green", "red") ) ``` ```{r echo=FALSE} fake_grade_chunks("exercise-5", solution = "ex-food-solution", check = "ex-food-check") ``` Based on this advice, the student removes the `"` around the values in the `vegetable` column to use R's logical `TRUE`. #### Checking column values Finally, `grade_this_table()` gives a hint as to what the values in each column should look like. Here, the student made a mistake during their transcription of the `vegetable` column. ```{r exercise-6} tibble( food = c("lettuce", "tomato"), vegetable = c(TRUE, TRUE), color = c("green", "red") ) ``` ```{r echo=FALSE} fake_grade_chunks("exercise-6", solution = "ex-food-solution", check = "ex-food-check") ``` Based on this advice, the student revises their submission, changing the second value of the `vegetable` column from `TRUE` to `FALSE`. ```{r exercise-7} tibble( food = c("lettuce", "tomato"), vegetable = c(TRUE, FALSE), color = c("green", "red") ) ``` ```{r echo=FALSE} fake_grade_chunks("exercise-7", solution = "ex-food-solution", check = "ex-food-check") ``` ## Grading vectors ### Automated vector checking Many of the table-grading tests that apply to the _columns_ of tables can also be applied to vectors — after all, data frame columns in R are _vectors_. When your exercise uses vectors rather than tables, `grade_this_vector()` and `vec_grade()` allows you to apply the same tests that are normally applied to the columns of a table to a vector. They check that the user's vector 1. is the correct class 1. is the correct length 1. has the correct factor levels (if the vector is a factor) 1. contains the correct values 1. has the correct names (if the vector has names) Like `grade_this_table()`, if a problem is detected by any of these checks, the student will see a single message with the first detected problem, based on the order described above. ### Usage To grade an exercise where the solution is a vector, ensure you have a `-solution` chunk and call `grade_this_vector()` in your `-check` chunk ````markdown ```{r vector-example-check, chunk_label = "vector-check"} grade_this_vector() ``` ```` or add `vec_grade()` to the grading code in your `-check` chunk, e.g. ````markdown ```{r vector-example-check-gt, chunk_label = "vector-check", grade_this = TRUE} pass_if_equal() vec_grade() pass() ``` ```` ::: callout Just like `tbl_grade()` and other tblcheck functions, `vec_grade()` automatically compares the user's `.result` to the `.solution` when used in `gradethis::grade_this()`. While `grade_this_vector()` always returns a passing or failing grade, note that `vec_grade()` only returns feedback when a problem is detected. Be sure to include `gradethis::pass()` or `gradethis::pass_if_equal()` when using `vec_grade()` to ensure that students can get a passing grade. ::: ### Finding problems If the user's submitted vector differs from the correct vector, `grade_this_vector()` returns a failing grade and a message with an explanation for what went wrong. If there are multiple problems with a student's submission, `grade_this_vector()` tries to give the most actionable item first. Suppose an exercise asks a student to create a factor of the sandwich toppings — _lettuce_, _tomato_, _avocado_. ```{r} factor(c("lettuce", "tomato", "avocado")) ``` In the R Markdown source of the learnr tutorial, we use an exercise chunk labelled `toppings`, with a `toppings-solution` chunk with the expected solution and a `toppings-check` chunk with the exercise checking code using [gradethis] and tblcheck. ````markdown ```{r ex-vector, chunk_label = "toppings", is_exercise = TRUE} ``` ```{r ex-vector-solution, chunk_label = "toppings-solution"} factor(c("lettuce", "tomato", "avocado")) ``` ```{r ex-vector-check, chunk_label = "toppings-check"} grade_this_vector() ``` ```` For example, if the student submits a vector of the wrong class, that will be the first message returned by `grade_this_vector()`. ```{r vector-1} c("lettuce", "tomato", "avocado") ``` ```{r echo=FALSE} fake_grade_chunks("vector-1", "ex-vector-solution", "ex-vector-check") ``` If the student submits a factor with the wrong factor levels, `grade_this_vector()` will warn the student about their mistake. ```{r vector-2} factor(c("lettuce", "tomato", "avocado"), c("lettuce", "tomato", "avocado")) ``` ```{r echo=FALSE} fake_grade_chunks("vector-2", "ex-vector-solution", "ex-vector-check") ``` ## Custom Grading There are a number of ways to control which mistakes are detected and how the feedback is given to the students. - The first is to [enable or disable specific checks](#skipping-tests) using the `check_*` arguments of `grade_this_table()` and `grade_this_vector()` (or their counterparts, `tbl_grade()` and `vec_grade()`). - Both `grade_this_table()` and `grade_this_vector()` include `pre_check` and `post_check` options that allow you to add [additional tests and logic to the grading code](#additional-checks). - You may also choose to [call specific grading functions](#specific-grading-functions) associated with the checks underlying `tbl_grade()` and `vec_grade()`. - Or you can [`check` rather than `grade` for specific problems](#checking-for-problems-with-custom-feedback) to obtain a `problem` object, i.e. a description of the problem found by tblcheck. You can then use the problem object to construct a feedback message using `gradethis::fail()`. ### Skipping tests Every test performed by `grade_this_table()` and `grade_this_vector()` (or `tbl_grade()` and `vec_grade()`) can be enabled or disabled with an argument. The argument names are prefixed with `check_` — such as `check_class` or `check_groups` — and each take a `TRUE` or `FALSE` value. For example, suppose a student answering our `food` exercise used a `data.frame` when the exercise expects a `tibble`. ```{r skip-check-class} data.frame( food = c("lettuce", "tomato"), vegetable = c(TRUE, FALSE), color = c("green", "red"), stringsAsFactors = FALSE ) ``` ```{r echo=FALSE} fake_grade_chunks("skip-check-class", solution = "ex-food-solution", check = "ex-food-check") ``` If you don't care about the class of the table, you can add `check_class = FALSE` to `grade_this_table()`. This will skip checking the table's class, but still run all other tests. ````markdown ```{r skip-check-class-check, chunk_label = "food-check"} grade_this_table(check_class = FALSE) ``` ```` ```{r echo=FALSE} fake_grade_chunks("skip-check-class", solution = "ex-food-solution", check = "skip-check-class-check") ``` Since the only problem with the student's submission was the class of the table, `grade_this_table()` doesn't directly return any feedback. ### Additional Checks Both `grade_this_table()` and `grade_this_vector()` provide two additional arguments, `pre_check` and `post_check`, that allow you to add additional checks or modify the `.result` or `.solution`. For both functions, the `gradethis::grade_this()` flow is roughly equivalent to the following code sketch: ```r grade_this({ # ... pre_check ... # if requested pass_if_equal() # grade the table or vector tbl_grade() # ... post_check ... pass() }) ``` Two examples of reasons why you might want to use these arguments are to limit the table grading checks to specific columns only, or to include additional checks after `tbl_grade()` or `vec_grade()`. Suppose we extend our `food` example into an exercise labelled `food-percentage` that adds a `count` column to our `foods` table and asks students to add a new column, `pct` with the percentage of our total food is represented by each food. ````markdown ```{r ex-additional-setup, chunk_label = "food-percentage-setup"} library(dplyr) foods <- tibble( food = c("lettuce", "tomato"), vegetable = c(TRUE, FALSE), color = c("green", "red"), count = c(5, 3) ) ``` ```{r ex-additional-prompt, chunk_label = "food-percentage", is_exercise = TRUE} ``` ```{r ex-additional-solution, chunk_label = "food-percentage-solution"} .solution <- foods %>% mutate(pct = count / sum(count)) ``` ```` We expect the final solution to look like this ```{r ref.label="ex-additional-setup", include=FALSE} ``` ```{r ref.label="ex-additional-solution", echo=FALSE} ``` but a student might decide to store the total food in a temporary `total` column. ```{r ex-additional-student} .result <- foods %>% mutate( total = sum(count), pct = count / total ) ``` Knowing that we don't mind the additional column, we can use the `pre_check` argument to limit `.result` to the columns that also appear in `.solution`. ````markdown ```{r ex-additional-check-1, eval=FALSE} grade_this_table(pre_check = { tbl_grade_is_table(.result) .result <- .result[intersect(names(.result), names(.solution))] }) ``` ```` ```{r echo=FALSE} fake_grade_chunks( user = "ex-additional-student", solution = "ex-additional-solution", check = "ex-additional-check-1", setup = "ex-additional-setup" ) ``` ### Specific grading functions `tbl_grade()` and `vec_grade()` calls a number of grading functions internally. You can call these functions directly to perform more specific grading, either in the `pre_check` or `post_check` arguments of `grade_this_table()` or `grade_this_vector()`, or in standard `gradethis::grade_this()` grading code. | Function | Grades | | ------------------------------------------------- | ---------------------------------------------------------------- | | `tbl_grade_class()` `vec_grade_class()` | the class of an object | | `tbl_grade_column()` | applies the tests in `vec_grade()` to a single column of a table | | `tbl_grade_dimensions()` `vec_grade_dimensions()` | the length and dimensions of an object | | `tbl_grade_groups()` | the groups of a table | | `tbl_grade_names()` `vec_grade_names()` | the names of an object | | `vec_grade_levels()` | the levels of a factor | | `vec_grade_values()` | the values of a vector | Suppose we modified our `food` example, telling students that we have 3 tomatoes and 5 heads of lettuce. We'd like the students to create a fourth column `count` containing the number of each food item in our possession. For this example, we'll use the lower-level functions in conjunction with `gradethis::grade_this()`. ````markdown ```{r ex-specific-setup, chunk_label = "food-count-setup"} library(dplyr) foods <- tibble( food = c("lettuce", "tomato"), vegetable = c(TRUE, FALSE), color = c("green", "red") ) ``` ```{r ex-specific-prompt, chunk_label = "food-count", is_exercise = TRUE} ``` ```{r ex-specific-solution, chunk_label = "food-count-solution"} foods %>% mutate(count = c(5, 3)) ``` ```` In our grading code, we may choose to grade only the `count` column of `foods` using `tbl_grade_column()`, ignoring the other columns since they were provided by our setup code. ````markdown ```{r ex-specific-check, chunk_label = "food-count-check", grade_this = TRUE} tbl_grade_column("count") pass_if_equal() fail() ``` ```` A student who quickly scanned the exercise prompt might reverse the expected order of the values in the `count` column. ```{r ref.label="ex-specific-setup", include=FALSE} ``` ```{r ex-specific} foods %>% mutate(count = c(3, 5)) ``` ```{r echo=FALSE} fake_grade_chunks("ex-specific") ``` ### Custom mistake handling Sometimes, we want to handle specific circumstance in a special way. Every `tbl_grade_` and `vec_grade_` function includes a `tbl_check_` or `vec_check_` counterpart that returns the detected **problem** rather than converting the problem into feedback for the user (a **grade** in gradethis terms). If we replace `tbl_grade_column()` with `tbl_check_column()`, we can store and inspect the problem detected by the column checking function. We'll experiment in our local R console before writing our final exercise checking code. ```{r custom-checking-local} solution <- foods %>% mutate(count = c(5, 3)) user <- foods %>% mutate(count = c(3, 5)) problem <- tbl_check_column("count", object = user, expected = solution) problem ``` Every problem object contains at least three items: 1. The problem `type` describes the issue discovered by the checking function. The help pages for every check function contain a section named **Problems** where the problem types detected by the check function are enumerated. - **Your Turn:** What types of problems are discovered by `tbl_check_column()`? Use the help pages to find out. 1. `actual` contains the value returned by the user's code and inspected by the check function. 1. `expected` contains the value returned by the solution code and inspected by the check function. Problems also include additional information depending on the problem type. In the case of a `values` problem detected by `tbl_check_column()`, the problem object also includes the `column` name. tblcheck includes a helper function, `is_problem()` that you can use to detect and differentiate between different problem types. ```{r custom-problem-ex-1} is_problem(problem) ``` We can use the `type` argument of `is_problem()` to differentiate between the problem types detected by `tbl_check_column()`. ```{r custom-problem-ex-2} is_problem(problem, type = "length") is_problem(problem, type = "values") ``` In this exercise, we know in advance that our wording is likely to trip up students, so we may want to create feedback specifically for the case where a student has reversed the food counts. We can use `is_problem()` together with `all.equal()` to isolate this specific case. ```{r custom-problem-ex-feedback} if (is_problem(problem, "values") && all.equal(problem$actual, c(3, 5))) { feedback <- paste( "Make sure that the values in `count` are ordered", "to match their respective `food`.", "Remember, we have **3** tomatoes and **5** heads of lettuce." ) gradethis::fail(feedback) } ``` For problems not handled by your custom grading code, you can pass the problem to `tbl_grade()` to create a grade with the default feedback provided by tblcheck's `grade` functions. If there are no problems, `problem_grade(problem)` won't return anything. Here's the default feedback `tbl_grade_column()` _would have returned_ without our custom grading code. ```{r custom-problem-ex-fallback} problem_grade(problem) ``` **Tip**: You can also use `if` statements to ignore differences that you don't care about in your grading code. Putting everything together into our grading `food-count-check` chunk, our grading code for this exercise would look like this: ````markdown ```{r custom-problem-check, chunk_label = "food-count-check", grade_this = TRUE} problem <- tbl_check_column("count") if (is_problem(problem, "values") && all.equal(problem$actual, c(3, 5))) { feedback <- paste( "Make sure that the values in `count` are ordered", "to match their respective `food`.", "Remember, we have **3** tomatoes and **5** heads of lettuce." ) fail(feedback) } problem_grade(problem) pass("Great job!") ``` ```` And the student who reversed the `count` column values ```{r ref.label="ex-specific"} ``` would receive our custom feedback. ```{r echo=FALSE} fake_grade_chunks("ex-specific", check = "custom-problem-check") ``` By following our specific advice, the student revises their code to correctly create the `count` column. ```{r ex-specific-correct} foods %>% mutate(count = c(5, 3)) ``` ```{r echo=FALSE} fake_grade_chunks("ex-specific-correct", check = "custom-problem-check", solution = "ex-specific-solution", setup = "ex-specific-setup") ```