R - Read Excel File (readxl::read_excel)

The `readxl` package comes bundled with the tidyverse but can be installed independently. It reads both modern `.xlsx` files and legacy `.xls` formats without external dependencies.

Key Insights

  • The readxl package provides fast, dependency-free Excel file reading without requiring Java or external libraries, making it the preferred method for importing .xlsx and .xls files in R
  • Understanding sheet selection, cell range specification, and column type inference prevents common data import issues that corrupt downstream analysis
  • Proper handling of merged cells, multiple header rows, and Excel formulas requires explicit parameter configuration rather than relying on defaults

Installation and Basic Usage

The readxl package comes bundled with the tidyverse but can be installed independently. It reads both modern .xlsx files and legacy .xls formats without external dependencies.

# Install if needed
install.packages("readxl")

# Load the package
library(readxl)

# Basic read operation
data <- read_excel("sales_data.xlsx")
head(data)

The simplest invocation reads the first sheet with automatic column type detection. The function returns a tibble, which provides better printing and more consistent behavior than base R data frames.

Specifying Sheets

Excel workbooks contain multiple sheets. Three methods exist for targeting specific sheets:

# Method 1: By sheet name
sales_q1 <- read_excel("financial_report.xlsx", sheet = "Q1 Sales")

# Method 2: By position (1-indexed)
sales_q1 <- read_excel("financial_report.xlsx", sheet = 1)

# Method 3: List all sheets first
excel_sheets("financial_report.xlsx")
# [1] "Q1 Sales" "Q2 Sales" "Q3 Sales" "Q4 Sales"

# Read multiple sheets into a list
all_quarters <- lapply(excel_sheets("financial_report.xlsx"), 
                       function(x) read_excel("financial_report.xlsx", sheet = x))
names(all_quarters) <- excel_sheets("financial_report.xlsx")

The excel_sheets() function proves essential for programmatic processing of workbooks with unknown structure.

Cell Range Selection

Excel files often contain formatting, titles, or metadata that shouldn’t be imported. The range parameter provides precise control.

# Read specific cell range (Excel notation)
data <- read_excel("report.xlsx", range = "B3:F100")

# Read from specific cell to end
data <- read_excel("report.xlsx", range = cell_cols("B:F"))

# Read specific rows
data <- read_excel("report.xlsx", range = cell_rows(3:100))

# Combine sheet and range
data <- read_excel("report.xlsx", 
                   sheet = "Summary",
                   range = "A5:J50")

The cell_cols() and cell_rows() helper functions from the cellranger package (loaded with readxl) enable programmatic range specification:

library(cellranger)

# Dynamic range based on known structure
start_row <- 5
end_row <- start_row + 100
data <- read_excel("data.xlsx", range = cell_rows(start_row:end_row))

# Read only specific columns by letter
data <- read_excel("data.xlsx", range = cell_cols(c("A", "C", "E:G")))

Column Type Specification

By default, read_excel() guesses column types from the first 1000 rows. Override this behavior for consistent imports:

# Automatic type guessing (default)
data <- read_excel("data.xlsx")

# Explicit column types
data <- read_excel("data.xlsx",
                   col_types = c("text", "numeric", "date", "numeric", "logical"))

# Use "guess" for specific columns
data <- read_excel("data.xlsx",
                   col_types = c("text", "guess", "date", "guess"))

# Skip columns with "skip"
data <- read_excel("data.xlsx",
                   col_types = c("text", "numeric", "skip", "date"))

Available type specifications: "skip", "guess", "logical", "numeric", "date", "text", "list".

The "list" type preserves all cell information including formulas and formatting:

raw_data <- read_excel("complex.xlsx", col_types = "list")
# Each cell becomes a list element with metadata

Handling Missing Values

Excel represents missing data inconsistently. Configure how read_excel() interprets blanks and specific values:

# Treat specific strings as NA
data <- read_excel("data.xlsx", na = c("", "NA", "N/A", "NULL", "-"))

# Example with real data
df <- read_excel("survey.xlsx", 
                 na = c("", "No Response", "Prefer not to answer"))

