Skip to content

R: A Beginner's Tutorial

R is a programming language and environment for statistics, data analysis, and visualization. On Deceema, an R analysis should run from explicit input files, write results to explicit output files, and execute without depending on an interactive session.

This tutorial assumes no R or programming experience. It uses base R so you can learn the language and workflow before adding packages.

By the end, you will be able to:

  • Run R expressions and scripts with Rscript.
  • Work with vectors, data frames, factors, and missing values.
  • Select, transform, summarize, and visualize data.
  • Use conditions, loops, vectorized operations, and functions.
  • Read and write CSV files with explicit paths.
  • Understand errors, warnings, and debugging basics.
  • Accept command-line arguments and validate input.
  • Record an R environment for reproducibility.
  • Run a tested R analysis through Slurm.

Before you begin

Complete Linux (RHEL) and Bash first if terminals, paths, files, or scripts are unfamiliar. Work only inside the disposable training directory created below.

1. Meet R and Rscript

Prepare the Deceema environment and inspect R:

$ module purge
$ module load deceema
$ R --version
$ Rscript --version

R starts an interactive console. Rscript executes a script without an interactive prompt, making it the usual entry point for repeatable batch work.

Term Plain-language meaning
Expression R code that produces a value.
Object A value stored under a name.
Vector A sequence of values with a common underlying type.
Data frame A rectangular table whose columns can have different types.
Function Reusable code that accepts arguments and returns a value.
Package An installable collection of R functions, data, and documentation.
Working directory The base location used by relative paths.
Session information R, operating-system, and attached package details.

Run one expression

$ Rscript -e 'print(2 + 3)'
[1] 5

[1] labels the position of the first displayed value. It is not part of the value itself.

Try the interactive console

$ R
> values <- c(2, 4, 6, 8)
> mean(values)
[1] 5
> q(save = "no")

The > is R's prompt; do not type it when copying examples. Interactive R is useful for exploration. Move valuable work into a script so it can be repeated.

2. Create a Project Workspace

$ mkdir -p "$HOME/training/r-beginner"/{data,src,logs,plots,results}
$ cd "$HOME/training/r-beginner"
$ pwd

Create src/hello.R:

src/hello.R
message <- "Hello from R on Deceema"
print(message)

Run it:

$ Rscript src/hello.R
[1] "Hello from R on Deceema"

R executes a script from top to bottom. <- assigns the text value to message, and print() displays it.

3. Learn Objects and Basic Types

project <- "deceema-training"  # character
sample_count <- 4L             # integer
mean_value <- 5.0              # double
completed <- TRUE              # logical
missing <- NA                  # missing value

R object names are case-sensitive: sample_count and Sample_Count are different.

Inspect a value:

typeof(mean_value)
class(mean_value)
str(mean_value)
  • typeof() reports how R stores the object.
  • class() describes its object-oriented class.
  • str() gives a compact structural summary.

Convert types

text <- "42"
count <- as.integer(text)
measurement <- as.numeric("3.14")
label <- as.character(count)

An invalid conversion can produce NA and a warning:

$ Rscript -e 'print(as.numeric("not-a-number"))'

Warnings deserve investigation even when the process exits successfully.

4. Work with Vectors

Create a vector with c():

values <- c(2, 4, 6, 8)

length(values)
sum(values)
mean(values)
min(values)
max(values)

Most arithmetic is vectorized:

values * 2
values + 1
values > 4

Select vector elements

R indexes begin at one:

values[1]            # first value
values[length(values)]
values[2:3]
values[c(1, 4)]
values[values >= 4]  # logical selection

Negative indexes exclude positions:

values[-1]

Do not mix positive and negative indexes in the same selection.

Name elements

scores <- c(alpha = 10, beta = 20, gamma = 30)
scores["beta"]

Names often communicate meaning more clearly than numeric positions.

5. Understand Missing Values

NA represents a missing value:

values <- c(2, 4, NA, 8)

is.na(values)
sum(values)
sum(values, na.rm = TRUE)
mean(values, na.rm = TRUE)

Many summary functions return NA when missing values are present unless na.rm = TRUE is supplied.

Never test a missing value with value == NA; that result is itself unknown. Use is.na(value).

Removing missing values is an analytical decision

