Import
Getting data into R
Ian Weidling
University of Kansas Medical Center
July 15, 2026
Data import
Data import is the bridge between real-world data and analysis-ready R objects.
Today we will focus on the practical question every data analysis begins with
Emphasize that this lecture is not just about memorizing functions. The goal is to help students recognize what kind of data source they have and choose an import strategy that preserves the meaning of the data.
Learning goals
By the end of this session, you should be able to:
Choose the right import function for common data sources
Use project-oriented file paths
Diagnose common import problems
Understand when data are loaded immediately versus queried lazily
Recognize hierarchical data and choose a rectangling strategy
Extract simple data from an HTML page
Frame these goals as practical skills. Students do not need mastery of every edge case today. They need enough conceptual understanding to know what tool to reach for and what to check after importing.
Class plan
We will alternate between short lecture/demo sections and coding breaks.
1
Importing files into R
~15 min
Coding Break 1 — 10 min
2
Working with databases and large datasets
~15 min
Coding Break 2 — 15 min
3
Hierarchical and web data
~15 min
Coding Break 3 — 10 min
💡 The goal is not to memorize every argument. The goal is to practice the workflow: import → inspect → fix → continue .
This slide replaces the original long lecture followed by long practice plan. Tell students that they will practice after each major section while the material is still fresh.
The import mindset
Importing data is not complete when the file loads without an error.
A good import workflow asks:
Did the data load?
Are the rows and columns what I expected?
Did R guess the column types correctly?
Are missing values represented correctly?
Is the result ready for transformation or visualization?
This is the central “why” behind the lecture. R may import a file successfully but still misinterpret IDs, dates, missing values, or column names. Encourage students to slow down and inspect their objects immediately after importing.
Project-oriented workflow
Use relative paths that describe where files live inside your project.
data-import-workshop/
├── data/
│ ├── penguins.csv
│ └── sales.xlsx
├── data_import_practice.qmd
└── data-import-workshop.Rproj
read_csv ("data/penguins.csv" )
Avoid file paths that only work on your computer, like C:/Users/yourname/Downloads/data.csv.
Explain that many “import problems” are really file path problems. Relative paths make code shareable across computers because they describe the location of a file within the project rather than the location of the project on one person’s machine.
Section 1: Importing files into R
CSV, TSV, delimited text files, URLs, and spreadsheets
We will start with the file types students are most likely to encounter first.
This section combines the text-file and spreadsheet material. The emphasis should be on choosing the right read function and inspecting the result.
Common import functions
CSV
readr
read_csv()
TSV
readr
read_tsv()
Other delimiters
readr
read_delim()
Excel
readxl
read_excel()
Google Sheets
googlesheets4
read_sheet()
The function you choose should match how the data are stored.
This slide gives students a mental lookup table. It also introduces the idea that import functions are specialized because different file formats store data differently.
Delimiters
A delimiter is the character that separates one column from the next.
.csv
comma
read_csv()
.tsv
tab
read_tsv()
.txt
varies
read_delim()
read_delim ("data/labs.txt" , delim = "|" )
💡 File extensions can be misleading. If import looks wrong, open the file as plain text and inspect the delimiter.
Explain that delimited text files are just plain-text tables. The delimiter tells R where one column ends and the next begins. If the delimiter is wrong, R may put everything into one column or split columns incorrectly.
Read a CSV file
library (tidyverse)
penguins <- read_csv ("data/penguins.csv" )
penguins
#> # A tibble: 344 × 8
#> species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
#> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <chr> <dbl>
#> 1 Adelie Torgersen 39.1 18.7 181 3750 male 2007
#> 2 Adelie Torgersen 39.5 17.4 186 3800 female 2007
#> 3 Adelie Torgersen 40.3 18 195 3250 female 2007
The code and printed output are kept in one non-evaluated code block so the example stays together and always renders consistently. Point out the tibble dimensions and the column type abbreviations under each column name.
Read directly from a URL
penguins_url <- "https://raw.githubusercontent.com/allisonhorst/palmerpenguins/master/inst/extdata/penguins.csv"
penguins <- read_csv (penguins_url)
readr functions can read from local files or URLs.
This is useful for reproducible examples, teaching materials, and public datasets.
Emphasize that students do not always need to manually download a file. If the data are available at a stable URL, they can import directly from the link.
Inspect after import
penguins |> glimpse ()
#> Rows: 344
#> Columns: 8
#> $ species <chr> "Adelie", "Adelie", "Adelie", ...
#> $ island <chr> "Torgersen", "Torgersen", "Torgersen", ...
#> $ bill_length_mm <dbl> 39.1, 39.5, 40.3, ...
#> $ bill_depth_mm <dbl> 18.7, 17.4, 18.0, ...
#> $ flipper_length_mm <dbl> 181, 186, 195, ...
#> $ body_mass_g <dbl> 3750, 3800, 3250, ...
#> $ sex <chr> "male", "female", "female", ...
#> $ year <dbl> 2007, 2007, 2007, ...
💡 A successful import should still be checked with glimpse() and, when needed, spec().
Explain that glimpse() is a quick import quality check. It shows dimensions, names, types, and example values. This is often the first thing you should do after reading in a dataset.
Column type guessing and specifications
read_csv() guesses column types automatically.
penguins <- read_csv (penguins_url)
spec (penguins)
#> cols(
#> species = col_character(),
#> island = col_character(),
#> bill_length_mm = col_double(),
#> bill_depth_mm = col_double(),
#> flipper_length_mm = col_double(),
#> body_mass_g = col_double(),
#> sex = col_character(),
#> year = col_double()
#> )
💡 Guessing is convenient, but not perfect. Use col_types when a column has meaning that R cannot infer, such as IDs, zip codes, or years you want treated as labels.
penguins <- read_csv (
penguins_url,
col_types = cols (
year = col_character ()
)
)
This slide consolidates the old column type guessing, specification, and manual column type slides. The key message is that readr guesses types, but analysts should verify those guesses. Explain that years, IDs, and codes often look numeric but may be better represented as character variables.
Why spreadsheets are different
Spreadsheets are designed for humans first and computers second.
Common spreadsheet issues:
Multiple sheets
Extra titles or notes above the data
Merged cells
Non-data formatting
Inconsistent column names
library (readxl)
read_excel ("data/sales.xlsx" , sheet = "January" )
The purpose of this slide is to explain why Excel import is a separate topic. Spreadsheets often contain presentation choices that make sense for people but complicate analysis.
Column names from Excel
Sometimes the first row is not the real header.
read_excel ("data/sales.xlsx" , skip = 2 )
Or you can specify a range:
read_excel ("data/sales.xlsx" , range = "A3:F20" )
💡 janitor::clean_names() can standardize messy names, and purrr::map() can help read many sheets using the same workflow.
This slide now preserves the key Excel-header point and folds in janitor and purrr as tips rather than separate slides. Explain that the strategy is to identify where the actual rectangular data begins.
Coding Break 1: Read functions
Goal: import a public CSV and control one column type.
Open your practice file and complete the following tasks:
Load the tidyverse.
Read the Palmer Penguins CSV directly from the URL.
Use glimpse() to inspect the imported data.
Re-import the data and force year to be a character column.
Confirm that year changed from numeric to character.
penguins_url <- "https://raw.githubusercontent.com/allisonhorst/palmerpenguins/master/inst/extdata/penguins.csv"
💡 Finish early? Try forcing species to a factor with col_factor().
Give students about 15 minutes. The point is to practice import, inspection, and column type control with a public dataset that does not require local files.
Coding Break 1: Solution
library (tidyverse)
penguins_url <- "https://raw.githubusercontent.com/allisonhorst/palmerpenguins/master/inst/extdata/penguins.csv"
penguins <- read_csv (penguins_url)
penguins |> glimpse ()
penguins_year_chr <- read_csv (
penguins_url,
col_types = cols (
year = col_character ()
)
)
penguins_year_chr |> glimpse ()
During discussion, ask students why they might want to treat a year as character. The answer is not that year is always character, but that analysts should decide based on how the variable will be used.
Section 2: Working with databases and large datasets
Sometimes the data are too large, too shared, or too structured to live in a single CSV.
This section introduces database-backed tables and larger-than-memory data workflows.
The goal is conceptual. Students do not need to become database administrators. They need to understand why databases exist and how dplyr can help them query database tables.
Why databases?
Compared with a CSV, a database can:
Store many related tables
Support larger datasets
Let multiple users access the same data
Query only the rows and columns you need
Preserve structure and relationships
With databases, you usually query first and collect later.
This slide provides the “why” for databases. A CSV is loaded into memory all at once. A database can stay where it is, while R sends instructions about what subset to retrieve.
Database workflow: connect, reference, then query
When working with a database, R does not usually import the whole dataset right away.
Instead, the workflow is:
Connect to the database
Reference a table with tbl()
Build a query using dplyr verbs
Collect only the result you need
tbl() is like a pointer to a table that lives somewhere else. It lets you work with the table without copying everything into R.
💡 In the coding break, we will create a small temporary database from Palmer Penguins so you can practice this workflow without needing access to a real institutional database.
This slide replaces the large setup code block with the conceptual takeaway. Emphasize that the setup code is not the lesson; the lesson is the mental model. In database workflows, you first connect, then reference a table, then write a query, and only at the end collect a smaller result into R.
Use dplyr on a database
You can write familiar dplyr code:
adelie_query <- penguins_db |>
filter (species == "Adelie" ) |>
select (species, island, body_mass_g, sex)
adelie_query
Behind the scenes, dbplyr translates the dplyr pipeline into SQL.
adelie_query |> show_query ()
The code feels like dplyr, but the work happens in the database.
This combines the dplyr and SQL translation slides. Emphasize that students do not need to write SQL to benefit from databases, although seeing the SQL can help them understand what is happening.
Collect results into R
Use collect() when you are ready to bring the result into memory.
adelie_local <- adelie_query |>
collect ()
adelie_local
💡 Disconnect when finished:
Explain that collect() is the boundary between database work and local R work. Once collected, the result is a regular tibble in R memory. Also remind students to disconnect from database connections when done.
Where Arrow and Parquet fit
Parquet is a file format; Arrow is a toolkit for working with columnar data efficiently.
Why this matters:
Parquet files are often smaller than CSV files
Columnar storage is efficient for selecting variables
Arrow can work with large data without loading everything at once
The workflow often resembles database work: filter first, collect later
For this course, focus on the concept: large data workflows often avoid loading everything into memory immediately .
This single conceptual slide replaces multiple Arrow-specific workflow slides. The aim is to connect Arrow to the database idea of lazy querying without overwhelming students with another API.
Coding Break 2: Database queries
Goal: query a simulated database using dplyr and collect a summary.
Run this setup code first:
library (tidyverse)
library (DBI)
library (RSQLite)
library (dbplyr)
library (palmerpenguins)
con <- dbConnect (SQLite (), ":memory:" )
copy_to (con, penguins, "penguins" , temporary = FALSE )
Then:
Reference the penguins table with tbl().
Filter to Gentoo penguins.
Select species, island, body_mass_g, and sex.
Group by sex and calculate mean body mass.
Use collect() to bring the result into R.
💡 Finish early? Run show_query() before collect().
Give students the database setup code so the exercise focuses on database-backed dplyr logic rather than setup. The key learning objective is when to use collect().
Coding Break 2: Solution
library (tidyverse)
library (DBI)
library (RSQLite)
library (dbplyr)
library (palmerpenguins)
con <- dbConnect (SQLite (), ":memory:" )
copy_to (con, penguins, "penguins" , temporary = FALSE )
penguins_db <- tbl (con, "penguins" )
body_mass_summary <- penguins_db |>
filter (species == "Gentoo" ) |>
select (species, island, body_mass_g, sex) |>
group_by (sex) |>
summarize (mean_body_mass_g = mean (body_mass_g, na.rm = TRUE )) |>
collect ()
body_mass_summary
dbDisconnect (con)
In the discussion, highlight the order of operations. Students first create a lazy query against the database, then collect only the small summary table.
Section 3: Hierarchical and web data
Not all data arrive as rectangles.
This section covers:
Lists and list-columns
Rectangling nested data
JSON
HTML tables and web scraping
This merged section replaces the separate hierarchical data and web scraping title slides. Emphasize that web data often arrive in nested or semi-structured formats.
Rectangular versus hierarchical data
A rectangular dataset has rows and columns:
species | island | body_mass_g
Adelie | Torgersen | 3750
Adelie | Torgersen | 3800
A hierarchical dataset is tree-like:
penguin
├── species
├── island
└── measurements
├── bill_length_mm
└── body_mass_g
Rectangling means converting tree-like data into rows and columns.
This is the big conceptual transition. Students already know how to work with rectangular data. Hierarchical data require inspecting structure and deciding how to turn nested components into rows or columns.
Lists store hierarchical data
Lists can contain different kinds of objects.
x <- list (
species = "Adelie" ,
measurements = list (
bill_length_mm = 39.1 ,
body_mass_g = 3750
),
tags = c ("Torgersen" , "male" )
)
str (x)
#> List of 3
#> $ species : chr "Adelie"
#> $ measurements:List of 2
#> ..$ bill_length_mm: num 39.1
#> ..$ body_mass_g : num 3750
#> $ tags : chr [1:2] "Torgersen" "male"
This slide follows the R4DS chapter’s emphasis on lists as the structure that makes hierarchical data possible. Use str() as the main inspection tool.
List-columns
A list-column is a list stored inside a tibble.
df <- tibble (
id = 1 : 2 ,
bird = c ("A" , "B" ),
measurements = list (
list (bill = 39.1 , mass = 3750 ),
list (bill = 46.5 , mass = 4200 )
)
)
df
#> # A tibble: 2 × 3
#> id bird measurements
#> <int> <chr> <list>
#> 1 1 A <named list [2]>
#> 2 2 B <named list [2]>
df |> pull (measurements) |> str ()
#> List of 2
#> $ :List of 2
#> ..$ bill: num 39.1
#> ..$ mass: num 3750
#> $ :List of 2
#> ..$ bill: num 46.5
#> ..$ mass: num 4200
💡 Tibbles summarize list-columns as <list>. Use pull() + str() to inspect what is inside.
The requested change was to print the str() result so students can see the underlying structure. Explain that list-columns are normal columns, but each cell can contain a more complex object.
Rectangling decision guide
Choose the function based on what the nested values represent.
Named values should become variables
unnest_wider()
More columns
Repeated values should become observations
unnest_longer()
More rows
You only need a few pieces
hoist()
Selected columns
You are not sure yet
str() / View()
Inspect structure
Rectangling is iterative: inspect → choose wider/longer/hoist → inspect again .
This consolidates the many individual rectangling function slides into one decision guide. Use this slide to keep the why front and center. The function is chosen based on the meaning of the nested data.
Compact rectangling example
df <- tibble (
id = 1 : 2 ,
measurements = list (
list (bill = 39.1 , mass = 3750 ),
list (bill = 46.5 , mass = 4200 )
),
tags = list (
c ("Torgersen" , "male" ),
c ("Biscoe" , "female" )
)
)
# Named elements become columns
df |> unnest_wider (measurements)
# Repeated values become rows
df |> unnest_longer (tags)
This replaces separate wider/longer slides with a compact comparison. Explain that named measurement values behave like variables, while repeated tags behave like multiple observations.
What is JSON?
JSON stands for JavaScript Object Notation . It is a common format for storing and exchanging hierarchical data.
You will often encounter JSON when data comes from:
Web APIs
Public data portals
Application logs
Web services
Nested metadata files
JSON can represent structures that do not fit neatly into rows and columns right away:
{
"name": "Ava",
"scores": [90, 92],
"metadata": {"group": "A", "site": "KC"}
}
💡 Many APIs return JSON because it can store nested information. In R, JSON often becomes a list or list-column, so we use rectangling tools to turn it into a tibble.
Use this slide to connect rectangling to real data sources. JSON is common in APIs because it can represent nested structures: one person can have multiple scores, metadata can have named subfields, and records do not need to be perfectly rectangular at first. The key transition is that JSON data often enters R as lists, and rectangling turns those lists into columns and rows.
Read JSON, then rectangle it
library (jsonlite)
library (tidyr)
json <- '[
{"id": 1, "name": "Ava", "scores": [90, 92]},
{"id": 2, "name": "Ben", "scores": [85, 88, 91]}
]'
students <- fromJSON (json, simplifyVector = FALSE )
tibble (student = students) |>
unnest_wider (student) |>
unnest_longer (scores)
#> # A tibble: 5 × 3
#> id name scores
#> <int> <chr> <dbl>
#> 1 1 Ava 90
#> 2 1 Ava 92
#> 3 2 Ben 85
#> 4 2 Ben 88
#> 5 2 Ben 91
This slide keeps the code and its displayed output together in one code block using the #> convention. Explain that JSON often becomes a list in R, and rectangling turns that list into a tibble.
Web scraping
Web scraping means extracting data from HTML.
A common workflow is:
library (rvest)
page <- read_html ("https://example.com" )
tables <- page |>
html_elements ("table" ) |>
html_table ()
💡 Always check whether a site allows scraping, and prefer stable public pages or official APIs when available.
Keep this slide practical and cautious. Web scraping is powerful, but fragile. HTML structure can change, and not all sites allow scraping.
Coding Break 3: Web scraping challenge
Goal: scrape a public HTML table and turn it into a tibble.
Use this public page:
url <- "https://en.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations)"
Tasks:
Read the page with read_html().
Extract all tables with html_elements("table") and html_table().
Pull out the first table.
Use glimpse() to inspect it.
Select a few columns that look useful.
💡 Finish early? Clean the column names with janitor::clean_names(). Because web pages change, inspect the tables before assuming which one you need.
Give students about 15 minutes. Remind them that web pages can contain many tables, so scraping often begins by extracting a list of tables and inspecting which one you need.
Coding Break 3: Solution
library (tidyverse)
library (rvest)
url <- "https://en.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations)"
page <- read_html (url)
tables <- page |>
html_elements ("table" ) |>
html_table ()
population <- tables[[1 ]]
population |> glimpse ()
population_small <- population |>
select (1 : 4 )
population_small
If the page structure changes, adapt live by inspecting length(tables) and viewing the first few table names. This is a teachable moment: web scraping is useful but more fragile than reading a stable file.
Summary
Text files
Choose the function that matches the delimiter
Spreadsheets
Find the actual rectangle inside the workbook
Databases
Query first, collect() later
Arrow / Parquet
Large data workflows often avoid loading everything at once
Hierarchical data
Inspect lists and rectangle iteratively
Web pages
Extract HTML elements carefully and check the result
Import is successful only when the object in R matches the meaning of the original data.
End by returning to the core theme: data import is about preserving meaning. A dataset that loads successfully can still be wrong if types, names, paths, or structure were misinterpreted.
Practice files
Continue working in the companion activity file.
Recommended files:
data_import_activities.qmd for the three coding breaks
The rendered slides for reference
The R4DS import chapters for deeper examples
Use this as the bridge from the lecture to the companion practice document. If time remains, students can repeat the breaks or extend them.