R code snippets for Item Response Theory and Wright Maps. This site is no longer active. For information about the WrightMap package visit WrightMap.org
Install Theme

WrightMap Tutorial 4 - More Flexibility

Using the person and item side functions

Introduction

Version 1.2 of the WrightMap package allows you to directly access the functions used for drawing the person and item sides of the map in order to allow more flexible item person maps. The parts can be put together on the same plot using the split.screen function.

Calling the functions

Let’s start by installing the latest version of the package from CRAN.

install.packages('WrightMap')
library(WrightMap)

And set up some item data.

items.loc <- sort( rnorm( 20))
  thresholds <- data.frame(
    l1 = items.loc - 0.5 ,
    l2 = items.loc - 0.25,
    l3 = items.loc + 0.25,
    l4 = items.loc + 0.5)

We can draw a simple item map by calling one of the item side functions. Currently there are three: itemModern, itemClassic, and itemHist.

The itemModern function is the default called by wrightMap.

itemModern(thresholds)

The itemClassic function creates item sides inspired by text-based Wright Maps.

itemClassic(thresholds)

Finally, the itemHist function plots the items as a histogram.

itemHist(thresholds)

Similarly, the person side functions allow you to graph the person parameters. There are two, personHist and personDens.

## Mock results
  multi.proficiency <- data.frame(
    d1 = rnorm(1000, mean =  -0.5, sd = .5),
    d2 = rnorm(1000, mean =   0.0, sd = 1),
    d3 = rnorm(1000, mean =  +0.5, sd = 1),
    d4 = rnorm(1000, mean =   0.0, sd = .5),
    d5 = rnorm(1000, mean =  -0.5, sd = .75))
personHist(multi.proficiency)

personDens(multi.proficiency)

To use these plots in a Wright Map, use the item.side and person.side parameters.

wrightMap(multi.proficiency,thresholds,item.side = itemClassic,person.side = personDens)

Use with CQmodel: The personData and itemData functions

The person side and item side functions are expecting data in the form of matrices. They do not recognize CQmodel objects. When a CQModel object is sent to wrightMap, it first extracts the necessary data, and then sends the data to the plotting functions. In 1.2, the data processing functions have also been made directly accessible to users in the form of the personData and itemData functions. These are fast ways to pull the data out of a CQmodel object in such a way that it is ready to be sent to wrightMap or any of the item and person plotting functions.

The personData function is very simple. It can take either a CQmodel object or a string containing the name of a ConQuest person parameter file. It extracts the person estimates as a matrix.

fpath <- system.file("extdata", package="WrightMap")
model1 <- CQmodel(file.path(fpath,"ex7a.eap"), file.path(fpath,"ex7a.shw"))
head(model1$p.est)
##   casenum est (d1) error (d1) pop (d1) est (d2) error (d2) pop (d2)
## 1       1  1.37364    0.70308  0.60309  1.73654    0.60556  0.52928
## 2       2 -0.17097    0.64866  0.66216  0.75620    0.54852  0.61379
## 3       3  0.46677    0.64837  0.66246  0.85146    0.55129  0.60987
## 4       4  0.67448    0.66017  0.65006  1.16098    0.56368  0.59214
## 5       5  0.89717    0.67704  0.63195  1.49079    0.58539  0.56012
## 6       6  1.64704    0.72529  0.57762  2.11784    0.62916  0.49188
m1.person <- personData(model1)
head(m1.person)
##         d1      d2
## 1  1.37364 1.73654
## 2 -0.17097 0.75620
## 3  0.46677 0.85146
## 4  0.67448 1.16098
## 5  0.89717 1.49079
## 6  1.64704 2.11784
personHist(m1.person,dim.lab.side = 1)

The itemData function uses the GIN table (Thurstonian thresholds) if it is there, and otherwise tries to create delta parameters out of the RMP tables. You can also specify tables to use as items, steps, and interactions, and it will add them together appropriately to create delta parameters.

model2 <- CQmodel(file.path(fpath,"ex4a.mle"), file.path(fpath,"ex4a.shw"))
names(model2$RMP)
## [1] "rater"                     "topic"                    
## [3] "criteria"                  "rater*topic"              
## [5] "rater*criteria"            "topic*criteria"           
## [7] "rater*topic*criteria*step"
m2.item <- itemData(model2,item.table = "topic", interactions = "rater*topic", step.table = "rater")
itemModern(m2.item)

See Tutorial 3 for details on specifying tables from CQmodel objects.

Having these data functions pulled out also makes it easier to combine parameters from different models onto a single plot (when appropriate).

wrightMap(m1.person,m2.item)

