Historical Context & Motivation
The ability to generate random samples computationally is one of the most consequential developments in modern statistics and computer science. Before electronic computers, researchers who needed random numbers relied on physical devices—dice, shuffled cards, or tables of digits laboriously compiled by hand. The demand for large-scale random sampling grew dramatically during World War II, when scientists at Los Alamos needed to simulate neutron transport through fissile material, a problem far too complex for closed-form analysis.
That wartime necessity led directly to the Monte Carlo method, pioneered by Stanislaw Ulam and John von Neumann, which depends entirely on the rapid generation of pseudorandom numbers. As statistical computing matured, languages like S (and later R) embedded random number generation as a first-class primitive, giving analysts direct access to functions such as rnorm(), runif(), and sample(). Understanding these functions is prerequisite to simulation studies, bootstrapping, permutation tests, and stochastic optimization—tools that pervade both applied statistics and algorithm design.
The central question this lesson addresses is both practical and conceptual: how does R translate a deterministic algorithm into draws that behave as if they came from a specified probability distribution, and how should you wield rnorm(), runif(), and sample() to produce correct, reproducible simulations?
Core Principles & Definitions
Before invoking any sampling function in R, you need to grasp several foundational concepts that govern how pseudorandom numbers are produced and why they are trustworthy enough for rigorous statistical inference. R's random number infrastructure rests on a pseudorandom number generator (PRNG) that deterministically advances an internal state to produce a stream of values indistinguishable, by standard statistical tests, from true randomness. The following principles form the conceptual scaffold for everything that follows.
Pseudorandom Number Generator (PRNG)
Seed & Reproducibility
set.seed(n) initializes the PRNG state so that subsequent draws are identical across runs and machines, enabling reproducible research and debugging.The d/p/q/r Convention
Continuous vs. Discrete Sampling
Inverse Transform & Box-Muller
set.seed() selects which deck you pick up, and each call to rnorm() or runif() draws the next card. The sequence looks random, but it is completely determined by the seed—exactly the property you need for reproducible simulation experiments.Visual Explanation — How the Three Functions Map to Distributions
The diagram below illustrates the relationship between R's PRNG engine and the three primary sampling functions. The Mersenne Twister produces a stream of uniform variates on (0, 1). From there, runif() rescales to an arbitrary interval [a, b], rnorm() applies a transformation (typically Box-Muller or Kinderman-Ramage) to produce Normal variates, and sample() uses the uniform stream to index discrete elements from a user-supplied vector.
set.seed() initializing the Mersenne Twister, which produces a stream of U(0,1) variates. Each function branches from this stream: runif() rescales to [a, b], rnorm() applies a Normal transformation, and sample() selects discrete elements.Notice how the entire pipeline is deterministic once the seed is fixed. If you call set.seed(42) followed by rnorm(5) on any machine running the same version of R with the default PRNG, you will obtain the same five values. This reproducibility is critical for peer review, unit testing of stochastic algorithms, and debugging simulation code. The diagram also clarifies that sample() is fundamentally different from the other two: it operates on a finite, user-supplied vector rather than a continuous probability distribution.
Mathematical Framework — Transformations Under the Hood
All of R's random variate generators ultimately rely on draws from a Uniform(0, 1) distribution produced by the PRNG. Understanding the mathematical transformations that convert these raw uniform values into other distributions gives you deeper control over your simulations and helps you reason about edge cases.
runif — Linear Rescaling
runif(n, min = a, max = b) applies this affine transformation to n independent U(0,1) draws.rnorm — Box-Muller Transform
rnorm(n, mean = μ, sd = σ).sample — Discrete Selection Mechanism
prob is supplied, R normalizes the weights so they sum to 1 and uses them to bias selection. Without prob, all elements are equally likely. The replace argument controls whether sampling is with or without replacement.r* family. While R may use more efficient algorithms (e.g., Ahrens-Dieter for Gamma), the inverse transform principle underpins them all.Detailed Breakdown — Function Signatures and Parameters
Each of the three primary sampling functions in R has a distinct signature with different defaults and semantics. The table below provides a comprehensive reference, followed by a diagram that visualizes the output characteristics of each function side by side.
| Function | Signature | Distribution / Domain | Key Parameters |
|---|---|---|---|
runif() | runif(n, min=0, max=1) | Continuous Uniform on [min, max] | n: count; min, max: interval bounds |
rnorm() | rnorm(n, mean=0, sd=1) | Normal (Gaussian) on (−∞, ∞) | n: count; mean: center μ; sd: standard deviation σ |
sample() | sample(x, size, replace=FALSE, prob=NULL) | Discrete elements from vector x | x: source vector; size: draw count; replace: with replacement?; prob: weight vector |
runif(), bell-shaped for rnorm(), and discrete bars for sample(). Bottom row: common use-case guidance for each function.A few subtleties worth noting for computer science students: when sample() is called with a single integer argument like sample(10), R interprets this as sample(1:10, 10)—a random permutation of 1 through 10. This is a common source of bugs: if x is a length-1 vector containing a large integer, you will not get a single-element sample but rather a permutation of 1:x. Guard against this edge case by always specifying size explicitly and, when appropriate, wrapping your vector in sample.int() or indexing defensively.
Worked Example — Monte Carlo Estimation of π
A classic demonstration that integrates all three functions is the Monte Carlo estimation of π. We generate random points inside a unit square, count how many fall inside the inscribed quarter-circle, and use the ratio to approximate π. This example combines runif() for coordinate generation, rnorm() for adding Gaussian noise in a follow-up, and sample() for subsampling results.
set.seed(2024)n <- 100000
x <- runif(n, min = 0, max = 1)
y <- runif(n, min = 0, max = 1)inside <- (x^2 + y^2) <= 1pi_estimate <- 4 * sum(inside) / n
cat("Estimated pi:", pi_estimate)idx_inside <- which(inside)
chosen <- sample(idx_inside, size = 500, replace = FALSE)
x_noisy <- x[chosen] + rnorm(500, mean = 0, sd = 0.01)x_noisy now contains 500 perturbed x-coordinates drawn from the interior of the quarter-circle—a realistic model of noisy sensor data.Strengths, Limitations & Pitfalls
While R's random sampling functions are powerful and convenient, each has specific strengths and limitations that become critical in production-grade simulation pipelines. The table below contrasts the three functions along dimensions relevant to real-world statistical computing and algorithm design.
| Dimension | runif() | rnorm() | sample() |
|---|---|---|---|
| Domain | Continuous [min, max] | Continuous (−∞, ∞) | Discrete (finite vector) |
| Speed (n = 10⁷) | Very fast (~0.3 s) | Fast (~0.5 s) | Moderate (~1.0 s with replace) |
| Common pitfall | Forgetting that endpoints may or may not be included (implementation detail) | Confusing sd (σ) with variance (σ²); passing variance instead of sd | Single-integer trap: sample(10) ≠ sample(c(10), 1) |
| Extensibility | Basis for inverse transform; use qnorm(runif(n)) for custom transforms | Generates multivariate normal via MASS::mvrnorm(); key to Bayesian MCMC | Powers bootstrap, cross-validation splits, and permutation tests |
| Limitation | Cannot model natural phenomena that cluster around a mean | Symmetric tails; not suitable for skewed or heavy-tailed data | Without replacement limited to length(x) draws; prob weights must be non-negative |
runif() is like a raw array (primitive, general-purpose), rnorm() is like a specialized hash map (optimized for a specific access pattern—the bell curve), and sample() is like a set operation (selecting elements from a collection). Use the most semantically appropriate one for your problem; never use runif() with manual rounding when sample() exists for discrete draws.Connection to Advanced Topics — Bootstrapping & MCMC
The three sampling primitives you have learned are not merely pedagogical tools—they are the operational foundation for some of the most powerful techniques in modern statistical computing. Two areas where these functions play a central role are bootstrapping and Markov Chain Monte Carlo (MCMC) methods.
| Concept | This Lesson's Functions | Advanced Extension |
|---|---|---|
| Bootstrap confidence intervals | sample(data, n, replace = TRUE) draws B resamples of the dataset | The boot package automates this; BCa intervals, parametric bootstrap, and wild bootstrap extend the basic idea |
| Permutation tests | sample(labels) shuffles group assignments under H₀ | Exact permutation enumeration; approximate p-values via Monte Carlo; coin package |
| Metropolis-Hastings (MCMC) | rnorm() proposes candidate moves; runif(1) decides acceptance | Hamiltonian Monte Carlo (HMC), No-U-Turn Sampler (NUTS) in Stan/RStan; Gibbs sampling |
| Cross-validation | sample(1:n, k) partitions data into k folds | Stratified k-fold, repeated CV, nested CV for hyperparameter tuning via caret or tidymodels |
| Stochastic gradient descent | sample(1:n, batch_size) selects mini-batches | Adam, RMSProp, and learning rate schedules; integration with torch for R |
As you advance into Bayesian inference, machine learning pipelines, or large-scale simulation, you will find that nearly every stochastic algorithm reduces to repeated calls to the primitives covered in this lesson. Mastering runif(), rnorm(), and sample() now gives you a composable toolkit for building arbitrarily complex stochastic systems later.
Practice Problems
set.seed(123) before rnorm(5) always produces the same five values. What would happen if you inserted a call to runif(1) between set.seed(123) and rnorm(5)? Would the five Normal values change? Why?set.seed(1) for reproducibility.data. Write R code to perform a nonparametric bootstrap with B = 5,000 replicates to estimate the 95% confidence interval for the median. Use sample() with replacement inside a replicate() call.sample() to randomly select 10 sensors whose readings you will transmit (simulating bandwidth constraints).Lesson Summary
R's random sampling infrastructure is built on the Mersenne Twister PRNG, which produces a deterministic stream of Uniform(0, 1) variates initialized by set.seed(). From this stream, runif(n, min, max) rescales to any continuous uniform interval, rnorm(n, mean, sd) applies the Box-Muller transform to generate Gaussian variates, and sample(x, size, replace, prob) selects discrete elements with optional weights and replacement.
These three functions form the composable primitives underlying Monte Carlo simulation, bootstrap resampling, MCMC sampling, and cross-validation splits. Always set a seed for reproducibility, watch for the single-integer trap in sample(), and remember that rnorm() takes standard deviation (σ), not variance (σ²).