SimDesign in R

Structure for Organizing Monte Carlo Simulation Designs

Daisy Nguyen

Monte Carlo Simulation

A computational algorithm that uses repeated random sampling

  • to estimate the possible outcomes of an uncertain event like stock prices, sales forecasting, project management in business and finance setting.
  • to study the behavior of statistical estimates and estimators in statistics and data science.

Approximate solutions to problems that do not have an analytic solution.

What is SimDesign?

SimDesign provides tools to safely and efficiently organize and execute Monte Carlo simulation experiments.

The types of questions SimDesign tackles are:

  • Is an estimator biased?
  • How do different estimators compare in terms of efficiency?

SimDesign Skeleton

flowchart TD
  
  A(Design) --> B(Generate)
  B --> C(Analyze)
  C --> D(Summarize)
  D --> E(Results)

Set up the predictive model with the interested parameters/conditions
Generate data from some probability sampling distribution for analysis
Perform analysis to obtain information about the statistical estimators
Run simulations repeatedly and summarize the results using relevant meta-statistics
Return the results from the “near infinite” number of possible combinations generated

Main Functions of SimDesign

Function Description
createDesign() Take in vectors of independent variables such as sample size, distributions, group sizes, etc to create a dataframe of unique combination of simulation conditions to test
Generate() User-defined function that reads the current design condition and uses pseudo-random number generators to construct a synthetic dataset
Analyse() User-defined function that processes the generated dataset to calculate specific statistical estimators, or parameters
Summarise() User-defined function that aggregates the estimates across all replications of a condition to compute final evaluation metrics (examples: bias, RMSE, SD)
runSimulation() Output final summary table on the predefined simulation functions, design conditions, and number of replications

Estimating Pi using Monte Carlo Simulation

#install.packages("SimDesign")
library(SimDesign)
library(ggplot2)
set.seed(1)

# 1. DESIGN: Test different sample sizes (n)
Design <- createDesign(sample_size = c(10,100,1000, 10000, 100000))

# 2. GENERATE: Create random points
Generate <- function(condition, fixed_objects = NULL) {
  # Generate x and y coordinates between -1 and 1
  # condition$sample_size = the number of points for each trial
  x <- runif(condition$sample_size, min = -1, max = 1) 
  y <- runif(condition$sample_size, min = -1, max = 1)
  
  # Return the coordinates as a data frame
  data.frame(x = x, y = y)
}

# 3. ANALYSE: Calculate the estimate for pi
Analyse <- function(condition, dat, fixed_objects = NULL) {
  # radius of the circle is 1
  # points inside/on the circle has a distant to the origin <=1, so distance^2 <=1
  inside <- sum(dat$x^2 + dat$y^2 <= 1) # count how many points lie inside/on the circle
  
  # Pi approximation = 4 * (points_inside / total_points)
  pi_est <- 4 * (inside / condition$sample_size)
  
  # Return the single estimate
  return(pi_est)
}

# 4. SUMMARISE: Evaluate accuracy across replications
Summarise <- function(condition, results, fixed_objects = NULL) {
  # Calculate mean estimate and bias from true pi
  ret <- c(mean_pi = mean(results), 
           bias = mean(results) - pi,
           sd = sd(results))
  return(ret)
}

# 5. RUN SIMULATION
FinalResults <- runSimulation(design = Design, 
                             replications = 100, 
                             generate = Generate, 
                             analyse = Analyse, 
                             summarise = Summarise)

print(FinalResults)
# A tibble: 5 × 8
  sample_size mean_pi        bias        sd REPLICATIONS SIM_TIME       SEED
        <dbl>   <dbl>       <dbl>     <dbl>        <dbl> <chr>         <int>
1          10  3.148   0.0064073  0.52               100 0.04s    1140350788
2         100  3.1256 -0.015993   0.17843            100 0.13s     312928385
3        1000  3.1353 -0.0062727  0.051464           100 0.03s     866248189
4       10000  3.1406 -0.0010327  0.015974           100 0.08s    1909893419
5      100000  3.1421  0.00047015 0.0050436          100 0.28s     554504146
# ℹ 1 more variable: COMPLETED <chr>

Interactive Simulation

How to choose estimator to recover missing data?

set.seed(789)

library(tidyverse)
ames_housing <- read_csv("ames_housing.csv")

# We isolate the complete, clean rows where SalePrice and GrLivArea are not NA
ames_clean <- ames_housing[!is.na(ames_housing$SalePrice) & !is.na(ames_housing$GrLivArea), 
                       c("SalePrice", "GrLivArea")]


