R - Write Excel File (writexl)
The R ecosystem offers several Excel writing solutions: xlsx (Java-dependent), openxlsx (requires zip utilities), and writexl. The writexl package stands out by having zero external dependencies...
Key Insights
- The writexl package provides a fast, dependency-free solution for writing Excel files in R without requiring Java or external libraries
- Unlike other Excel packages, writexl writes .xlsx files directly with minimal overhead while preserving data types and formatting basics
- The package handles data frames, lists of data frames, and supports column formatting options through simple function parameters
Why writexl Over Other Excel Packages
The R ecosystem offers several Excel writing solutions: xlsx (Java-dependent), openxlsx (requires zip utilities), and writexl. The writexl package stands out by having zero external dependencies beyond R itself. It uses a portable C library (libxlsxwriter) that compiles on any platform without additional system requirements.
This matters in production environments where installing Java or managing system dependencies creates friction. writexl installs cleanly, runs fast, and produces standard-compliant .xlsx files that open in Excel, LibreOffice, and Google Sheets.
# Installation
install.packages("writexl")
library(writexl)
Basic Single Sheet Export
The primary function write_xlsx() exports data frames to Excel files. The simplest use case writes a single data frame to a workbook with one sheet.
library(writexl)
# Sample dataset
sales_data <- data.frame(
date = as.Date(c("2024-01-01", "2024-01-02", "2024-01-03")),
product = c("Widget A", "Widget B", "Widget A"),
quantity = c(150, 200, 175),
revenue = c(4500.50, 8000.00, 5250.75),
stringsAsFactors = FALSE
)
# Write to Excel
write_xlsx(sales_data, "sales_report.xlsx")
The function automatically converts R data types to Excel equivalents: Date objects become Excel dates, numerics become numbers, and characters become text. Column names from the data frame become Excel headers.
Multi-Sheet Workbooks
Production reports typically require multiple related sheets. Pass a named list of data frames to create a workbook with multiple sheets.
# Create related datasets
products <- data.frame(
product_id = 1:3,
product_name = c("Widget A", "Widget B", "Widget C"),
unit_price = c(30.00, 40.00, 35.00)
)
orders <- data.frame(
order_id = 101:105,
product_id = c(1, 2, 1, 3, 2),
quantity = c(150, 200, 175, 100, 250),
order_date = as.Date("2024-01-01") + 0:4
)
customers <- data.frame(
customer_id = 1:3,
customer_name = c("Acme Corp", "TechStart Inc", "Global Solutions"),
region = c("North", "South", "East")
)
# Write multiple sheets
workbook_data <- list(
Products = products,
Orders = orders,
Customers = customers
)
write_xlsx(workbook_data, "sales_database.xlsx")
Sheet names come from the list names. Excel enforces a 31-character limit on sheet names and prohibits certain characters ([ ] : * ? / \). Sanitize names beforehand if generating them dynamically.
Column Formatting and Width
While writexl prioritizes simplicity over extensive formatting, it provides the format_headers parameter to control header styling and col_names to toggle header row inclusion.
# Write without headers
write_xlsx(sales_data, "data_no_headers.xlsx", col_names = FALSE)
# Disable bold headers (default is TRUE)
write_xlsx(sales_data, "plain_headers.xlsx", format_headers = FALSE)
For column widths, writexl doesn’t provide direct control. Excel auto-adjusts widths on first open, but you can pre-process string columns to influence width:
# Add padding to influence column width perception
sales_data$product <- sprintf("%-20s", sales_data$product)
write_xlsx(sales_data, "padded_columns.xlsx")
For advanced formatting needs (colors, fonts, borders), transition to openxlsx or officer packages.
Handling Large Datasets
writexl efficiently handles large datasets through its C implementation. Testing with datasets up to several million rows shows good performance:
# Generate large dataset
large_data <- data.frame(
id = 1:1000000,
timestamp = Sys.time() + 1:1000000,
value = rnorm(1000000),
category = sample(LETTERS[1:10], 1000000, replace = TRUE)
)
# Time the write operation
system.time(
write_xlsx(large_data, "large_dataset.xlsx")
)
On typical hardware, writing 1 million rows takes 10-30 seconds. For datasets exceeding Excel’s 1,048,576 row limit, split into multiple sheets:
# Split large dataset into chunks
chunk_size <- 1000000
chunks <- split(large_data, ceiling(seq_along(large_data$id) / chunk_size))
# Create named list for sheets
sheet_list <- setNames(chunks, paste0("Data_", seq_along(chunks)))
write_xlsx(sheet_list, "large_dataset_split.xlsx")
Data Type Preservation
writexl preserves R data types intelligently. Understanding the mapping prevents surprises in the output:
# Demonstrate type handling
type_demo <- data.frame(
logical_col = c(TRUE, FALSE, TRUE),
integer_col = c(1L, 2L, 3L),
numeric_col = c(1.5, 2.7, 3.9),
character_col = c("Text", "More text", "Even more"),
date_col = as.Date(c("2024-01-01", "2024-02-01", "2024-03-01")),
datetime_col = as.POSIXct(c("2024-01-01 10:30:00", "2024-01-01 14:45:00",
"2024-01-01 18:00:00")),
factor_col = factor(c("Low", "Medium", "High"),
levels = c("Low", "Medium", "High"))
)
write_xlsx(type_demo, "data_types.xlsx")
Key mappings:
- Logical: Excel boolean (TRUE/FALSE)
- Integer/Numeric: Excel number
- Character: Excel text
- Date: Excel date (serial number with date format)
- POSIXct: Excel datetime
- Factor: Excel text (factor levels lost, only labels preserved)
Practical Pipeline Integration
In production workflows, combine writexl with data processing pipelines. Here’s a complete example generating a monthly report:
library(writexl)
library(dplyr)
# Simulate data processing
raw_data <- data.frame(
transaction_date = seq(as.Date("2024-01-01"), as.Date("2024-01-31"), by = "day"),
amount = runif(31, 1000, 5000),
category = sample(c("Sales", "Services", "Products"), 31, replace = TRUE)
)
# Create summary sheets
daily_summary <- raw_data %>%
group_by(transaction_date) %>%
summarise(
total_amount = sum(amount),
transaction_count = n(),
.groups = "drop"
)
category_summary <- raw_data %>%
group_by(category) %>%
summarise(
total_amount = sum(amount),
avg_amount = mean(amount),
transaction_count = n(),
.groups = "drop"
)
# Export report
monthly_report <- list(
"Raw Data" = raw_data,
"Daily Summary" = daily_summary,
"Category Summary" = category_summary
)
write_xlsx(monthly_report, paste0("monthly_report_", format(Sys.Date(), "%Y%m"), ".xlsx"))
Error Handling and Validation
Implement validation before writing to catch issues early:
safe_write_xlsx <- function(data, path) {
# Validate input
if (!is.data.frame(data) && !is.list(data)) {
stop("Input must be a data frame or list of data frames")
}
# Check for valid path
dir_path <- dirname(path)
if (!dir.exists(dir_path)) {
stop("Directory does not exist: ", dir_path)
}
# Validate sheet names if list
if (is.list(data) && !is.data.frame(data)) {
sheet_names <- names(data)
if (any(nchar(sheet_names) > 31)) {
warning("Sheet names truncated to 31 characters")
names(data) <- substr(sheet_names, 1, 31)
}
}
# Write with error handling
tryCatch(
{
write_xlsx(data, path)
message("Successfully wrote: ", path)
return(invisible(TRUE))
},
error = function(e) {
stop("Failed to write Excel file: ", e$message)
}
)
}
# Usage
safe_write_xlsx(sales_data, "output/report.xlsx")
The writexl package delivers on its core promise: fast, reliable Excel file writing without complexity. For 80% of Excel export needs, it’s the right tool. When you need pivot tables, complex formatting, or formula injection, graduate to openxlsx. For everything else, writexl keeps your code clean and your dependencies minimal.