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 |
||||||
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 |
||||||
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 |
||||||
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 |
||||||
100 | |||||||
101 | If C |
||||||
102 | |||||||
103 | If C |
||||||
104 | |||||||
105 | If C |
||||||
106 | |||||||
107 | If C |
||||||
108 | |||||||
109 | =item precision_s | ||||||
110 | |||||||
111 | Precision (I |
||||||
112 | |||||||
113 | =item method | ||||||
114 | |||||||
115 | Method for estimating I |
||||||
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 , the hit-rate, and B |
||||||
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 |
||||||
163 | |||||||
164 | Each C |
||||||
165 | |||||||
166 | Optionally, the method also initialises any values you give it for L |
||||||
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 |
||||||
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 |
||||||
228 | |||||||
229 | To I |
||||||
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 |
||||||
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 |
||||||
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 ): |
||||||
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 L |
||||||
328 | |||||||
329 | If there are more than two states (not only signal and noise-plus-signal), then I |
||||||
330 | |||||||
331 | =item forced_choice | ||||||
332 | |||||||
333 | An estimate of I |
||||||
334 | |||||||
335 | At least a couple methods are available to estimate I |
||||||
336 | |||||||
337 | B>: satisfies "the 2% bound for all I |
||||||
338 | |||||||
339 | For I |
||||||
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 |
||||||
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 |
||||||
360 | |||||||
361 | =for html d' = ( phi–1(hr) – phi–1(1/n) ) / An |
||||||
362 | |||||||
363 | where I |
||||||
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 |
||||||
376 | |||||||
377 | If both the hit-rate and false-alarm-rate are either 0 or 1, then C |
||||||
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 |
||||||
494 | |||||||
495 | With a I |
||||||
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 |
||||||
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 |
||||||
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 |
||||||
514 | |||||||
515 | =item c (or) decision_bias | ||||||
516 | |||||||
517 | Implements the I |
||||||
518 | |||||||
519 | Values less than 0 indicate a bias toward the I |
||||||
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 |
||||||
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 |
||||||
586 | |||||||
587 | Returns the value of the criterion for given values of sensitivity 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 = phi(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 |
||||||
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 |
||||||
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__ |