# R Development Guidelines

> Objective: Define standards for reproducible, clean, and collaborative R code in data science, statistical analysis, and package development contexts, covering project structure, code style, functional patterns, reporting, and testing.

## 1. Project Structure & Reproducibility

### Package & Dependency Management

- Use **`{renv}`** for dependency management in all projects. Create and commit `renv.lock` for fully reproducible environments:

  ```r
  renv::init()           # initialize renv in the project
  renv::snapshot()       # snapshot current package versions to renv.lock
  renv::restore()        # restore packages from renv.lock (used in CI)
  ```

  CI setup: `Rscript -e "renv::restore()"` before running any analysis.

- Pin the R version alongside `{renv}`. Use `renv::lockfile_read()` to inspect and document the R version used.

### Pipeline Management

- Use the **`{targets}`** pipeline framework for complex, multi-step data analyses. It caches intermediate results as `.rds` files and automatically re-runs only what changed — essential for long-running computations:

  ```r
  # _targets.R
  library(targets)

  list(
    tar_target(raw_data,  read_csv(here::here("data/raw/survey.csv"))),
    tar_target(clean_data, clean_survey(raw_data)),
    tar_target(model,      fit_model(clean_data)),
    tar_target(report,     render_report(model), format = "file"),
  )
  ```

  Run with `targets::tar_make()` — only invalidated targets re-run.

### Directory Structure

```text

R/                    # Package functions or sourced analysis functions
data/
├── raw/              # Original, immutable raw data (never modify!)
└── processed/        # Transformed data (generated by scripts)
output/               # Figures, tables, reports (generated output)
reports/              # R Markdown / Quarto documents
tests/testthat/       # Unit tests
_targets/             # targets pipeline cache (add to .gitignore)
renv/                 # renv library (add to .gitignore except renv/activate.R)
renv.lock             # ✅ Commit this
.Rprofile             # ✅ Commit this (renv::autoload() hook)

```

- **Never modify raw data files directly.** All transformations must be scripted and reproducible from the raw source. Treat `data/raw/` as read-only.
- Use `.gitignore` to exclude `_targets/`, `renv/` (except `renv/activate.R`), `.RData`, `.Rhistory`.

## 2. Code Style & Conventions

- Follow the **tidyverse style guide**. Format code automatically with **`{styler}`**:

  ```r
  styler::style_file("R/analysis.R")       # format single file
  styler::style_dir("R/")                  # format all R files in directory
  ```

- **Naming conventions**:
  - `snake_case`: variables, functions, file names
  - `PascalCase`: R6 class names
  - Descriptive names that reveal intent: `user_count` not `n`, `filter_active_users` not `filter_fun`
- Use the native **pipe operator** (`|>`, R ≥ 4.1) for readable transformation chains:

  ```r
  # ✅ Idiomatic tidyverse pipeline with native pipe
  result <- raw_data |>
    dplyr::filter(!is.na(email)) |>
    dplyr::mutate(name = stringr::str_squish(name)) |>
    dplyr::group_by(region) |>
    dplyr::summarise(count = dplyr::n(), .groups = "drop") |>
    dplyr::arrange(dplyr::desc(count))
  ```

  Use `%>%` (magrittr) only in pre-R-4.1 projects or when using the `.` placeholder is necessary.

- Run **`{lintr}`** in CI to catch syntax issues or stylistic deviations:

  ```r
  lintr::lint_dir("R/")  # lint all R files
  ```

  Commit `.lintr` to configure project-specific rules.

- Prefer **explicit namespace calls** to prevent masking and improve code clarity in scripts and packages:

  ```r
  # ✅ Explicit — clear which package provides filter()
  dplyr::filter(df, active == TRUE)

  # ❌ Implicit — ambiguous if multiple packages export filter()
  filter(df, active == TRUE)
  ```

## 3. Functions & Validation

- Write **small, single-purpose functions**. Functions longer than ~30 lines are candidates for decomposition. One function = one task.
- Document all exported package functions with **`{roxygen2}`**:

  ```r
  #' Summarize user activity by region
  #'
  #' @param data A data frame with columns `user_id`, `region`, and `event_date`.
  #' @param min_events Minimum number of events to include a user. Default 1.
  #' @return A summarized data frame with columns `region`, `user_count`, `event_count`.
  #' @examples
  #' summarize_activity(activity_data, min_events = 5)
  #' @export
  summarize_activity <- function(data, min_events = 1L) {
    checkmate::assert_data_frame(data)
    checkmate::assert_integerish(min_events, lower = 0)
    ...
  }
  ```

