0

I created a function that calculates dPrime. The function takes a data frame as its argument. This works fine, however the columns must be called "stimDiff" and "stimSame", as the function calculates dPrime using these specific names. I would like to apply this function to a data frame that has multiple subjects, and be able to calculate dPrime for each subject, with the result being a new data frame with the dPrime score of each subject. The test data frame looks like this:

stimDiff0 <- c(rep("diff", 20), rep("same", 5))
stimSame0 <- c(rep("diff", 10), rep("same", 15))

stimDiff1 <- c(rep("diff", 10), rep("same", 15))
stimSame1 <- c(rep("diff", 10), rep("same", 15))

stimDiff2 <- c(rep("diff", 19), rep("same", 6))
stimSame2 <- c(rep("diff", 11), rep("same", 14))

stimDiff3 <- c(rep("diff", 21), rep("same", 4))
stimSame3 <- c(rep("diff",  9), rep("same", 16))

stimDiff4 <- c(rep("diff", 18), rep("same", 7))
stimSame4 <- c(rep("diff", 12), rep("same", 13))

stimDiff5 <- c(rep("diff", 22), rep("same", 3))
stimSame5 <- c(rep("diff", 14), rep("same", 11))

stimDiff <- c(stimDiff0, stimDiff1, stimDiff2,
              stimDiff3, stimDiff4, stimDiff5)
stimSame <- c(stimSame0, stimSame1, stimSame2,
              stimSame3, stimSame4, stimSame5)
subject <- rep(0:5, each = 25)

x <- data.frame(subject = subject, stimDiff = stimDiff, stimSame = stimSame)

I am trying to obtain a dPrim by subject data frame using the following code:

tapply(c(x$stimDiff, x$stimSame), x$subject, data = x, FUN = dPrime)

I get the following error:

Error en tapply(list(x$stimDiff, x$stimSame), x$subject, data = x, FUN = dPrime) : 
arguments must have same length

I am aware of the fact that there are packages that can calculate dPrime. I am doing this in order to learn how to write functions. I would prefer to find a solution using base R.

Here is the code for the function dPrime:

dPrime <- function(x) {

# Calculate number of same, diff and total responses
# for the stimuli that were actually different
stimDiffRdiff <- nrow(x[x$stimDiff == 'diff', ])
stimDiffRsame <- nrow(x[x$stimDiff == 'same', ])
stimDiffTotal <- length(x$stimDiff)

# Calculate number of same, diff and total responses
# for the stimuli that were actually the same
stimSameRdiff <- nrow(x[x$stimSame == 'diff', ])
stimSameRsame <- nrow(x[x$stimSame == 'same', ])
stimSameTotal <- length(x$stimSame)

# Hit rate = the number of correct responses 'diff'
# when the stimuli were actually diff, divided by 
# the total number of responses
hitRate <- stimDiffRdiff / stimDiffTotal

# Miss rate = the number of incorrect responses
# 'same' when the stimuli were actually diff
# divided by the total number of responses
missRate <- stimDiffRsame / stimDiffTotal

# False alarm = the number responses 'diff'
# when the stimuli were actually the same
# divided by the total number of responses
falseAlarm <- stimSameRdiff / stimSameTotal

# Correct rejection = the number of responses
# same when the stimuli were actually the same
# divided by the number of total responses
corrReject <- stimSameRsame / stimSameTotal

# Calculate z-critical values for hit rate
# and false alarm rate
zHitRate <- qnorm(hitRate)
zFalseAlarm <- qnorm(falseAlarm)

# Calculate d prime
dPrime <- zHitRate - zFalseAlarm

print(dPrime)
}
babylinguist
  • 392
  • 1
  • 13

3 Answers3

2

To build on @jvcasill's original function and on other users' responses:

