File Coverage

blib/lib/Statistics/SDT.pm
Criterion Covered Total %
statement 244 292 83.5
branch 93 140 66.4
condition 24 61 39.3
subroutine 49 56 87.5
pod 11 11 100.0
total 421 560 75.1


line stmt bran cond sub pod time code
1             package Statistics::SDT;
2 8     8   439452 use strict;
  8         60  
  8         192  
3 8     8   41 use warnings;
  8         11  
  8         173  
4 8     8   30 use Carp qw(carp croak);
  8         11  
  8         330  
5 8     8   3656 use List::AllUtils qw(all any);
  8         106455  
  8         572  
6 8     8   2979 use Math::Cephes qw(:dists :explog);
  8         36305  
  8         1940  
7 8     8   3031 use String::Numeric qw(is_int is_float);
  8         16824  
  8         392  
8 8     8   3168 use String::Util qw(hascontent nocontent);
  8         34368  
  8         26224  
9             $Statistics::SDT::VERSION = '0.07';
10            
11             my %counts_dep = (
12             hits => [qw/signal_trials misses/],
13             false_alarms => [qw/noise_trials correct_rejections/],
14             misses => [qw/signal_trials hits/],
15             correct_rejections => [qw/noise_trials false_alarms/],
16             );
17             my %trials_dep = (
18             signal_trials => [qw/hits misses/],
19             noise_trials => [qw/false_alarms correct_rejections/]
20             );
21             my %rates_dep = (
22             hr => [qw/hits signal_trials/],
23             far => [qw/false_alarms noise_trials/]
24             );
25            
26             =head1 NAME
27            
28             Statistics::SDT - Signal detection theory (SDT) measures of sensitivity and bias in frequency data
29            
30             =head1 VERSION
31            
32             This is documentation for B of Statistics::SDT.
33            
34             =head1 SYNOPSIS
35            
36             use Statistics::SDT 0.07;
37             use feature qw{say};
38            
39             my $sdt = Statistics::SDT->new(
40             correction => 1,
41             precision_s => 2,
42             );
43            
44             $sdt->init(
45             hits => 50,
46             signal_trials => 50, # or misses => 0,
47             false_alarms => 17,
48             noise_trials => 25, # or correct_rejections => 8
49             ); # or init these into 'new' &/or pass their values as 2nd arg. hashrefs in calling the following methods
50            
51             say 'Hit rate = ', $sdt->rate('hr'); # or 'far', 'mr', 'crr'
52             say 'Sensitivity d = ', $sdt->sens('d'); # or 'Ad', 'A'
53             say 'Bias beta = ', $sdt->bias('b'); # or 'log', 'c', 'griers'
54             say 'Criterion k = ', $sdt->crit(); # -0.47
55             say 'Hit rate by d & c = ', $sdt->dc2hr(); # .99
56             say 'FAR by d & c = ', $sdt->dc2far(); # .68
57             say 'LogBeta by d & c = ', $sdt->dc2logbeta(); # -2.60
58            
59             # m-AFC:
60             say 'd_fc = ', $sdt->sens('f' => {hr => .866, alternatives => 3, correction => 0, method => 'alexander'})); # or 'smith'
61            
62             =head1 DESCRIPTION
63            
64             This module implements algorithms for Signal Detection Theory (SDT) measures of sensitivity and response-bias, e.g., I, I, I, as based on frequency data. These are largely as defined in Stanislav & Todorov (1999; see L), as well as other sources including Alexander (2006). Output from this module per method are tested for agreement with example data and calculation from those sources.
65            
66             For any particular analysis, (1) create the SDT object with L, (2) initialise the object with relevant data with L, and then (3) call the measure wanted.
67            
68             For those measures that involve I-score transformation of probabilities, this is made via the C function in L, and this is denoted in the equations below by the Greek letter phi^-1 (for inverse phi). The function can be directly accessed by the present module as "Statistics::SDT::ndtri()". The complementary C for converting I-scores into probabilities is also used/available in this way.
69            
70             Most methods assume a yes/no rather than I-AFC design. For I-AFC designs, only sensitivity measures are offered/relevant, approximated from the hit-rate for the given number of hits and signal trials, which are assumed to indicate all trials.
71            
72             =head1 PARAMETERS
73            
74             The following named parameters need to be given as a hash or hash-reference: either to the L constructor method, L, or into each measure-function. To calculate the hit-rate, provide the (i) count of hits and signal-trials, (ii) the counts of hits and misses, or (iii) the count of signal-trials and misses. To calculate the false-alarm-rate, provide (i) the count of false-alarms and noise-trials, (ii) the count of false-alarms and correct-rejections, or (iii) the count of noise-trials and correct-rejections. Or supply the hit-rate and false-alarm-rate. Or see L and L to get back the rates via given/calculated sensitivity and criterion. If a method depends on these counts/rates and they are not provided, or what it depends on cannot be calculated from the provided values, the methods will generally return an empty string.
75            
76             =over 4
77            
78             =item hits => POSINT
79            
80             The number of hits.
81            
82             =item false_alarms => POSINT
83            
84             The number of false alarms.
85            
86             =item signal_trials => POSINT
87            
88             The number of signal trials. The hit-rate is derived by dividing the number of hits by the number of signal trials.
89            
90             =item noise_trials => POSINT
91            
92             The number of noise trials. The false-alarm-rate is derived by dividing the number of false-alarms by the number of noise trials.
93            
94             =item hr => FLOAT [0 .. 1]
95            
96             The hit-rate -- instead of passing the number of hits and signal trials, give the hit-rate directly.
97            
98             =item far => FLOAT [0 .. 1]
99            
100             The false-alarm-rate -- instead of passing the number of false alarms and noise trials, give the false-alarm-rate directly.
101            
102             =item alternatives => POSINT
103            
104             The number of response alternatives; when estimating for a forced-choice rather than yes/no design. If defined (and greater than or equal to 2), then, by default, Smith's (1982) estimate of I is used; otherwise Alexander's.
105            
106             =item correction => POSINT [0, 1, 2, undef]
107            
108             Indicate whether or not to perform a correction on the number of hits and false-alarms when the hit-rate or false-alarm-rate equals 0 or 1 (due, e.g., to strong inducements against false-alarms, or easy discrimination between signals and noise). This is relevant to all functions that make use of the I function (all except I option with L, and the L option with L). As C must die with an error if given 0 or 1, there is a default correction.
109            
110             If B = 0, no correction is performed to calculation of rates. This should only be used when (1) using the parametric measures and the rates will never be at the extremes of 0 and 1; or (2) using only the nonparametric measures (L and L).
111            
112             If B = 1 (default), extreme rates (of 0 and 1) are corrected: 0 is replaced with 0.5 / I; 1 is replaced with (I - 0.5) / I, where I = number of signal or noise trials. This is the most common method of handling extreme rates (Stanislav and Todorov, 1999) but it might bias sensitivity measures and not be as satisfactory as the loglinear transformation applied to all hits and false-alarms, as follows.
113            
114             If B > 1, the loglinear transformation is applied to I values: 0.5 is added to both the number of hits and false-alarms, and 1 is added to the number of signal and noise trials.
115            
116             If B is undefined: To avoid errors thrown by the C function, any values that equal 1 or 0 will be corrected as if it equals 1.
117            
118             =item precision_s => POSINT
119            
120             Precision (I decimal places) of any of the statistics. Default = 0 to have all possible decimals returned.
121            
122             =item method => STR ['smith', 'alexander']
123            
124             Method for estimating I for forced-choice design. Default is I; otherwise I.
125            
126             =back
127            
128             =head1 SUBROUTINES/METHODS
129            
130             =head2 new
131            
132             Creates the class object that holds the values of the parameters, as above, and accesses the following methods (without having to pass the all values again).
133            
134             As well as storing parameter values, the class-object returned by C will stores B
, the hit-rate, and B, the false-alarm-rate. These can be specifically given as named arguments to the method (ensuring that they do not equal zero or 1 in order to avoid errors thrown by the inverse-phi function). Otherwise, calculation of the hit-rate and false-alarm-rate from the given number of signal/noise trials, and hits/misses (etc., as defined above) corrects for this limitation; i.e., correction can only be done by supplying the relevant counts, not just the rate - see the notes on the | option.
135            
136             =cut
137            
138             sub new {
139 5     5 1 335 my ( $class, @args ) = @_;
140 5         12 my $self = {};
141 5         10 bless $self, $class;
142 5         28 $self->init(@args);
143 5         11 return $self;
144             }
145            
146             =head2 init
147            
148             $sdt->init(...)
149            
150             Instead of passing the number of hits, signal-trials, etc., with every call to the measure-functions, or creating a new class object for every set of data, initialise the class object with the values for parameters, key => value pairs, as defined above. This method is called by L (if the parameter values are passed to it). The hit-rates and false-alarm rates are always calculated anew from the hits and signal trials, and the false-alarms and noise trials, respectively; unless a value for one or the other, or both (as hr and far) is passed in a call to L.
151            
152             Each L replaces the values only of those attributes passed to it - any values set in previous Ls are retained for those attributes that are not set in a call to L. To reset everything, first use L
153            
154             The method also stores any given values for L, L, L and L.
155            
156             =cut
157            
158             sub init {
159 32     32 1 1351 my ( $self, @args ) = @_;
160 32 100       68 if ( scalar @args ) { # have some params?
161 18 50       57 my $href = ref $args[0] ? $args[0] : {@args};
162            
163             # Initialise any given counts and arguments:
164 18         86 foreach my $arg (
165             qw/hits false_alarms misses correct_rejections signal_trials noise_trials hr far alternatives states correction precision_s method/
166             )
167             {
168 234 100       351 if ( defined $href->{$arg} ) {
169 59 50       81 if ( $arg eq 'states' ) {
170 0         0 carp
171             'Argument named is deprecated - use the name instead';
172 0         0 $self->{'alternatives'} = $href->{$arg};
173             }
174             else {
175 59         113 $self->{$arg} = $href->{$arg};
176             }
177             }
178             }
179 18   100     54 $self->{'method'} ||= 'smith';
180 18   100     49 $self->{'precision_s'} ||= 0;
181            
182 18         38 _init_performance_counts($self);
183 18         32 _init_trial_counts($self);
184 18         29 _init_hr_far($self);
185             }
186            
187             # no params - assume the values are already in $self
188 32         94 return ( $self->{'hr'}, $self->{'far'}, $self->{'alternatives'} );
189             }
190            
191             # Initialise any missing performance counts of hits, false-alarms, misses & correct rejections
192             ## from what has just been given (just initialised)
193             ## e.g., number of hits from the given number of signal-trials and misses:
194             sub _init_performance_counts {
195 18     18   26 my $self = shift;
196 18         44 foreach ( keys %counts_dep ) {
197 72 100       115 if ( !defined $self->{$_} ) {
198 43 100 66     107 if ( is_float( $self->{ $counts_dep{$_}->[0] } )
199             && is_float( $self->{ $counts_dep{$_}->[1] } ) )
200             {
201             $self->{$_} =
202             $self->{ $counts_dep{$_}->[0] } -
203 1         21 $self->{ $counts_dep{$_}->[1] };
204             }
205             else {
206 42         264 $self->{$_} = 0;
207             }
208             }
209             }
210 18         28 return;
211             }
212            
213             # Initialise any missing trial counts (of number of signal or noise trials) from what has been given,
214             ## e.g., number of signal trials from the sum of hits and misses:
215             sub _init_trial_counts {
216 18     18   23 my $self = shift;
217 18         36 foreach ( keys %trials_dep ) {
218 36 100       66 if ( !defined $self->{$_} ) {
219 21 50 33     50 if ( is_float( $self->{ $trials_dep{$_}->[0] } )
220             && is_float( $self->{ $trials_dep{$_}->[1] } ) )
221             {
222             $self->{$_} =
223             $self->{ $trials_dep{$_}->[0] } +
224 21         346 $self->{ $trials_dep{$_}->[1] };
225             }
226             else {
227 0         0 $self->{$_} = 0;
228             }
229             }
230             }
231 18         23 return;
232             }
233            
234             # Initialise the rates of hits and false-alarms if not already done
235             ## by given counts, e.g., HR from number of hits and signal trials:
236             sub _init_hr_far {
237 18     18   25 my $self = shift;
238 18         30 foreach ( keys %rates_dep ) {
239 36 100 66     162 if ( !defined $self->{$_}
      100        
240             && defined $self->{ $rates_dep{$_}->[0] }
241             && $self->{ $rates_dep{$_}->[1] } )
242             {
243             $self->{$_} = _init_rate(
244             $self->{ $rates_dep{$_}->[0] },
245             $self->{ $rates_dep{$_}->[1] },
246 9         33 $self->{'correction'}
247             );
248             }
249             }
250 18         36 return;
251             }
252            
253             =head2 clear
254            
255             $sdt->clear()
256            
257             Sets all attributes to undef: C, C, C, C, C
, C, C, C, and C.
258            
259             =cut
260            
261             sub clear {
262 10     10 1 2173 my $self = shift;
263 10         20 foreach (
264             qw/hits false_alarms misses correct_rejections signal_trials noise_trials hr far alternatives correction precision_s method/
265             )
266             {
267 120         143 $self->{$_} = undef;
268             }
269 10         14 return;
270             }
271            
272             =head2 rate
273            
274             $sdt->rate('hr|far|mr|crr') # return the indicated rate
275             $sdt->rate(hr => PROB, far => PROB, mr => PROB, crr => PROB) # set 1 or more rate => probability pairs
276             $sdt->rate('hr' => {signal_trials => INT, hits => INT}) # or misses instead of hits
277             $sdt->rate('far' => {noise_trials => INT, false_alarms => INT}) # or correct_rejections instead of false_alarms
278             $sdt->rate('mr' => {signal_trials => INT, misses => INT}) # or hits instead of misses
279             $sdt->rate('crr' => {noise_trials => INT, correct_rejections => INT}) # or false_alarms instead of correct_rejections
280            
281             Generic method to get or set the conditional response proportions:
282            
283             =for html

  HR (hit-rate) = N(Rs|Ss) / N(Ss)