Putting it all together with split.screen

By calling these functions directly and using, we can make Wright Maps with other arrangements of persons and items. The item side functions can be combined using any of the base graphics options for combining plots (layout, par(mfrow)), but the person side functions are based on split.screen, which is incompatible with those options. We will be combining item and person maps, so we need to use split.screen.

The first step of combining these functions is to set up the screens. Details for screen functions are in the documentation for split.screen. The function takes as a parameter a 4-column matrix, in which each row is a screen, and the columns represent the left, bottom, right, and top of the screens respectively. Each value is expressed as a number from 0 to 1, where 0 is the left/bottom of the current device and 1 is the right/top.

To make a Wright Map with the items on the left and the persons on the right, we will set up two screens, with 80% of the width on the left and 20% on the right.

split.screen(figs = matrix(c(0,.8,0,1
                            ,.8,1,0,1),ncol = 4, byrow = TRUE)))

Next, we’ll draw the item side. IMPORTANT NOTE: Make sure to explicitly set the yRange variable when combining plots to ensure they are on the same scale. We can also adjust some of the other parameters to work better with a left-side item plot. We’ll move the logit axis to the left with the show.axis.logit parameter, and set the righthand outer margin to 2 to give us a space between the plots.

itemModern(thresholds, yRange = c(-3,4), show.axis.logits = "L", oma = c(0,0,0,2))

We can also add a title at this time.

mtext("Wright Map", side = 3, font = 2, line = 1)

Finally, we will move to screen 2 and draw the person side. This plot will be adjusted to move the persons label and remove the axis.

screen(2)
personHist(multi.proficiency, axis.persons = "",yRange = c(-3,4)
    , axis.logits = "Persons", show.axis.logits = FALSE)

The last thing to do is to close all the screens to prevent them from getting in the way of any future plotting.

close.screen(all.screens = TRUE)

Here is the complete plot:

 split.screen(figs = matrix(c(0,.8,0,1,.8,1,0,1),ncol = 4, byrow = TRUE)) 
    itemModern(thresholds, yRange = c(-3,4), show.axis.logits = "L", oma = c(0,0,0,2))
    mtext("Wright Map", side = 3, font = 2, line = 1)
    screen(2)
    personHist(multi.proficiency, axis.persons = "",yRange = c(-3,4)
    , axis.logits = "Persons", show.axis.logits = FALSE)
    close.screen(all.screens = TRUE)

Countless arrangements are possible. As one last example, here are two ways to put two dimensions put side by side in separate Wright Maps.

Explicitly splitting the device into four screens:

  d1 = rnorm(1000, mean =  -0.5, sd = 1)
    d2 = rnorm(1000, mean =   0.0, sd = 1)

    dim1.diff <- rnorm(5)
    dim2.diff <- rnorm(5)
  
    split.screen(figs = matrix(c(0,.09,0,1,
                                .11,.58,0,1,
                                .5,.59,0,1,
                                .51,1,0,1),ncol = 4,byrow = TRUE))
                              
    personDens(d1,yRange = c(-3,3),show.axis.logits = FALSE
  , axis.logits = "")
    screen(2)
    itemModern(dim1.diff,yRange = c(-3,3),show.axis.logits = FALSE)
    mtext("Wright Map", side = 3, font = 2, line = 1)
    screen(3)
    personDens(d2,yRange = c(-3,3),show.axis.logits = FALSE
  , axis.logits = ""
  , axis.persons = "",dim.names = "Dim2")
    screen(4)
    itemModern(dim2.diff,yRange = c(-3,3),show.axis.logits = FALSE
  , label.items = paste("Item",6:10))

    close.screen(all.screens = TRUE)

Splitting the device into two screens with a Wright Map on each:

    split.screen(figs = matrix(c(0,.5,0,1,
                                .5,1,0,1),ncol = 4,byrow = TRUE))
                              
    wrightMap(d1,dim1.diff,person.side = personDens,show.axis.logits = FALSE)
    screen(2)
    wrightMap(d2,dim2.diff,person.side = personDens,show.axis.logits = FALSE)
    close.screen(all.screens = TRUE)

New release - WrightMap 1.2

A (too long in the making) new version of WrightMap is now available on CRAN (Actually, version 1.2.1 is already up).

This new version includes a lot of features that have long been requested, including cut-point lines, alternative item displays (classical Wright Map look, histogram view), and differential item functioning plots among others.

Wright Map - Simple ExampleWe have updated the package tutorials on our website to reflect the changes to this version, and we will be publishing additional posts showing the new features.

We hope these changes can be of use to you, and we look forward to hearing your feedback!

