File Coverage

blib/lib/Statistics/SDT.pm
Criterion Covered Total %
statement 170 217 78.3
branch 62 106 58.4
condition 27 50 54.0
subroutine 30 34 88.2
pod 10 10 100.0
total 299 417 71.7


line stmt bran cond sub pod time code
1             package Statistics::SDT;
2            
3 2     2   47536 use strict;
  2         5  
  2         72  
4 2     2   11 use warnings;
  2         2  
  2         62  
5 2     2   11 use Carp qw(croak);
  2         6  
  2         152  
6 2     2   1847 use Math::Cephes qw(:dists :explog);
  2         20920  
  2         882  
7 2     2   16 use vars qw($VERSION);
  2         5  
  2         7002  
8             $VERSION = 0.05;
9            
10             my %counts_dep = (
11             hits => [qw/signal_trials misses/],
12             false_alarms => [qw/noise_trials correct_rejections/],
13             misses => [qw/signal_trials hits/],
14             correct_rejections => [qw/noise_trials false_alarms/],
15             );
16             my %trials_dep = (
17             signal_trials => [qw/hits misses/],
18             noise_trials => [qw/false_alarms correct_rejections/]
19             );
20             my %rates_dep = (
21             hr => [qw/hits signal_trials/],
22             far => [qw/false_alarms noise_trials/]
23             );
24            
25             =head1 NAME
26            
27             Statistics::SDT - Signal detection theory (SDT) measures of sensitivity and response-bias
28            
29             =head1 SYNOPSIS
30            
31             The following is based on example data from Stanislav & Todorov (1999), and Alexander (2006), with which the module's results agree.
32            
33             use Statistics::SDT 0.05;
34            
35             my $sdt = Statistics::SDT->new(
36             correction => 1,
37             precision_s => 2,
38             );
39            
40             $sdt->init(
41             hits => 50,
42             signal_trials => 50, # or misses => 0,
43             false_alarms => 17,
44             noise_trials => 25, # or correct_rejections => 8
45             ); # or init these into 'new' &/or update any of their values as 2nd arg. hashrefs in calling the following methods
46            
47             printf("Hit rate = %s\n", $sdt->rate('h') ); # .99
48             printf("False-alarm rate = %s\n", $sdt->rate('f') ); # .68
49             printf("Miss rate = %s\n", $sdt->rate('m') ); # .00
50             printf("Correct-rej'n rate = %s\n", $sdt->rate('c') ); # .32
51             printf("Sensitivity d' = %s\n", $sdt->sens('d') ); # 1.86
52             printf("Sensitivity Ad' = %s\n", $sdt->sens('Ad') ); # 0.91
53             printf("Sensitivity A' = %s\n", $sdt->sens('A') ); # 0.82
54             printf("Bias beta = %s\n", $sdt->bias('b') ); # 0.07
55             printf("Bias logbeta = %s\n", $sdt->bias('log') ); # -2.60
56             printf("Bias c = %s\n", $sdt->bias('c') ); # -1.40
57             printf("Bias Griers B'' = %s\n", $sdt->bias('g') ); # -0.91
58             printf("Criterion k = %s\n", $sdt->crit() ); # -0.47
59             printf("Hit rate via d & c = %s\n", $sdt->dc2hr() ); # .99
60             printf("FAR via d & c = %s\n", $sdt->dc2far() ); # .68
61             printf("LogBeta via d & c = %s\n", $sdt->dc2logbeta() ); # -2.60
62            
63             # If the number of alternatives is greater than 2, there are two method options:
64             printf("JAlex. d_fc = %.2f\n", $sdt->sens('f' => {hr => .866, states => 3, correction => 0, method => 'alexander'})); # 2.00
65             printf("JSmith d_fc = %.2f\n", $sdt->sens('f' => {hr => .866, states => 3, correction => 0, method => 'smith'})); # 2.05
66            
67             =head1 DESCRIPTION
68            
69             Signal Detection Theory (SDT) measures of sensitivity and response-bias, e.g., I, I, I. For any particular analysis, you go through the stages of (1) creating the SDT object (see L), (2) initialising the object with relevant data (see L), and then (3) calling the statistic you want, with any statistic-specific arguments.
70            
71             =head1 KEY NAMED PARAMS
72            
73             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, you need to feed the (i) count of hits and signal_trials, or (ii) the counts of hits and misses, or (iii) the count of signal_trials and misses. To calculate the false-alarm-rate, you need to feed (i) the count of false_alarms and noise_trials, or (ii) the count of false_alarms and correct_rejections, or (iii) the count of noise_trials and correct_rejections. Or you supply the hit-rate and false-alarm-rate. Or see L and L if you already have the measures, and want to get back to the rates.
74            
75             =over 4
76            
77             =item hits
78            
79             The number of hits.
80            
81             =item false_alarms
82            
83             The number of false alarms.
84            
85             =item signal_trials
86            
87             The number of signal trials. The hit-rate is derived by dividing the number of hits by the number of signal trials.
88            
89             =item noise_trials
90            
91             The number of noise trials. The false-alarm-rate is derived by dividing the number of false-alarms by the number of noise trials.
92            
93             =item states
94            
95             The number of response states, or "alternatives", "options", etc.. Default = 2 (for the classic signal-detection situation of discriminating between signal+noise and noise-only). If the number of alternatives is greater than 2, when calling L, Smith's (1982) estimation of I is used (otherwise Alexander's) - see L.
96            
97             =item correction
98            
99             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 I option with L). As C must die with an error if given 0 or 1, there is a default correction.
100            
101             If C = 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 (I and I).
102            
103             If C = 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.
104            
105             If C > 1, the loglinear transformation is appliedt 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.
106            
107             If C 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.
108            
109             =item precision_s
110            
111             Precision (I decimal places) of any of the statistics. Default = 0, which actually means that you get all decimal bits possible.
112            
113             =item method
114            
115             Method for estimating I when number of states/alternatives is greater than 2. Default value is I; otherwise I; see L for application and description of these methods.
116            
117             =item hr
118            
119             The hit-rate. Instead of passing the number of hits and signal trials, give the hit-rate directly - but, if doing so, ensure the rate does not equal zero or 1 in order to avoid errors thrown by the inverse-phi function (which will be given as "ndtri domain error").
120            
121             =item far
122            
123             This is the false-alarm-rate. Instead of passing the number of false alarms and noise trials, give the false-alarm-rate directly - but, if doing so, ensure the rate does not equal zero or 1 in order to avoid errors thrown by the inverse-phi function (which will be given as "ndtri domain error").
124            
125             =back
126            
127             =head1 METHODS
128            
129             =head2 new
130            
131             Creates the class object that holds the values of the parameters, as above, and accesses the following methods, without having to resubmit all the values.
132            
133             As well as holding the values of the parameters submitted to it, the class-object returned by C will hold two arguments, B
, the hit-rate, and B, the false-alarm-rate. You can supply the hit-rate and false-alarm-rate themselves, but ensure that they do not equal zero or 1 in order to avoid errors thrown by the inverse-phi function. The calculation of the hit-rate and false-alarm-rate by the module corrects for this limitation; correction can only be done by supplying the relevant counts, not just the rate - see the notes on the C parameter, above.
134            
135             =cut
136            
137             sub new {
138 1     1 1 13 my $class = shift;
139 1         2 my $self = {};
140 1         3 bless $self, $class;
141 1         7 $self->init(@_);
142 1         3 return $self;
143             }
144            
145             =head2 init
146            
147             $sdt->init(
148             hits => integer,
149             misses => ?integer,
150             false_alarms => integer,
151             correct_rejections => ?integer,
152             signal_trials => integer (>= hits), # or will be calculated from hits and misses
153             noise_trials => integer (>= false_alarms), # or will be calculated from false_alarms and correction_rejections
154             hr => probability 0 - 1,
155             far => probablity 0 - 1,
156             correction => 0|1|2 (default = 1),
157             states => integer >= 2 (default = 2),
158             precision_s => integer (default = 0),
159             method => undef|smith|alexander (default = undef)
160             )
161            
162             Instead of sending 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 these values, as named parameters, key => value pairs. This method is called by L in case you pass the values to it in construction. 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 you send a value for one or the other, or both (as hr and far) in a call to C.
163            
164             Each C replaces the values only of those attributes that you pass to it - any values set in previous Cs are retained for those attributes that you do not set in a call to C. If this is not what you want, and you actually want everything reset, first use L
165            
166             Optionally, the method also initialises any values you give it for L, L, L and L. If you have already set these values, and you do not do so in another call to C; the previous values will be retained.
167            
168             =cut
169            
170             sub init {
171 20     20 1 787 my $self = shift;
172 20 100       43 if (scalar @_) { # have some params?
173 4 50       28 my $args = ref $_[0] ? shift : {@_};
174 4         11 foreach (qw/hits false_alarms misses correct_rejections signal_trials noise_trials states correction precision_s method hr far/) {
175 48 100       128 $self->{$_} = $args->{$_} if defined $args->{$_}; # only (re)set those params given values in this call
176             }
177 4   100     17 $self->{'states'} ||= 2;
178 4   100     14 $self->{'method'} ||= 'smith';
179 4   50     9 $self->{'precision_s'} ||= 0;
180             # Go round and round:
181 4         14 foreach (keys %counts_dep) {
182 16 100 100     86 if (! defined $self->{$_} && $self->{$counts_dep{$_}->[0]} && defined $self->{$counts_dep{$_}->[1]}) {
      66        
183 2         9 $self->{$_} = $self->{$counts_dep{$_}->[0]} - $self->{$counts_dep{$_}->[1]};
184             }
185             }
186 4         14 foreach (keys %trials_dep) {
187 8 50 66     32 if (! defined $self->{$_} && defined $self->{$trials_dep{$_}->[0]} && defined $self->{$trials_dep{$_}->[1]}) {
      33        
188 0         0 $self->{$_} = $self->{$trials_dep{$_}->[0]} + $self->{$trials_dep{$_}->[1]};
189             }
190             }
191 4         9 foreach (keys %rates_dep) {
192 8 100 100     58 if (! defined $args->{$_} && defined $self->{$rates_dep{$_}->[0]} && $self->{$rates_dep{$_}->[1]}) {
      66        
193 4         17 $self->{$_} = _init_rate($self->{$rates_dep{$_}->[0]}, $self->{$rates_dep{$_}->[1]}, $self->{'correction'});
194             }
195             }
196             }
197             # no params - assume the values are already in $self
198 20         63 return ($self->{'hr'}, $self->{'far'}, $self->{'states'});
199             }
200            
201             =head2 clear
202            
203             $sdt->clear()
204            
205             Sets all attributes to undef: C, C, C, C, C
, C, C, C, and C.
206            
207             =cut
208            
209             sub clear {
210 0     0 1 0 my $self = shift;
211 0         0 foreach (qw/hits false_alarms misses correct_rejections signal_trials noise_trials hr far states correction precision_s method/) {
212 0         0 $self->{$_} = undef;
213             }
214             }
215            
216             =head2 rate
217            
218             $sdt->rate('hr|far|mr|crr') # scalar string to return the indicated rate
219             $sdt->rate(hr => 'prob.', far => 'prob.', mr => 'prob.', crr => 'prob.') # one or more key => value pairs to set the rate
220             $sdt->rate('h' => {signal_trials => integer, hits => integer}) # or misses instead of hits
221             $sdt->rate('f' => {noise_trials => integer, false_alarms => integer}) # or correct_rejections instead of false_alarms
222             $sdt->rate('m' => {signal_trials => integer, misses => integer}) # or hits instead of misses
223             $sdt->rate('c' => {noise_trials => integer, correct_rejections => integer}) # or false_alarms instead of correct_rejections
224            
225             Generic method to get or set any rate.
226            
227             To I a rate, pass only a string that indicates the rate: hit, false-alarm, miss, correct-rejection: only checks the first letter, so any passable abbreviation will do. The rate is returned to the precision indicated by the present value of L, if anything.
228            
229             To I a rate, either give the actual probability as key => value pairs, or send a hashref giving sufficient info to calculate the rate (if this has not already been sent to L or one of the measure-methods).
230            
231             Also performs any required or requested corrections, depending on the present value of L.
232            
233             Unless the values of the rates are directly given, then they will be calculated from the presently sent counts and trial-numbers, or whatever has been cached of these values. For the hit-rate, there must be a value for C and C, and for the false_alarm_rate, there must be a value for C and C. If these values are not sent, they will be taken from any prior value, unless this has been Led or never existed - in which case expect a C.
234            
235             =cut
236            
237             # --------------------
238             sub rate {
239             # --------------------
240 1     1 1 2 my $self = shift;
241            
242 1 50       5 if (scalar(@_) == 1) { # Get the rate:
    50          
243 0         0 local $_ = shift;
244             CASE:{
245 0 0       0 /^h/i && do { return $self->_hit_rate();};
  0         0  
  0         0  
246 0 0       0 /^f/i && do { return $self->_false_alarm_rate(); };
  0         0  
247 0 0       0 /^m/i && do { return $self->_miss_rate();};
  0         0  
248 0 0       0 /^c/i && do { return $self->_correct_rejection_rate()};
  0         0  
249             } #end CASE
250             }
251             ##else {
252             elsif (scalar(@_) > 1) { # Set the rate:
253 1         4 my %params = @_;
254 1         3 foreach (keys %params) {
255 2 50       7 my @args = ref $params{$_} ? %{$params{$_}} : $params{$_}; # optimistic
  0         0  
256             CASE:{
257 2 100       3 /^h/i && do { $self->_hit_rate(@args); last CASE; };
  2         13  
  1         9  
  1         4  
258 1 50       5 /^f/i && do { $self->_false_alarm_rate(@args); last CASE; };
  1         4  
  1         3  
259 0 0       0 /^m/i && do { $self->_miss_rate(@args); last CASE; };
  0         0  
  0         0  
260 0 0       0 /^c/i && do { $self->_correct_rejection_rate(@args)};
  0         0  
261             } #end CASE
262             }
263             }
264             }
265            
266             sub _hit_rate {
267 1     1   3 my $self = shift;
268 1 50       13 if (@_ > 1) { # set the rate via params
    50          
269 0         0 my (%params) = @_;
270 0         0 $self->{$_} = $params{$_} foreach keys %params;
271 0         0 $self->{'hr'} = _init_rate($self->{'hits'}, $self->{'signal_trials'}, $self->{'correction'});
272             }
273             elsif (@_ == 1) { # set the rate as given
274 1 50       3 $self->{'hr'} = _valid_p($_[0]) ? shift : croak __PACKAGE__, ' Rate needs to be between 0 and 1 inclusive';
275             }
276 1         3 return _precisioned($self->{'precision_s'}, $self->{'hr'});
277             }
278            
279             sub _false_alarm_rate {
280 1     1   1 my $self = shift;
281 1 50       5 if (@_ > 1) { # set the rate via params
    50          
282 0         0 my (%params) = @_;
283 0         0 $self->{$_} = $params{$_} foreach keys %params;
284 0         0 $self->{'far'} = _init_rate($self->{'false_alarms'}, $self->{'noise_trials'}, $self->{'correction'});
285             }
286             elsif (@_ == 1) { # set the rate as given
287 1 50       3 $self->{'far'} = _valid_p($_[0]) ? shift : croak __PACKAGE__, ' Rate needs to be between 0 and 1 inclusive';
288             }
289 1         5 return _precisioned($self->{'precision_s'}, $self->{'far'});
290             }
291            
292             sub _miss_rate {
293 0     0   0 my ($self, %params) = @_;
294 0         0 $self->{$_} = $params{$_} foreach keys %params;
295             ##$self->{'misses'} = $self->{'signal_trials'} - $self->{'hits'} if ! defined $self->{'misses'};# be optimistic
296 0         0 return _precisioned($self->{'precision_s'}, $self->{'misses'} / $self->{'signal_trials'});
297             }
298            
299             sub _correct_rejection_rate {
300 0     0   0 my ($self, %params) = @_;
301 0         0 $self->{$_} = $params{$_} foreach keys %params;
302             #$self->{'correct_rejections'} = $self->{'noise_trials'} - $self->{'false_alarms'} if ! defined $self->{'correct_rejections'};# be optimistic
303 0         0 return _precisioned($self->{'precision_s'}, $self->{'correct_rejections'} / $self->{'noise_trials'});
304             }
305            
306             # --------------------
307             # Sensitivity measures:
308             # --------------------
309            
310             =head2 sens
311            
312             $s = $sdt->sens('dprime|forcedchoice|area|aprime') # based on values of the measure variables already inited or otherwise set
313             $s = $sdt->sens('dprime' => { signal_trials => integer}) # update any of the measure variables
314            
315             I: C, C
316            
317             Get 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.
318            
319             =over 4
320            
321             =item dprime
322            
323             Returns the index of sensitivity, or discrimination, I (d prime), found by subtracting the I-score that corresponds to the false-alarm rate (B) from the I-score that corresponds to the hit rate (B
):
324            
325             =for html

          d' = phi–1(hr) – phi–1(far)

