rguides

Importing and Exporting Data in R

Working with data means getting it in and out of R. Whether you’re loading a dataset from a colleague or saving your analysis results, R provides multiple ways to handle file I/O. This tutorial covers the most common approaches, from base R functions to the tidyverse tools that make data import faster and more flexible.

What you’ll learn

This tutorial covers the key concepts and practical techniques for working with Importing and Exporting Data in R. By the end, you will know how to apply the core functions in real data analysis workflows.

Reading CSV files

The CSV (Comma-Separated Values) format is the workhorse of data sharing. It’s plain text, universal, and supported by every data tool. R handles CSVs natively through read.csv(), but the tidyverse offers a faster alternative through the readr package.

Base R: read.csv()

Base R’s read.csv() is part of the standard installation. It works well for simple cases:

# Basic CSV import
my_data <- read.csv("data/survey.csv")

# Check the structure
head(my_data)
str(my_data)

The function automatically treats the first row as headers and infers column types. You can customize behavior with parameters like sep (separator), header, and na.strings:

# Handle missing values and different separators
my_data <- read.csv(
  "data/survey.csv",
  na.strings = c("NA", "", "missing"),
  stringsAsFactors = FALSE
)

readr: read_csv()

The readr package (part of tidyverse) offers read_csv() with better defaults and faster performance for large files:

library(readr)

# Fast CSV import with explicit types
my_data <- read_csv(
  "data/survey.csv",
  col_types = cols(
    id = col_integer(),
    name = col_character(),
    score = col_double(),
    group = col_factor()
  )
)

Key advantages of readr:

  • Doesn’t convert strings to factors by default
  • Shows a progress bar for large files
  • More predictable behavior with missing values
  • Faster on files over a few megabytes

Reading Excel files

Excel files (.xlsx and .xls) require additional packages. The readxl package is the tidyverse solution:

library(readxl)

# Read an Excel sheet by name or number
excel_data <- read_excel("data/results.xlsx", sheet = "Q1 Results")
excel_data <- read_excel("data/results.xlsx", sheet = 2)

# List sheets in a workbook
excel_sheets("data/results.xlsx")

For more complex Excel files with multiple sheets or specific ranges:

# Read a specific range
excel_data <- read_excel(
  "data/results.xlsx",
  sheet = "Q1 Results",
  range = "A1:D100"
)

# Handle both .xls and .xlsx
excel_data <- read_excel("data/old_format.xls")

Exporting data

Saving data follows similar patterns. Base R provides write.csv(), while readr offers write_csv().

Writing CSV files

# Base R
write.csv(
  my_data,
  "output/cleaned_data.csv",
  row.names = FALSE
)

# readr (faster for large data)
library(readr)
write_csv(my_data, "output/cleaned_data.csv")

The row.names = FALSE parameter is essential in base R—it prevents R from writing row numbers as an extra column.

Exporting to Excel

library(writexl)

# Save as Excel
write_xlsx(my_data, "output/cleaned_data.xlsx")

# Multiple sheets in one workbook
write_xlsx(
  list(
    "Q1 Results" = q1_data,
    "Q2 Results" = q2_data
  ),
  "output/quarterly_results.xlsx"
)

Other common formats

JSON

library(jsonlite)

# Read JSON
json_data <- fromJSON("data/api_response.json")

# Write JSON
toJSON(my_data, pretty = TRUE, file = "output/data.json")

Delimited files (TSV, etc.)

# Tab-separated values
read_tsv("data/file.tsv")
write_tsv(my_data, "output/file.tsv")

# Any delimiter
read_delim("data/file.txt", delim = "|")

Best practices

  1. Use stringsAsFactors = FALSE in base R functions to avoid automatic factor conversion—it causes surprises more often than it helps.

  2. Check your working directory with getwd() and use relative paths. Move files or set paths explicitly when needed.

  3. Handle encodings when working with international text. Use encoding = "UTF-8" or encoding = "latin1" when special characters misbehave.

  4. Consider column types upfront with readr’s col_types. It prevents type surprises later and catches data problems early.

  5. Name your files descriptively: analysis_2026_03.RData beats data1.rdata.

Writing data

write.csv(df, "output.csv", row.names = FALSE) writes a CSV without the row index column. readr::write_csv(df, "output.csv") does the same with better defaults and no row names by default. For Excel: writexl::write_xlsx(df, "output.xlsx"). For RDS (R’s native binary format): saveRDS(df, "data.rds"); readRDS("data.rds"). For Parquet: arrow::write_parquet(df, "data.parquet").

Database connections

DBI::dbConnect(RSQLite::SQLite(), "data.db") opens a SQLite connection. DBI::dbGetQuery(con, "SELECT * FROM table WHERE id > 10") returns a data frame. dplyr::tbl(con, "table") creates a lazy database reference for use with dplyr verbs. DBI::dbDisconnect(con) closes the connection. Always disconnect in a on.exit(dbDisconnect(con)) call to ensure cleanup even on error.

Reading and writing multiple file formats

readr::read_delim("file.tsv", delim = " ") reads tab-separated values. read_csv2("file.csv") reads CSV with semicolon separators and comma decimal marks (European convention). read_fwf("file.fwf", col_positions = fwf_widths(c(10, 5, 8))) reads fixed-width files.

readxl::read_excel("file.xlsx", sheet = "Data", range = "A2:D100") reads an Excel range. openxlsx2::write_xlsx(df, "output.xlsx") writes Excel. writexl::write_xlsx(list(Sheet1 = df1, Sheet2 = df2), "multi.xlsx") writes multiple sheets.