na.rm = TRUE is not automatically correct. Understand why data is missing and document the treatment used by the analysis.

6. Create and Inspect Data Frames

A data frame stores rectangular data in named columns:

measurements <- data.frame(
  sample = c("alpha", "beta", "gamma", "delta"),
  group = c("control", "control", "treated", "treated"),
  value = c(2, 4, 6, 8)
)

Inspect it:

print(measurements)
str(measurements)
summary(measurements)
names(measurements)
nrow(measurements)
ncol(measurements)
head(measurements)

Select columns and rows

measurements$value
measurements[["value"]]
measurements[1:2, ]
measurements[, c("sample", "value")]
measurements[measurements$value >= 4, ]

In data[row, column], the comma separates row and column selection.

Add a column

measurements$doubled <- measurements$value * 2

Prefer a new object when you need to preserve the original data unchanged.

7. Recognize Factors

A factor represents categorical values with a defined set of levels:

group <- factor(
  c("control", "treated", "control"),
  levels = c("control", "treated")
)

levels(group)
table(group)

Factors are useful when the set or ordering of categories matters. Inspect column structure after importing data rather than assuming every text-like column behaves the same way.

8. Make Decisions with Conditions

value <- 6

if (value > 7) {
  category <- "high"
} else if (value >= 4) {
  category <- "moderate"
} else {
  category <- "low"
}

print(category)

Common logical operators:

Operator Meaning
==, != Equal; not equal.
<, <= Less than; less than or equal.
>, >= Greater than; greater than or equal.
&, | Element-wise AND and OR.
&&, || Short-circuit AND and OR for single conditions.
! Logical NOT.
%in% Membership in a set of values.

An if condition must resolve to one non-missing logical value.

9. Repeat Work with Loops and Vectorization

For loop

values <- c(2, 4, 6, 8)

for (value in values) {
  print(value^2)
}

Sequence loop

for (index in seq_along(values)) {
  message(sprintf("Value %d is %s", index, values[[index]]))
}

seq_along() safely generates indexes for an object, including an empty one.

Vectorized alternative

squares <- values^2

Prefer clear vectorized operations when they express the intent directly. A loop is appropriate when each iteration needs several steps or diagnostics.

Apply a function

datasets <- list(a = c(2, 4), b = c(6, 8))
means <- vapply(datasets, mean, numeric(1))

vapply() declares the expected result type and can catch inconsistent output.

10. Write Reusable Functions

calculate_summary <- function(values) {
  if (!is.numeric(values)) {
    stop("values must be numeric")
  }
  if (length(values) == 0L) {
    stop("values cannot be empty")
  }

  data.frame(
    count = length(values),
    mean = mean(values),
    minimum = min(values),
    maximum = max(values)
  )
}

result <- calculate_summary(c(2, 4, 6, 8))
print(result)
  • function() defines parameters and a body.
  • The final evaluated expression is returned automatically.
  • stop() raises an error with a useful message.

You can also make the return explicit:

return(result)

Keep functions focused: validate input, do one coherent task, and return a predictable type.

11. Use R's Help System

From the interactive console:

?mean
help("read.csv")
args(mean)
example(mean)

Search when you know a concept but not the function name:

help.search("linear model")

Inspect a package's help index with help(package = "packageName").

12. Read and Write CSV Files

Create a small dataset:

$ printf 'sample,group,value\nalpha,control,2\nbeta,control,4\ngamma,treated,6\ndelta,treated,8\n' \
    > data/measurements.csv

Read it:

input_path <- "data/measurements.csv"
measurements <- read.csv(input_path)

str(measurements)
summary(measurements)

Validate expected columns:

required <- c("sample", "group", "value")
missing_columns <- setdiff(required, names(measurements))

if (length(missing_columns) > 0L) {
  stop(sprintf("missing columns: %s", paste(missing_columns, collapse = ", ")))
}

Write a result without row names:

result <- calculate_summary(measurements$value)
dir.create("results", recursive = TRUE, showWarnings = FALSE)
write.csv(result, "results/summary.csv", row.names = FALSE)

Always inspect imported structure and write to an explicit location.

13. Summarize Groups

Base R can summarize data by category with aggregate():

group_summary <- aggregate(
  value ~ group,
  data = measurements,
  FUN = mean
)

