Probability in R Exercises: 15 Real-World Practice Problems
Probability questions in R show up everywhere from A/B tests to fraud scoring to clinical trials, but reading formulas is no substitute for running simulations and reading the numbers. This set walks you through 15 problems covering sampling, discrete and continuous distributions, conditional reasoning, the Monty Hall puzzle, and Bayesian updating. Every solution is hidden behind a click-to-reveal block so you can try first.
Section 1. Sampling and simulation basics (3 problems)
Exercise 1.1: Estimate P(heads) from five thousand fair coin tosses
Task: Toss a fair coin 5,000 times using sample() with set.seed(101) for reproducibility, then estimate the empirical probability of heads by taking the mean of the indicator tosses == "H". Save the estimated probability to ex_1_1.
Expected result:
#> [1] 0.5024
Difficulty: Beginner
The empirical probability is just the fraction of tosses that landed heads, so simulate every toss first and then count.
Draw the tosses with sample(c("H","T"), size = 5000, replace = TRUE), then take mean() of the logical tosses == "H".
Click to reveal solution
Explanation: sample() draws with replacement from {"H","T"} so each of the 5,000 trials is independent and fair. The expression tosses == "H" is a logical vector and mean() of logicals returns the proportion of TRUE entries, which is exactly the Monte Carlo estimate of P(heads). With 5,000 trials the standard error is roughly 1/sqrt(20000) = 0.007, so landing within 0.01 of 0.5 is exactly what the Law of Large Numbers predicts. A common mistake is forgetting replace = TRUE; without it sample() shuffles a 2-element vector and errors as soon as the third draw.
Exercise 1.2: Probability of rolling at least one six in four dice rolls
Task: Estimate the probability that at least one six appears when a fair six-sided die is rolled four times in a row. Use 20,000 replicates with set.seed(102) and replicate() plus sample() per trial, then take the mean of the logical results. Save the estimate to ex_1_2.
Expected result:
#> [1] 0.5183
Difficulty: Beginner
Each replicate is one experiment of four rolls, and you only care whether a six showed up at all, not how many.
Wrap sample(1:6, size = 4, replace = TRUE) inside replicate(20000, ...) with any(rolls == 6), then take the mean().
Click to reveal solution
Explanation: replicate() runs the curly-braced expression 20,000 times and collects the results into a vector. Each trial draws four dice rolls and asks any(rolls == 6), which returns TRUE if at least one six appeared. The theoretical answer is 1 - (5/6)^4 = 0.5177, which matches the simulation to three decimals. This problem is historically famous because gambler Antoine Gombaud (the Chevalier de Mere) lost money betting on the same proposition with three rolls and made money with four, which sparked the Pascal-Fermat correspondence that founded probability theory.
Exercise 1.3: Estimate defect rate variability in a quality-control batch
Task: A factory QA engineer pulls 50 parts at random from a production line with a true defect rate of 4 percent and counts how many are defective. Simulate this batch-sampling procedure 5,000 times using rbinom(5000, size = 50, prob = 0.04), then compute the mean, standard deviation, and the proportion of batches with zero defects. Save the named numeric vector to ex_1_3.
Expected result:
#> mean sd p_zero_def
#> 2.00100 1.39842 0.13140
Difficulty: Intermediate
Simulate the count of defective parts across many batches, then summarise how those counts spread out.
Use rbinom(5000, size = 50, prob = 0.04) and combine mean(), sd(), and mean(defects == 0) into a named vector.
Click to reveal solution
Explanation: rbinom(n, size, prob) returns n independent draws, each of which is the number of successes in size Bernoulli trials with probability prob. Here the simulated mean (2.00) matches n*p = 2.0 exactly to two decimals, and the simulated SD matches sqrt(n*p*(1-p)) = 1.40. The proportion of zero-defect batches (about 0.13) matches dbinom(0, 50, 0.04), which is the analytical probability. QA teams use this kind of simulation to set acceptance-sampling thresholds: a stricter cutoff (zero tolerance) only catches 87 percent of bad batches.
Section 2. Discrete distributions (3 problems)
Exercise 2.1: Probability a marketing campaign hits its conversion floor
Task: A marketing analyst running a paid-search test expects a conversion rate of 8 percent. The campaign drives 100 landing-page visits per day. Compute the probability of getting at least 10 conversions in a single day under a Binomial(100, 0.08) model using pbinom() with lower.tail = FALSE. Save the probability to ex_2_1.
Expected result:
#> [1] 0.247155
Difficulty: Intermediate
An "at least" probability is the upper tail, so you want the complement of the cumulative probability up to one below your threshold.
Call pbinom() with q = 9, size = 100, prob = 0.08, and lower.tail = FALSE.
Click to reveal solution
Explanation: pbinom(q, size, prob) returns P(X <= q) by default. To get P(X >= 10), pass q = 9 with lower.tail = FALSE, which returns P(X > 9) and is identical to P(X >= 10) because the distribution is integer-valued. A common mistake is passing q = 10, which excludes the boundary value of interest. The expected count is n*p = 8, so "at least 10" is one standard deviation above the mean and a 25 percent chance is the right order of magnitude. This calculation is the basis of statistical power for one-sample proportion tests.
Exercise 2.2: Server alert rate under a Poisson model
Task: An ops engineer monitors a service that generates errors at a rate of lambda = 1.5 per hour. Compute the probability of seeing three or more errors in a single hour under a Poisson(1.5) model using ppois() with lower.tail = FALSE. Save the probability rounded to four decimals to ex_2_2.
Expected result:
#> [1] 0.1912
Difficulty: Intermediate
"Three or more" is the upper tail, so set the cutoff one below three and ask for the probability above it.
Use ppois(2, lambda = 1.5, lower.tail = FALSE) and wrap it in round(..., 4).
Click to reveal solution
Explanation: The Poisson distribution models counts of rare independent events over a fixed window. ppois(q, lambda) returns P(X <= q), so to get P(X >= 3) you pass q = 2 with lower.tail = FALSE, which is the cleanest way to compute the upper tail without dealing with 1 - ppois(2, 1.5) and the small floating-point loss that comes with it. The expected count is 1.5 per hour, so seeing three or more is about a 19 percent event. Ops teams calibrate paging thresholds against numbers like this to keep alert noise tolerable.
Exercise 2.3: Compare empirical and theoretical CDFs of a Geometric distribution
Task: Simulate 10,000 draws from a Geometric distribution with success probability p = 0.3 using rgeom() and set.seed(203). For k in 0:8 compare the empirical CDF (mean(draws <= k)) to the theoretical CDF pgeom(k, 0.3) and build a data.frame with columns k, empirical, theoretical, abs_diff (all numeric to 4 decimals). Save the data.frame to ex_2_3.
Expected result:
#> k empirical theoretical abs_diff
#> 1 0 0.2987 0.3000 0.0013
#> 2 1 0.5092 0.5100 0.0008
#> 3 2 0.6572 0.6570 0.0002
#> 4 3 0.7595 0.7599 0.0004
#> 5 4 0.8329 0.8319 0.0010
#> 6 5 0.8820 0.8824 0.0004
#> 7 6 0.9168 0.9176 0.0008
#> 8 7 0.9419 0.9424 0.0005
#> 9 8 0.9590 0.9596 0.0006
Difficulty: Advanced
Compare the share of simulated draws at or below each value with the model's cumulative probability at that same value.
Simulate with rgeom(10000, prob = 0.3), get the theoretical side from pgeom(k, prob = 0.3), and assemble a data.frame with the rounded differences.
Click to reveal solution
Explanation: R parameterises the Geometric as the number of failures before the first success, so rgeom(n, p) returns non-negative integers and pgeom(k, p) = 1 - (1-p)^(k+1). All abs_diff values are well under 0.005, which matches the rough Monte Carlo error of sqrt(p*(1-p)/n) for n = 10,000. A common pitfall is mixing this up with the "trials including the success" parameterisation used in some textbooks; in that version the support starts at 1 and pgeom would not match. Comparing empirical to theoretical CDFs is the standard sanity check before using a simulation in production analytics.
Section 3. Continuous distributions (3 problems)
Exercise 3.1: Probability a standard normal draw lands below 1.96
Task: Use pnorm() to compute the probability that a standard normal random variable Z is less than 1.96, then compute the probability that it falls between minus 1.96 and 1.96 (the central 95 percent interval). Save the two probabilities to a named numeric vector ex_3_1 with names left_tail and central_95.
Expected result:
#> left_tail central_95
#> 0.9750021 0.9500042
Difficulty: Beginner
A central interval probability is the difference between the cumulative probabilities at its two endpoints.
Use pnorm(1.96) for the left tail and pnorm(1.96) - pnorm(-1.96) for the central mass, stored in a named vector.
Click to reveal solution
Explanation: pnorm(q) defaults to mean = 0, sd = 1 and returns P(Z <= q). The classic "95 percent confidence interval" uses plus and minus 1.96 because that interval traps 95 percent of the standard normal mass (here, 0.9500042 to four decimals of precision). A related shortcut: qnorm(0.975) returns 1.959964, which is where the 1.96 round number comes from. Knowing pnorm, qnorm, dnorm, and rnorm is the four-function toolkit you reach for in every hypothesis-test and simulation problem.
Exercise 3.2: Wait-time tail probability for an exponential arrival process
Task: A transit ops team finds that buses arrive at a stop with rate 0.2 per minute (mean wait 5 minutes), modelled as Exponential(0.2). Compute the probability that a rider has to wait more than 8 minutes using pexp() with lower.tail = FALSE, and also the median wait time using qexp(0.5, 0.2). Save both results to a named numeric vector ex_3_2.
Expected result:
#> p_wait_gt_8 median_wait
#> 0.201897 3.465736
Difficulty: Intermediate
One part asks for an upper-tail probability, and the other asks for the value with half the mass below it.
Combine pexp(8, rate = 0.2, lower.tail = FALSE) and qexp(0.5, rate = 0.2) into a named numeric vector.
Click to reveal solution
Explanation: The Exponential is memoryless, so the wait time from any moment onward has the same distribution regardless of how long you have already waited. P(wait > 8) for rate 0.2 equals exp(-0.2 * 8) = 0.2019, which matches pexp to six decimals. Note the median (3.47 minutes) is well below the mean (5 minutes), a hallmark of right-skewed distributions. A common modelling mistake is treating the Exponential parameter as the mean rather than the rate; R uses the rate convention (rate = 1/mean), and Python's scipy.stats.expon uses the opposite.
Exercise 3.3: Empirical 95th percentile of daily wind speed from airquality
Task: Using the built-in airquality dataset, drop the rows with missing Wind values and compute both the empirical 95th percentile of wind speed via quantile(..., probs = 0.95) and the theoretical 95th percentile of a normal distribution fit to the same mean and standard deviation via qnorm(). Save both numbers to a named numeric vector ex_3_3.
Expected result:
#> empirical theoretical
#> 14.9000 15.6232
Difficulty: Intermediate
Read the cutoff straight from the sorted data, then ask where the same cutoff sits under a fitted normal model.
After dropping missing values with na.omit(), use quantile(w, probs = 0.95) and qnorm(0.95, mean = mean(w), sd = sd(w)).
Click to reveal solution
Explanation: The empirical quantile reads the 95th-percentile value directly from the sorted data (here 14.9 mph). The theoretical quantile assumes a normal model with the same mean and SD and asks "where is the 95th percentile of THAT distribution" via qnorm(). The two values disagree by about 0.7 mph, which is a soft signal that the wind distribution is not exactly normal (in fact it is slightly right-skewed). Comparing empirical to model-based quantiles is the cheapest goodness-of-fit check you can run before committing to a parametric model.
Section 4. Conditional probability and independence (3 problems)
Exercise 4.1: The birthday problem by simulation for a group of 25
Task: Estimate the probability that at least two people in a room of 25 share a birthday, assuming uniform birthdays over 365 days and ignoring leap years. Use 30,000 replicates with set.seed(401) and check each replicate with any(duplicated(sample(1:365, 25, replace = TRUE))), then take the mean. Save the estimate to ex_4_1.
Expected result:
#> [1] 0.5681
Difficulty: Intermediate
Each replicate fills a room with random birthdays and checks whether any date repeats.
Inside replicate(30000, ...), test any(duplicated(sample(1:365, 25, replace = TRUE))) and take the mean().
Click to reveal solution
Explanation: The analytical answer is 1 - prod((365:341)/365) = 0.5687, so the simulation lands within one Monte Carlo standard error. The result surprises most people because the relevant count is "pairs" not "people": with 25 people there are 300 pairs, and each pair has a 1-in-365 chance of matching. The function duplicated() returns a logical vector flagging the second-and-later occurrence of any repeated value, and any() collapses it to a single TRUE/FALSE per trial. The same pattern catches hash collisions and password reuse, which is why the birthday paradox shows up in cryptography.
Exercise 4.2: Monty Hall switching versus staying over ten thousand trials
Task: Simulate the Monty Hall problem 10,000 times with set.seed(402). In each trial pick a random door for the car, a random initial guess, have Monty open a non-car non-guess door, then record whether "staying" or "switching" wins. Return the empirical win rates for both strategies in a named numeric vector saved as ex_4_2.
Expected result:
#> stay switch
#> 0.3361 0.6639
Difficulty: Advanced
Each trial places the car, makes a guess, opens a losing door, and records whether keeping the guess wins and whether the other door wins.
Use replicate() with setdiff() to find Monty's door and the switch door, then rowMeans() the stay/switch matrix.
Click to reveal solution
Explanation: Switching wins exactly two-thirds of the time because Monty's door-opening rule injects information: he is not allowed to reveal the car, so the door he opens is conditional on the contestant's initial pick. The simulation reproduces the 1/3-versus-2/3 split to two decimals. Notice the if guard around sample(): when only one door is available, sample(x, 1) with a length-1 vector would treat it as sample(1:x, 1) and pick a wrong value, which is one of R's classic foot-guns. Always check length() before calling sample() on an unknown-length pool.
Exercise 4.3: Conditional survival probability by sex from the Titanic dataset
Task: Using the built-in Titanic array, compute P(survived | sex) for both adult males and adult females. Aggregate counts over class and survival, then divide survivors by row totals. Save the resulting named numeric vector with elements p_surv_male and p_surv_female to ex_4_3.
Expected result:
#> p_surv_male p_surv_female
#> 0.20337 0.74358
Difficulty: Advanced
Collapse the table down to sex-by-survival counts, then divide each sex's survivors by that sex's total.
Slice the adults with Titanic[, , "Adult", ], sum over class with apply(..., c("Sex","Survived"), sum), and divide survivors by row totals.
Click to reveal solution
Explanation: Titanic is a 4-dimensional contingency array (Class x Sex x Age x Survived). Slicing [, , "Adult", ] drops the Age dimension while keeping the other three, then apply(..., c("Sex","Survived"), sum) collapses Class to leave a 2x2 table of counts. The conditional probability P(survived | sex) is row-normalised: 75 percent for adult women versus 20 percent for adult men, the textbook "women and children first" effect quantified. Conditional probabilities from contingency tables are the foundation of base-rate reasoning and a standard data-quality audit.
Section 5. Law of Large Numbers and Bayesian inference (3 problems)
Exercise 5.1: Cumulative mean of fair die rolls converging to 3.5
Task: Simulate 2,000 rolls of a fair six-sided die with set.seed(501), compute the cumulative mean after each roll using cumsum(rolls) / seq_along(rolls), and report the cumulative mean at rolls 10, 100, 500, and 2000 as a named numeric vector. Save the four-element vector to ex_5_1.
Expected result:
#> n10 n100 n500 n2000
#> 3.700 3.640 3.450 3.502
Difficulty: Beginner
Track how the running average of the rolls settles down as more and more rolls accumulate.
Build the running average with cumsum(rolls) / seq_along(rolls) and index it at positions 10, 100, 500, and 2000.
Click to reveal solution
Explanation: The theoretical mean of a fair die is 3.5. The cumulative mean walks toward 3.5 as the sample grows, which is the Law of Large Numbers in action. The shrinkage of the gap is order 1/sqrt(n): from a gap of 0.2 at n = 10 to about 0.002 at n = 2000 is the right ballpark. cumsum() is a vectorised running sum and seq_along(x) is the standard way to get 1:length(x) without breaking on length-zero input. The pair is your go-to idiom for online statistics on streaming data.
Exercise 5.2: Posterior on a coin's bias from a Beta-Binomial update
Task: A coin is suspected of being unfair. Start with a weakly-informative Beta(2, 2) prior on the success probability, observe 8 heads in 12 flips, then compute the posterior parameters (Beta(10, 6)) along with the posterior mean and the 95 percent equal-tailed credible interval using qbeta(c(0.025, 0.975), 10, 6). Save the four numbers to a named numeric vector ex_5_2 with names post_a, post_b, post_mean, ci_low, ci_high.
Expected result:
#> post_a post_b post_mean ci_low ci_high
#> 10.00000 6.00000 0.62500 0.38317 0.83557
Difficulty: Advanced
Updating a Beta prior with binomial data simply adds the observed successes and failures to the prior counts.
Form post_a and post_b by adding successes and failures to the prior, then get the interval from qbeta(c(0.025, 0.975), post_a, post_b).
Click to reveal solution
Explanation: The Beta is the conjugate prior of the Binomial likelihood: if the prior is Beta(a, b) and you observe k successes in n trials, the posterior is Beta(a + k, b + n - k). With a Beta(2,2) prior and 8 heads in 12 flips, the posterior is Beta(10, 6) and its mean (0.625) is between the prior mean (0.5) and the MLE (0.667), pulled by the prior toward 0.5. The 95 percent credible interval (0.38 to 0.84) covers 0.5, so on this evidence alone you cannot reject the fair-coin hypothesis at the 5 percent level. Conjugacy makes Bayesian inference here a one-line update, no MCMC required.
Exercise 5.3: Bayes rule for a screening test with low disease prevalence
Task: A screening test has sensitivity 0.99 (P(positive | disease)) and specificity 0.95 (P(negative | no disease)). The disease prevalence in the screened population is 0.01 (P(disease)). Compute the positive predictive value P(disease | positive) using Bayes rule and the negative predictive value P(no disease | negative). Save the two probabilities to a named numeric vector ex_5_3 with names ppv and npv.
Expected result:
#> ppv npv
#> 0.1666667 0.9998940
Difficulty: Advanced
A predictive value weighs how often a test result is correct against how often that result occurs at all, given the rare base rate.
Compute the marginal p_pos and p_neg first, then divide sens * prev and spec * (1 - prev) by them.
Click to reveal solution
Explanation: Despite sensitivity of 99 percent, the positive predictive value is only 17 percent. With 1,000 screened people you expect about 10 true positives and 50 false positives (5 percent of 990 healthy people), so among 60 positives only 10 actually have the disease. This base-rate trap is the single most common probabilistic reasoning error in clinical decision-making and machine-learning fraud detection alike. Rule of thumb: when prevalence is much smaller than the false-positive rate, PPV will be poor no matter how high sensitivity is.
What to do next
Now that you have worked through 15 probability problems in R, take the next step:
- Review the foundations of probability simulation if any of the simulate-and-count patterns felt fuzzy.
- Move on to dplyr Exercises in R to practice wrangling the kinds of datasets you will be running these probability tools on.
- Try the Hypothesis Testing Exercises in R for the natural next layer: turning probabilities into formal test decisions.
- Browse all hubs at the Practice Exercises index to keep building.
r-statistics.co · Verifiable credential · Public URL
This document certifies mastery of
Probability in R Mastery
Every certificate has a public verification URL that proves the holder passed the assessment. Anyone with the link can confirm the recipient and date.
77 learners have earned this certificate