diff --git a/NAMESPACE b/NAMESPACE index 874649f..64d2dad 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,6 +1,7 @@ # Generated by roxygen2: do not edit by hand S3method(as_draws,measrfit) +S3method(as_measrfit,default) S3method(c,measrprior) S3method(fit_m2,measrdcm) S3method(loo,measrfit) @@ -21,6 +22,7 @@ export(add_reliability) export(add_respondent_estimates) export(as_draws) export(as_label) +export(as_measrfit) export(as_name) export(create_profiles) export(default_dcm_priors) @@ -35,6 +37,7 @@ export(loo_compare) export(measr_dcm) export(measr_examples) export(measr_extract) +export(measrfit) export(measrprior) export(prior) export(prior_) diff --git a/NEWS.md b/NEWS.md index 126f766..8bfaa76 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,8 @@ * Updated reliability functionality to allow for the calculation of accuracy and consistency with different thresholds for determining attribute classifications. +* Added new `measrfit()` function for creating measrfit objects from *Stan* models that were not originally created with measr. + # measr 1.0.0 ## New documentation diff --git a/R/measrfit-class.R b/R/measrfit-class.R index adf656c..e92c64c 100644 --- a/R/measrfit-class.R +++ b/R/measrfit-class.R @@ -1,3 +1,134 @@ +#' Create a `measrfit` object +#' +#' Models fitted with **measr** are represented as a `measrfit` object. If a +#' model is estimated with *Stan*, but not **measr**, a `measrfit` object can be +#' created in order to access other functionality in **measr** (e.g., model fit, +#' reliability). +#' +#' @param data The data and Q-matrix used to estimate the model. +#' @param type The type of DCM that was estimated. +#' @param prior A [measrprior][measrprior()] object containing information on +#' the priors used in the model. +#' @param stancode The model code in **Stan** language. +#' @param method The method used to fit the model. +#' @param algorithm The name of the algorithm used to fit the model. +#' @param backend The name of the backend used to fit the model. +#' @param model The fitted Stan model. This will object of class +#' [rstan::stanfit-class] if `backend = "rstan"` and +#' [`CmdStanMCMC`](https://mc-stan.org/cmdstanr/reference/CmdStanMCMC.html) +#' if `backend = "cmdstanr"` was specified when fitting the model. +#' @param respondent_estimates An empty list for adding estimated person +#' parameters after fitting the model. +#' @param fit An empty list for adding model fit information after fitting the +#' model. +#' @param criteria An empty list for adding information criteria after fitting +#' the model. +#' @param reliability An empty list for adding reliability information after +#' fitting the model. +#' @param file Optional name of a file which the model objects was saved to +#' or loaded from. +#' @param version The versions of **measr**, **Stan**, **rstan** and/or +#' **cmdstanr** that were used to fit the model. +#' @param class Additional classes to be added (e.g., `measrdcm` for a +#' diagnostic classification model). +#' +#' @return A [measrfit][measrfit-class] object. +#' @export +#' @seealso [measrfit-class], [measr_dcm()] +#' @examplesIf measr_examples() +#' rstn_mdm_lcdm <- measr_dcm( +#' data = mdm_data, missing = NA, qmatrix = mdm_qmatrix, +#' resp_id = "respondent", item_id = "item", type = "lcdm", +#' method = "optim", seed = 63277, backend = "rstan" +#' ) +#' +#' new_obj <- measrfit( +#' data = rstn_mdm_lcdm$data, +#' type = rstn_mdm_lcdm$type, +#' prior = rstn_mdm_lcdm$prior, +#' stancode = rstn_mdm_lcdm$stancode, +#' method = rstn_mdm_lcdm$method, +#' algorithm = rstn_mdm_lcdm$algorithm, +#' backend = rstn_mdm_lcdm$backend, +#' model = rstn_mdm_lcdm$model, +#' respondent_estimates = rstn_mdm_lcdm$respondent_estimates, +#' fit = rstn_mdm_lcdm$fit, +#' criteria = rstn_mdm_lcdm$criteria, +#' reliability = rstn_mdm_lcdm$reliability, +#' file = rstn_mdm_lcdm$file, +#' version = rstn_mdm_lcdm$version, +#' class = "measrdcm" +#' ) +measrfit <- function(data = list(), type = character(), + prior = default_dcm_priors(type = type), + stancode = character(), method = character(), + algorithm = character(), backend = character(), + model = NULL, respondent_estimates = list(), + fit = list(), criteria = list(), reliability = list(), + file = NULL, version = list(), class = character()) { + obj <- list(data = data, + type = type, + prior = prior, + stancode = stancode, + method = method, + algorithm = algorithm, + backend = backend, + model = model, + respondent_estimates = respondent_estimates, + fit = fit, + criteria = criteria, + reliability = reliability, + file = file, + version = version) + + validate_measrfit(new_measrfit(obj, class = class)) +} + +#' Coerce objects to a `measrfit` +#' +#' @param x An object to be coerced to a `measrfit`. +#' @param class Additional classes to be added (e.g., `measrdcm` for a +#' diagnostic classification model). +#' +#' @return An object of class [measrfit-class]. +#' @export +#' @seealso [measrfit-class], [measrfit()] +#' +#' @examplesIf measr_examples() +#' rstn_mdm_lcdm <- measr_dcm( +#' data = mdm_data, missing = NA, qmatrix = mdm_qmatrix, +#' resp_id = "respondent", item_id = "item", type = "lcdm", +#' method = "optim", seed = 63277, backend = "rstan" +#' ) +#' +#' new_obj <- as_measrfit(rstn_mdm_lcdm, class = "measrdcm") +as_measrfit <- function(x, class = character()) { + UseMethod("as_measrfit") +} + +#' @export +#' @rdname as_measrfit +as_measrfit.default <- function(x, class = character()) { + measrfit( + data = x$data, + type = x$type, + prior = x$prior, + stancode = x$stancode, + method = x$method, + algorithm = x$algorithm, + backend = x$backend, + model = x$model, + respondent_estimates = x$respondent_estimates, + fit = x$fit, + criteria = x$criteria, + reliability = x$reliability, + file = x$file, + version = x$version, + class = class + ) +} + + #' Class `measrfit` of models fitted with the **measr** package #' #' Models fitted with the **measr** package are represented as a `measrfit` @@ -5,7 +136,6 @@ #' relevant information. #' #' @name measrfit-class -#' @aliases measrfit #' @docType class #' #' @slot data The data and Q-matrix used to estimate the model. @@ -46,3 +176,51 @@ new_measrfit <- function(model = list(), ..., class = character()) { new_measrdcm <- function(x) { new_measrfit(x, class = "measrdcm") } + +validate_measrfit <- function(x) { + # check names ---------------------------------------------------------------- + stopifnot(all(names(x) == c("data", "type", "prior", "stancode", "method", + "algorithm", "backend", "model", + "respondent_estimates", "fit", "criteria", + "reliability", "file", "version"))) + + # check types ---------------------------------------------------------------- + stopifnot(is.list(x$data)) + stopifnot(tibble::is_tibble(x$data$data)) + stopifnot(all(colnames(x$data$data) == c("resp_id", "item_id", "score"))) + stopifnot(tibble::is_tibble(x$data$qmatrix)) + stopifnot(is.character(x$data$resp_id)) + stopifnot(is.character(x$data$item_id)) + stopifnot(is.character(x$type)) + stopifnot(x$type %in% dcm_choices()) + stopifnot(is.measrprior(x$prior)) + stopifnot(is.character(x$stancode)) + stopifnot(is.character(x$method)) + stopifnot(x$method %in% c("mcmc", "optim")) + stopifnot(is.character(x$algorithm)) + stopifnot(is.character(x$backend)) + stopifnot(x$backend %in% backend_choices()) + if (x$backend == "rstan" && x$method == "optim") { + stopifnot(is.list(x$model)) + } else if (x$backend == "rstan" && x$method == "mcmc") { + stopifnot(any(class(x$model) == "stanfit")) + } else if (x$backend == "cmdstanr") { + stopifnot(any(class(x$model) == "CmdStanFit")) + } + stopifnot(is.list(x$respondent_estimates)) + stopifnot(is.list(x$fit)) + stopifnot(is.list(x$criteria)) + stopifnot(is.list(x$reliability)) + stopifnot(is.null(x$file) || is.character(x$file)) + stopifnot(is.list(x$version)) + stopifnot(is.package_version(x$version$R)) + stopifnot(is.package_version(x$version[[2]])) + stopifnot(is.package_version(x$version$rstan)) + stopifnot(is.package_version(x$version$StanHeaders)) + if (x$backend == "cmdstanr") { + stopifnot(is.package_version(x$version$cmdstanr)) + stopifnot(is.package_version(x$version$cmdstan)) + } + + x +} diff --git a/man/as_measrfit.Rd b/man/as_measrfit.Rd new file mode 100644 index 0000000..2b02c83 --- /dev/null +++ b/man/as_measrfit.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/measrfit-class.R +\name{as_measrfit} +\alias{as_measrfit} +\alias{as_measrfit.default} +\title{Coerce objects to a \code{measrfit}} +\usage{ +as_measrfit(x, class = character()) + +\method{as_measrfit}{default}(x, class = character()) +} +\arguments{ +\item{x}{An object to be coerced to a \code{measrfit}.} + +\item{class}{Additional classes to be added (e.g., \code{measrdcm} for a +diagnostic classification model).} +} +\value{ +An object of class \linkS4class{measrfit}. +} +\description{ +Coerce objects to a \code{measrfit} +} +\examples{ +\dontshow{if (measr_examples()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} +rstn_mdm_lcdm <- measr_dcm( + data = mdm_data, missing = NA, qmatrix = mdm_qmatrix, + resp_id = "respondent", item_id = "item", type = "lcdm", + method = "optim", seed = 63277, backend = "rstan" +) + +new_obj <- as_measrfit(rstn_mdm_lcdm, class = "measrdcm") +\dontshow{\}) # examplesIf} +} +\seealso{ +\linkS4class{measrfit}, \code{\link[=measrfit]{measrfit()}} +} diff --git a/man/loo.measrfit.Rd b/man/loo.measrfit.Rd index f1c4204..c6f473c 100644 --- a/man/loo.measrfit.Rd +++ b/man/loo.measrfit.Rd @@ -14,13 +14,14 @@ \item{r_eff}{Vector of relative effective sample size estimates for the likelihood (\code{exp(log_lik)}) of each observation. This is related to the relative efficiency of estimating the normalizing term in -self-normalizing importance sampling when using posterior draws obtained +self-normalized importance sampling when using posterior draws obtained with MCMC. If MCMC draws are used and \code{r_eff} is not provided then the reported PSIS effective sample sizes and Monte Carlo error estimates -will be over-optimistic. If the posterior draws are independent then -\code{r_eff=1} and can be omitted. The warning message thrown when \code{r_eff} is -not specified can be disabled by setting \code{r_eff} to \code{NA}. See the -\code{\link[loo:relative_eff]{relative_eff()}} helper functions for computing \code{r_eff}.} +can be over-optimistic. If the posterior draws are (near) independent then +\code{r_eff=1} can be used. \code{r_eff} has to be a scalar (same value is used +for all observations) or a vector with length equal to the number of +observations. The default value is 1. See the \code{\link[loo:relative_eff]{relative_eff()}} helper +functions for help computing \code{r_eff}.} \item{force}{If the LOO criterion has already been added to the model object with \code{\link[=add_criterion]{add_criterion()}}, should it be recalculated. Default is \code{FALSE}.} diff --git a/man/measrfit-class.Rd b/man/measrfit-class.Rd index cad99b9..c6ed252 100644 --- a/man/measrfit-class.Rd +++ b/man/measrfit-class.Rd @@ -3,7 +3,6 @@ \docType{class} \name{measrfit-class} \alias{measrfit-class} -\alias{measrfit} \title{Class \code{measrfit} of models fitted with the \strong{measr} package} \description{ Models fitted with the \strong{measr} package are represented as a \code{measrfit} diff --git a/man/measrfit.Rd b/man/measrfit.Rd new file mode 100644 index 0000000..43913f7 --- /dev/null +++ b/man/measrfit.Rd @@ -0,0 +1,105 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/measrfit-class.R +\name{measrfit} +\alias{measrfit} +\title{Create a \code{measrfit} object} +\usage{ +measrfit( + data = list(), + type = character(), + prior = default_dcm_priors(type = type), + stancode = character(), + method = character(), + algorithm = character(), + backend = character(), + model = NULL, + respondent_estimates = list(), + fit = list(), + criteria = list(), + reliability = list(), + file = NULL, + version = list(), + class = character() +) +} +\arguments{ +\item{data}{The data and Q-matrix used to estimate the model.} + +\item{type}{The type of DCM that was estimated.} + +\item{prior}{A \link[=measrprior]{measrprior} object containing information on +the priors used in the model.} + +\item{stancode}{The model code in \strong{Stan} language.} + +\item{method}{The method used to fit the model.} + +\item{algorithm}{The name of the algorithm used to fit the model.} + +\item{backend}{The name of the backend used to fit the model.} + +\item{model}{The fitted Stan model. This will object of class +\link[rstan:stanfit-class]{rstan::stanfit} if \code{backend = "rstan"} and +\href{https://mc-stan.org/cmdstanr/reference/CmdStanMCMC.html}{\code{CmdStanMCMC}} +if \code{backend = "cmdstanr"} was specified when fitting the model.} + +\item{respondent_estimates}{An empty list for adding estimated person +parameters after fitting the model.} + +\item{fit}{An empty list for adding model fit information after fitting the +model.} + +\item{criteria}{An empty list for adding information criteria after fitting +the model.} + +\item{reliability}{An empty list for adding reliability information after +fitting the model.} + +\item{file}{Optional name of a file which the model objects was saved to +or loaded from.} + +\item{version}{The versions of \strong{measr}, \strong{Stan}, \strong{rstan} and/or +\strong{cmdstanr} that were used to fit the model.} + +\item{class}{Additional classes to be added (e.g., \code{measrdcm} for a +diagnostic classification model).} +} +\value{ +A \link[=measrfit-class]{measrfit} object. +} +\description{ +Models fitted with \strong{measr} are represented as a \code{measrfit} object. If a +model is estimated with \emph{Stan}, but not \strong{measr}, a \code{measrfit} object can be +created in order to access other functionality in \strong{measr} (e.g., model fit, +reliability). +} +\examples{ +\dontshow{if (measr_examples()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} +rstn_mdm_lcdm <- measr_dcm( + data = mdm_data, missing = NA, qmatrix = mdm_qmatrix, + resp_id = "respondent", item_id = "item", type = "lcdm", + method = "optim", seed = 63277, backend = "rstan" +) + +new_obj <- measrfit( + data = rstn_mdm_lcdm$data, + type = rstn_mdm_lcdm$type, + prior = rstn_mdm_lcdm$prior, + stancode = rstn_mdm_lcdm$stancode, + method = rstn_mdm_lcdm$method, + algorithm = rstn_mdm_lcdm$algorithm, + backend = rstn_mdm_lcdm$backend, + model = rstn_mdm_lcdm$model, + respondent_estimates = rstn_mdm_lcdm$respondent_estimates, + fit = rstn_mdm_lcdm$fit, + criteria = rstn_mdm_lcdm$criteria, + reliability = rstn_mdm_lcdm$reliability, + file = rstn_mdm_lcdm$file, + version = rstn_mdm_lcdm$version, + class = "measrdcm" +) +\dontshow{\}) # examplesIf} +} +\seealso{ +\linkS4class{measrfit}, \code{\link[=measr_dcm]{measr_dcm()}} +} diff --git a/man/model_evaluation.Rd b/man/model_evaluation.Rd index 79641e2..2aa1146 100644 --- a/man/model_evaluation.Rd +++ b/man/model_evaluation.Rd @@ -56,13 +56,14 @@ file will not be updated.} \item{r_eff}{Vector of relative effective sample size estimates for the likelihood (\code{exp(log_lik)}) of each observation. This is related to the relative efficiency of estimating the normalizing term in -self-normalizing importance sampling when using posterior draws obtained +self-normalized importance sampling when using posterior draws obtained with MCMC. If MCMC draws are used and \code{r_eff} is not provided then the reported PSIS effective sample sizes and Monte Carlo error estimates -will be over-optimistic. If the posterior draws are independent then -\code{r_eff=1} and can be omitted. The warning message thrown when \code{r_eff} is -not specified can be disabled by setting \code{r_eff} to \code{NA}. See the -\code{\link[loo:relative_eff]{relative_eff()}} helper functions for computing \code{r_eff}.} +can be over-optimistic. If the posterior draws are (near) independent then +\code{r_eff=1} can be used. \code{r_eff} has to be a scalar (same value is used +for all observations) or a vector with length equal to the number of +observations. The default value is 1. See the \code{\link[loo:relative_eff]{relative_eff()}} helper +functions for help computing \code{r_eff}.} \item{method}{A vector of model fit methods to evaluate and add to the model object.} diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index 6b1312d..8374181 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -79,6 +79,8 @@ reference: - contents: - measr_dcm - measrfit-class + - measrfit + - as_measrfit - title: Model evaluation diff --git a/tests/testthat/test-mcmc.R b/tests/testthat/test-mcmc.R index 4f7bf17..f8cd29a 100644 --- a/tests/testthat/test-mcmc.R +++ b/tests/testthat/test-mcmc.R @@ -29,6 +29,11 @@ if (!identical(Sys.getenv("NOT_CRAN"), "true")) { ) } +test_that("validation works", { + expect_identical(validate_measrfit(cmds_mdm_lcdm), cmds_mdm_lcdm) + expect_identical(validate_measrfit(cmds_mdm_dina), cmds_mdm_dina) +}) + test_that("as_draws works", { skip_on_cran() diff --git a/tests/testthat/test-measrfit-class.R b/tests/testthat/test-measrfit-class.R new file mode 100644 index 0000000..22404dc --- /dev/null +++ b/tests/testthat/test-measrfit-class.R @@ -0,0 +1,34 @@ +test_that("validation works", { + expect_identical(validate_measrfit(rstn_dina), rstn_dina) + expect_identical(validate_measrfit(rstn_dino), rstn_dino) +}) + +test_that("creation works", { + expect_identical( + rstn_dina, + measrfit( + data = rstn_dina$data, + type = rstn_dina$type, + prior = rstn_dina$prior, + stancode = rstn_dina$stancode, + method = rstn_dina$method, + algorithm = rstn_dina$algorithm, + backend = rstn_dina$backend, + model = rstn_dina$model, + respondent_estimates = rstn_dina$respondent_estimates, + fit = rstn_dina$fit, + criteria = rstn_dina$criteria, + reliability = rstn_dina$reliability, + file = rstn_dina$file, + version = rstn_dina$version, + class = "measrdcm" + ) + ) +}) + +test_that("coercion works", { + expect_identical( + rstn_dino, + as_measrfit(rstn_dino, class = "measrdcm") + ) +}) diff --git a/vignettes/articles/fits/ecpe-lcdm.rds b/vignettes/articles/fits/ecpe-lcdm.rds index a57f138..e0432e2 100644 Binary files a/vignettes/articles/fits/ecpe-lcdm.rds and b/vignettes/articles/fits/ecpe-lcdm.rds differ diff --git a/vignettes/fits/ecpe-optim-lcdm.rds b/vignettes/fits/ecpe-optim-lcdm.rds index 0b157a6..3fcfee1 100644 Binary files a/vignettes/fits/ecpe-optim-lcdm.rds and b/vignettes/fits/ecpe-optim-lcdm.rds differ