names(group_summary)[names(group_summary) == "value"] <- "mean_value"
print(group_summary)

For several statistics, split the values and apply your function:

by_group <- split(measurements$value, measurements$group)
group_results <- lapply(by_group, calculate_summary)

Check that grouping columns contain the expected categories before interpreting the result.

14. Save a Plot to a File

Batch jobs have no interactive graphics window. Open a file device, draw the plot, and close the device:

dir.create("plots", recursive = TRUE, showWarnings = FALSE)

png("plots/measurements.png", width = 1200, height = 800, res = 150)
plot(
  measurements$value,
  type = "b",
  xlab = "Observation",
  ylab = "Value",
  main = "Training measurements"
)
dev.off()

dev.off() completes and closes the image file. Verify that the file exists and is non-empty after the script runs.

15. Understand Errors and Warnings

  • An error stops the current operation.
  • A warning reports a concern but normally allows execution to continue.
  • A message communicates progress or context.
message("Starting analysis")
warning("This is a demonstration warning")
stop("This is a demonstration error")

Catch only errors you can handle meaningfully:

result <- tryCatch(
  read.csv("data/missing.csv"),
  error = function(error) {
    message("Could not read input: ", conditionMessage(error))
    NULL
  }
)

Do not catch every error and report success. In a Slurm job, an unhandled R error should normally make Rscript return a non-zero status.

Investigate an error

In an interactive session, traceback() shows the recent call stack after an error. Other useful techniques include:

  • Print str(object) before the failing operation.
  • Test with a tiny input that reproduces the error.
  • Use stopifnot() for internal assumptions.
  • Use browser() only during interactive debugging, then remove it before a batch run.

16. Work with Packages Responsibly

Inspect installed packages:

$ Rscript -e 'installed.packages()[, c("Package", "Version")] |> head() |> print()'

Load a package explicitly in a script:

library(packageName)

For project packages:

  • Use the approved Deceema package source and installation method.
  • Prefer a project-specific library or reproducible environment.
  • Do not install packages during every compute job.
  • Record exact versions and repositories.
  • Test package loading in a small job before scaling.
  • Never embed repository passwords or tokens in source or logs.

The exact package-management approach depends on Deceema policy and the project's needs; contact Support when unsure.

17. Accept Command-Line Arguments

commandArgs(trailingOnly = TRUE) returns values placed after the script name.

Create src/arguments.R:

src/arguments.R
1
2
3
4
5
6
7
8
args <- commandArgs(trailingOnly = TRUE)

if (length(args) != 1L) {
  stop("Usage: Rscript src/arguments.R INPUT.csv", call. = FALSE)
}

input_path <- args[[1]]
message("Input: ", input_path)

Test both paths:

$ Rscript src/arguments.R
$ Rscript src/arguments.R data/measurements.csv

Use arguments or configuration rather than setwd() and personal hard-coded paths.

18. Build a Complete Data-Summary Program

Create src/summarize.R:

src/summarize.R
parse_args <- function() {
  args <- commandArgs(trailingOnly = TRUE)
  if (length(args) < 2L || length(args) > 3L) {
    stop(
      "Usage: Rscript src/summarize.R INPUT.csv OUTPUT.csv [RUN_NAME]",
      call. = FALSE
    )
  }

  list(
    input = args[[1]],
    output = args[[2]],
    run_name = if (length(args) == 3L) args[[3]] else "training-run"
  )
}

read_values <- function(path) {
  if (!file.exists(path)) {
    stop(sprintf("input file does not exist: %s", path), call. = FALSE)
  }

  data <- read.csv(path, stringsAsFactors = FALSE)
  if (!"value" %in% names(data)) {
    stop("input CSV must contain a 'value' column", call. = FALSE)
  }
  if (nrow(data) == 0L) {
    stop("input CSV contains no data rows", call. = FALSE)
  }
  if (!is.numeric(data$value) || anyNA(data$value)) {
    stop("the 'value' column must contain only numeric, non-missing data", call. = FALSE)
  }

  data$value
}

summarize_values <- function(values) {
  data.frame(
    count = length(values),
    mean = mean(values),
    minimum = min(values),
    maximum = max(values)
  )
}

