Intro to R worksheet

Author

MI-Support

Published

March 13, 2026

Welcome!

Use this worksheet to follow along with the live coding demonstration led by Dr. Marisa Eisenberg.

For steps involving code, we will include some helpful functions and formatting tips, along with links to documentation where we can.

What are we doing here today?

Our goal is to take you from “I’ve never touched R before, :(” to “I can make a nice-looking graph in R! :)”

To do that, we’re going to work with a fake result of an MDSS query for pertussis cases in Ingham County to make a graph that closely matches the formatting of a figure in Ingham County’s 2024 Communicable Disease Surveillance Report. The figure was originally generated by Danielle Siwula.

Pertussis rate figure from Ingham County Health Department's 2024 Communicable Disease Surveillance Report

Ingham County report figure

Image of the graph to be constructed in this workshop, focusing on a time series line plot of four series

The graph we’ll work on today

Along the way, we hope to cover everything you need for R to be a useful tool in your toolkit tomorrow! (Well… Monday.)

Getting started

Load the files you’ll need

Download resources from MICOM Hub’s workshop page. Extract any zipped files and relocate all of the files you downloaded or unzipped to a new folder on your computer.

Open a file to write code in

Open RStudio, and open a new R script file (‘File’ > ‘New File’ > ‘R Script’, or Ctrl+Shift+N)

Add a “comment block” to the top of your script file that tells future you what’s going on here. Comments are lines or phrases in your code preceded by this # symbol. Putting that symbol before text tells R to ignore whatever comes after it, until the next line. Go ahead and just copy-paste the following onto the first line of your new file.

# intro_R_workshop_20260724.R
#
# This script file contains the code I wrote during MI-Support's Intro to R
#   workshop hosted by the Ingham County Health Department on
#   Friday, July 24, 2026.
# For help on this stuff later, I can use MI-Support's R Office Hours link:
#   https://calendly.com/jackjacobs-misupport/30min
#
# Author: {your name here} ({your@email.here})

When you save this script (Ctrl+S), we recommend you use the filename on that first line.

Install and “load in” the functions we’ll use in this exercise

In your console pane, type the following command, then press Enter.

install.packages(c("tidyverse","this.path","janitor"))

Write the following at or near the top of your script. With your cursor blinking on the end of this line, press Ctrl+Enter (Cmd+Enter for Mac users) to load the tidyverse libraries (a bunch of useful functions) into your R session.

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.1     ✔ readr     2.1.6
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.3     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.1     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
Tip

See RStudio’s online documentation for instructions on how to easily and quickly execute only the code you want from the editor pane with the Ctrl+Enter keyboard shortcut.

Woah!! What’s all this output mean? It’s not essential for you to understand now, but tidyverse is not, itself, a single library. It’s actually a collection of these nine libraries. (Each “library” is just a group of functions that are related to each other.)

Loading data into R

Tell your computer “where you are”

Save this script file in the same folder on your computer where you put the data from our website. (Before you work with data in R, you need to tell R where to grab it from!)

Once you’ve saved this script file, you’re ready to tell R where to look for other files you want to use. Run the following code to see how you can find this information later.

library(this.path)
this.dir()
[1] "C:/Users/jckjcbs/Documents/mi_support/workshop_Intro_R"

You should see similar output in your console.

What does this do? It automatically finds that long, annoying string of C:/Users/this/that/the_other folder names so you can focus on just the files you care about. To see how this.path can help you, check this out:

here("fake file name")
[1] "C:/Users/jckjcbs/Documents/mi_support/workshop_Intro_R/fake file name"

Any time you invoke some file name (in this example, fake file name), it automatically adds on the correct “filepath” for you!

Load your dataframe

We’re going to work with a randomly generated, fake export of an MDSS query. Our data tracks cases of pertussis in Ingham County from 2015-2024.

Look at the filename of the dataset we’re interested in right now: FAKE_pertussis_mdss.csv. If you open the properties of this file (highlight the file and press Alt+Enter), you’ll see it’s a “Comma Separated Values File”, which is why its filename ends with .csv. This mean’s we are going to use the read_csv() function (from the readr library) to load it in.