326            
327             In this way, sensitivity is measured in standard deviation units, larger positive values indicating greater sensitivity. If both the hit-rate and false-alarm-rate are either 0 or 1, then Litivity returns 0. A value of 0 indicates no sensitivity to the presence of the signal, i.e., it cannot be discriminated from noise. Values less than 0 indicate a lack of sensitivity that might result from a consistent, state-specific "mix-up" or inhibition of responses.
328            
329             If there are more than two states (not only signal and noise-plus-signal), then I will be estimated by the following.
330            
331             =item forced_choice
332            
333             An estimate of I based on the percent correct in a forced-choice task with any number of alternatives. This method is automatically called via Litivity if the value of C is greater than 2. Only for this condition is it not necessary to calculate the false-alarm rate; the hit-rate is formed, as usual, as the count of hits divided by signal_trials.
334            
335             At least a couple methods are available to estimate I when states > 2; accordingly, there is the option - set either in L or Litivity or otherwise - for C: its default value is I (this is the method cited by Stanislav & Todorov (1999)); otherwise, you can use the more generally applicable I method:
336            
337             B>: satisfies "the 2% bound for all I [states] and all percentiles and, except for I = 3 or 4, satisfies a 1% error bound". The specific algorithm used depends on number of states:
338            
339             For I states E 12:
340            
341             =for html

          d' = KM.log( ( (n– 1).hr ) / ( 1 – hr ) )

