R PROGRAMMING • R-SPECIFIC TOPICS (STATISTICAL COMPUTING)

set.seed() for Reproducibility — Set seeds for reproducibility (set.seed)

Controlling pseudorandom number generation to guarantee reproducible statistical results across sessions and machines.

Historical Context & Motivation

Modern statistical computing relies heavily on pseudorandom number generators (PRNGs), deterministic algorithms that produce sequences of numbers which approximate the properties of truly random sequences. The foundational tension in computational statistics has always been the need for randomness—to perform simulations, bootstrap analyses, and Monte Carlo integration—alongside the equally critical requirement that scientific results be reproducible. If a colleague cannot re-run your simulation and obtain the same results, the analysis loses much of its credibility and debugging becomes nearly impossible.

The concept of seeding a PRNG is as old as PRNGs themselves. When John von Neumann proposed the middle-square method in 1946, the initial number fed into the algorithm was already understood as the mechanism that determined the entire subsequent sequence. Over the decades, as generators grew more sophisticated—from linear congruential generators to the Mersenne Twister—the seed remained the single point of control for reproducibility. R's set.seed() function is the standard interface for setting this initial state, and mastering it is essential for any researcher or engineer who works with stochastic methods.

1946
Von Neumann's Middle-Square Method
John von Neumann introduces one of the first PRNGs, where a starting seed value determines the entire output sequence through repeated squaring and digit extraction.
1958
Linear Congruential Generators (LCGs)
W. E. Thomson and others formalize the LCG family (Xₙ₊₁ = (aXₙ + c) mod m), making the dependence on the seed mathematically explicit and analyzing period lengths.
1997
Mersenne Twister Published
Matsumoto and Nishimura publish the Mersenne Twister (MT19937) with a period of 2¹⁹⁹³⁷ − 1, which becomes the default PRNG in R and many other languages.
2000
R Adopts Mersenne Twister as Default
R incorporates MT19937 as its default random number generator, accessible via set.seed(), cementing reproducibility workflows for the growing R user community.
2019
R 3.6.0 Changes Default Sample Algorithm
R 3.6.0 introduces a new default for sample.kind, breaking backward compatibility with older seeds and highlighting how seed semantics depend on the generator version.

The central question this lesson addresses is straightforward yet frequently misunderstood: how does a single integer, passed to set.seed(), deterministically control all subsequent random output in R, and what are the best practices, pitfalls, and edge cases that computer scientists must understand to guarantee genuine reproducibility across environments, R versions, and collaborative workflows?

Core Principles & Definitions

Before diving into the mechanics of set.seed(), it is important to formalize the core ideas that underpin reproducible random number generation in R. These principles govern not just the function itself but the broader architecture of R's RNG subsystem.

1

Deterministic Sequence from Seed

A seed is an integer that initializes the PRNG's internal state vector. Given the same seed and the same generator algorithm, R will produce an identical sequence of pseudorandom numbers every time.
2

The .Random.seed State Vector

R stores its full RNG state in the hidden global variable .Random.seed. For the default Mersenne Twister, this is an integer vector of length 626 (1 header + 625 state words). Calling set.seed() overwrites this vector entirely.
3

Generator Algorithm Dependence

Reproducibility requires the same RNG kind. R supports multiple generators (Mersenne Twister, Wichmann-Hill, etc.). The same seed with different generators yields completely different sequences.
4

Scope: Global Side Effect

set.seed() modifies a global variable in the base environment. It is a side-effecting operation, meaning any function that draws random numbers anywhere in your session will consume from the same sequence.
5

Consumption Order Matters

The PRNG state advances with every random draw. If you insert an additional runif(1) before your main computation, all subsequent values shift. Reproducibility demands identical call sequences after setting the seed.
KEY TAKEAWAY
Think of set.seed() like entering coordinates into a GPS before a road trip: once you set the starting point and agree on the map (the algorithm), the exact route (sequence of random numbers) is completely determined. If two drivers enter the same coordinates on the same map, they follow the same path—even if neither knows the road ahead.