Here’s how we load a CSV into R and save it to a “variable” (we’ll get to that) called mdss

mdss <- read_csv(here("FAKE_pertussis_mdss.csv"))
New names:
Rows: 202 Columns: 16
── Column specification
──────────────────────────────────────────────────────── Delimiter: "," chr
(16): Exported from MDSS on July 20, 2026 at 8:46 AM, ...2, ...3, ...4, ...
ℹ Use `spec()` to retrieve the full column specification for this data. ℹ
Specify the column types or set `show_col_types = FALSE` to quiet this message.
• `` -> `...2`
• `` -> `...3`
• `` -> `...4`
• `` -> `...5`
• `` -> `...6`
• `` -> `...7`
• `` -> `...8`
• `` -> `...9`
• `` -> `...10`
• `` -> `...11`
• `` -> `...12`
• `` -> `...13`
• `` -> `...14`
• `` -> `...15`
• `` -> `...16`

Let’s see what we got with the View() function.

View(mdss)

When stuff goes wrong in R, you usually end up learning more about R

Oh no! This looks terrible! Using what you can see in RStudio and what you can see when you open the file in Excel, can you figure out what happened here?

Luckily, there’s a quick resolution. Let’s open “the docs” for this read_csv() function. Look on this page for how we might skip a line before asking R to look at our data. What can you find?

This problem is introducing a couple of new concepts, function documentation and function arguments, that will come up again and again.

  • Documentation exists for just about every single function in the tidyverse, and you can access it directly in your RStudio console (no internet connection needed!) with the ? shortcut.
    • Try running the following in your RStudio console: ?read_csv
  • Arguments are anything that you put in the parentheses of a function. You may know about functions from Excel! Excel “code” like =MIN(A1:A10) or =IF(C2="yes",D2,0) are functions, and the values separated by commas are the different “inputs” that you are “passing” to different “arguments”.
Excel is almost a coding language!

Similar to the example above, Excel may have already made you familiar with the concept of “variables” and how it’s a, let’s say, flexible concept. Look at A1:A10 in the example above. A1 might be a cell that contains the value 8. This is equivalent to the following R code assigning A1 as a variable with the value 8.

A1 <- 8

The same is true for the collection of values (in R, a “vector”) represented by A1:A10. This symbol is both 10 distinct, ordered values and something you can pass to Excel’s =MIN() function as a single thing—that’s a variable!

Fixing our data import

From here on out, this worksheet is going to give you less and less of the code you need to make this work. But I’ll give you some hints:

  • Check the documentation for read_csv(), and look for information about the skip= argument.
  • For the first argument you pass to this function, where you tell it what file to look for, remember to use the here() function.
  • Remember to “assign” the result to a variable called mdss using the “assignment operator” <-.

To recap, the whole thing should look something like this:

mdss <- read_csv(here(...), ...)

To see if your solution worked, run View(mdss) in the console.

A bit of data cleaning

Okay! Hopefully you were able to see something like this in RStudio.

A view of the mdss dataset with 16 columns and 201 entries, i.e. rows. This is meant to represent the outcome of the correct data loading procedure.

With a table of data loaded into our R session, let’s clean it up a bit.

Cleaning up our dataset (with the “pipe” operator: |>)

Paste this code into your script, run it, then View() your mdss table again. We’ll explain what happened afterward.

# Let's clean this table up a bit.
# Our first pipeline: remove some columns and clean the names
mdss <- mdss |> 
  
  # Grab just the columns we care about
  select(
    InvestigationID, Onset_Date, Diagnosis_Date, Referral_Date, Patient_Status,
    Case_Disposition, Date_of_Death, Date_of_Birth, Sex, Age, Admission_Date
  ) |> 
  
  # janitor is a nice little package
  # https://sfirke.github.io/janitor/articles/janitor.html
  janitor::clean_names()