342            
343             where
344            
345             =for html

          KM = .86 – .085 . log(n – 1).

346            
347             If I >= 12,
348            
349             =for html

          d' = A + B . phi–1(hr)

350            
351             where
352            
353             =for html

          A = (–4 + sqrt(16 + 25 . log(n – 1))) / 3

354            
355             and
356            
357             =for html

          B = sqrt( (log(n – 1) + 2) / (log(n – 1) + 1) )

358            
359             B>: "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 [states] > 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:
360            
361             =for html

          d' = ( phi–1(hr) – phi–1(1/n) ) / An

362            
363             where I is the number of L (or alternatives, alphabet-size, etc.), and I is estimated by:
364            
365             =for html

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

366            
367             =item aprime
368            
369             Returns the nonparametric index of sensitivity, I.
370            
371             Ranges from 0 to 1. Values greater than 0.5 indicate positive discrimination (1 = perfect performance); values less than 0.5 indicate a failure of discrimination (perhaps due to consistent "mix-up" or inhibition of state-specific responses); and a value of 0.5 indicates no sensitivity to the presence of the signal, i.e., it cannot be discriminated from noise.
372            
373             =item adprime
374            
375             Returns I, the area under the receiver-operator-characteristic (ROC) curve, equalling the proportion of correct responses for the task as a two-alternative forced-choice task.
376            
377             If both the hit-rate and false-alarm-rate are either 0 or 1, then C with this argument returns 0.5.
378            
379             =back
380            
381             =cut
382            
383             # --------------------
384             sub sens {
385             # --------------------
386 10     10 1 3417 my $self = shift;
387 10         16 local $_ = shift;
388 10 100       24 my %args = ref $_[0] ? %{(shift)} : (); # optimistic
  2         10  
389             CASE:{
390 10 100       12 /^d/i && do { return $self->_d_sensitivity(%args); };
  10         32  
  6         16  
391 4 100       12 /^f/i && do { return $self->_d_sensitivity_fc(%args); };
  2         8  
392 2 100       7 /^a(p|\b)/i && do { return $self->_a_sensitivity(%args)};
  1         5  
393 1 50       5 /^ad/i && do { return $self->_ad_sensitivity(%args); };
  1         4  
394             } #end CASE
395             }
396             *discriminability = \&sens; # Alias
397             *sensitivity =\&sens;
398            
399             sub _d_sensitivity {
400 6     6   7 my $self = shift;
401 6         14 my ($h, $f, $m, $d) = $self->init(@_);
402 6   50     15 $m ||= 2;
403             # If there are more than 2 states, use a forced-choice method:
404 6 50       11 if ($m > 2) {
405             #croak 'No hit-rate for calculating d-sensitivity' if ! defined $h;
406 0         0 $self->rate(hr => $h, states => $m);
407 0         0 $d = $self->_sensitivity_fc();
408             }
409             else {
410             # Assume d' = 0 if both rates = 0 or both = 1:
411 6 50 33     34 if ( (!$h && !$f) || ($h == 1 && $f == 1) ) {
      33        
      33        
412 0         0 $d = 0;
413             }
414             else {
415 6         7 my ($a, $b) = ();
416 6         50 $a = ndtri($h);
417 6         13 $b = ndtri($f);
418 6         10 $d = $a - $b;
419             }
420             }
421 6         12 return _precisioned($self->{'precision_s'}, $d);
422             }
423            
424             sub _d_sensitivity_fc {
425 2     2   4 my $self = shift;
426 2         5 my ($h, $f, $m, $d) = $self->init(@_); # $d is undefined
427 2 50       34 croak "No hit-rate for calculating forced-choice sensitivity" if ! defined $h;
428 2 50       7 croak "No number of alternative choices for calculating forced-choice sensitivity" if ! defined $m;
429 2 100       6 if ($self->{'method'} eq 'smith') { # Smith (1982) method:
430 1 50       4 if ($m < 12) {
431 1         15 my $km = .86 - .085 * log($m - 1);
432 1         3 my $lm = ( ($m - 1) * $h) / (1 - $h);
433 1         4 $d = $km * log($lm);
434             }
435             else {
436 0         0 my $a = ( -4 + sqrt(16 + 25 * log($m - 1) ) )/3;
437 0         0 my $b = sqrt( ( log($m - 1) + 2) / (log($m - 1) + 1) );
438 0         0 $d = $a + ($b * ndtri($h));
439             }
440             }
441             else {# Alexander (2006/1990) method:
442 1         3 my $An = _An($m);
443 1         4 my $a = ndtri($h);
444 1         5 my $b = ndtri(1/$m);
445 1         2 $d = ($a - $b) / $An;
446             }
447 2         6 return _precisioned($self->{'precision_s'}, $d);
448             }
449            
450             sub _An {
451 1     1   2 my $n = shift;
452 1         11 return 1 - ( 1 / ( 1.93 + 4.75 * log10($n) + .63 * (log10($n)**2 ) ) );
453             }
454            
455             sub _a_sensitivity {
456 1     1   1 my $self = shift;
457 1         4 my ($h, $f, $d) = $self->init(@_);
458            
459 1 50       3 if ($h >= $f) {
460 1         6 $d = (.5 + ( ($h - $f) * (1 + $h - $f) ) / ( 4 * $h * (1 - $f) ) );
461             }
462             else {
463 0         0 $d = (.5 + ( ($f - $h) * (1 + $f - $h) ) / ( 4 * $f * (1 - $h) ) );
464             }
465 1         3 return _precisioned($self->{'precision_s'}, $d);
466             }
467            
468             sub _ad_sensitivity {
469 1     1   2 my $self = shift;
470 1         3 my ($h, $f, $d) = $self->init(@_);
471            
472             # Assume A(d') = 0.5 if both rates = 0 or both = 1:
473 1 50 33     10 if ( (!$h && !$f) || ($h == 1 && $f == 1) ) {
      33        
      33        
474 0         0 $d = 0.5;
475             }
476             else {
477 1         5 $self->rate(h => $h, f => $f);
478 1         15 $d = ndtr($self->sensitivity('d') / sqrt(2));
479             }
480 1         4 return _precisioned($self->{'precision_s'}, $d);
481            
482             }
483            
484             # --------------------
485             # Bias measures:
486             # --------------------
487            
488             =head2 bias
489            
490             $b = $sdt->bias('likelihood|loglikelihood|decision|griers') # based on values of the measure variables already inited or otherwise set
491             $b = $sdt->bias('likelihood' => { signal_trials => integer}) # update any of the measure variables
492            
493             Get one of the decision/response-bias measures, as indicated below, by the first argument string, optionally updating any of the measure variables and options with a subsequent hashref (as given by example for C, above).
494            
495             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.
496            
497             The measures are as follows, accessed by giving the name (or at least its first two letters) as the first argument to C.
498            
499             =over 4
500            
501             =item beta (or) likelihood_bias
502            
503             Returns the 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.
504            
505             =for html

          beta = exp( ( (phi–1(far)2 – phi–1(hr)2) ) / 2 )

