Skip to main content

14. Best Practices and Optimization: Writing Clean, Performant R Code

 

R best practices, code optimization, profvis profiling, Tidyverse style guide, R project structure

As your R projects grow in complexity, adhering to coding best practices and optimization techniques becomes essential. Clean, consistent code not only scales better, it’s easier to debug, review, and share. In this comprehensive guide, we’ll cover:

  • Tidyverse style guide and naming conventions

  • Profiling and performance tuning with profvis and other tools

  • Organizing scripts, data, and outputs for maintainability

  • Additional tips: caching, testing, documentation, and version control

By the end, you’ll have a robust framework to keep your R projects efficient, reproducible, and team-ready.

1. Tidyverse Style Guide and Naming Conventions

Consistency in style reduces cognitive load when reading or reviewing code. The Tidyverse style guide by Hadley Wickham offers clear rules for indentation, naming, and formatting.

1.1 Indentation and Line Breaks

  • Use two spaces, not tabs, for indentation.

  • Keep line length under 80 characters for readability.

  • When breaking long pipelines, start new lines after the pipe (%>%):

r
result <- raw_data %>%
  filter(status == "active") %>%
  mutate(
    revenue = quantity * unit_price,
    profit  = revenue - cost
  ) %>%
  group_by(region) %>%
  summarise(
    total_revenue = sum(revenue),
    avg_profit    = mean(profit)
  )

1.2 Naming Conventions

  • Objects (variables, functions): snake_case or dot.case, consistently within a project. For example, sales_summary or sales.summary.

  • Functions: verbs describing action, e.g., calculate_profit(), fetch_data().

  • Constants: use ALL_CAPS if needed, e.g., MAX_ITERATIONS.

  • Package names: all lowercase, no special characters.

r
# Good
load_raw_data <- function(path) { … }
region_codes   <- c("NA", "EU", "APAC")

# Avoid
LOADRAW <- function() { … }
x1 <- data

1.3 Spacing and Operators

  • Surround binary operators (+, -, %in%, ==) with spaces.

  • No spaces after unary operators (-x, !flag).

  • Place commas immediately after values, not before:

r
# Good
x <- 1 + 2
y <- -x
filtered <- df %>% filter(x > 0, y < 10)

# Avoid
x<-1+2
y <- - x
filtered <- df %>% filter( x>0 , y<10 )

1.4 Function Arguments

  • One argument per line when function calls or definitions exceed 80 characters:

r
process_data <- function(
  input_data,
  filter_column,
  threshold = 0.5,
  verbose   = FALSE
) {
  …
}
  • Default values aligned vertically for readability.

1.5 Commenting and Documentation

  • Use comments sparingly to explain why, not what:

    r
    # Remove rows with missing revenue because they distort average
    cleaned <- raw_data %>%
      filter(!is.na(revenue))
    
  • For longer explanations, use a block comment at the top of scripts.

  • Document functions with roxygen2 for automatic help file generation.

2. Profiling and Performance Tuning with profvis

Profiling pinpoints bottlenecks in your code. The profvis package offers an interactive flame graph to visualize where time is spent.

2.1 Installing and Loading profvis

r
install.packages("profvis")
library(profvis)

2.2 Profiling a Script

Wrap the code you want to profile in profvis({ ... }):

r
profvis({
  # Example computation
  result <- large_df %>%
    mutate(
      score = complex_calculation(x, y),
      flag  = str_detect(text, "pattern")
    ) %>%
    group_by(category) %>%
    summarise(
      avg_score = mean(score),
      count     = n()
    ) %>%
    inner_join(metadata, by = "category")
})

A browser window will display an interactive timeline showing:

  • Execution time per line and function

  • Memory allocation hotspots

  • Call stacks

2.3 Interpreting Results

  • Flat graph: colors denote functions, width indicates time consumed.

  • Data table: sorted by total time. Identify “hot” functions or heavy loops.