A chunk of code written in this style is called a “pipeline”. (This isn’t exactly the same as the more general term “data pipeline”, but it’s more-or-less the same concept.) In this pipeline…

  1. We start with our mdss table. The mdss <- mdss |> part means that whatever we’re about to do to mdss is going to overwrite what we had in mdss before.
  2. The select function allows us to grab only the columns we might want to use from our table, then “returns” (or “outputs”) the table with only the columns we indicated.
  3. The clean_names() function cleans up our column names so they’re easier to type.
    • From the often very helpful janitor package - check it out later!

All of this was done in a few lines of code with plenty of comments that explain what happened, plenty of whitespace to make everything easy to read, and plenty of line breaks so that we don’t need to scroll side to side to see what’s happening in the code.

A quick preview of where we’re headed

We have pretty much everything we need to make a graph. Here, try this!

# Quick preview
mdss |> 
  ggplot(aes(x = year(mdy(diagnosis_date)), fill = sex)) +
  geom_col(stat = "count", position = "dodge")

Looks like an epidemic curve, right? We’ll get into it more later. Now please bear with us as we introduce more of the basics of R.

What is R?

We’ll walk you through some of the building blocks of the R language. These concepts will be extremely useful if you ever talk about R code with others.

Remember to use the ? shortcut in your console to see documentation for any of these functions.

What is my dataset?

  • str()
  • glimpse()
  • The $ operator
  • nrow()
  • dim()
  • length()
  • unique()
  • n_distinct()
  • table()
  • colnames()

Backing up even further… what is R?

  • Printing to the console with cat()
  • Basic arithmetic
  • Variables and the assignment operator (<-)
  • Vectors (c())
    • Indexing with []
    • Using the : operator
    • is.na()
  • Numeric vectors
    • round()
    • mean(), sum()
  • Logical vectors (TRUE/FALSE)
    • Logical operators: ==, >, <=, etc.
    • Logical conjunctions (huh?): & and |
    • !
    • any() and all()
  • Character vectors
    • str_split_1()
    • paste0() and paste()
    • as.numeric() and as.character()
  • Date vectors
    • mdy() and lubridate, the tidyverse library that deals with dates and times (along with hms)
    • Intervals and the %--% operator
    • today()
    • as.period()
  • “Types” in general
    • class()

More basic data cleaning

Before we continue cleaning our mdss dataset, we’re going to load in some data that might be useful later: the populations of Ingham County, broken down by sex, over the years in our data according to figures from the US Census Bureau’s 1-year American Community Survey estimates. You can find this in the population.csv file that was included with the workshop materials. (To save time, we’ve cleaned this dataset a bit already.)

Load this data into a variable called pop using read_csv() and here()

Modifying variables or making new ones with mutate()

Recall, we want to make a vertical bar plot (aka a “column plot”) of the case rate over time with our data. That means we will need some quantity variable on the y-axis and some time variable on the x-axis. What variable(s) should we use to track time in our visualization?

There’s not really a single right answer here. It just depends on what we want to look at. Can you identify pros or cons of different options? Can you imagine a new variable that’s based on the ones we have?

Whichever variable we choose, you may have noticed that all of these variables are “string” variables. We can use functions from the lubridate library to prompt R to understand these (i.e. to “parse” them) as dates. Here are lubridate’s parsing functions. Convert the diagnosis_date variable from a string-type to a date-type, then make a new variable called diagnosis_year that represents just the year part of the diagnosis_date variable.

Selecting a subset of rows with filter()

In the graph we’re trying to make, we only consider data from 2019 onward. But we have data going back to 2015! The filter() function selects which rows to keep from your dataset based on some TRUE/FALSE condition that you enter as an argument.

Use filter() in your pipeline along with the > or >= operator to keep only rows where the diagnosis date is in 2019 or later.

Tip

Our version of the code we’re walking you through includes the >= operator. From the example, I’m sure you can understand its usefulness! It’s not obvious, though, how to look up documentation for binary operators (e.g. +, /, ^, <-, ==, $, :, %in%) if you want to fully understand how they work. Here’s how to do it: for the %in% operator, run the following command in your console.