Visual Explanation: How set.seed() Controls Output

The following diagram illustrates the core mechanism. An integer seed is expanded into a full internal state vector by the initialization routine. The PRNG algorithm then reads and transforms this state vector each time a random number is requested, producing a deterministic sequence. Two sessions with the same seed and algorithm produce identical output, while different seeds diverge immediately.

Sessions A and B use the same seed (42) and produce identical output sequences across both runif() and rnorm() calls. Session C uses seed 7 and diverges completely, demonstrating that different seeds produce entirely different sequences.

Notice that the correspondence between Session A and Session B extends beyond a single function call: the rnorm(3) values that follow the runif(5) call are also identical. This occurs because both sessions consume from the same deterministic sequence in the same order. The PRNG state is stateful and sequential—each draw advances the internal pointer by one or more positions, and the next draw reads from wherever the pointer landed. This is why inserting or removing any random draw between set.seed() and your target computation will break reproducibility for all subsequent output.

How It Works: The Mersenne Twister and Seed Initialization

R's default PRNG is the Mersenne Twister (MT19937), a twisted generalized feedback shift register (TGFSR) algorithm. Understanding even a simplified version of its initialization and generation steps clarifies why set.seed() works the way it does and why the same seed always yields the same output.

Seed Initialization

When you call set.seed(s), R uses the seed integer s to fill a 624-element state array through a recurrence relation. The initialization is deterministic: given the same s, the exact same 624 integers are generated to populate the state.

MT19937 STATE INITIALIZATION
x[i] = f × (x[i−1] ⊕ (x[i−1] >> 30)) + i
Where x[0] = s (the seed), f = 1812433253, ⊕ is bitwise XOR, >> is right-shift by 30 bits, and i ranges from 1 to 623. All arithmetic is performed modulo 2³².

Generation Step (Twist and Temper)

Once initialized, the generator produces outputs through a two-phase process. The twist step recombines elements of the state array to produce new state values, and the temper step applies a series of bitwise operations to transform each state word into a high-quality pseudorandom output. The tempering operations are reversible and serve to improve the equidistribution properties of the output.

TWIST RECURRENCE
x[k] = x[k + m] ⊕ (upper(x[k]) | lower(x[k+1])) × A
Where m = 397, upper() extracts the most significant bit, lower() extracts the lower 31 bits, | is concatenation, and A is a constant matrix applied via conditional XOR.

The set.seed() Function Signature

In R, the full signature is set.seed(seed, kind = NULL, normal.kind = NULL, sample.kind = NULL). The kind parameter selects the uniform PRNG (default: "Mersenne-Twister"), normal.kind selects the method for generating normal deviates (default: "Inversion"), and sample.kind controls the discrete sampling algorithm (default: "Rejection" since R 3.6.0). Changing any of these parameters while keeping the same integer seed will produce a different sequence.

⚠️ R Version Compatibility Warning
R 3.6.0 changed the default sample.kind from "Rounding" to "Rejection". This means that code using sample() with a set seed produces different results in R ≥ 3.6 compared to R < 3.6, even with the same seed integer. To reproduce old results, explicitly set RNGkind(sample.kind = "Rounding") before set.seed().

R's RNG Kinds and Seed Interactions

R provides several built-in PRNG algorithms selectable via the kind argument of set.seed() or RNGkind(). Understanding these alternatives is important for both cross-platform reproducibility and for specialized applications where generator properties matter—such as parallel computing or cryptographic contexts.