2.4 Optimization Strategies

  1. Vectorize operations: replace explicit loops with vectorized functions or purrr::map() calls.

  2. Avoid repeated computations: cache intermediate results.

  3. Use for very large tables: its syntax and C implementations can be orders of magnitude faster.

  4. Preallocate memory: for growing objects use functions like vector(), numeric(), or initialize data frames before a loop.

r
# Bad: growing inside a loop
out <- NULL
for (i in seq_len(nrow(df))) {
  out <- c(out, heavy_function(df$x[i]))
}

# Better: preallocate
out <- vector("numeric", nrow(df))
for (i in seq_len(nrow(df))) {
  out[i] <- heavy_function(df$x[i])
}
  1. Parallelize CPU-intensive tasks with packages like future, foreach, or parallel.

3. Organizing Scripts, Data, and Outputs

A well-structured project folder makes it easy for collaborators (and future you) to navigate.

3.1 Standard Directory Layout

Codice
my_project/
├─ data/
│  ├─ raw/           # immutable original data
│  ├─ processed/     # cleaned and transformed data
├─ R/                # custom functions (.R files)
├─ scripts/          # analysis scripts (e.g., 01_data_prep.R)
├─ reports/          # output reports (HTML, PDF)
├─ figures/          # plot images and dashboards
├─ tests/            # unit tests with testthat
├─ docs/             # vignettes, presentations
├─ renv/             # renv package library
├─ .gitignore        # files to exclude from Git
├─ my_project.Rproj  # RStudio project file
└─ README.md

Benefits

  • Clarity: know where to find raw versus processed data.

  • Modularity: separate function definitions (R/) from execution scripts (scripts/).

  • Reproducibility: locking dependencies with renv.

3.2 Script Naming and Ordering

  • Prefix scripts with numbers to define execution order:

    Codice
    01_data_import.R
    02_data_cleaning.R
    03_feature_engineering.R
    04_model_training.R
    05_reporting.R
    
  • Each script ends by saving key outputs (datasets, plots, models) to data/processed or models/.

3.3 Centralized Configuration

Create a single config.R or 01-wrangle.R to:

  • Define file paths via here::here().

  • Load required packages:

    r
    library(tidyverse)
    library(lubridate)
    library(stringr)
    
  • Set global options:

    r
    options(stringsAsFactors = FALSE)
    theme_set(theme_minimal())
    

Other scripts then start with:

r
source("scripts/config.R")
source("R/utils.R")    # custom helper functions

4. Additional Optimization Tips

Beyond profiling and structure, these practices further enhance performance and maintainability.

4.1 Caching Intermediate Results

Use cache chunks in R Markdown or explicit saveRDS() / readRDS() to avoid recomputing expensive steps:

r
if (!file.exists("data/processed/cleaned.rds")) {
  cleaned <- raw_data %>% ...heavy pipeline...
  saveRDS(cleaned, "data/processed/cleaned.rds")
} else {
  cleaned <- readRDS("data/processed/cleaned.rds")
}

4.2 Automated Testing

Integrate testthat tests to catch regressions when refactoring:

r
# tests/testthat/test-utils.R
test_that("calculate_profit returns numeric", {
  expect_type(calculate_profit(100, 60), "double")
})

Regular testing ensures new optimizations don’t break existing logic.

4.3 Documentation and Vignettes

  • Document functions with roxygen2.

  • Create vignettes (usethis::use_vignette()) to demonstrate workflows from raw data to results.

  • Well-written docs reduce onboarding time for new collaborators.

4.4 Version Control Integration

  • Commit often with clear messages:

    bash
    git add R/utils.R
    git commit -m "Refactor calculate_profit for vectorized inputs"
    
  • Tag releases when hitting stable milestones.

  • Use GitHub Actions to run tests and render reports on push.

5. Putting It All Together: Case Study