?`%in%`

The “backticks” (usually below your Esc button) let the console know that you want to treat the operator as its own thing, so to speak.

Real quick, let’s rename() a column.

Renaming columns is easy. Check out the documentation for rename() and use it in your pipeline to change age to age_years

Some more powerful data manipulation

Aggregating mdss data with group_by() and summarize()

Recall the visual we want to make: there is a time component in it that represents the population-wise rate of cases in calendar years. You may have already noticed that this isn’t how the data is organized. How do we calculate this? We calculate it using group_by() and summarize(), two operations that very often go together.

We think it’s easier to visualize what happens here than to explain it, so, using the new “time chunk” variable we created earlier (which we called diagnosis_year), try running these code chunks and view the resulting dataset.

mdss |> 
  summarize(cases = n())
# A tibble: 1 × 1
  cases
  <int>
1   109
mdss |> 
  group_by(diagnosis_year) |> 
  summarize(cases = n()) 
# A tibble: 6 × 2
  diagnosis_year cases
           <dbl> <int>
1           2019    21
2           2020     1
3           2021     3
4           2022     4
5           2023     9
6           2024    71
mdss |> 
  group_by(diagnosis_year, sex) |> 
  summarize(cases = n())
`summarise()` has regrouped the output.
ℹ Summaries were computed grouped by diagnosis_year and sex.
ℹ Output is grouped by diagnosis_year.
ℹ Use `summarise(.groups = "drop_last")` to silence this message.
ℹ Use `summarise(.by = c(diagnosis_year, sex))` for per-operation grouping
  (`?dplyr::dplyr_by`) instead.
# A tibble: 11 × 3
# Groups:   diagnosis_year [6]
   diagnosis_year sex    cases
            <dbl> <chr>  <int>
 1           2019 Female    10
 2           2019 Male      11
 3           2020 Male       1
 4           2021 Female     2
 5           2021 Male       1
 6           2022 Female     3
 7           2022 Male       1
 8           2023 Female     4
 9           2023 Male       5
10           2024 Female    32
11           2024 Male      39

In a new pipeline that creates a new dataset called summary, use these functions to create an average count of per year for each sex in the data. Call this variable cases. We think you’ll at least use group_by(), summarize(), and n() functions for this.

Incorporating these years’ populations

Remember how we loaded in that population dataset earlier? Now we want to use it to improve our analysis, but we need to be thoughtful about how it gets added to our data. We have 6 years’ populations in that dataset, broken down by sex. How do we put these two datasets together?

We need to join our datasets. That is, we need to bring columns from two datasets together based on a particular relationship between some shared column or columns. In this case, we want to add the relevant year-sex population to each row in our mdss dataset. Look into dplyr’s mutating joins vignette to gain a better understanding of this.

Cross-joins: an intermediate topic we can’t avoid right now

Blink and you’ll miss it: there’s a missing row in summary where 2020 Female cases should be. That’s because there weren’t any of these cases! That’s good in the real world, obviously, but it complicates the R code required for this analysis. We need every combination of year & sex to make the visualization look how we want it to look. So we’re going to just link the cross-join documentation here and attach the code below in case you’re interested in how this is resolved, but we’re going to mostly blow right past this for now.

# There weren't any cases among Females in 2020!
#   Ensure you have all categories with a "cross-join"
all_categories <- cross_join(
  tibble(diagnosis_year = 2019:2024),
  tibble(sex = c("Female","Male"))
)

# Join these categories to your data
summary <- right_join(
  summary, all_categories,
  join_by(diagnosis_year, sex)
) |> 
  replace_na(list(cases = 0)) |>  # Replace the missing cases value with 0
  arrange(diagnosis_year, sex)

Now, for joining populations to this table, we encourage you to try to figure this out yourself! But you can unfold the code block below if you’d like to see how you can do this.

Code
# We want to incorporate the scale of each state's population!
# We'll do something called a "join"
summary <- left_join(
  summary, pop,
  by = c("diagnosis_year" = "year", "sex")
)
Tip