# Check NA counts
colSums(is.na(df))

Working with Headers

Excel files frequently have non-standard header structures. Control header interpretation with the col_names parameter:

# Skip rows and use specific row as headers
data <- read_excel("report.xlsx", skip = 3)

# No headers - generate automatic names
data <- read_excel("data.xlsx", col_names = FALSE)
# Columns named: ...1, ...2, ...3, etc.

# Provide custom column names
data <- read_excel("data.xlsx", 
                   col_names = c("id", "name", "value", "date"),
                   skip = 1)  # Skip the existing header row

# Multiple header rows - read and process separately
headers <- read_excel("data.xlsx", range = "A1:F2", col_names = FALSE)
data <- read_excel("data.xlsx", skip = 2, 
                   col_names = paste(headers[1,], headers[2,], sep = "_"))

Date and Time Handling

Excel stores dates as numbers (days since 1900-01-01 on Windows, 1904-01-01 on Mac). The readxl package handles this automatically but requires awareness:

# Dates automatically converted
data <- read_excel("dates.xlsx")
class(data$date_column)  # "POSIXct" "POSIXct"

# Force date column to be read as text (to inspect issues)
data <- read_excel("dates.xlsx", col_types = c("text", "text", "numeric"))

# Handle Excel serial dates manually if needed
library(lubridate)
excel_date <- 44562  # Excel serial number
real_date <- as.Date(excel_date, origin = "1899-12-30")  # Windows Excel

Reading from Non-Standard Locations

Read Excel files from various sources beyond local paths:

# From URL (download first)
temp_file <- tempfile(fileext = ".xlsx")
download.file("https://example.com/data.xlsx", temp_file, mode = "wb")
data <- read_excel(temp_file)
unlink(temp_file)

# From package data
system.file("extdata", "datasets.xlsx", package = "readxl")

# From compressed archives
unzip("data.zip", files = "data.xlsx", exdir = tempdir())
data <- read_excel(file.path(tempdir(), "data.xlsx"))

Performance Considerations

For large Excel files, optimize performance through strategic parameter use:

# Read only needed columns
data <- read_excel("large_file.xlsx", 
                   range = cell_cols("A:C"),  # Only 3 columns
                   col_types = c("text", "numeric", "date"))

# Limit rows read with n_max
data <- read_excel("large_file.xlsx", n_max = 10000)

# Use explicit types to avoid guessing overhead
data <- read_excel("large_file.xlsx",
                   col_types = rep("text", 20),  # All 20 columns as text
                   guess_max = 0)  # Disable guessing

# For truly massive files, consider reading in chunks
chunk_size <- 5000
total_rows <- 50000
chunks <- lapply(seq(1, total_rows, chunk_size), function(start) {
  read_excel("huge_file.xlsx", 
             skip = start, 
             n_max = chunk_size,
             col_names = FALSE)
})
combined <- do.call(rbind, chunks)

Error Handling and Validation

Robust import workflows include validation and error handling:

# Check if file exists and is readable
if (!file.exists("data.xlsx")) {
  stop("File not found")
}

# Validate sheet exists
available_sheets <- excel_sheets("data.xlsx")
target_sheet <- "Sales"
if (!target_sheet %in% available_sheets) {
  stop(sprintf("Sheet '%s' not found. Available: %s", 
               target_sheet, paste(available_sheets, collapse = ", ")))
}

# Safe read with tryCatch
safe_read <- tryCatch({
  read_excel("data.xlsx", sheet = target_sheet)
}, error = function(e) {
  message("Error reading Excel file: ", e$message)
  return(NULL)
})

# Validate expected columns
expected_cols <- c("id", "name", "value")
if (!all(expected_cols %in% names(safe_read))) {
  missing <- setdiff(expected_cols, names(safe_read))
  warning("Missing expected columns: ", paste(missing, collapse = ", "))
}

The readxl package provides a reliable foundation for Excel integration in R workflows. Explicit parameter specification prevents the silent data corruption that plagues many analysis pipelines. When working with Excel files from external sources, always inspect the first import manually before automating the process.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.