| 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__ |