You’ll see in dplyr’s join docs that you can use join_by() in the by = argument. This might seem intimidating, but we think it actually looks better!

Code
summary <- summary |> left_join(pop, join_by(diagnosis_year == year, sex))

join_by() is an interesting function in that it’s built only to be used within these dplyr::*_join() functions. Sometimes this happens: very useful functions have arguments complex enough to have their own special function or class of functions. Another example might be a logistic “link” function in a linear regression function, though this is outside the scope of this workshop.

Making graphs (finally!)

Before we dive into it, let’s take stock of the data we have in ili_summary.

glimpse(summary)
Rows: 12
Columns: 5
Groups: diagnosis_year [6]
$ diagnosis_year <dbl> 2019, 2019, 2020, 2020, 2021, 2021, 2022, 2022, 2023, 2…
$ sex            <chr> "Female", "Male", "Female", "Male", "Female", "Male", "…
$ cases          <int> 10, 11, 0, 1, 2, 1, 3, 1, 4, 5, 32, 39
$ geo            <chr> "Ingham County, Michigan", "Ingham County, Michigan", "…
$ population     <dbl> 150176, 142230, 147390, 140830, 144604, 139430, 145739,…

Let’s start by getting around to calculating rates, and lets assign the table we get to a new dataframe object we’ll use as the basis for building this graph, viz_df.

# Now that we have the population, we can calculate rates! (per 100k pop)
viz_df <- summary |> 
  mutate(cases_per_100k = 1e5 * cases / population)

Making something quick for exploratory data analysis

For our first graph (or second, actually), we’ll walk through the code step-by-step.

# Let's look at what our bar graph will roughly look like
viz_df |> 
  ggplot(aes(x = diagnosis_year, y = cases_per_100k, fill = sex)) +
  geom_col(position = "dodge")

You should recognize the first line of code, which uses the |> operator to send our dataframe as the first argument into the function below it.

The next function is a big one: ggplot(). ggplot2 is the tidyverse library for plotting. A weird thing I want to point out right away is that—for historical reasons—ggplot2 uses the + operator as its “pipe” instead of |> or %>%. (Even I’m not 100% sure why, but I know that ggplot2 is older than both of the pipe operators and tidyverse as a whole!)

The first argument in ggplot() is your data, but you don’t see it in the function above because we piped it in! The second argument is special, it always takes the aes() function. This function specifies which variables match which components of your visual. In this case, we specify x = diagnosis_year, y = cases_per_100k, and fill = sex. Try running just the first 2 lines of code (don’t include the +) using the Ctrl+Enter trick. What do you see?

On the next line after the +, we add a geom_col() layer to tell ggplot2 how to map our x and y layers onto the plot: as a set of vertically oriented bars. That’s it for the basics! Now we’ll walk through how to make something like this pretty, stopping at each step along the way to see how each piece contributes to the whole thing!

Making our target figure

We’ll walk through this last part together, but we’ll be using the following functions, which we’ve roughly grouped into the relevant concepts.

  • Preparing vectors for plotting
    • as.character(): Converts numeric values, like years, to strings. This tells ggplot to plot every value.
    • if_else() Useful for data suppression, if you want to, say, replace values smaller than 5 with 0, or replace number labels with “**”
  • Change the relative position of the side-by-side columns with position_dodge().
  • Assign custom colors to the bars representing different categories with scale_fill_manual().
  • Add data labels above your bars (or anywhere else!) with geom_text().
  • Specifying plot labels with labs()
  • Themes! ggplot2 has several default themes to make your graph look close to how you want it in a number of different ways.
    • Modify small pieces of your graph, after you’ve already chosen a default theme, using theme() and specifying changes to particular components.
  • Saving your plot as an image with ggsave()

At the end, you hopefully have something that looks like this!

Thank you for following along!

Don’t forget that MI-Support offers R office hours if you’d ever like to continue the conversation about R or discuss a project (free of charge!) to integrate it into your workflow.





MI-Support logo