284            
285             =for html

  FAR (false-alarm-rate) = N(Rs|Sn) / N(Sn)

286            
287             =for html

  MR (miss-rate) = N(Rn|Ss) / N(Ss)

288            
289             =for html

  CRR (correct-rejection-rate) = N(Rn|Sn) / N(Sn)

290            
291             where S = stimulus (trial-type, expected response), R = response, subscript I indicates signal-plus-noise trials and I indicates noise-only trials.
292            
293             To I a rate, these string abbreviations do the trick; the method only checks the first letter, so any passable abbreviation will do, case-insensitively. The rate is returned to the precision indicated by the optional L argument (given here or in L).
294            
295             To I a rate for use by other methods (such as for sensitivity or bias), either give the actual proportion as key => value pairs, e.g., HR => .7, or a hashref giving sufficient info to calculate the rate (if this has not already been paased to L).
296            
297             Also performs any required or requested corrections, depending on value of L (given here or in L).
298            
299             Unless the values of the rates are directly given, then they will be calculated from the presently passed counts and trial-numbers, or whatever has been cached of these values. For the hit-rate, there must be a value for L and L, and for the false-alarm-rate, there must be a value for L and L. If these values are not passed, they will be taken from any prior value, unless this has been Led or never existed - in which case expect a C.
300            
301             =cut
302            
303             sub rate {
304 13     13 1 1238 my ( $self, @args ) = @_;
305 13         18 my $rate;
306 13 100       40 if ( scalar @args == 1 ) { # Get the rate:
    50          
307 8         12 local $_ = $args[0];
308             CASE: {
309 8 100       12 /^h/ixsm && do { $rate = $self->_hr(); };
  8         25  
  2         5  
310 8 100       21 /^f/ixsm && do { $rate = $self->_far(); };
  6         25  
311 8 50       17 /^m/ixsm && do { $rate = $self->_mr(); };
  0         0  
312 8 50       17 /^c/ixsm && do { $rate = $self->_crr() };
  0         0  
313             } #end CASE
314             }
315             ##else {
316             elsif ( scalar @args > 1 ) { # Set the rate:
317 5         13 my %params = @args;
318 5         12 foreach ( keys %params ) {
319             my @args2 =
320             ref $params{$_}
321 2         6 ? %{ $params{$_} }
322 6 100       23 : $params{$_}; # hash(ref) to ari
323             CASE: {
324 6 100       9 /^h/ixsm && do { $rate = $self->_hr(@args2); last CASE; };
  6         20  
  3         8  
  3         10  
325 3 50       10 /^f/ixsm && do { $rate = $self->_far(@args2); last CASE; };
  3         9  
  3         7  
326 0 0       0 /^m/ixsm && do { $rate = $self->_mr(@args2); last CASE; };
  0         0  
  0         0  
327 0 0       0 /^c/ixsm && do { $rate = $self->_crr(@args2) };
  0         0  
328             } #end CASE
329             }
330             }
331 13         28 return _precisioned( $self->{'precision_s'}, $rate );
332             }
333            
334             sub _hr {
335 5     5   11 my ( $self, @args ) = @_;
336 5 100       31 if ( scalar @args > 1 ) { # set the rate via params
    100          
337 1         4 my (%params) = @args;
338 1         3 foreach ( keys %params ) {
339 4         6 $self->{$_} = $params{$_};
340             }
341             $self->{'hr'} = _init_rate( $self->{'hits'}, $self->{'signal_trials'},
342 1         3 $self->{'correction'} );
343             }
344             elsif ( scalar @args == 1 ) { # set the rate as given
345 2 50       7 $self->{'hr'} = _valid_p( $args[0] ) ? $args[0] : croak __PACKAGE__,
346             ' Rate needs to be between 0 and 1 inclusive';
347             }
348 5         17 return $self->{'hr'};
349             }
350            
351             sub _far {
352 9     9   48 my ( $self, @args ) = @_;
353 9 100       28 if ( scalar @args > 1 ) { # set the rate via params
    100          
354 1         3 my %params = @args;
355 1         2 foreach ( keys %params ) {
356 4         6 $self->{$_} = $params{$_};
357             }
358             $self->{'far'} = _init_rate(
359             $self->{'false_alarms'},
360             $self->{'noise_trials'},
361 1         4 $self->{'correction'}
362             );
363             }
364             elsif ( scalar @args == 1 ) { # set the rate as given
365 2 50       6 $self->{'far'} = _valid_p( $args[0] ) ? $args[0] : croak __PACKAGE__,
366             ' Rate needs to be between 0 and 1 inclusive';
367             }
368 9         21 return $self->{'far'};
369             }
370            
371             sub _mr {
372 0     0   0 my ( $self, %params ) = @_;
373 0         0 foreach ( keys %params ) {
374 0         0 $self->{$_} = $params{$_};
375             }
376 0 0 0     0 if ( !is_float( $self->{'signal_trials'} )
377             || !is_float( $self->{'misses'} ) )
378             {
379             #if ( !$self->{'signal_trials'} || !defined $self->{'misses'} ) {
380 0         0 carp 'Uninitialised counts for calculating MR';
381 0         0 return q{};
382             }
383 0         0 return $self->{'misses'} / $self->{'signal_trials'};
384             }
385            
386             sub _crr {
387 0     0   0 my ( $self, %params ) = @_;
388 0         0 foreach ( keys %params ) {
389 0         0 $self->{$_} = $params{$_};
390             }
391 0 0 0     0 if ( !is_float( $self->{'signal_trials'} )
392             || !is_float( $self->{'correct_rejections'} ) )
393             {
394             #if ( !$self->{'signal_trials'} || !defined $self->{'correct_rejections'} ) {
395 0         0 carp 'Uninitialised counts for calculating CRR';
396 0         0 return q{};
397             }
398 0         0 return $self->{'correct_rejections'} / $self->{'noise_trials'};
399             }
400            
401             sub _init_rate { # Initialise hit and false-alarm rates:
402 11     11   18 my ( $count, $trials, $correction ) = @_;
403 11         16 my $rate;
404 11 100       31 if ( !defined $correction ) {
405 3         5 $correction = 1; # default correction
406             }
407            
408             # Need (i) no. of hits and signal trials, or (ii) no. of false alarms and noise trials:
409 11 50 33     37 croak __PACKAGE__,
410             ' Number of hits/false-alarms and signal/noise trials needed to calculate rate'
411             if !defined $count || !defined $trials;
412 11 50 33     26 return if not is_int($trials) or $trials == 0;
413            
414 11 50       135 if ( $correction > 1 ) { # loglinear correction, regardless of values:
415 0         0 $rate = _loglinear_correct( $count, $trials );
416             }
417             else
418             { # get rate first, applying corrections if needed (unless explicitly verboten):
419 11         20 $rate = $count / $trials;
420 11 50       40 if ( $correction != 0 ) {
421 11         29 $rate = _n_correct( $rate, $trials );
422             }
423             }
424 11         26 return $rate;
425             }
426            
427             =head2 zrate
428            
429             $z = $sdt->zrate('hr'); # or 'far', 'mr', 'crr'
430            
431             Returns the I-transformation of the given rate using the inverse-phi function (C from L).
432            
433             =cut
434            
435             sub zrate {
436 0     0 1 0 my ( $self, @args ) = @_;
437 0         0 return ndtri( $self->rate(@args) );
438             }
439            
440             =head2 dc2hr
441            
442             $sdt->dc2hr() # assume d' and c can be calculated from already inited param values
443             $sdt->dc2hr(d => FLOAT, c => FLOAT)
444            
445             Returns the hit-rate estimated from given values of sensitivity I and bias I, viz.:
446            
447             =for html

  HR = φ(d’ / 2 – c)