Change log for this version:

Major changes:

wrightMap split into data handling and plotting, each of which is further split into person & item side. The data handling functions are further split by filetype and the plotting functions are split by plot types. For data handling, this is meant to make it possible to have item and person data of different filetypes. For plotting, this is meant to make the plotting function more flexible and make it easier to add different plot styles. Details:

  • wrightMap.default, wrightMap.CQmodel, wrightMap.character are now removed. There is only wrightMap.R, which calls the appropriate data handling and plotting functions

  • helper functions for data handling

  • parameter person.side accepts person plot functions personHist (default) and personDens

  • parameter item.side accepts item plot functions itemModern (default), itemClassic, and itemHist

Wright Map - Double Histogram Wright Map

API changes

  • the “type” parameter in wrightMap is now called “item.type” to avoid collision with the “type” parameter in the “plot” function.

  • For the same reason, the “type” parameter in fitgraph is now called “fit.type”

  • the “use.hist” parameter in wrightMap is deprecated. Create a histogram with person.side = personHist (default) and density with person.side = personDens

  • fitgraph and make.thresholds now explicitly include a version for numeric and matrix respectively. This should fix namespace problems for users who prefer to include external functions with :: notation.

New functions:

  • plotCI
  • difplot
  • ppPlot

image

New features:

  • now possible to add points and ranges to person side

  • can easily add cutpoints on item side

  • added “classic” and “hist” item map

  • “throld” parameters added to make.thresholds and wrightMap, allowing for the calculation of thresholds other than .5

  • “alpha” and “c.params” added to make.thresholds and wrightMap, supporting the 2PL and 3PL models

  • label.items.cex and dim.lab.cex added to wrightMap to control label sizes

  • support for ConQuest4 added

  • “equation” added to CQmodel and wrightMap.CQmodel, to handle CQ output without a summary of estimation table

  • it is now possible to plot the person and item sides separately

Improvements:

  • wrightMap now remembers your original graphical parameters and restores them after drawing the map

  • fitgraph no longer calls a new window

  • better handling of generics

Bugfix:

  • Fixes to CQmodel on reading the #IO variance errors and included fix for the var/covar matrix on CQ3

  • Fixed a bug where thr.lab.pos couldn’t take a matrix

  • Fixed a bug where fitgraph.CQmodel assumed there was always a parameter called “item” in your table

  • Fixed a bug where CQmodel assumed there were always an error column in the RMP tables

  • wrightMap will no longer crash if the p.est parameter is null

Other notes:

  • Removed some of the runtime messages

Anonymous asked: Hi, Is there a way to upsize the x-axis-labels? Changing positions for threshold labels with mix of dichotomous, three and four cat polytomous items by matrix did not work, although all other manipulations of thr-lab-appearance work fine. Very best regards, Roman

Hi, Is there a way to upsize the x-axis-labels?

Right now there is no way to change the size of the x-axis labels. We’ll try to add it in the next version!

Changing positions for threshold labels with mix of dichotomous, three and four cat polytomous items by matrix did not work, although all other manipulations of thr-lab-appearance work fine.

Do you mean the thr.lab.pos parameter? You’re right, this is a bug. The function is not set up to work correctly if you send it a matrix, despite what we claim in the manual. Oops. We will definitely try to fix this in the next version.

Thanks for the reports!

WrightMap: Multifaceted models

We received an email from a user who was interested in displaying results from a multifaceted model in WrightMap. In the WrightMap manual, we show how to use multifaceted results from ConQuest:

fpath <- system.file("extdata", package = "WrightMap")

model4 <- CQmodel(file.path(fpath, "ex4a.mle"), file.path(fpath, "ex4a.shw"))
wrightMap(model4, item.table = "rater", interactions = "rater*topic", step.table = "topic")
image

(See this tutorial for more details.)

But if your results aren’t from ConQuest, this example won’t be as useful to you. However, the basic WrightMap function is program-independent. If you can put your results into a matrix, WrightMap can graph it. The most important thing to remember is that WrightMap treats rows as “items” and columns as “steps”. So if you would like to make a graph like the one above with your data, each rater (Amy, Beverley, etc.) should be associated with a column, and each topic (School, Family) with a row.

For this example, we will be using results from the R package TAM (created by Thomas Kiefer, Alexander Robitzsch, and Margaret Wu). We discussed using TAM results in WrightMap in this post and this follow-up, using a simple dichotomous model. For this tutorial, we’ll use a item * rater model.

The setup and comments here are taken from the TAM manual:

library(TAM)
library(WrightMap)
data(data.ex10)
dat <- data.ex10

