File Coverage

blib/lib/Statistics/RankCorrelation.pm
Criterion Covered Total %
statement 147 149 98.6
branch 47 56 83.9
condition 14 21 66.6
subroutine 20 20 100.0
pod 16 16 100.0
total 244 262 93.1


line stmt bran cond sub pod time code
1             package Statistics::RankCorrelation;
2             our $AUTHORITY = 'cpan:GENE';
3             # ABSTRACT: Compute the rank correlation between two vectors
4              
5 2     2   41976 use strict;
  2         5  
  2         51  
6 2     2   10 use warnings;
  2         5  
  2         86  
7              
8             our $VERSION = '0.1205';
9              
10 2     2   10 use Carp;
  2         4  
  2         3703  
11              
12             sub new {
13 15     15 1 490 my $proto = shift;
14 15   33     67 my $class = ref($proto) || $proto;
15 15         26 my $self = {};
16 15         25 bless $self, $class;
17 15         39 $self->_init(@_);
18 15         47 return $self;
19             }
20              
21             sub _init {
22 15     15   18 my $self = shift;
23              
24             # Handle vector and named parameters.
25 15         69 while( my $arg = shift ) {
26 29 100       68 if( ref $arg eq 'ARRAY' ) {
    50          
27 28 100       57 if( !defined $self->x_data ) { $self->x_data( $arg ) }
  14 50       27  
28 14         23 elsif( !defined $self->y_data ) { $self->y_data( $arg ) }
29             }
30             elsif( !ref $arg ) {
31 1         2 my $v = shift;
32 1 50       7 $self->{$arg} = defined $v ? $v : $arg;
33             }
34             }
35              
36             # Automatically compute the ranks if given data.
37 15 50 66     31 if( $self->x_data && $self->y_data &&
      66        
      33        
38 14         28 @{ $self->x_data } && @{ $self->y_data }
  14         58  
39             ) {
40             # "Co-normalize" the vectors if they are of unequal size.
41 14         26 my( $x, $y ) = pad_vectors( $self->x_data, $self->y_data );
42              
43             # "Co-sort" the bivariate data set by the first one.
44 14 100       37 ( $x, $y ) = co_sort( $x, $y ) if $self->{sorted};
45              
46             # Set the massaged data.
47 14         30 $self->x_data( $x );
48 14         25 $self->y_data( $y );
49              
50             # Set the size of the data vector.
51 14         14 $self->size( scalar @{ $self->x_data } );
  14         28  
52              
53             # Set the ranks and ties of the vectors.
54 14         30 ( $x, $y ) = rank( $self->x_data );
55 14         89 $self->x_rank( $x );
56 14         28 $self->x_ties( $y );
57 14         29 ( $x, $y ) = rank( $self->y_data );
58 14         36 $self->y_rank( $x );
59 14         33 $self->y_ties( $y );
60             }
61             }
62              
63             sub size {
64 196     196 1 244 my $self = shift;
65 196 100       404 $self->{size} = shift if @_;
66 196         415 return $self->{size};
67             }
68              
69             sub x_data {
70 130     130 1 742 my $self = shift;
71 130 100       274 $self->{x_data} = shift if @_;
72 130         365 return $self->{x_data};
73             }
74              
75             sub y_data {
76 101     101 1 129 my $self = shift;
77 101 100       203 $self->{y_data} = shift if @_;
78 101         292 return $self->{y_data};
79             }
80              
81             sub x_rank {
82 18     18 1 32 my $self = shift;
83 18 100       51 $self->{x_rank} = shift if @_;
84 18         38 return $self->{x_rank};
85             }
86              
87             sub y_rank {
88 19     19 1 29 my $self = shift;
89 19 100       53 $self->{y_rank} = shift if @_;
90 19         39 return $self->{y_rank};
91             }
92              
93             sub x_ties {
94 46     46 1 118 my $self = shift;
95 46 100       100 $self->{x_ties} = shift if @_;
96 46         145 return $self->{x_ties};
97             }
98              
99             sub y_ties {
100 44     44 1 65 my $self = shift;
101 44 100       100 $self->{y_ties} = shift if @_;
102 44         148 return $self->{y_ties};
103             }
104              
105             sub spearman {
106 14     14 1 23 my $self = shift;
107             # Algorithm contributed by Jon Schutz :
108 14         22 my($x_sum, $y_sum) = (0, 0);
109 14         17 $x_sum += $_ for @{$self->{x_rank}};
  14         77  
110 14         20 $y_sum += $_ for @{$self->{y_rank}};
  14         60  
111 14         34 my $n = $self->size;
112 14         26 my $x_mean = $x_sum / $n;
113 14         17 my $y_mean = $y_sum / $n;
114             # Compute the sum of the difference of the squared ranks.
115 14         22 my($x_sum2, $y_sum2, $xy_sum) = (0, 0, 0);
116 14         34 for( 0 .. $self->size - 1 ) {
117 119         955 $x_sum2 += ($self->{x_rank}[$_] - $x_mean) ** 2;
118 119         196 $y_sum2 += ($self->{y_rank}[$_] - $y_mean) ** 2;
119 119         234 $xy_sum += ($self->{x_rank}[$_] - $x_mean) * ($self->{y_rank}[$_] - $y_mean);
120             }
121 14 100 66     75 return 1 if $x_sum2 == 0 || $y_sum2 == 0;
122 13         152 return $xy_sum / sqrt($x_sum2 * $y_sum2);
123             }
124              
125              
126             sub rank {
127 28     28 1 34 my $u = shift;
128              
129             # Make a list of tied ranks for each datum.
130 28         30 my %ties;
131 28         71 push @{ $ties{ $u->[$_] } }, $_ for 0 .. @$u - 1;
  238         609  
132              
133 28         42 my ($old, $cur) = (0, 0);
134              
135             # Set the averaged ranks.
136 28         32 my @ranks;
137 28         108 for my $x (sort { $a <=> $b } keys %ties) {
  398         539  
138             # Get the number of ties.
139 197         188 my $ties = @{ $ties{$x} };
  197         284  
140 197         244 $cur += $ties;
141              
142 197 100       294 if ($ties > 1) {
143             # Average the tied data.
144 15         28 my $average = $old + ($ties + 1) / 2;
145 15         45 $ranks[$_] = $average for @{ $ties{$x} };
  15         71  
146             }
147             else {
148             # Add the single rank to the list of ranks.
149 182         287 $ranks[ $ties{$x}[0] ] = $cur;
150             }
151              
152 197         289 $old = $cur;
153             }
154              
155             # Remove the non-tied ranks.
156 28         77 delete @ties{ grep @{ $ties{$_} } <= 1, keys %ties };
  197         368  
157              
158             # Return the ranks arrayref in a scalar context and include ties
159             # if called in a list context.
160 28 50       117 return wantarray ? (\@ranks, \%ties) : \@ranks;
161             }
162              
163             sub co_sort {
164 1     1 1 2 my( $u, $v ) = @_;
165 1 50       5 return unless @$u == @$v;
166             # Ye olde Schwartzian Transforme:
167             $v = [
168 10         17 map { $_->[1] }
169 23 50       50 sort { $a->[0] <=> $b->[0] || $a->[1] <=> $b->[1] }
170 1         3 map { [ $u->[$_], $v->[$_] ] }
  10         29  
171             0 .. @$u - 1
172             ];
173             # Sort the independent vector last.
174 1         6 $u = [ sort { $a <=> $b } @$u ];
  23         29  
175 1         2 return $u, $v;
176             }
177              
178             sub csim {
179 3     3 1 6 my $self = shift;
180              
181             # Get the pitch matrices for each vector.
182 3         9 my $m1 = correlation_matrix($self->{x_data});
183             #warn map { "@$_\n" } @$m1;
184 3         8 my $m2 = correlation_matrix($self->{y_data});
185             #warn map { "@$_\n" } @$m2;
186              
187             # Compute the rank correlation.
188 3         4 my $k = 0;
189 3         7 for my $i (0 .. @$m1 - 1) {
190 16         23 for my $j (0 .. @$m1 - 1) {
191 96 100       207 $k++ if $m1->[$i][$j] == $m2->[$i][$j];
192             }
193             }
194              
195             # Return the rank correlation normalized by the number of rows in
196             # the pitch matrices.
197 3         21 return $k / (@$m1 * @$m1);
198             }
199              
200             sub pad_vectors {
201 14     14 1 22 my ($u, $v) = @_;
202              
203 14 50       38 if (@$u > @$v) {
    50          
204 0         0 $v = [ @$v, (0) x (@$u - @$v) ];
205             }
206             elsif (@$u < @$v) {
207 0         0 $u = [ @$u, (0) x (@$v - @$u) ];
208             }
209              
210 14         26 return $u, $v;
211             }
212              
213             sub correlation_matrix {
214 6     6 1 9 my $u = shift;
215 6         5 my $c;
216              
217             # Is a row value (i) lower than a column value (j)?
218 6         13 for my $i (0 .. @$u - 1) {
219 32         54 for my $j (0 .. @$u - 1) {
220 192 100       414 $c->[$i][$j] = $u->[$i] < $u->[$j] ? 1 : 0;
221             }
222             }
223              
224 6         13 return $c;
225             }
226              
227             sub kendall {
228 13     13 1 27 my $self = shift;
229              
230             # Calculate number of concordant and discordant pairs.
231 13         16 my( $concordant, $discordant ) = ( 0, 0 );
232 13         27 for my $i ( 0 .. $self->size - 1 ) {
233 115         237 for my $j ( $i + 1 .. $self->size - 1 ) {
234 574         1270 my $x_sign = sign( $self->{x_data}[$j] - $self->{x_data}[$i] );
235 574         1395 my $y_sign = sign( $self->{y_data}[$j] - $self->{y_data}[$i] );
236 574 100 100     2371 if (not($x_sign and $y_sign)) {}
    100          
237 288         452 elsif ($x_sign == $y_sign) { $concordant++ }
238 178         285 else { $discordant++ }
239             }
240             }
241              
242             # Set the indirect relationship.
243 13         27 my $d = $self->size * ($self->size - 1) / 2;
244 13 100 100     44 if( keys %{ $self->x_ties } || keys %{ $self->y_ties } ) {
  13         26  
  11         23  
245 5         8 my $x = 0;
246 5         6 $x += @$_ * (@$_ - 1) for values %{ $self->x_ties };
  5         8  
247 5         11 $x = $d - $x / 2;
248 5         6 my $y = 0;
249 5         6 $y += @$_ * (@$_ - 1) for values %{ $self->y_ties };
  5         11  
250 5         10 $y = $d - $y / 2;
251 5         6 $d = sqrt($x * $y);
252             }
253              
254 13         136 return ($concordant - $discordant) / $d;
255             }
256              
257             sub sign {
258 1148     1148 1 1300 my $x = shift;
259 1148 100       2259 return 0 if $x == 0;
260 1024 100       1938 return $x > 0 ? 1 : -1;
261             }
262              
263             1;
264              
265             __END__