File Coverage

blib/lib/Statistics/RankCorrelation.pm
Criterion Covered Total %
statement 148 150 98.6
branch 49 58 84.4
condition 14 21 66.6
subroutine 21 21 100.0
pod 16 16 100.0
total 248 266 93.2


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