Design_Ames <- createDesign(impute_method = c("mean", "linear_regression", "hot_deck"))


Generate_Ames<- function(condition, fixed_objects) {
  
  df <- fixed_objects$clean_data
  n_rows <- nrow(df)
  
  # Keep a record of the true SalePrice values before we corrupt it
  true_SalePrice <- df$SalePrice
  
  # Intentionally punch missing holes (NA) into 20% of our real rows at random
  missing_indices <- sample(1:n_rows, size = round(0.20 * n_rows))
  df$SalePrice[missing_indices] <- NA
  
  return(list(corrupted_df = df, true_values = true_SalePrice, missing_ids = missing_indices))

}


Analyse_Ames <- function(condition, dat, fixed_objects) {
  test_df     <- dat$corrupted_df
  true_vector <- dat$true_values
  missing_ids <- dat$missing_ids
  
  # Track the true values of just the specific rows we deleted
  target_true <- true_vector[missing_ids]
  
  # Replace NA with the mean 
  if (condition$impute_method == "mean") {
    calculated_mean <- mean(test_df$SalePrice, na.rm = TRUE)
    imputed_values  <- rep(calculated_mean, length(missing_ids))
  }
  
  # Use a linear regression model to predict SalePrice from GrLivArea
  else if (condition$impute_method == "linear_regression") {
    fit            <- lm(SalePrice ~ GrLivArea, data = test_df)
    
    # Predict the missing SalePrice values using the corresponding GrLivArea values
    imputed_values <- predict(fit, newdata = test_df[missing_ids, ])
    
    # If regression outputs an NA due to missing GrLivArea, use overall mean
    imputed_values[is.na(imputed_values)] <- mean(test_df$SalePrice, na.rm = TRUE)
  }
  
  # Hot Deck Imputation: Randomly sample from rows that aren't missing and fill the missing values with them
  else if (condition$impute_method == "hot_deck") {
    valid_pool     <- test_df$SalePrice[!is.na(test_df$SalePrice)]
    imputed_values <- sample(valid_pool, size = length(missing_ids), replace = TRUE)
  }
  
  # Calculate estimation metrics for this run
  error <- imputed_values - target_true
  rmse  <- sqrt(mean(error^2))
  mean_imputed <- mean(imputed_values)
  mean_true    <- mean(target_true)
  
  return(c(RMSE = rmse, Imputed_Mean = mean_imputed, True_Mean = mean_true))
}


Summarise_Ames <- function(condition, results, fixed_objects) {
  bias_score <- mean(results[, "Imputed_Mean"]) - mean(results[, "True_Mean"])
  
  ret <- c(
    Average_RMSE = mean(results[, "RMSE"]),
    Empirical_Bias = bias_score
  )
  return(ret)
}


SimulationResults_Ames <- runSimulation(design = Design_Ames,
                                       replications = 200,
                                       generate = Generate_Ames,
                                       analyse = Analyse_Ames,
                                       summarise = Summarise_Ames,
                                       fixed_objects = list(clean_data = ames_clean))

print(SimulationResults_Ames)
# A tibble: 3 × 7
  impute_method     Average_RMSE Empirical_Bias REPLICATIONS SIM_TIME       SEED
  <chr>                    <dbl>          <dbl>        <dbl> <chr>         <int>
1 mean                    78914.         271.82          200 0.05s     858527728
2 linear_regression       56545.         143.75          200 0.28s      51091316
3 hot_deck               111817.         613.65          200 0.05s    2113733930
# ℹ 1 more variable: COMPLETED <chr>

Benefits and Limitations

Benefits

  • Provides a templated setup with the generate-analyse-summarise workflow to run Monte Carlo Simulations

  • Avoids the inefficient and error prone for-loops strategy

  • Implicitly supports parallel processing with high-quality random number generation

Limitations

  • While easily splits tasks across laptop’s CPU cores, it is structurally tied to the local machine

  • Defaults to single-core execution: users must manually enable parallel processing

References

  • https://cran.r-project.org/web/packages/SimDesign/vignettes/SimDesign-intro.html
  • https://cran.r-project.org/web/packages/SimDesign/SimDesign.pdf
  • https://philchalmers.github.io/SimDesign/doitagain.pdf
  • https://rpubs.com/cjp0803/montecarlo
  • https://www.ibm.com/think/topics/monte-carlo-simulation
  • https://en.wikipedia.org/wiki/Monte_Carlo_method
  • https://quarto.org/docs/presentations/revealjs/