main <- function() {
  args <- parse_args()
  values <- read_values(args$input)
  summary <- summarize_values(values)
  summary <- cbind(run = args$run_name, summary)

  dir.create(dirname(args$output), recursive = TRUE, showWarnings = FALSE)
  write.csv(summary, args$output, row.names = FALSE)
  message("Wrote ", args$output)
}

tryCatch(
  main(),
  error = function(error) {
    message("error: ", conditionMessage(error))
    quit(status = 1L, save = "no")
  }
)

Run it:

$ Rscript src/summarize.R \
    data/measurements.csv \
    results/summary.csv \
    first-analysis
$ cat results/summary.csv

Expected result:

"run","count","mean","minimum","maximum"
"first-analysis",4,5,2,8

Test a failure path:

$ Rscript src/summarize.R data/missing.csv results/missing.csv
error: input file does not exist: data/missing.csv

The script emits a clear diagnostic and explicitly exits non-zero.

19. Test R Code

Begin with a small standalone check. Create src/test_summary_function.R:

src/test_summary_function.R
summarize_values <- function(values) {
  data.frame(
    count = length(values),
    mean = mean(values),
    minimum = min(values),
    maximum = max(values)
  )
}

result <- summarize_values(c(2, 4, 6, 8))
stopifnot(
  identical(result$count, 4L),
  result$mean == 5,
  result$minimum == 2,
  result$maximum == 8
)
message("All checks passed")

Run it:

$ Rscript src/test_summary_function.R
All checks passed

As the project grows, use an approved R testing framework and keep tests small, deterministic, and independent of production data.

20. Capture the Environment

Write R and attached package information with the results:

dir.create("results", recursive = TRUE, showWarnings = FALSE)
writeLines(capture.output(sessionInfo()), "results/session-info.txt")

Record the R version from the shell:

$ Rscript --version > logs/r-version.txt 2>&1

For production work, preserve:

  • R and package versions.
  • Package repository sources.
  • Script and configuration revisions.
  • Input identifiers or checksums.
  • Random seeds where relevant.
  • Slurm job ID, standard output, and standard error.
  • Expected and observed result checks.

21. Use Randomness Reproducibly

Set a documented seed before a stochastic operation:

set.seed(2026)
sample_values <- rnorm(5)
print(sample_values)

The same R version and random-number behavior should reproduce the sequence. Record why a seed was chosen and whether independent parallel streams are required for a multi-task workload.

22. Run R Through Slurm

Create scripts/r-job.sh:

scripts/r-job.sh
#!/bin/bash
#SBATCH --job-name=r-training
#SBATCH --account=PROJECT_CODE
#SBATCH --qos=QOS_NAME
#SBATCH --time=00:10:00
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=1
#SBATCH --mem=1G
#SBATCH --output=logs/%x-%j.out
#SBATCH --error=logs/%x-%j.err

set -euo pipefail

module purge
module load deceema

Rscript src/summarize.R \
  data/measurements.csv \
  "results/slurm-${SLURM_JOB_ID}.csv" \
  "slurm-${SLURM_JOB_ID}"

Rscript -e \
  'writeLines(capture.output(sessionInfo()), commandArgs(TRUE)[1])' \
  "results/session-${SLURM_JOB_ID}.txt"

Replace PROJECT_CODE and QOS_NAME with the matching values from your approved Deceema access, then submit from the project directory:

$ sbatch scripts/r-job.sh

Use the returned ID:

$ squeue --jobs=JOB_ID
$ sacct --jobs=JOB_ID --format=JobID,State,ExitCode,Elapsed,MaxRSS

Inspect the result, session information, and both logs after completion.

23. Write R Responsibly on Deceema

  • Test with small, representative data before scaling.
  • Use explicit input, output, configuration, and seed values.
  • Do not depend on objects left in an interactive workspace.
  • Avoid setwd() with a personal hard-coded path.
  • Save tables and plots to explicit files.
  • Do not install packages during every compute job.
  • Inspect imported column types before analysis.
  • Treat missing-value handling as an analytical decision.
  • Avoid loading an entire large dataset when chunking is appropriate.
  • Keep secrets out of scripts, arguments, histories, and logs.
  • Run compute-intensive or long-lived analysis through Slurm.

24. Final Project: Summarize Multiple Datasets

Create another dataset:

$ printf 'sample,group,value\nepsilon,treated,10\nzeta,treated,20\n' \
    > data/more.csv

Build src/summarize_many.R that:

  1. Accepts an output CSV followed by one or more input CSV files.
  2. Validates every file and its value column.
  3. Writes one summary row per input.
  4. Includes filename, count, mean, minimum, and maximum.
  5. Exits non-zero with a clear message if any input is invalid.

One possible solution:

src/summarize_many.R
read_values <- function(path) {
  if (!file.exists(path)) {
    stop(sprintf("input file does not exist: %s", path), call. = FALSE)
  }

  data <- read.csv(path, stringsAsFactors = FALSE)
  if (!"value" %in% names(data) || nrow(data) == 0L) {
    stop(sprintf("invalid or empty input: %s", path), call. = FALSE)
  }
  if (!is.numeric(data$value) || anyNA(data$value)) {
    stop(sprintf("non-numeric or missing values in: %s", path), call. = FALSE)
  }
  data$value
}

summarize_file <- function(path) {
  values <- read_values(path)
  data.frame(
    input = basename(path),
    count = length(values),
    mean = mean(values),
    minimum = min(values),
    maximum = max(values)
  )
}

main <- function() {
  args <- commandArgs(trailingOnly = TRUE)
  if (length(args) < 2L) {
    stop(
      "Usage: Rscript src/summarize_many.R OUTPUT.csv INPUT.csv [INPUT.csv ...]",
      call. = FALSE
    )
  }

  output_path <- args[[1]]
  input_paths <- args[-1]
  rows <- lapply(input_paths, summarize_file)
  result <- do.call(rbind, rows)

  dir.create(dirname(output_path), recursive = TRUE, showWarnings = FALSE)
  write.csv(result, output_path, row.names = FALSE)
  message("Wrote ", output_path)
}

tryCatch(
  main(),
  error = function(error) {
    message("error: ", conditionMessage(error))
    quit(status = 1L, save = "no")
  }
)

Run it:

$ Rscript src/summarize_many.R \
    results/all-summaries.csv \
    data/measurements.csv data/more.csv
$ cat results/all-summaries.csv

Expected data:

"input","count","mean","minimum","maximum"
"measurements.csv",4,5,2,8
"more.csv",2,15,10,20

You can now build a reproducible R workflow

You have learned R's core objects and control flow, transformed and summarized data, saved a plot, handled failures, recorded the environment, and prepared a non-interactive analysis for Slurm.

R Cheat Sheet

Goal R
Assign an object name <- value
Create a vector c(a, b, c)
Inspect structure str(object)
Select values object[index]
Select a column data[["column"]]
Conditional if (condition) { ... }
Loop for (item in items) { ... }
Define a function function(argument) { ... }
Read CSV read.csv(path)
Write CSV write.csv(data, path, row.names = FALSE)
Raise an error stop("message")
Print a diagnostic message("text")
Get script arguments commandArgs(trailingOnly = TRUE)
Record environment sessionInfo()

Common Beginner Mistakes

Mistake Better habit
Confusing = or <- with == Use assignment for names and == for comparison.
Forgetting R indexes begin at one Verify selections on a small object.
Comparing a value with NA Use is.na().
Depending on an interactive workspace Start scripts from explicit files and objects.
Using setwd() with a personal path Pass paths as arguments or use a documented submission directory.
Ignoring warnings because the script completed Review and resolve unexpected warnings.
Installing packages inside every job Prepare and record the project environment beforehand.
Forgetting to close a graphics device Call dev.off() after writing the plot.
Omitting row.names = FALSE for data output Decide explicitly whether row names belong in the file.
Saving code without session details Capture sessionInfo() with production results.

Completion Checklist

  • I can run R interactively and execute a script with Rscript.
  • I understand basic types, vectors, missing values, and data frames.
  • I can select, transform, group, and summarize data.
  • I can use conditions, loops, vectorized operations, and functions.
  • I read and write CSV files using explicit paths.
  • I save plots to files for non-interactive jobs.
  • I understand errors, warnings, and focused error handling.
  • My scripts validate command-line arguments and input structure.
  • I record sessionInfo() and random seeds where relevant.
  • I completed and verified the multi-dataset final project.

Continue Learning

Learn Slurm fundamentals Explore the full jobs guide

Official R References