448            
449             =cut
450            
451             sub dc2hr {
452 1     1 1 1240 my ( $self, %args ) = @_;
453 1         4 my ( $d, $c ) = _get_dc( $self, %args );
454 2     2   9 return ( all { hascontent($_) } ( $d, $c ) )
455 1 50       5 ? _precisioned( $self->{'precision_s'}, ndtr( $d / 2 - $c ) )
456             : q{};
457             }
458            
459             =head2 dc2far
460            
461             $sdt->dc2far() # assume d' and c can be calculated from already inited param values
462             $sdt->dc2far(d => FLOAT, c => FLOAT)
463            
464             Returns the false-alarm-rate estimated from given values of sensitivity I and bias I, viz.:
465            
466             =for html

  FAR = φ(–d’ / 2 – c)

467            
468             =cut
469            
470             sub dc2far {
471 1     1 1 263 my ( $self, %args ) = @_;
472 1         4 my ( $d, $c ) = _get_dc( $self, %args );
473 2     2   18 return ( all { hascontent($_) } ( $d, $c ) )
474 1 50       4 ? _precisioned( $self->{'precision_s'}, ndtr( -1 * $d / 2 - $c ) )
475             : q{};
476             }
477            
478             # --------------------
479             # Sensitivity measures:
480             # --------------------
481            
482             =head2 sens
483            
484             $s = $sdt->sens('dprime'); # or 'aprime', 'adprime'
485             $s = $sdt->sens('dprime', { signal_trials => POSINT }); # set args, optionally
486             $s = $sdt->sens('d_a', { stdev_n => POS_FLOAT, stdev_s => POS_FLOAT }); # required args
487            
488             I: C
489            
490             Returns one of the sensitivity measures, as indicated by the first argument string, optionally updating any of the measure variables and options with a subsequent hashref. The measures are as follows, accessed by giving the name (or at least its first two letters) as the first argument.
491            
492             =over 4
493            
494             =item dprime
495            
496             Returns the index of standard deviation units of sensitivity, or discrimination, I (d prime). Assuming equal variances for the noise and signal+noise distributions, this is estimated by subtracting the I-score units of the false-alarm rate (or 1 - the correct-rejection-rate) from the I-score units of the hit rate:
497            
498             =for html

  d’ = φ–1(HR) – φ–1(FAR)
      = φ–1(HR) + φ–1(CR)

