mv_ask() writes a
complete R script from a plain-language description of the analysis you
want. You check the script, then run it or save it. The script is
ordinary, commented R code, so you can rerun it later, share it with a
colleague, or learn R from it.
library(mirtverse)
#> Loading required package: mirt
#> Loading required package: stats4
#> Loading required package: latticeHere is a small data set: twelve 0/1 items, a gender column and a student ID.
set.seed(1)
a <- matrix(rlnorm(12, 0.2, 0.25)); d <- matrix(rnorm(12))
mydata <- data.frame(student_id = 1:800, simdata(a, d, 800, itemtype = "2PL"),
gender = rep(c("F", "M"), 400))
names(mydata)[2:13] <- paste0("Q", 1:12)
scr <- mv_ask("Fit a 2PL, check DIF by gender and make a report", data = mydata,
provider = "none")
scr
#> Request: Fit a 2PL, check DIF by gender and make a report
#> Understood by the built-in keyword parser
#>
#> Analysis plan
#> - Data: mydata
#> - Items: 12 items (Q1 ... Q12)
#> - Model: 2PL, 1 dimension
#> - DIF by: gender
#> - Steps: fit, dif, report
#> Assumptions
#> - Used the 12 columns that look like item responses as items.
#>
#> R script (run it with run_script(), save it with save_script())
#> ----------------------------------------------------------------
#> # Analysis script written by mirtverse::mv_ask()
#> # 2026-10-29 22:04
#> # Request: Fit a 2PL, check DIF by gender and make a report
#> # Assumptions:
#> # - Used the 12 columns that look like item responses as items.
#> library(mirtverse)
#>
#> # Data
#> raw <- mydata
#> items <- raw[, c("Q1", "Q2", "Q3", "Q4", "Q5", "Q6", "Q7", "Q8", "Q9", "Q10", "Q11",
#> "Q12")]
#> group <- raw$gender
#>
#> # Fit the 2PL model
#> mod <- mirt(items, 1, itemtype = "2PL")
#> glance(mod)
#>
#> # Differential item functioning by gender
#> dif_res <- dif(items, group = group, model = "2PL")
#> dif_res
#> gg_dif(dif_res)
#> interpret(dif_res)
#>
#> # Report
#> mv_report(mod, file = "mirtverse-report.html", dif = dif_res)The printout shows how the request was understood (the plan), any assumptions mirtverse made, and the script. Nothing has been run yet.
run_script(scr) runs it and returns the objects it
created (mod, dif_res, …).
run_script(scr, check = TRUE) first runs the whole script
on simulated data with the same structure as yours, so errors show up
before your data are touched.save_script(scr, "analysis.R", open = TRUE) writes it
to a file and opens it in RStudio or Positron.Pass the script back with the change you want; the rest of the plan is kept.
scr <- mv_ask(scr, "use a 3PL instead and save EAP scores to scores.csv")
scr$plan
#> Analysis plan
#> - Data: mydata
#> - Items: 12 items (Q1 ... Q12)
#> - Model: 3PL, 1 dimension
#> - DIF by: gender
#> - Steps: fit, dif, scores, report
#> Assumptions
#> - Used the 12 columns that look like item responses as items.By default a built-in keyword parser reads the request. It works offline, needs nothing installed, and when something is unclear (for example, “check DIF” in a data set with several possible grouping columns) it asks a short question in the console.
For freer wording, connect an AI model once with
mv_ask_setup():
mv_ask_setup("claude") # Anthropic Claude, your own API key
mv_ask_setup("openai") # OpenAI GPT, your own API key
mv_ask_setup("ollama", model = "qwen2.5") # a free model on your own computerThe setup asks for the key with a masked prompt, sends one tiny test
request, and offers to save the settings in your .Renviron
file or the system keychain, so you only do it once. Keys are never
written into scripts or reports. Ollama (https://ollama.com) runs the model on your own machine,
so nothing leaves it; it needs about 8 GB of memory for a mid-size
model.
Whatever the model, it only fills in the plan. The R code always comes from mirtverse’s tested templates, and the plan is checked against your data before any code is written (do the columns exist, does the group have at least two levels, is a 2PL being asked of items with more than two categories).
Never individual responses. The model receives your request and a
summary of each column: its type, number of distinct values, range,
share missing, its guessed role, and the category labels of likely
grouping columns. This is what profile_data() shows:
profile_data(mydata)
#> Data profile: 800 rows, 14 columns (12 look like items)
#> column type n_distinct min max prop_missing role levels
#> student_id integer 800 1 800 0 id
#> Q1 integer 2 0 1 0 item
#> Q2 integer 2 0 1 0 item
#> Q3 integer 2 0 1 0 item
#> Q4 integer 2 0 1 0 item
#> Q5 integer 2 0 1 0 item
#> Q6 integer 2 0 1 0 item
#> Q7 integer 2 0 1 0 item
#> Q8 integer 2 0 1 0 item
#> Q9 integer 2 0 1 0 item
#> Q10 integer 2 0 1 0 item
#> Q11 integer 2 0 1 0 item
#> Q12 integer 2 0 1 0 item
#> gender text 2 NA NA 0 group F, MTo see exactly what would be sent, use dry_run = TRUE.
With anonymize = TRUE the column names are replaced by V1,
V2, … (also inside your request) and category labels are dropped;
mirtverse maps the names back locally.
mv_ask("check DIF by gender", data = mydata, anonymize = TRUE, dry_run = TRUE)
#> No AI model is set up, so nothing would be sent; an AI model would receive:
#>
#> Request: check DIF by V14
#>
#> Data: 800 rows, 14 columns.
#> Columns (name | type | distinct values | min | max | share missing | guessed role | categories):
#> - V1 | integer | 800 | 1 | 800 | 0.00 | id |
#> - V2 | integer | 2 | 0 | 1 | 0.00 | item |
#> - V3 | integer | 2 | 0 | 1 | 0.00 | item |
#> - V4 | integer | 2 | 0 | 1 | 0.00 | item |
#> - V5 | integer | 2 | 0 | 1 | 0.00 | item |
#> - V6 | integer | 2 | 0 | 1 | 0.00 | item |
#> - V7 | integer | 2 | 0 | 1 | 0.00 | item |
#> - V8 | integer | 2 | 0 | 1 | 0.00 | item |
#> - V9 | integer | 2 | 0 | 1 | 0.00 | item |
#> - V10 | integer | 2 | 0 | 1 | 0.00 | item |
#> - V11 | integer | 2 | 0 | 1 | 0.00 | item |
#> - V12 | integer | 2 | 0 | 1 | 0.00 | item |
#> - V13 | integer | 2 | 0 | 1 | 0.00 | item |
#> - V14 | text | 2 | | | 0.00 | group |The plan is an ordinary R object, so you can also skip the request and write it yourself:
pl <- mv_plan(items = "Q1:Q10", model = "Rasch", missing = 9,
steps = c("describe", "fitcheck", "scores", "report"))
mv_ask(data = mydata, plan = pl)
#> Analysis plan
#> - Data: mydata
#> - Items: 10 items (Q1 ... Q10)
#> - Missing codes: 9
#> - Model: Rasch, 1 dimension
#> - Steps: fit, describe, fitcheck, scores, report
#>
#> R script (run it with run_script(), save it with save_script())
#> ----------------------------------------------------------------
#> # Analysis script written by mirtverse::mv_ask()
#> # 2026-10-29 22:04
#> library(mirtverse)
#>
#> # Data
#> raw <- mydata
#> items <- raw[, c("Q1", "Q2", "Q3", "Q4", "Q5", "Q6", "Q7", "Q8", "Q9", "Q10")]
#> items[items == 9] <- NA
#>
#> # Describe the items
#> itemstats(items)
#>
#> # Fit the Rasch model
#> mod <- mirt(items, 1, itemtype = "Rasch")
#> glance(mod)
#>
#> # Check model and item fit
#> item_table(mod)
#> gg_itemfit(mod)
#> gg_q3(mod)
#>
#> # Person scores
#> scores <- augment(mod, method = "EAP")
#> gg_scores(mod)
#>
#> # Report
#> mv_report(mod, file = "mirtverse-report.html")