Importing Data with readr
Introduction
Getting data into R is the first step of almost every analysis. The readr package, part of the tidyverse, makes this straightforward. Importing data with readr means reading flat files like CSV and TSV into tibbles, with sensible defaults that save you from silent bugs like automatic factor conversion and dropped values.
The biggest difference is how readr handles strings. Base R’s read.csv() quietly converts character vectors to factors. readr never does this, strings stay as character vectors unless you explicitly ask for factors. readr also returns a tibble, not a data.frame, and it reports parsing problems instead of silently dropping bad values.
Reading CSV files with read_csv()
The read_csv() function is your main tool for comma-separated values.
library(readr)
df <- read_csv("data/surveys.csv")
read_csv() assumes the first row contains column names. If your file has no header, pass col_names = FALSE:
df <- read_csv("data/no_header.csv", col_names = FALSE)
You can also provide your own column names as a character vector:
df <- read_csv("data/no_header.csv",
col_names = c("id", "species", "weight", "year"))
Controlling what gets imported
The skip argument tells readr to skip lines at the top of the file. This is useful when files have metadata rows before the actual data:
# Skip the first 3 lines before reading
df <- read_csv("data/surveys.csv", skip = 3)
Use n_max to read only a subset of rows, helpful for previewing a large file without loading all of it:
# Read only the first 100 rows
df <- read_csv("data/large_survey.csv", n_max = 100)
Combining skip and n_max gives you fine-grained control over which rows to import.
Specifying column types
By default, readr guesses each column’s type from the first 1000 rows. This works well, but sometimes you need to be explicit. The col_types argument takes a call to cols() with named type functions.
df <- read_csv("data/surveys.csv",
col_types = cols(
record_id = col_integer(),
weight = col_double(),
species_id = col_character()
)
)
readr provides a type function for every common data type:
| Function | Result |
|---|---|
col_character() | character vector |
col_double() | numeric double |
col_integer() | integer |
col_factor() | factor with defined levels |
col_date() | Date (calendar date only) |
col_datetime() | POSIXct (date and time) |
col_logical() | logical (TRUE/FALSE/NA) |
col_skip() | ignore this column |
col_guess() | let readr guess (default) |
If you only need a few columns, use cols_only() instead of listing every column you want to skip:
df <- read_csv("data/surveys.csv",
col_types = cols_only(
species_id = col_character(),
weight = col_double()
)
)
This is more efficient than reading everything and then selecting columns with dplyr.
Factors require levels
One thing trips up people coming from base R: col_factor() requires you to specify levels upfront. This is intentional, it makes your import reproducible.
df <- read_csv("data/surveys.csv",
col_types = cols(
plot_id = col_factor(levels = c("1", "2", "3", "4", "5")),
species_id = col_factor(levels = c("DM", "DO", "PP", "SH"))
)
)
If you want factors but do not know the levels in advance, import as character first and convert with as.factor() after.
Reading other delimited files
Not every data file uses a comma as the separator. readr gives you two dedicated functions and one general-purpose tool.
For tab-separated values, use read_tsv():
df <- read_tsv("data/results.tsv")
It has the same interface as read_csv(), you get col_names, col_types, skip, n_max, and everything else.
For anything else, semicolons, pipes, tabs in custom positions, use read_delim() with an explicit delim argument:
# Semicolon-delimited (common in European data)
df <- read_delim("data/european.csv", delim = ";")
# Pipe-delimited
df <- read_delim("data/records.txt", delim = "|")
Handling locale differences
Different locales represent dates, decimals, and thousands separators differently. The locale() function bundles these settings together.
European spreadsheets often use a comma as the decimal separator and semicolon as the delimiter:
df <- read_delim("data/european.csv",
delim = ";",
locale = locale(decimal_mark = ",")
)
If your file uses a different encoding, specify it with the encoding argument:
df <- read_csv("data/latin1.csv",
locale = locale(encoding = "ISO-8859-1"))
To find the right encoding, try locale(encoding = "UTF-8") first (the default), then fall back to common legacy encodings like "ISO-8859-1" or "Windows-1252".
Fixed-Width format files
Some data files have no delimiters at all, columns are defined by their character positions. readr handles these with read_fwf().
You describe the layout using either fwf_widths() (give each column a width) or fwf_positions() (give each column a start and end position):
# Define by column widths: ID is chars 1-5, Name is 6-25, Score is 26-30
fwf_spec <- fwf_widths(
widths = c(5, 20, 5),
col_names = c("id", "name", "score")
)
df <- read_fwf("data/fixed.txt", fwf_spec)
# Same layout using start and end positions
fwf_spec <- fwf_positions(
start = c(1, 6, 26),
end = c(5, 25, 30),
col_names = c("id", "name", "score")
)
df <- read_fwf("data/fixed.txt", fwf_spec)
Both approaches produce the same result. Use whichever matches how your file format is documented.
Debugging imports
When import goes wrong, readr gives you tools to find out what happened.
Call spec() on a file path to see what column specification readr would use, without fully importing the data:
spec("data/surveys.csv")
# A col_spec:
# cols(
# record_id = col_double(),
# month = col_double(),
# day = col_double(),
# year = col_integer(),
# plot_id = col_integer(),
# species_id = col_character(),
# ...
# )
After importing, call problems() to see rows that readr could not parse:
df <- read_csv("data/messy.csv")
problems(df)
Each problem records the row number, the column, what readr expected, and the actual value it found. This is much better than base R, where a parsing failure silently converts the value to NA.
Reading lines directly
Sometimes you do not want a table at all, you want the raw lines. read_lines() returns a character vector where each element is one line from the file:
lines <- read_lines("data/raw.txt", n_max = 10)
Use n_max = -1 to read all lines. read_lines_raw() does the same but returns raw byte vectors instead of character strings, which is useful for binary-safe processing.
Whitespace and progress bars
By default, read_csv() does not strip leading or trailing whitespace from values. If your data has messy padding, add trim_ws = TRUE:
df <- read_csv("data/messy.csv", trim_ws = TRUE)
For large files, readr shows a progress bar by default once the file exceeds about 1 MB. Suppress it explicitly if you are running in a non-interactive environment:
df <- read_csv("data/large.csv", show_progress = FALSE)
Reading delimited files
read_csv() handles comma-separated values. For other delimiters: read_tsv() for tab-separated, read_delim("file.txt", delim = "|") for pipe-delimited. read_csv2() handles semicolon-delimited files with comma as decimal separator (common in European data).
col_names = FALSE for files without headers, columns get names X1, X2, etc. col_names = c("a", "b", "c") provides custom names. skip = 2 skips the first two lines (metadata rows above the data). n_max = 1000 reads only the first 1000 rows for testing.
Type guessing and overrides
read_csv() guesses types from the first 1000 rows. problems(result) shows rows that failed type parsing. To override: col_types = cols(id = col_character(), amount = col_number()). col_number() handles currency formats like “$1,234.56” by stripping non-numeric characters. col_date(format = "%m/%d/%Y") parses non-standard date formats.
Reading fixed-Width files
read_fwf() reads fixed-width format files where each column occupies a specific character range. fwf_widths(c(10, 5, 15)) specifies column widths. fwf_positions(start = c(1, 11, 16), end = c(10, 15, 30)) uses character positions. Fixed-width files are common in financial and government datasets where space efficiency was historically important.
Writing data
write_csv(df, "output.csv") writes UTF-8 CSV without row names (unlike base R’s write.csv()). write_tsv() writes tab-separated. write_delim(df, "file.txt", delim = "|") writes with a custom delimiter. For larger datasets, arrow::write_parquet(df, "output.parquet") writes Parquet, much faster to read than CSV for subsequent analysis.
readr’s design philosophy
readr was written to address specific shortcomings in base R’s read.csv function. The most important is speed: readr uses a C++ parser that is significantly faster than base R’s parser for large files. The second is predictability: readr does not convert strings to factors automatically, does not create row names, and does not silently modify column names. What you see in the file is what you get in the data frame.
readr also handles type inference more carefully. It reads the first 1000 rows of each column and infers the type from those samples. When it cannot reliably infer a type, it guesses character rather than applying a potentially wrong coercion. After reading a file, readr prints the column specification it inferred, which serves as both documentation and a prompt to verify the inference was correct.
Column specification
The column specification system in readr separates type inference from type declaration. After an initial read, copy the printed column spec and pass it back to read_csv as the col_types argument. This converts inference into declaration, making subsequent reads deterministic and fast — readr skips re-inferring types when the spec is explicit. Version-controlling the column spec alongside the data pipeline documents the expected schema.
Column collector functions handle special cases. col_date and col_datetime parse date strings with specified format strings. col_factor creates a factor with specified levels. col_skip omits a column entirely. col_guess enables inference for that column while others are explicit. The spec object also validates that the actual data matches the declared types, catching schema changes at import time rather than somewhere downstream in the analysis.
Large file handling
For files that do not fit in memory, readr provides read_csv_chunked, which reads the file in chunks and applies a callback function to each chunk. The accumulated results are the final output. This allows processing large files with bounded memory usage, though the analysis must be expressible as a series of chunk-level operations that compose into a final result.
The lazy reading functions from the vroom package, which readr uses internally, index the file first and load data on demand. This makes initial read times fast regardless of file size. For workflows that read a subset of columns from large files, lazy reading is particularly effective because unneeded columns are never fully parsed.
Next steps
Now that you understand importing data with readr, explore these related topics to deepen your knowledge and apply these techniques in more complex scenarios.
See also
- Data Frames and Tibbles — understand what readr actually returns
- Introduction to the Tidyverse — readr in the context of the tidyverse ecosystem
- Importing and Exporting Data — broader I/O options beyond flat text files