499            
500             Larger positive values indicate greater sensitivity. If both HR and FAR are either 0 or 1, then Litivity returns 0, indicating no sensitivity; the signal cannot be discriminated from noise. Values less than 0 (more false-alarms than hits) indicate a lack of sensitivity that might result from a consistent reponse-confusion or -inhibition.
501            
502             For estimating dprime for I-AFC tasks, the forced-choice design, there are two methods, as set by the L parameter in L or Litivity. The default method is I, the method cited by Stanislav & Todorov (1999); and there is the more generally applicable I method.
503            
504             The present interface to these methods is limited in that they are given, for proportion-correct, the hit-rate as for the yes/no design: as the count of hits divided by number of signal trials. Rather than give these methods a value for B
, the L method could be used setting the number of hit and signal trials as appropriate, and setting the number of false alarms and noise trials to zero. This is not optimal (intuitive) as the proportion correct is something else in the yes/no design (see L), but simply works by present L). So, in what follows, for HR, one should really read proportion-correct.
505            
506             B>: satisfies "the 2% bound for all I [alternatives] and all percentiles and, except for I = 3 or 4, satisfies a 1% error bound" (p. 95). The specific algorithm used depends on number of alternatives:
507            
508             Smith's I* applies when I alternatives E 12:
509            
510             =for html

  d’ = Kln( [ (n – 1)HR ] / [ 1 – HR ] )