R's built-in PRNG algorithms and their key properties
Generator KindState SizePeriodNotes
"Mersenne-Twister"625 integers2¹⁹⁹³⁷ − 1Default. Excellent equidistribution. Not suitable for cryptography.
"Wichmann-Hill"3 integers≈ 6.95 × 10¹²Combines three LCGs. Shorter period, but lightweight.
"Marsaglia-Multicarry"2 integers> 2⁶⁰Fast multiply-with-carry generator. Small state footprint.
"Super-Duper"2 integers≈ 4.6 × 10¹⁸Legacy generator. Combines a Tausworthe and congruential generator.
"Knuth-TAOCP-2002"101 integers≈ 2¹²⁹Lagged Fibonacci generator from Knuth's TAOCP.
"L'Ecuyer-CMRG"6 integers≈ 2¹⁹¹Combined multiple recursive. Designed for parallel streams via the parallel package.
The same seed value (42) fed to two different generator kinds produces completely different internal states and output sequences. Full reproducibility requires agreement on both the seed and the generator algorithm.
💡 Parallel Computing Tip
When running parallel simulations, use kind = "L'Ecuyer-CMRG" combined with mc.reset.stream() or the parallel::nextRNGStream() function. This ensures each worker gets a non-overlapping sub-stream of random numbers, all derived deterministically from a single master seed.

Worked Example: Reproducible Bootstrap Confidence Interval

Suppose you need to compute a 95% bootstrap confidence interval for the median of a dataset and you want another researcher to reproduce your exact interval. The following walkthrough demonstrates the correct use of set.seed() in this context.

Reproducible Bootstrap CI for the Median
1
Step 1 — Define the DataCreate a sample dataset. In practice this would be read from a file, but for illustration: x <- c(23, 17, 42, 35, 28, 19, 31, 44, 22, 36, 29, 40, 15, 33, 27). This vector has 15 observations.
n = 15, observed median = 29
2
Step 2 — Set the Seed Before ResamplingCall set.seed(2024) immediately before the resampling loop. This locks the RNG state so that the exact same bootstrap samples are drawn every time the script runs. It is critical that no random draws occur between set.seed() and the loop.
set.seed(2024)
3
Step 3 — Run the Bootstrap LoopDraw B = 10,000 bootstrap samples and compute the median of each: B <- 10000; boot_medians <- replicate(B, median(sample(x, replace = TRUE))). The sample() function consumes random numbers from the PRNG on each iteration, advancing the state deterministically.
boot_medians is a numeric vector of length 10,000
4
Step 4 — Extract the Confidence IntervalUse the percentile method to obtain the 2.5th and 97.5th percentiles: quantile(boot_medians, probs = c(0.025, 0.975)). Because the resampling was seeded, this interval will be identical on every run.
95% CI: [23, 35]
5
Step 5 — Document the Seed in Your ReportReport the seed value alongside your results. A complete reproducibility statement might read: "Bootstrap confidence intervals were computed using B = 10,000 resamples with set.seed(2024) under R version 4.3.2, kind = "Mersenne-Twister", sample.kind = "Rejection"." Including the R version and RNG kind is essential for cross-version reproducibility.
Full reproducibility metadata recorded
⚠️ Common Mistake
Do not place set.seed() inside the bootstrap loop. Calling set.seed(2024) on every iteration resets the state each time, meaning every bootstrap sample is identical—you would get the same resample 10,000 times, yielding a degenerate confidence interval equal to a single point.

Best Practices, Pitfalls, and Comparisons

Using set.seed() correctly involves more than just inserting the call at the top of your script. This section compares good and bad practices and highlights common pitfalls that can silently break reproducibility even when a seed appears to be set.

Best practices for reproducible seeding in R
PracticeGood ✓Bad ✗
Seed placementPlace set.seed() immediately before the stochastic operationPlace it at the top of the script with unrelated random draws in between
Documenting the seedRecord seed, R version, and RNG kind in methods sectionRely on oral communication or assume the reader will infer the configuration
Inside loopsCall set.seed() once before the loopCall set.seed() on every iteration (all iterations become identical)
Parallel computingUse "L'Ecuyer-CMRG" with stream splittingUse the same seed on every worker (correlated streams)
Saving/restoring stateSave .Random.seed to an RDS file for exact checkpoint-restartOnly save the seed integer, losing the mid-sequence state
Cross-version safetyLock R version in a Docker container or renv lockfileAssume the same seed will reproduce results across major R upgrades
KEY TAKEAWAY
A seed is like a version-control commit hash for randomness: it pins the exact state of the random number generator at a specific point. But just as a commit hash is meaningless without the repository (the codebase, the compiler version), a seed is meaningless without the RNG kind and the R version. Always record all three for true reproducibility.

