simPsyStudy

Simulation of Ordinal Responses for Psychometric Studies

simPsyStudy defines factorial simulation conditions and generates binary or ordinal item responses under common-factor and probit graded response model parameterizations. It records the parameters, seeds, datasets, and response summaries needed to reproduce each condition.

The multiple-condition interface lets you define named sets of theta values, loadings, and thresholds, then generate every combination with separate output folders and recorded seeds.

Development status: simPsyStudy continues development of the original AUTTT package. This is a development release; CRAN preparation is in progress.

Installation

Install the development version from GitHub:

# Install remotes once, if needed.
install.packages("remotes")

remotes::install_github("Boklauth/simPsyStudy", upgrade = "never")
library(simPsyStudy)

If you update an already loaded package, restart R before loading the newly installed version.

Current capabilities

Component Supported features
Latent traits User-supplied values, multivariate normal generation, and two correlated gamma-generation methods
Response models Common-factor ordinal-variable model and probit graded response model
Outcomes Binary and ordered categorical item responses
Study designs One or more factors and full factorial combinations of named theta, loading, and threshold sets
Reproducibility Explicit replication seeds, condition manifests, saved inputs, and session information

Quick start: simulate multiple conditions

This example creates eight conditions: two sample sizes, two loading levels, and two threshold patterns. With two replications per condition, it produces 16 datasets.

1. Define the factors and theta values

There are six items and two factors. Items 1-3 measure Factor 1; items 4-6 measure Factor 2. Each theta matrix has one row per person and one column per factor.

library(simPsyStudy)

model <- list(1:3, 4:6)

set.seed(2026)
make_theta <- function(n) {
  factor1 <- rnorm(n)
  factor2 <- 0.4 * factor1 + sqrt(1 - 0.4^2) * rnorm(n)
  cbind(Factor1 = factor1, Factor2 = factor2)
}

theta_sets <- list(
  N100 = make_theta(100),
  N200 = make_theta(200)
)

This generates normal theta values with a population correlation of 0.4. Sample correlations will vary. These matrices are held fixed across the item-response replications.

2. Define loadings and thresholds

Supply one standardized FAOV (Factor Analysis with Ordinal Variables) loading per item. A threshold vector is shared by all items; alternatively, supply a matrix with one row per item. Two thresholds give three response categories.

loading_sets <- list(
  low = rep(0.4, 6),
  high = rep(0.8, 6)
)

threshold_sets <- list(
  symmetric = c(-0.5, 0.5),
  asymmetric = c(-1.2, 0.2)
)

Use FAOV thresholds, not already converted GRM intercepts. The function converts the loadings and thresholds internally.

3. Run the simulation

# Choose a new folder for each run. This path is relative to getwd().
output_dir <- file.path(getwd(), "simPsyStudy_example_run_01")

conditions <- simulate_conditions(
  model = model,
  theta_sets = theta_sets,
  loading_sets = loading_sets,
  threshold_sets = threshold_sets,
  replications = 2,
  output_dir = output_dir,
  seed = 1234,
  file_prefix = "demo"
)

conditions[, c("condition", "theta", "loading", "threshold", "status")]

The function creates the output directory and a subfolder for each condition. If the destination already contains matching condition folders or run metadata, it stops before overwriting them. To repeat the example, change simPsyStudy_example_run_01 to a new folder name.

4. Read a generated dataset

first_file <- file.path(
  output_dir, conditions$folder[1], "demo_grm_rep1.dat"
)

responses <- read.table(first_file, header = FALSE)
dim(responses)  # 100 persons, 6 items
head(responses)

The .dat files contain space-separated responses with no header or row names. This example uses categories 1, 2, and 3. Binary simulations use categories 0 and 1.

What is saved?

Location Files Purpose
Run folder conditions.csv Condition names, seeds, sample sizes, status, and any error messages
Run folder simulation_plan.rds Input sets, design, simulator, and R session information
Run folder folders.Rdata Condition-folder names for subsequent workflows
Each condition demo_grm_rep1.dat, demo_grm_rep2.dat Simulated item responses
Each condition parameters.rds Loadings, thresholds, converted parameters, and replication seeds
Each condition study_cell.Rdata Simulator return object for that condition
Each condition Response-probability CSV files Observed response summaries
Each condition Theta CSV and replication-list .dat file Theta inputs and dataset filenames

A failed condition stops the run and records the error in conditions.csv; completed outputs are preserved. Automatic resuming is not currently supported.

Response-generation methods: "U" and "N"

The single-condition function simdata_grm() provides two methods for drawing the random values used to assign item-response categories:

Method Random draw How it is used
"U" Uniform distribution, U(0, 1) Draws a value between 0 and 1 for each person and item, then compares it with the model’s cumulative response probabilities to assign a category.
"N" Standard normal distribution, N(0, 1) Draws an independent standard normal value for each person and item, converts it to a probability, and uses that probability to assign a category.

These options describe the response-generation draws, not the distribution of the supplied theta values. Both methods draw independently for every person and item and have the same target distribution. simulate_conditions() accepts either method and defaults to "U".

Relationship between simdata_faov() and simdata_grm()

The two functions provide equivalent probit response models when their parameters are placed on corresponding scales. In the FAOV formulation, the continuous response underlying item j is

\[ Y_j^* = \lambda_j\theta + \sqrt{1-\lambda_j^2}\epsilon_j, \qquad \epsilon_j \sim N(0,1), \]

and thresholds \(\tau_{jk}\) divide that response into ordered categories. The corresponding GRM cumulative probability is

\[ P(Y_j \geq k+1 \mid \theta) = \Phi(a_j\theta-d_{jk}), \]

