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.
set.seed(), cementing reproducibility workflows for the growing R user community.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.
Deterministic Sequence from Seed
The .Random.seed State Vector
.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.Generator Algorithm Dependence
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.Consumption Order Matters
runif(1) before your main computation, all subsequent values shift. Reproducibility demands identical call sequences after setting the seed.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.
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.
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.
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.
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.
| Generator Kind | State Size | Period | Notes |
|---|---|---|---|
"Mersenne-Twister" | 625 integers | 2¹⁹⁹³⁷ − 1 | Default. 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. |
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.
x <- c(23, 17, 42, 35, 28, 19, 31, 44, 22, 36, 29, 40, 15, 33, 27). This vector has 15 observations.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)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.quantile(boot_medians, probs = c(0.025, 0.975)). Because the resampling was seeded, this interval will be identical on every run.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.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.
| Practice | Good ✓ | Bad ✗ |
|---|---|---|
| Seed placement | Place set.seed() immediately before the stochastic operation | Place it at the top of the script with unrelated random draws in between |
| Documenting the seed | Record seed, R version, and RNG kind in methods section | Rely on oral communication or assume the reader will infer the configuration |
| Inside loops | Call set.seed() once before the loop | Call set.seed() on every iteration (all iterations become identical) |
| Parallel computing | Use "L'Ecuyer-CMRG" with stream splitting | Use the same seed on every worker (correlated streams) |
| Saving/restoring state | Save .Random.seed to an RDS file for exact checkpoint-restart | Only save the seed integer, losing the mid-sequence state |
| Cross-version safety | Lock R version in a Docker container or renv lockfile | Assume the same seed will reproduce results across major R upgrades |
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.
| Aspect | Basic set.seed() | Advanced Approaches |
|---|---|---|
| Execution model | Single-threaded, sequential | Multi-core via mclapply(), future, foreach |
| RNG kind | "Mersenne-Twister" | "L'Ecuyer-CMRG" with nextRNGStream() |
| Independence guarantee | Trivially satisfied (one stream) | Requires provably non-overlapping sub-streams |
| State management | Global .Random.seed | Per-worker state via clusterSetRNGStream() or future.seed = TRUE |
| Environment control | Record R version manually | Docker + 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.
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
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?
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.
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.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.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.