Connection to Advanced Reproducibility and Parallel Streams

The basic set.seed() workflow is sufficient for sequential, single-threaded R scripts. However, modern statistical computing increasingly involves parallel and distributed computation, containerized environments, and multi-language pipelines. These settings introduce challenges that go beyond simple seeding.

Basic vs. advanced reproducibility approaches
AspectBasic set.seed()Advanced Approaches
Execution modelSingle-threaded, sequentialMulti-core via mclapply(), future, foreach
RNG kind"Mersenne-Twister""L'Ecuyer-CMRG" with nextRNGStream()
Independence guaranteeTrivially satisfied (one stream)Requires provably non-overlapping sub-streams
State managementGlobal .Random.seedPer-worker state via clusterSetRNGStream() or future.seed = TRUE
Environment controlRecord R version manuallyDocker + renv for full computational environment lockdown

The L'Ecuyer-CMRG generator is particularly important for parallel work. Its mathematical structure allows the single long-period sequence to be split into provably independent sub-streams by jumping ahead by a fixed number of steps. The parallel package in R uses this approach: after setting RNGkind("L'Ecuyer-CMRG") and set.seed(42), each call to nextRNGStream() jumps 2¹²⁷ steps ahead, guaranteeing that parallel workers do not share overlapping segments of the sequence. Newer packages like future abstract this further, accepting a future.seed argument that handles stream splitting transparently.

🔮 Looking Ahead
As R's ecosystem evolves, tools like targets (a pipeline toolkit) and renv (for dependency management) are increasingly paired with seed management to create fully reproducible analytical pipelines. Understanding set.seed() is the foundational building block; orchestrating it within these frameworks is the next level of reproducibility engineering.

Practice Problems

PROBLEM 1CONCEPTUAL
Explain why calling set.seed(123) followed by runif(5) always produces the same five values, even though runif() is described as generating 'random' numbers. What property of PRNGs makes this possible, and what would change if we used a true hardware random number generator instead?
PROBLEM 2BASIC CALCULATION
Consider the following R code: set.seed(7) a <- rnorm(3) set.seed(7) b <- rnorm(3) c <- rnorm(3) Will a and b be identical? Will b and c be identical? Explain why.
PROBLEM 3INTERMEDIATE
A researcher writes the following code to perform a permutation test with 1,000 iterations: results <- numeric(1000) for (i in 1:1000) { set.seed(42) perm <- sample(data) results[i] <- test_statistic(perm) } Identify the bug, explain its consequences, and rewrite the code correctly.
PROBLEM 4APPLIED
You are building a Monte Carlo simulation that runs on 8 cores using parallel::mclapply(). Your collaborator reports different results despite using the same seed and R version. Diagnose the likely cause and write a corrected code skeleton that guarantees reproducible parallel results.
PROBLEM 5CRITICAL THINKING
A colleague argues that using set.seed() compromises the statistical validity of simulations because it removes 'true' randomness, and that results should only be trusted if they are robust to seed choice. Evaluate this claim. Under what circumstances is seed dependence a legitimate concern, and how would you design a study to verify that your Monte Carlo results are not artifacts of a particular seed?

Summary

The set.seed() function in R initializes the pseudorandom number generator's internal state from an integer, ensuring that every subsequent random draw follows a deterministic, reproducible sequence. The default generator is the Mersenne Twister (MT19937), which expands a single seed into a 625-integer .Random.seed state vector. Full reproducibility requires agreement on three things: the seed value, the RNG kind (including normal.kind and sample.kind), and the R version.

Best practice dictates placing set.seed() immediately before the stochastic operation, never inside a loop (which would produce identical iterations). For parallel computing, switch to "L'Ecuyer-CMRG" and use stream-splitting functions to ensure non-overlapping, reproducible random streams across workers. Always document your seed, RNG configuration, and R version alongside published results to enable independent verification.

Varsity Tutors • R Programming • set.seed() for Reproducibility