Imagine a project processing 10 million retail transactions. Here’s how you apply these best practices:

  1. Project Initialization

    • Create an RStudio project with the standard layout.

    • Initialize renv and Git.

  2. Data Ingestion

    • Script 01_data_import.R reads raw CSV via readr and saves raw_transactions.rds.

    • Profile import with profvis to optimize chunk size.

  3. Data Cleaning & Feature Engineering

    • Script 02_data_cleaning.R uses vectorized dplyr pipelines.

    • Cache intermediate results with saveRDS().

    • Write custom functions in R/clean_helpers.R, documented via roxygen2.

  4. Modeling

    • Script 03_model_training.R loads cleaned data, uses for speed, and future for parallel cross-validation.

    • Profile model fitting to identify slow folds.

  5. Reporting

    • report.Rmd sources config.R, loads results, and generates HTML with interactive plotly charts.

    • Cache heavy plots to speed up knitting.

  6. Review & Deployment

    • Commit changes at each step.

    • Push to GitHub and open pull requests for code review.

    • GitHub Actions run tests and render the report automatically.

Conclusion

Adopting best practices and optimization strategies pays dividends as your R projects scale. You’ve learned to:

  • Follow the Tidyverse style guide for consistent, readable code

  • Profile and tune performance with profvis and vectorization

  • Organize scripts, data, and outputs in a maintainable folder structure

  • Leverage caching, testing, documentation, and version control for robust workflows

Implement these techniques in your next project to reduce bugs, accelerate development, and foster collaboration. Clean, efficient R code isn’t just a nicety—it’s the foundation of reproducible, high-impact data science.

Comments

Popular posts from this blog

Alfred Marshall – The Father of Modern Microeconomics

  Welcome back to the blog! Today we explore the life and legacy of Alfred Marshall (1842–1924) , the British economist who laid the foundations of modern microeconomics . His landmark book, Principles of Economics (1890), introduced core concepts like supply and demand , elasticity , and market equilibrium — ideas that continue to shape how we understand economics today. Who Was Alfred Marshall? Alfred Marshall was a professor at the University of Cambridge and a key figure in the development of neoclassical economics . He believed economics should be rigorous, mathematical, and practical , focusing on real-world issues like prices, wages, and consumer behavior. Marshall also emphasized that economics is ultimately about improving human well-being. Key Contributions 1. Supply and Demand Analysis Marshall was the first to clearly present supply and demand as intersecting curves on a graph. He showed how prices are determined by both what consumers are willing to pay (dem...

Part I: Getting Started with SQL Chapter 2: Setting Up Your Environment

  Chapter 2: Setting Up Your Environment Before you write your first query, it’s essential to install and configure a relational database on your workstation. A solid environment ensures you spend time learning SQL—not battling installation errors. In this chapter, we’ll: Compare four popular RDBMS options Walk through installation on Windows, macOS, and Linux Demonstrate how to connect via command-line and graphical tools Load a sample database so you can start querying immediately By the end, your machine will be a fully functional SQL playground. 1. Choosing an RDBMS: MySQL, PostgreSQL, SQLite, SQL Server Different relational database systems excel in different scenarios. Here’s a quick comparison to help you pick one: Feature MySQL PostgreSQL SQLite SQL Server Use case Web apps, LAMP stacks Analytics, GIS, advanced SQL Lightweight, embedded apps Enterprise .NET, Windows ecosystems Licensing GPL (Community) / Commercial (Enterprise) Open Source (PostgreSQL License) Public domain...

“This Sentence Is False”: The Liar Paradox, from Ancient Crete to Modern Code

 “All Cretans are liars,” said the Cretan Epimenides.  “This sentence is false,” echoes every logic textbook.  We’re still arguing 2,600 years later—and the paradox is winning.   _____________________________  /                             \ |   “THIS SENTENCE IS FALSE.”  |  \_____________________________/               |               |  self-reference               v    +---------------------------+    |  Truth flips back on     |    |  itself — paradox loop!  |    +---------------------------+ 1. Meet the Liar The classic one-liner: L: “This sentence is false.” If L is true, then what it asserts—its own falsity—must hold, so L is false. If L is false, then what it asserts isn’t the ca...