506            
507             Values less than 1 indicate a bias toward the I response, values greater than 1 indicate a bias toward the I response, and the value of 1 indicates no bias toward I or I.
508            
509             =item log_likelihood_bias
510            
511             Returns the natural logarithm of the likelihood bias, I.
512            
513             Ranges from -1 to +1, with values less than 0 indicating a bias toward the I response, values greater than 0 indicating a bias toward the I response, and a value of 0 indicating no response bias.
514            
515             =item c (or) decision_bias
516            
517             Implements the I parametric measure of response bias. Ranges from -1 to +1, with deviations from zero, measured in standard deviation units, indicating the position of the decision criterion with respect to the neutral point where the signal and noise distributions cross over, there is no response bias, and I = 0.
518            
519             Values less than 0 indicate a bias toward the I response; values greater than 0 indicate a bias toward the I response; and a value of 0 indicates no response bias.
520            
521             =item griers_bias
522            
523             Implements Griers I nonparametric measure of response bias.
524            
525             Ranges from -1 to +1, with values less than 0 indicating a bias toward the I response, values greater than 0 indicating a bias toward the I response, and a value of 0 indicating no response bias.
526            
527             =back
528            
529             =cut
530            
531             # --------------------
532             sub bias {
533             # --------------------
534 8     8 1 5248 my $self = shift;
535 8         11 local $_ = shift;
536 8 50       22 my %args = ref $_[0] ? %{(shift)} : (); # optimistic
  0         0  
537             CASE:{
538 8 100       9 /^b|li/i && do { return $self->_likelihood_bias(%args); };
  8         30  
  1         5  
539 7 100       17 /^lo/i && do { return $self->_log_likelihood_bias(%args); };
  1         10  
540 6 100       18 /^c|d/i && do { return $self->_decision_bias(%args); };
  5         14  
541 1 50       8 /^g/i && do { return $self->_griers_bias(%args)};
  1         3  
542             } #end CASE
543             }
544            
545             sub _likelihood_bias { # beta
546 1     1   2 my $self = shift;print "args = ", join(', ', @_), "\n";
  1         232  
547 1         26 my ($h, $f) = $self->init(@_);#print "init: hr = $h far = $f\n";
548 1         34 return _precisioned($self->{'precision_s'}, exp( ( ( (ndtri($f)**2) - (ndtri($h)**2) ) / 2 ) ) );
549             }
550            
551             sub _log_likelihood_bias { # ln(beta)
552 1     1   2 my $self = shift;
553 1         3 my ($h, $f) = $self->init(@_);
554 1         10 return _precisioned($self->{'precision_s'}, ( ( (ndtri($f)**2) - (ndtri($h)**2) ) / 2 ));
555             }
556            
557             sub _decision_bias { # c
558 5     5   6 my $self = shift;
559 5         8 my ($h, $f) = $self->init(@_);
560 5         26 return _precisioned($self->{'precision_s'}, -1 *( ( ndtri($h) + ndtri($f) ) / 2 ) );
561             }
562            
563             sub _griers_bias { # B''
564 1     1   2 my $self = shift;
565 1         4 my ($h, $f) = $self->init(@_);
566 1         2 my ($a, $b, $c) = ();
567 1 50       3 if ($h >= $f) {
568 1         4 $a = ( $h * (1 - $h) );
569 1         2 $b = ( $f * (1 - $f) );
570 1         3 $c = ( $a - $b ) / ( $a + $b );
571             }
572             else {
573 0         0 $a = ( $f * (1 - $f) );
574 0         0 $b = ( $h * (1 - $h) );
575 0         0 $c = ( $a - $b ) / ( $a + $b );
576             }
577 1         9 return _precisioned($self->{'precision_s'}, $c);
578             }
579            
580             =head2 criterion
581            
582             $sdt->criterion() # assume d' and c can be calculated from already inited param values
583             $sdt->criterion(d => float, c => float)
584            
585             I: C, C
586            
587             Returns the value of the criterion for given values of sensitivity I and bias I, viz.: I = I / 2 + I.
588            
589             =cut
590            
591             # --------------------
592             sub criterion {
593             # --------------------
594 1     1 1 370 my $dc = _get_dc(@_);
595 1         9 return _precisioned($_[0]->{'precision_s'}, ($dc->{'d'} / 2) + $dc->{'c'} );
596             }
597             *dc2k = \&criterion; # Alias
598             *crit = \&criterion;
599            
600            
601             =head2 dc2hr
602            
603             $sdt->dc2hr() # assume d' and c can be calculated from already inited param values
604             $sdt->dc2hr(d => float, c => float)
605            
606             Returns the hit-rate estimated from given values of sensitivity I and bias I, viz.: I
= phi(I / 2 - I).
607            
608             =cut
609            
610             # --------------------
611             sub dc2hr {
612             # --------------------
613 1     1 1 460 my $dc = _get_dc(@_);
614 1         14 return _precisioned($_[0]->{'precision_s'}, ndtr(($dc->{'d'} / 2) - $dc->{'c'}) );
615             }
616            
617             =head2 dc2far
618            
619             $sdt->dc2far() # assume d' and c can be calculated from already inited param values
620             $sdt->dc2far(d => float, c => float)
621            
622             Returns the false-alarm-rate estimated from given values of sensitivity I and bias I, viz.: I = phi(-I / 2 - I).
623            
624             =cut
625            
626             # --------------------
627             sub dc2far {
628             # --------------------
629 1     1 1 380 my $dc = _get_dc(@_);
630 1         8 return _precisioned($_[0]->{'precision_s'}, ndtr(-1*($dc->{'d'} / 2) - $dc->{'c'}) );
631             }
632            
633             =head2 dc2logbeta
634            
635             $sdt->dc2logbeta() # assume d' and c can be calculated from already inited param values
636             $sdt->dc2logbeta(d => float, c => float)
637            
638             Returns the log-likelihood (beta) bias estimated from given values of sensitivity I and bias I, viz.: I = I . I.
639            
640             =cut
641            
642             # --------------------
643             sub dc2logbeta {
644             # --------------------
645 1     1 1 392 my $dc = _get_dc(@_);
646 1         7 return _precisioned($_[0]->{'precision_s'}, $dc->{'d'} * $dc->{'c'} );
647             }
648            
649             sub _get_dc {
650 4     4   8 my ($self, %params) = @_;
651 4         6 my %dc = ();
652 4         6 foreach (qw/d c/) {
653 8 50       22 $dc{$_} = $params{$_} if defined $params{$_};
654             }
655 4 50       16 $dc{'d'} = $self->sensitivity('d') if !defined $dc{'d'};
656 4 50       24 $dc{'c'} = $self->bias('c') if !defined $dc{'c'};
657 4         10 return \%dc;
658             }
659            
660             sub _init_rate {# Initialise hit and false-alarm rates:
661            
662 4     4   6 my ($count, $trials, $correction) = @_;
663 4         5 my $rate;
664 4 50       9 $correction = 1 if !defined $correction; # default correction
665            
666             # Need (i) no. of hits and signal trials, or (ii) no. of false alarms and noise trials:
667 4 50 33     16 croak __PACKAGE__, " Number of hits/false-alarms and signal/noise trials needed to calculate rate" if ! defined $count || ! defined $trials;
668            
669 4 50       15 if ($correction > 1) { # loglinear correction, regardless of values:
670 0         0 $rate = _loglinear_correct($count, $trials);
671             }
672             else { # get rate first, applying corrections if needed (unless explicitly verboten):
673 4         7 $rate = $count / $trials;
674 4 100       10 unless ($correction == 0) {
675 2         5 $rate = _n_correct($rate, $trials);
676             }
677             }
678 4         20 return $rate;
679             }
680            
681             sub _loglinear_correct {
682 0     0   0 return ($_[0] + .5) / ($_[1] + 1); # either hits & signal_trials; or false_alarms and noise_trials
683             }
684            
685             sub _n_correct {
686 2     2   9 my ($rate, $trials) = @_;
687 2         3 my $retval;
688 2 50       7 if (! $rate) {
    100          
689 0         0 $retval = .5 / $trials;
690             }
691             elsif ($rate == 1) {
692 1         3 $retval = ($trials - .5) / $trials;
693             }
694             else {
695 1         2 $retval = $rate;
696             }
697 2         22 return $retval;
698             }
699            
700             sub _precisioned {
701 24 50   24   207 return $_[0] ? sprintf('%.' . $_[0] . 'f', $_[1]) : $_[1];
702             }
703            
704             sub _valid_p {
705 2     2   4 my $p = shift;
706 2 50 33     28 return ($p !~ /^0?\.\d+$/) || ($p < 0 || $p > 1) ? 0 : 1;
707             }
708            
709             1;
710            
711             __END__