- Use **`{checkmate}`** for expressive input validation at the start of functions. Fail fast with informative messages:

  ```r
  checkmate::assert_data_frame(data, min.rows = 1)
  checkmate::assert_character(email, min.len = 1, pattern = "@")
  checkmate::assert_numeric(threshold, lower = 0, upper = 1)
  ```

- **Avoid `<<-`** (global assignment from within a function). It creates hidden state and makes code hard to test and debug. Use return values, environments, or R6 class fields for shared mutable state.
- Use **`{purrr}`** for functional programming over lists and vectors instead of `for` loops:

  ```r
  # ✅ Functional — returns a list of results
  results <- purrr::map(file_paths, read_and_clean)
  errors  <- purrr::keep(results, purrr::is_error)

  # Walk for side effects (logging, saving)
  purrr::walk(plots, function(p) ggplot2::ggsave(filename(p), plot = p))
  ```

## 4. Reproducibility & Reporting

- Set a **random seed** (`set.seed(42)`) at the beginning of any script or function involving randomness (sampling, simulation, ML model training). Document the seed in a comment:

  ```r
  set.seed(2024)  # Seed for reproducibility — do not change without updating all outputs
  ```

- Use **`{here}`** for all file path construction — it resolves from the project root regardless of working directory:

  ```r
  # ✅ Project-root-relative, works on any machine
  data <- readr::read_csv(here::here("data", "raw", "survey.csv"))

  # ❌ Fragile absolute path — breaks on any other machine
  data <- readr::read_csv("data/raw/survey.csv")

  # ❌ Relative path — depends on working directory
  data <- readr::read_csv("../data/raw/survey.csv")
  ```

  Never use `setwd()` — it is fragile and non-reproducible.

- Use **R Markdown** or **Quarto** for all reports combining code, output, and narrative. Render in CI to verify they execute cleanly:

  ```bash
  Rscript -e "rmarkdown::render('reports/analysis.Rmd', output_dir = 'output/')"
  quarto render reports/analysis.qmd --output-dir output/
  ```

- Configure default chunk options globally in R Markdown for consistent output:

  ```r
  knitr::opts_chunk$set(echo = FALSE, message = FALSE, warning = FALSE, fig.width = 8, fig.height = 5)
  ```

## 5. Data Handling & Testing

### Data Handling

- Use **`{dplyr}`**, **`{tidyr}`**, and **`{purrr}`** for data manipulation. Prefer tidy, rectangular data (one observation per row, one variable per column) over nested lists for analysis.
- Use **`readr::read_csv()`** over base R `read.csv()` for consistent type inference, faster parsing, and UTF-8 encoding handling:

  ```r
  # Explicit column types for safety — prevents type guessing
  data <- readr::read_csv(
    here::here("data/raw/users.csv"),
    col_types = readr::cols(
      user_id    = readr::col_character(),
      created_at = readr::col_datetime(),
      score      = readr::col_double()
    )
  )
  readr::problems(data)  # inspect parse warnings
  ```

- Handle missing values explicitly with `dplyr::coalesce()`, `tidyr::replace_na()`, and `dplyr::na_if()`. Document every source of `NA` values and the strategy for handling them.

### Testing

- Write unit tests for all non-trivial functions with **`{testthat}`**:

  ```r
  # tests/testthat/test-summarize-activity.R
  test_that("summarize_activity returns correct counts", {
    data <- tibble::tibble(
      user_id = c("1", "1", "2"),
      region  = c("US", "US", "EU"),
      event_date = as.Date(c("2024-01-01", "2024-01-02", "2024-01-01"))
    )
    result <- summarize_activity(data)

    expect_equal(nrow(result), 2L)
    expect_equal(result$user_count[result$region == "US"], 1L)
  })

  test_that("summarize_activity errors on non-data-frame input", {
    expect_error(summarize_activity(list()), class = "checkmate_error")
  })
  ```

- Test edge cases: empty data frames, `NA` values, single-row inputs, unexpected column types, out-of-range parameters.
- Use **`{vdiffr}`** for snapshot testing of ggplot2 plots — it compares SVG outputs and detects visual regressions:

  ```r
  test_that("activity_plot renders correctly", {
    p <- plot_activity(sample_data)
    vdiffr::expect_doppelganger("activity-by-region", p)
  })
  ```

- Use **`{bench}`** for performance benchmarking. Track timings with `bench::mark()`:

  ```r
  bench::mark(
    base  = base_implementation(data),
    dplyr = dplyr_implementation(data),
    check = FALSE
  )
  ```

- Run tests in CI with: `Rscript -e "devtools::test()"` or `Rscript -e "testthat::test_dir('tests/testthat/')"`.