dPrime <- function (data, subj = 1, stimDiff = 2, stimSame = 3) {
    # dPrime() returns a vector of the length of the number of subjects
    #+ in data[, subj] that contains the sensitivity index "d'" for each.
    # `data`:     data frame
    # `subj`:     index of "subject" column in `data`; default is 1
    # `stimDiff`: index of "stimDiff" column in `data`; default is 2
    # `stimSame`: index of "stimSame" column in `data`; default is 3
    if (is.data.frame(data)) {
        # Divide `data` by subject with split(), as have done others who've
        #+ responded to this question
        data.by.subj   <- split(data, data[, subj])
        # Calculate number of subjects and create vector of same length
        #+ to return
        n.subj         <- length(data.by.subj)
        dPrime.by.subj <- vector(mode = "double", length = n.subj)
        # Loop through "data.by.subj" subject by subject and calculate d'
        for (subj in seq_len(n.subj)) {
            # For clarity, create temporary data set with data of
            #+ current "subj"
            data.tmp      <- data.by.subj[[subj]]
            stimDiffRdiff <- nrow(data.tmp[data.tmp[, stimDiff] == "diff", ])
            stimDiffRsame <- nrow(data.tmp[data.tmp[, stimDiff] == "same", ])
            stimDiffTotal <- length(data.tmp[, stimDiff])
            stimSameRdiff <- nrow(data.tmp[data.tmp[, stimSame] == "diff", ])
            stimSameRsame <- nrow(data.tmp[data.tmp[, stimSame] == "same", ])
            stimSameTotal <- length(data.tmp[, stimSame])
            hitRate       <- stimDiffRdiff / stimDiffTotal
            missRate      <- stimDiffRsame / stimDiffTotal
            falseAlarm    <- stimSameRdiff / stimSameTotal
            # The following appears unused in the original function
            # corrReject  <- stimSameRsame / stimSameTotal
            zHitRate      <- qnorm(hitRate)
            zFalseAlarm   <- qnorm(falseAlarm)
            dPrime        <- zHitRate - zFalseAlarm
            dPrime.by.subj[subj] <- dPrime
        }
        # For clarity, give each d' value in vector to be returned,
        #+ "dPrime.by.subj", name of corresponding subject
        names(dPrime.by.subj) <- names(data.by.subj)
        return(dPrime.by.subj)
    } else stop("'data' is not a data frame")
}

Note that I'm not sure if the values it returns, which --- for the example data set provided by @jvcasill --- are the same as those obtained with @Splendour's method, make sense.

user109114
  • 403
  • 4
  • 10
1

Try data.table (using length function rather than dPrime):

library(data.table)
xt = data.table(x)
xt[,list(len=length(c(stimSame,stimDiff))),by=subject]
   subject len
1:       0  50
2:       1  50
3:       2  50
4:       3  50
5:       4  50
6:       5  50

With base R:

sapply(split(x, x$subject), dPrime)
[1] 1.094968
[1] 0
[1] 0.8572718
[1] 1.352917
[1] 0.6329951
[1] 1.024018
        0         1         2         3         4         5 
1.0949683 0.0000000 0.8572718 1.3529167 0.6329951 1.0240176 

Duplicate output is because of 'print(dPrime)' statement in your dPrime function. You should replace that by return(dPrime). Better still, since dPrime is a function also, you should replace dPrime in 'dPrime <- zHitRate - zFalseAlarm' statement to some other name, say 'ret':

ret = dPrime <- zHitRate - zFalseAlarm
return(ret)
rnso
  • 23,686
  • 25
  • 112
  • 234
1

Here's a (somewhat inelegant) solution in base R:

Split the dataframe into lists, one per subject:

by.subject <- split(x, x$subject)

Calculate dPrime for every chunk, returning a named numeric vector:

dPrime.values <- unlist(lapply(by.subject, dPrime), use.names=T)

Construct a new dataframe:

df <- data.frame(dPrime=dPrime.values)
df$subject <- as.numeric(rownames(df))
Splendour
  • 106
  • 1
  • 5