facets <- dat[, "rater", drop = FALSE]  # define facet (rater)
pid <- dat$pid  # define person identifier (a person occurs multiple times)
resp <- dat[, -c(1:2)]  # item response data
formulaA <- ~item * rater  # formula

mod <- tam.mml.mfr(resp = resp, facets = facets, formulaA = formulaA, pid = dat$pid)

persons.mod <- tam.wle(mod)
theta <- persons.mod$theta

The tam.thresholds command provides us with the estimated difficulty for each item-by-rater (item + rater + item * rater).

thr <- tam.threshold(mod)
item.labs <- c("I0001", "I0002", "I0003", "I0004", "I0005")
rater.labs <- c("rater1", "rater2", "rater3")

Now we need to turn it into a matrix formatted the way WrightMap expects. We could organize it by item:

thr1 <- matrix(thr, nrow = 5, byrow = TRUE)
wrightMap(theta, thr1, label.items = item.labs, thr.lab.text = rep(rater.labs, each = 5))
image

Or by rater:

thr2 <- matrix(thr, nrow = 3)
wrightMap(theta, thr2, label.items = rater.labs, thr.lab.text = rep(item.labs,  each = 3), axis.items = "Raters")
image

Another option is to show the item, rater, and item * rater parameters separately. We can get these from the xsi.facets table.

pars <- mod$xsi.facets$xsi
facet <- mod$xsi.facets$facet

item.par <- pars[facet == "item"]
rater.par <- pars[facet == "rater"]
item_rat <- pars[facet == "item:rater"]

We could put them in separate bands, adding NA to make them all the same length.

len <- length(item_rat)
item.long <- c(item.par, rep(NA, len - length(item.par)))
rater.long <- c(rater.par, rep(NA, len - length(rater.par)))
ir.labs <- mod$xsi.facets$parameter[facet == "item:rater"]

wrightMap(theta, rbind(item.long, rater.long, item_rat), label.items = c("Items",  "Raters", "Item*Raters"), thr.lab.text = rbind(item.labs, rater.labs, ir.labs), axis.items = "")
image

But the item*rater band is a little crowded. So let’s separate it by rater:

ir_rater<- matrix(item_rat, nrow = 3, byrow = TRUE)

wrightMap(theta, rbind(item.par, c(rater.par, NA, NA), ir_rater), label.items = c("Items", "Raters", "Item*Raters (R1)", "Item*Raters (R2)", "Item*Raters (R3)"), axis.items = "", thr.lab.text = rbind(item.labs, rater.labs, matrix(item.labs, nrow = 3, ncol = 5, byrow = TRUE)))
image

Or by item:

ir_item <- matrix(item_rat, nrow = 5)

wrightMap(theta, rbind(item.par, c(rater.par, NA, NA), cbind(ir_item, NA, NA)), label.items = c("Items", "Raters", "Item*Raters (I1)", "Item*Raters (I2)", "Item*Raters (I3)", "Item*Raters (I4)", "Item*Raters (I5)"), axis.items = "", thr.lab.text = rbind(item.labs, matrix(c(rater.labs, NA, NA), nrow = 6, ncol = 5, byrow = TRUE)))
image

WrightMap and TAM - Example continued…

As a follow up on the previous post on integrating the TAM and WrightMap packages, we received a message from one of the TAM developers, Alexander Robitzsch, suggesting that it is possible to generate the Wright Map directly from the MML estimated distribution (instead of using the WLE estimates used in the previous post).

Let’s start with the same setup:

library(TAM)
library(WrightMap)
data( sim.rasch )
str( sim.rasch )
dat <- sim.rasch

# Run Rasch Model
mod1 <- tam.mml( dat )
summary( mod1 )
`</pre>

After estimating the Rasch model in our data, we can get the item parameters directly from our model object:

<pre>`difficulties <- tam.threshold( mod1 )
`</pre>

Now, for the new part, Alexander has sent us this snippet to recover the estimated person distribution:

<pre>`uni.proficiency <- rep( mod1$theta[,1] , round( mod1$pi.k * mod1$ic$n) )
`</pre>

This extracts the ability distribution node locations as a vector, and then repeats each entry in the vector a specific number of times, according to the weight assigned that location in the distribution. This results in a vector of abilities that can be used to plot that distribution on `wrightMap`:

<pre>`wrightMap( thetas = uni.proficiency, thresholds = mod1$xsi[,1], label.items.rows = 3)

Wright Map - MML Distribution

This final Wright Map shows the distribution based on the nodes and weights estimated directly as part of the marginal maximum likelihood estimation, without needing to resort to generating person estimates on a separate step.