511            
512             where
513            
514             =for html

    K = .86 – .085 * ln(n – 1).

515            
516             Smith's I** applies when I >= 12:
517            
518             =for html

    d’ = (A + B)φ–1(HR)

519            
520             where
521            
522             =for html

    A = (–4 + sqrt[16 + 25 * ln(n – 1)]) / 3

523            
524             and
525            
526             =for html

    B = sqrt( [ln(n – 1) + 2] / [ln(n – 1) + 1] )

527            
528             The limits of the method can be noted in that, when I >= 14, I does not equal zero when the proportion correct (HR) is simply 1/I.
529            
530             B> (which never fails the latter elementary test): "gives values of I with an error of less than 2% (mostly less than 1%) from those obtained by integration for the range I = 0 (or 1% correct for I [alternatives] > 1000) to 75% correct and an error of less than 4% up to 95% correct for I up to at least 10000, and slightly greater maximum errors for I = 100000. This approximation is comparable to the accuracy of Elliott's table (0.02 in proportion correct) but can be used for any I." (Elliott's table being that in Swets, 1964, pp. 682-683). The estimation is offered by:
531            
532             =for html

          d’ = [ φ–1(HR) – φ–1(1/n) ] / An

533            
534             where I is the number of L, and I is estimated by:
535            
536             =for html

          An = 1 - 1 / (1.93 + 4.75 * log10(n) + .63[log10(n)]2)

537            
538             =item d_a
539            
540             Returns estimate of SDT sensitivity for without assuming equal variances, given values of B for standard deviation of the noise distribution, and B for standard deviation of the signal-plus-noise distribution.
541            
542             =for html

  d’ = sqrt[ 2 / (1 + b2) ][φ–1(HR) – bφ–1(FAR)]

543            
544             where
545            
546             =for html

  b = σ(N) / σ(S)

547            
548             =item aprime
549            
550             Returns the nonparametric index of sensitivity, I, a.k.a. I (e.g., Pastore & Scheirer, Eq. 6). It makes no assumption about the homogeneity of variances of the underlying distributions, and is the average of the maximum and minimum possible areas under the receiver-operating-characteristic curve (based on one ROC point).
551            
552             =for html

  a’ = [ .5 + d(1 + d) ] / 4j

553            
554             where, if HR >= FAR, I = (HR - FAR), and I = HR(1 - FAR), otherwise I = (FAR - HR) and I = FAR(1 - HR).
555            
556             Ranges from 0 to 1. Values greater than 0.5 indicate positive discrimination (1 = perfect performance); a value of 0.5 indicates no sensitivity to the presence of the signal (it cannot be discriminated from noise); and values less than 0.5 indicate negative discrimination (perhaps given consistent response confusion or inhibition).
557            
558             =item adprime
559            
560             Returns I, the area under the receiver-operator-characteristic (ROC) curve, estimating the proportion of correct responses for the task as a two-alternative forced-choice task.
561            
562             =for html

  Ad = φ(d’ / sqrt(2))

563            
564             Ranges between 0 and 1, with a value of 0.5 reflecting no discriminative ability when comparing two stimuli. If both the hit-rate and false-alarm-rate are either 0 or 1, then the returned value of C is 0.5.
565            
566             =back
567            
568             =cut
569            
570             sub sens {
571 15     15 1 1557 my ( $self, $meas, $args ) = @_;
572 15         22 local $_ = $meas;
573 15         16 my $d;
574             CASE: {
575 15 100       16 /^d|f/ixsm && do { $d = $self->_d_sensitivity( %{$args} ); };
  15         62  
  13         15  
  13         50  
576 15 100       42 /^a[p\b]/ixsm && do { $d = $self->_a_sensitivity( %{$args} ) };
  1         2  
  1         7  
577 15 100       33 /^ad/ixsm && do { $d = $self->_ad_sensitivity( %{$args} ); };
  1         2  
  1         4  
578            
579             #/^h/ixsm && do { $d = $self->_hthresh_sensitivity( %{$args} ); };
580             #/^p/ixsm && do { $d = $self->_pcorrect( %{$args} ); };
581             #/^lp/ixsm && do { $d = $self->_lpcorrect( %{$args} ); };
582             } #end CASE
583 15         43 return _precisioned( $self->{'precision_s'}, $d );
584             }
585             *discriminability = \&sens; # Alias
586             *sensitivity = \&sens;
587            
588             sub _d_sensitivity {
589 13     13   26 my ( $self, %args ) = @_;
590 13         33 my ( $h, $f, $m ) = $self->init(%args);
591            
592             #croak 'No hit-rate for calculating d-sensitivity' if ! defined $h;
593 13         17 my $d;
594            
595             # If there are more than 2 alternatives, use a forced-choice method:
596 13 100 66     67 if ( defined $m and $m >= 2 ) {
    50          
597            
598             #$self->rate(hr => $h, alternatives => $m);
599             $d =
600 8 100       18 $self->{'method'} eq 'smith'
601             ? _fc_smith( $h, $m )
602             : _fc_alexander( $h, $m );
603             }
604 5     5   13 elsif ( all { defined $args{$_} } qw/stdev_n stdev_s/ ) {
605             $d =
606 0     0   0 ( all { hascontent($_) } ( $h, $f ) )
607 0 0       0 ? _d_a( $h, $f, $args{'stdev_s'}, $args{'stdev_n'} )
608             : q{};
609             }
610             else {
611 5 50   10   15 $d = ( all { hascontent($_) } ( $h, $f ) ) ? _dprime( $h, $f ) : q{};
  10         80  
612             }
613 13         37 return $d;
614             }
615            
616             sub _dprime {
617 5     5   47 my ( $hr, $far ) = @_;
618 5         6 my $d;
619            
620             # Assume d' = 0 if both rates = 0 or both = 1:
621 5 50 33     49 if ( ( !$hr && !$far ) || ( $hr == 1 && $far == 1 ) ) {
      33        
      33        
622 0         0 $d = 0;
623             }
624             else {
625 5         62 $d = ndtri($hr) - ndtri($far);
626             }
627 5         10 return $d;
628             }
629            
630             sub _d_a {
631 0     0   0 my ( $hr, $far, $stdev_s, $stdev_n ) = @_;
632 0         0 my $d;
633            
634             # Assume d' = 0 if both rates = 0 or both = 1:
635 0 0 0     0 if ( ( !$hr && !$far ) || ( $hr == 1 && $far == 1 ) ) {
      0        
      0        
636 0         0 $d = 0;
637             }
638             else {
639 0         0 my $z_hr = ndtri($hr);
640 0         0 my $z_far = ndtri($far);
641 0         0 my $b = $stdev_n / $stdev_s;
642 0         0 $d = sqrt( 2 / ( 1 + $b**2 ) ) * ( $z_hr - $b * $z_far );
643             }
644 0         0 return $d;
645             }
646            
647             # Smith (1982) method:
648             sub _fc_smith {
649 2     2   4 my ( $h, $m ) = @_;
650 2         3 my $d;
651 2 100       3 if ( $m < 12 ) {
652 1         5 my $km = .86 - .085 * log( $m - 1 );
653 1         4 my $lm = ( ( $m - 1 ) * $h ) / ( 1 - $h );
654 1         2 $d = $km * log $lm;
655             }
656             else {
657 1         7 my $a = ( -4 + sqrt( 16 + 25 * log( $m - 1 ) ) ) / 3;
658 1         5 my $b = sqrt( ( log( $m - 1 ) + 2 ) / ( log( $m - 1 ) + 1 ) );
659 1         3 $d = $a + $b * ndtri($h);
660             }
661 2         4 return $d;
662             }
663            
664             # Alexander (2006/1990) method:
665             sub _fc_alexander {
666 6     6   9 my ( $h, $m ) = @_;
667 6         48 my $an = 1 - ( 1 / ( 1.93 + 4.75 * log10($m) + .63 * ( log10($m)**2 ) ) );
668 6         24 return ( ndtri($h) - ndtri( 1 / $m ) ) / $an;
669             }
670            
671             sub _a_sensitivity {
672 1     1   3 my ( $self, @args ) = @_;
673 1         3 my ( $h, $f ) = $self->init(@args);
674 1 50   2   6 return q{} if any { nocontent($_) } ( $h, $f );
  2         14  
675 1         11 my $d;
676 1 50       3 if ( $h >= $f ) {
677 1         4 $d =
678             ( .5 + ( ( $h - $f ) * ( 1 + $h - $f ) ) / ( 4 * $h * ( 1 - $f ) ) );
679             }
680             else {
681 0         0 $d =
682             ( .5 + ( ( $f - $h ) * ( 1 + $f - $h ) ) / ( 4 * $f * ( 1 - $h ) ) );
683             }
684 1         2 return $d;
685             }
686            
687             sub _ad_sensitivity {
688 1     1   2 my ( $self, @args ) = @_;
689 1         3 my ( $h, $f ) = $self->init(@args);
690 1 50   2   5 return q{} if any { nocontent($_) } ( $h, $f );
  2         16  
691 1         11 my $d;
692            
693             # Assume A(d') = 0.5 if both rates = 0 or both = 1:
694 1 50 33     8 if ( ( !$h && !$f ) || ( $h == 1 && $f == 1 ) ) {
      33        
      33        
695 0         0 $d = 0.5;
696             }
697             else {
698 1         4 $self->rate( h => $h, f => $f );
699 1         5 $d = ndtr( $self->sensitivity('d') / sqrt 2 );
700             }
701 1         3 return $d;
702            
703             }
704            
705             # --------------------
706             # Bias measures:
707             # --------------------
708            
709             =head2 bias
710            
711             $b = $sdt->bias('likelihood|loglikelihood|decision|griers') # based on values of the measure variables already inited or otherwise set
712             $b = $sdt->bias('likelihood' => { signal_trials => INT}) # pass to any of the measure variables
713            
714             Returns an estimate of the SDT decision threshold/response-bias. The particular estimate is named by the first argument string (or at least its first two letters), as below. optionally updating any of the measure variables and options with a subsequent hashref (as given by example for L).
715            
716             With a I response indicating that the decision variable exceeds the criterion, and a I response indicating that the decision variable is less than the criterion, the measures indicate if there is a bias toward the I response, and so a liberal/low criterion, or a bias toward the I response, and so a conservative/high criterion.
717            
718             =over 4
719            
720             =item beta, likelihood_bias
721            
722             Returns the paramteric I measure of response bias, based on the ratio of the likelihood the decision variable obtains a certain value on signal trials, to the likelihood that it obtains the value on noise trials.
723            
724             =for html

          β = exp( [φ–1(FAR)2 – φ–1(HR)2] / 2 )

725            
726             Values less than 1 indicate a bias toward the I response (more hits and FAs than misses and CRs), values greater than 1 indicate a bias toward the I response (more misses and CRs than hits and FAs), and the value of 1 indicates no bias toward I or I.
727            
728             =item log_likelihood_bias
729            
730             Returns the natural logarithm of the likelihood bias, I.
731            
732             =for html

          lnβ = [ φ–1(FAR)2 – φ–1(HR)2 ] / 2

733            
734             Ranges from -1 to +1, with values less than 0 indicating a bias toward the I response (more hits and FAs than misses and CRs), values greater than 0 indicating a bias toward the I response (more misses and CRs than hits and FAs), and a value of 0 indicating no response bias.
735            
736             =item c, distance
737            
738             Returns the I parametric measure of response bias (Macmillan & Creelman, 1991, Eq. 12), defined as the distance between the criterion and the point where beta = 1 (crossing-point of the noise and signal distributions, with neither response favoured; where signal+noise is as likely as noise-only, and so how different the response criterion is from an unbiased criterion).
739            
740             =for html

          c = –[ φ–1(HR) + φ–1(FAR) ] / 2

741            
742             Ranges from -1 to +1, with deviations from zero, measured in standard deviation units. Values less than 0 indicate a bias toward the I response (more hits and FAs than misses and CRs); values greater than 0 indicate a bias toward the I response (more misses and CRs than hits and FAs); and a value of 0 indicates unbiased responding.
743            
744             =item griers
745            
746             Returns Griers I nonparametric measure of response bias. Defining I = HR(1 - HR) and I = FAR(1 - FAR) if HR >= FAR, otherwise I = FAR(1 - FAR) and I = HR(1 - HR), then I = ( I - I ) / ( I + I ); or, summarily:
747            
748             =for html

          B” = sign(HR – FAR)[ HR(1 – HR) – FAR(1 – FAR) ] / [ HR(1 – HR) + FAR(1 – FAR) ]

749            
750             Ranges from -1 to +1, with values less than 0 indicating a bias toward the I response (more hits and FAs than misses and CRs), values greater than 0 indicating a bias toward the I response (more misses and CRs than hits and FAs), and a value of 0 indicating no response bias.
751            
752             =back
753            
754             =cut
755            
756             sub bias {
757 7     7 1 1060 my ( $self, $meas, $args ) = @_;
758 7         10 local $_ = $meas;
759 7         8 my $v;
760             CASE: {
761 7 100       9 /^b|li/ixsm && do { $v = $self->_likelihood_bias( %{$args} ); };
  7         27  
  1         1  
  1         3  
762 7 100       15 /^lo/ixsm && do { $v = $self->_log_likelihood_bias( %{$args} ); };
  1         2  
  1         4  
763 7 100       20 /^c|d/ixsm && do { $v = $self->_distance_bias( %{$args} ); };
  4         6  
  4         11  
764 7 100       24 /^g/ixsm && do { $v = $self->_griers_bias( %{$args} ) };
  1         2  
  1         4  
765             } #end CASE
766 7         17 return _precisioned( $self->{'precision_s'}, $v );
767             }
768            
769             sub _likelihood_bias { # beta
770 1     1   2 my ( $self, @args ) = @_;
771 1         3 my ( $h, $f ) = $self->init(@args);
772 1 50   2   5 return q{} if any { nocontent($_) } ( $h, $f );
  2         18  
773 1         36 my $diff = ( ndtri($f)**2 - ndtri($h)**2 ) / 2;
774 1         6 return exp $diff;
775             }
776            
777             sub _log_likelihood_bias { # ln(beta)
778 1     1   2 my ( $self, @args ) = @_;
779 1         3 my ( $h, $f ) = $self->init(@args);
780 1 50   2   5 return q{} if any { nocontent($_) } ( $h, $f );
  2         26  
781 1         17 return ( ndtri($f)**2 - ndtri($h)**2 ) / 2;
782             }
783            
784             sub _distance_bias { # c
785 4     4   20 my ( $self, @args ) = @_;
786 4         9 my ( $h, $f ) = $self->init(@args);
787 4 50   8   16 return q{} if any { nocontent($_) } ( $h, $f );
  8         59  
788 4         68 return -1 * ( ( ndtri($h) + ndtri($f) ) / 2 );
789             }
790            
791             sub _griers_bias { # B''
792 1     1   2 my ( $self, @args ) = @_;
793 1         6 my ( $h, $f ) = $self->init(@args);
794 1 50   2   5 return q{} if any { nocontent($_) } ( $h, $f );
  2         14  
795 1         11 my $v1 = $h * ( 1 - $h );
796 1         2 my $v2 = $f * ( 1 - $f );
797 1         3 return _sign( $h - $f ) * ( ( $v1 - $v2 ) / ( $v1 + $v2 ) );
798             }
799            
800             =head2 dc2logbeta
801            
802             $sdt->dc2logbeta() # assume d' and c can be calculated from already inited param values
803             $sdt->dc2logbeta(d => FLOAT, c => FLOAT)
804            
805             Returns the log-likelihood (beta) bias estimated from given values of sensitivity I and bias I, viz.:
806            
807             =for html

  lnβ = dc

808            
809             =cut
810            
811             sub dc2logbeta {
812 1     1 1 314 my ( $self, %args ) = @_;
813 1         4 my ( $d, $c ) = _get_dc( $self, %args );
814 1 50   2   5 return q{} if any { nocontent($_) } ( $d, $c );
  2         10  
815 1         11 return _precisioned( $self->{'precision_s'}, $d * $c );
816             }
817            
818             =head2 criterion
819            
820             $sdt->criterion() # from FAR or from d' and c from already inited param values
821             $sdt->criterion(far => PROPORTION) # from FAR or from d' and c from already inited param values
822             $sdt->criterion(d => FLOAT, c => FLOAT)
823            
824             I: C
825            
826             Returns the value of the decision criterion (critical output value of the input process) on the basis of either:
827            
828             (1) the false-alarm-rate:
829            
830             =for html

  xc = –φ–1(FAR)

831            
832             or (2) both sensitivity I and bias I as:
833            
834             =for html

  xc = d’ / 2 + c

835            
836             The method firstly checks if FAR can be calculated from given data or specific argument B, or similarly by I and I.
837            
838             =cut
839            
840             sub criterion {
841 2     2 1 424 my ( $self, %args ) = @_;
842 2         4 my $xc;
843 2 50       5 if ( is_float( $self->rate('far') ) ) {
844 2         23 $xc = -1 * ndtri( $self->rate('far') );
845             }
846             else {
847 0         0 my ( $d, $c ) = _get_dc( $self, %args );
848 0 0   0   0 if ( all { hascontent($_) } ( $d, $c ) ) {
  0         0  
849 0         0 $xc = $d / 2 + $c;
850             }
851             }
852 2 50       9 return hascontent($xc) ? _precisioned( $self->{'precision_s'}, $xc ) : q{};
853             }
854             *dc2k = \&criterion; # Alias
855             *crit = \&criterion;
856            
857             sub _get_dc {
858 3     3   5 my ( $self, %params ) = @_;
859 3 50       12 my $d = defined $params{'d'} ? $params{'d'} : $self->sensitivity('d');
860 3 50       10 my $c = defined $params{'c'} ? $params{'c'} : $self->bias('c');
861 3         9 return ( $d, $c );
862             }
863            
864             # give count of either hits & signal_trials; or false_alarms and noise_trials
865             sub _loglinear_correct {
866 0     0   0 my ( $count, $trials ) = @_;
867 0         0 return ( $count + .5 ) / ( $trials + 1 );
868             }
869            
870             sub _n_correct {
871 11     11   20 my ( $rate, $trials ) = @_;
872 11         15 my $retval;
873 11 50       31 if ( !$rate ) {
    100          
874 0         0 $retval = .5 / $trials;
875             }
876             elsif ( $rate == 1 ) {
877 5         10 $retval = ( $trials - .5 ) / $trials;
878             }
879             else {
880 6         14 $retval = $rate;
881             }
882 11         21 return $retval;
883             }
884            
885             sub _precisioned {
886 41     41   346 my ( $lim, $val ) = @_;
887 41 50       87 return q{} if !is_float($val);
888 41 100       660 return $lim ? sprintf( q{%.} . $lim . q{f}, $val ) : $val;
889             }
890            
891             sub _valid_p {
892 6     6   509 my $p = shift;
893 6 100 66     70 return ( $p !~ /^ 0 ? [.] \d+ $/msx ) || ( $p < 0 || $p > 1 ) ? 0 : 1;
894             }
895            
896             sub _sign {
897 4     4   697 my $v = shift;
898 4 100       15 return $v >= 0 ? 1 : -1;
899             }
900            
901             1;
902            
903             __END__