with the conversion

\[ a_j = \frac{\lambda_j}{\sqrt{1-\lambda_j^2}}, \qquad d_{jk} = \frac{\tau_{jk}}{\sqrt{1-\lambda_j^2}}. \]

In R, the conversion is:

a <- loadings / sqrt(1 - loadings^2)
d <- sweep(thresholds, 1, sqrt(1 - loadings^2), "/")

Here, d is a positive boundary parameter. If the alternative intercept form \(\Phi(a\theta+\delta)\) is used, its intercept is \(\delta=-d\). The IRT difficulty is \(b=d/a=\tau/\lambda\) when the loading is nonzero.

The implementations are equivalent when:

With identical theta values and seeds, simdata_faov() and simdata_grm(method = "N") generate identical responses because both use standard-normal response draws. With method = "U", the individual datasets generally differ, but they follow the same response distribution.

The functions are not directly equivalent when simdata_faov(theta_matrix = NULL) generates new theta values within each replication, when factor variances differ from 1, or when the intended model includes cross-loadings, correlated item residuals, or a logistic link. If factor variances are not 1 and the underlying item responses must remain standardized, residual variances need to be based on \(1-\operatorname{diag}(\Lambda\Phi\Lambda')\) instead of \(1-\lambda^2\).

Input requirements and current scope

Other simulation helpers include TSK() for thresholds and response-distribution summaries, matrix-conversion functions, and multivariate normal and correlated gamma theta generators. gamma_from_normal() retains the original rejection-sampling procedure, while gamma_from_normal2() uses a Gaussian-copula transformation.

Gamma-based skewed multivariate distributions

Both gamma functions are designed to generate positively skewed multivariate data, but they use different procedures and should not be expected to return identical samples or correlations.

Function Approach Dimensions Main inputs
gamma_from_normal() Original rejection-sampling procedure: draws one correlated multivariate-normal vector and one uniform value per attempt, then retains the complete vector when all marginal gamma-to-normal criteria pass Exactly three shape, rate, mean_vec, cov_matrix, size, c, and seed_num
gamma_from_normal2() Gaussian-copula procedure: transforms correlated normal values to uniform probabilities and then to gamma values One or more shape, rate, mean_vec, cov_matrix, size, and seed_num

Use the original rejection approach as follows:

correlation <- to_cormatrix(c(0.49, 0.74, 0.87), n_dim = 3)
covariance <- to_covmatrix(correlation, c(0.95, 0.98, 1.1))

theta_rejection <- gamma_from_normal(
  shape = 5,
  rate = 5,
  mean_vec = c(-0.1, 0, 0.1),
  cov_matrix = covariance,
  size = 300,
  c = 4,
  seed_num = 45679
)

Use the Gaussian-copula approach with:

theta_copula <- gamma_from_normal2(
  shape = 5,
  rate = 5,
  mean_vec = c(-0.1, 0, 0.1),
  cov_matrix = covariance,
  size = 300,
  seed_num = 45679
)

Both functions return X, scaled.X, marginal skewness and kurtosis, and sample correlation and covariance matrices. The covariance matrix controls the dependence of the normal values used by each procedure; the sample covariance of the final skewed variables can differ from the input covariance.

Other tools and help

TSK(): thresholds and distribution summaries

TSK() converts expected category proportions into normal-theory thresholds and summarizes the shape of the corresponding ordinal response distribution. Supply the planned sample size with n and a vector of category probabilities with res_prop. The probabilities must be positive and sum to 1.

response_probabilities <- c(0.04, 0.06, 0.11, 0.37, 0.42)

item_distribution <- TSK(
  n = 300,
  res_prop = response_probabilities
)

item_distribution$thresholds
item_distribution$skew
item_distribution$Kurt1

The returned list contains the original category probabilities, cumulative probabilities, thresholds, skewness, adjusted skewness, excess kurtosis (Kurt1), ordinary kurtosis (Kurt2), and a sample-size-adjusted excess-kurtosis estimate (Kurt3). The thresholds can be repeated across items or assembled into an item-by-threshold matrix for simdata_faov() or simulate_conditions().

create_theta_mvn(): multivariate normal theta values

create_theta_mvn() generates correlated multivariate-normal latent-trait values. Supply the sample size, one mean and standard deviation per factor, the pairwise factor correlations, and a seed. Pairwise correlations are ordered (1, 2), (1, 3), ..., (m - 1, m).

theta <- create_theta_mvn(
  size = 300,
  mean_vec = c(-0.1, 0, 0.1),
  sd_vec = c(0.95, 0.98, 1.10),
  ifcor_vec = c(0.30, 0.50, 0.60),
  seed_num = 45679
)

head(theta$X)
head(theta$scaled.X)
theta$cor_mat

The returned X matrix has the requested means, standard deviations, and population correlation structure subject to sampling variation. scaled.X standardizes every generated factor and is the recommended input when loadings are interpreted as standardized FAOV loadings. The function also returns marginal skewness and kurtosis and the sample correlation and covariance matrices.

For complete function documentation, run:

?simulate_conditions
?TSK
?create_theta_mvn

See NEWS.md for development changes. Report problems through the GitHub issue tracker, including a small reproducible example and the output of sessionInfo().

Citation

To obtain the package citation, run:

cite <- format(citation("simPsyStudy"), style = "text")
cat(gsub("_", "", cite, fixed = TRUE), "\n")

Klauth B (2026). simPsyStudy: Simulation of Ordinal Responses for Psychometric Studies. R package version 1.1.8, https://github.com/Boklauth/simPsyStudy.

Author and license

Developed by Bo Klauth. simPsyStudy is distributed under the MIT license.