haven::read_sav("survey.sav") reads SPSS files, preserving variable labels and value labels as attributes. haven::read_dta("stata.dta") reads Stata. haven::read_sas("data.sas7bdat") reads SAS. The labelled package provides tools for working with the value labels these formats carry.

arrow::read_feather("data.feather") reads the Feather format (fast binary for R and Python). arrow::read_parquet("data.parquet") reads Parquet. Both preserve column types better than CSV and read significantly faster.

JSON and nested data

jsonlite::read_json("data.json") reads a JSON file as a nested R list. jsonlite::fromJSON("data.json", simplifyDataFrame = TRUE) attempts to flatten nested JSON into data frames. For deeply nested structures, flatten level by level with tidyr::unnest_wider() and unnest_longer().

jsonlite::stream_in(file("data.ndjson")) reads newline-delimited JSON (one object per line). This is memory-efficient for large JSON files because it processes one record at a time. jsonlite::stream_out(df, file("output.ndjson")) writes NDJSON.

Database import

DBI::dbGetQuery(con, "SELECT * FROM table LIMIT 1000") imports a sample from a database table. dplyr::tbl(con, "table") %>% collect() imports via dbplyr with lazy evaluation.

For bulk import from R to database: DBI::dbWriteTable(con, "new_table", df, overwrite = TRUE) creates and populates a table. For PostgreSQL, RPostgres::postgresWriteTable() uses the COPY protocol, importing millions of rows in seconds rather than minutes.

File path management

fs::path("data", "raw", "file.csv") constructs platform-independent file paths. fs::path_expand("~/data/file.csv") expands tilde paths. fs::file_exists("file.csv") checks existence. fs::dir_ls("data/", glob = "*.csv") lists matching files.

here::here("data", "file.csv") constructs a path relative to the project root (where the .Rproj or .here file is), regardless of the current working directory. This is essential for writing code that works on any team member’s machine without modifying paths.

readr::read_csv(here::here("data", "raw", "file.csv")) combines here with readr for portable data imports. The here package is the standard solution to the “setwd() at the top of every script” antipattern.

Data validation after import

Always validate imported data before analysis. Check dimensions: dim(df). Check column types: glimpse(df). Check value ranges: summary(df). Check for unexpected NAs: colSums(is.na(df)). Check key columns for uniqueness: df %>% count(key_col) %>% filter(n > 1).

validate::confront(df, rules) runs systematic data validation rules. Define rules as expressions: v <- validator(x >= 0, !is.na(id), nchar(code) == 3). summary(confront(df, v)) shows how many records pass each rule.

pointblank provides a data testing framework similar to dbt tests for SQL. Define an “agent” with expectations, then “interrogate” the data frame. The result is a detailed report of which expectations passed and failed, useful for monitoring data quality in automated pipelines.

Data import as the first step

Data import is the first step of every analysis, and getting it right determines what is possible downstream. A well-imported dataset has correct column types, meaningful column names, appropriate handling of missing values, and no extraneous rows. A poorly imported dataset passes wrong types through the entire analysis, causing subtle errors that surface far from their source. Spending time on careful import setup pays dividends throughout the project.

R’s import functions make assumptions about data format that may not match your file. The most common assumption is the delimiter character, commas, semicolons, tabs, and pipes all appear in data files from different sources. The second is the decimal separator, North American files use periods while European files often use commas, which means a file from a European system may need dec = "," to parse numbers correctly. Check the import result immediately after reading by examining column types and spot-checking values.

Encoding and international characters

Text encoding determines how byte sequences represent characters. ASCII covers English letters and common punctuation. UTF-8 encodes all Unicode characters using one to four bytes and is the universal standard for new data. Latin-1 and Windows-1252 encode Western European characters but cannot represent characters outside their range. Receiving a file from a system with a different default encoding, older Windows systems often use Windows-1252 — produces garbled characters if you read with the wrong encoding.

Always specify encoding explicitly for files that contain non-ASCII characters. Guessing the encoding from the file contents is unreliable. If the file’s source system is known, its default encoding is usually documented. The readr package defaults to UTF-8 and warns when it encounters bytes that are not valid UTF-8, which helps identify encoding mismatches early.

Exporting for interoperability

Choosing an export format depends on the audience and the next use. CSV is universally readable and appropriate for sharing with users of Excel, Python, or any other tool. The limitation is type loss — column types are not encoded in CSV, so a reader must re-infer or be told the types. Parquet preserves types including factors, dates, and integers, making it better for pipelines where the next step is another R or Python process. RDS is R-specific and not interoperable but preserves all R object attributes including factor levels, ordered factors, and class attributes.

For production data pipelines, avoid formats that require special readers to be practically usable. A pipeline that produces RDS files can only be consumed by R code. A pipeline that produces Parquet can be consumed by R, Python, Spark, DuckDB, and most modern analytical tools. The small overhead of converting to Parquet is worthwhile for any pipeline output that other systems might need to read.

Summary

R’s data import ecosystem covers every common format. For most tasks, read.csv() gets you started quickly, while readr functions offer speed and control for production workflows. The key is choosing the right tool: base R for quick exploration, readr for reproducibility and large files, and specialized packages (readxl, jsonlite) for domain-specific formats.

With these tools, you can pull in data from anywhere and save your results in any format your collaborators need.

Next steps

Now that you understand importing and exporting data in r, explore these related topics to deepen your knowledge and apply these techniques in more complex scenarios.