File Coverage

blib/lib/GitInsight.pm
Criterion Covered Total %
statement 15 17 88.2
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 21 23 91.3


line stmt bran cond sub pod time code
1             package GitInsight;
2              
3             # XXX: Add behavioural change detection, focusing on that period for predictions
4              
5             BEGIN {
6 2     2   14982 $| = 1;
7 2         75 $^W = 1;
8             }
9             our $VERSION = '0.06';
10              
11             #use Carp::Always;
12 2     2   703 use GitInsight::Obj -base;
  2         4  
  2         10  
13 2     2   8 use strict;
  2         2  
  2         43  
14 2     2   6 use warnings;
  2         2  
  2         38  
15 2     2   40 use 5.008_005;
  2         6  
  2         60  
16 2     2   1123 use GD::Simple;
  0            
  0            
17              
18             use Carp;
19             use Storable qw(dclone);
20             use POSIX;
21             use Time::Local;
22             use GitInsight::Util
23             qw(markov markov_list LABEL_DIM gen_m_mat gen_trans_mat info error warning wday label prob label_step);
24             use List::Util qw(max);
25              
26             use LWP::UserAgent;
27             use POSIX qw(strftime ceil);
28              
29             has [qw(username contribs calendar)];
30             has 'verbose' => sub {0};
31             has 'no_day_stats' => sub {0};
32             has 'statistics' => sub {0};
33             has 'ca_output' => sub {1};
34             has 'accuracy' => sub {0};
35             has [qw(left_cutoff cutoff_offset file_output)];
36              
37             sub contrib_calendar {
38             my $self = shift;
39             my $username = shift || $self->username;
40             $self->username($username) if !$self->username;
41             my $ua = LWP::UserAgent->new;
42             $ua->timeout(10);
43             $ua->env_proxy;
44             my $response
45             = $ua->get(
46             'https://github.com/users/' . $username . '/contributions' );
47             info "Getting "
48             . 'https://github.com/users/'
49             . $username
50             . '/contributions'
51             if $self->verbose;
52              
53             if ( $response->is_success ) {
54             $self->decode( $response->decoded_content );
55             return $self->contribs;
56             }
57             else {
58             die $response->status_line;
59             }
60             }
61              
62             sub draw_ca {
63             my $self = shift;
64             my @CA = @_;
65             my $cols = ceil( $#CA / 7 ) + 1;
66             my $rows = 7;
67              
68             my $cell_width = 50;
69             my $cell_height = 50;
70             my $border = 3;
71             my $width = $cols * $cell_width;
72             my $height = $rows * $cell_height;
73              
74             my $img = GD::Simple->new( $width, $height );
75              
76             $img->font(gdSmallFont); #i'll need that later
77             for ( my $c = 0; $c < $cols; $c++ ) {
78             for ( my $r = 0; $r < $rows; $r++ ) {
79             my $color = $CA[ $c * $rows + $r ]
80             or
81             next; #infering ca from sequences of colours generated earlier
82             my @topleft = ( $c * $cell_width, $r * $cell_height );
83             my @botright = (
84             $topleft[0] + $cell_width - $border,
85             $topleft[1] + $cell_height - $border
86             );
87             eval {
88             $img->bgcolor( @{$color} );
89             $img->fgcolor( @{$color} );
90             };
91             $img->rectangle( @topleft, @botright );
92             $img->moveTo( $topleft[0] + 2, $botright[1] + 2 );
93             $img->fgcolor( 255, 0, 0 )
94             and $img->rectangle( @topleft, @botright )
95             if ( $c * $rows + $r >= ( scalar(@CA) - 7 ) );
96             $img->fgcolor( 0, 0, 0 )
97             and $img->string( $GitInsight::Util::wday[$r] )
98             if ( $c == 0 );
99             }
100             }
101             if ( defined $self->file_output ) {
102             my $filename = $self->file_output . ".png";
103              
104             #. "/"
105             #. join( "_", $self->start_day, $self->last_day ) . "_"
106             #. $self->username . "_"
107             #. scalar(@CA) .
108             open my $PNG, ">" . $filename;
109             binmode($PNG);
110             print $PNG $img->png;
111             close $PNG;
112             info "File written in : " . $filename if $self->verbose;
113             return $filename;
114             }
115             else {
116             return $img->png;
117             }
118              
119             }
120              
121             # useful when interrogating the object
122             sub start_day { shift->{first_day}->{data} }
123             sub last_day { @{ shift->{result} }[-1]->[2] }
124             sub prediction_start_day { @{ shift->{result} }[0]->[2] }
125              
126             sub _accuracy {
127             my $self = shift;
128             my ( @chunks, @commits );
129             push @chunks, [ splice @{ $self->calendar }, 0, 7 ]
130             while @{ $self->calendar };
131              
132             #@chunks contain a list of arrays of 7 days each
133              
134             my $total_days = 0;
135             my $accuracy = 0;
136             for (@chunks) {
137              
138             # next if @{$_} < 4;
139             push( @commits, @{$_} );
140             my $Insight = GitInsight->new(
141             no_day_stats => $self->no_day_stats,
142             ca_output => 0,
143             username => $self->username
144             ); #disable png generation
145             $Insight->decode( [@commits] )
146             ; #using $_ for small contributors is better
147             $Insight->process;
148             foreach my $res ( @{ $Insight->{result} } ) {
149             next if ( !exists $self->contribs->{ $res->[2] }->{l} );
150             $accuracy++
151             if ( $self->contribs->{ $res->[2] }->{l} == $res->[1] );
152             $total_days++;
153             }
154             }
155             my $accuracy_prob = prob( $total_days, $accuracy );
156             $self->{accuracy} = $accuracy_prob;
157             info "Accuracy is $accuracy / $total_days" if $self->verbose;
158             info sprintf( "%.5f", $accuracy_prob * 100 ) . " \%" if $self->verbose;
159             return $self;
160             }
161              
162             sub _decode_calendar {
163             shift;
164             my $content = shift;
165             my @out;
166             push( @out, [ $2, $1 ] )
167             while ( $content =~ m/data\-count="(.*?)" data\-date="(.*?)"/g );
168              
169             return \@out;
170             }
171              
172             # first argument is the data:
173             # it should be a string in the form [ [2013-01-20, 9], .... ] a stringified form of arrayref. each element must be an array ref containing in the first position the date, and in the second the commits .
174             sub decode {
175             my $self = shift;
176              
177             #my $response = ref $_[0] ne "ARRAY" ? eval(shift) : shift;
178             my $response
179             = ref $_[0] ne "ARRAY" ? $self->_decode_calendar(shift) : shift;
180             $self->calendar( dclone($response) );
181             my %commits_count;
182             my $min = $self->left_cutoff || 0;
183             $self->{result} = []; #empty the result
184             $min = 0 if ( $min < 0 ); # avoid negative numbers
185             my $max
186             = $self->cutoff_offset || ( scalar( @{$response} ) - 1 );
187             $max = scalar( @{$response} )
188             if $max > scalar( @{$response} )
189             ; # maximum cutoff boundary it's array element number
190             info "$min -> $max portion" if $self->verbose;
191             my $max_commit
192             = max( map { $_->[1] } @{$response} ); #Calculating label steps
193             label_step( 0 .. $max_commit ); #calculating quartiles over commit count
194             info( "Max commit is: " . $max_commit ) if $self->verbose;
195             $self->{first_day}->{day} = wday( $response->[0]->[0] )
196             ; #getting the first day of the commit calendar, it's where the ca will start
197             my ($index)
198             = grep { $GitInsight::Util::wday[$_] eq $self->{first_day}->{day} }
199             0 .. $#GitInsight::Util::wday;
200             $self->{first_day}->{index} = $index;
201             $self->{first_day}->{data} = $response->[$min]->[0];
202             push( @{ $self->{ca} }, [ 255, 255, 255 ] )
203             for (
204             0 .. scalar(@GitInsight::Util::wday) #white fill for labels
205             + ( $index - 1 )
206             ); #white fill for no contribs
207              
208             $self->{transition} = gen_trans_mat( $self->no_day_stats );
209             my $last;
210             $self->{last_week}
211             = [ map { [ $_->[0], label( $_->[1] ) ] }
212             ( @{$response} )[ ( ( $max + $min ) - 6 ) .. ( $max + $min ) ] ]
213             ; # cutting the last week from the answer and substituting the label instead of the commit number
214             #print( $self->{transition}->{$_} ) for (last_week keys $self->{transition} );
215             # $self->{max_commit} =0;
216             $self->contribs(
217             $self->no_day_stats
218             ? { map {
219             my $l = label( $_->[1] );
220             push( @{ $self->{ca} }, $GitInsight::Util::CA_COLOURS{$l} )
221             ; #building the ca
222             $last = $l if ( !$last );
223              
224             # $commits_count{ $_->[1] } = 1;
225             $self->{stats}->{$l}++
226             if $self->statistics == 1; #filling stats hashref
227             $self->{transition_hash}->{$last}->{$l}++
228             ; #filling transition_hash hashref from $last (last seen label) to current label
229             $self->{transition}
230             ->slice("$last,$l")++; #filling transition matrix
231             #$self->{max_commit} = $_->[1] if ($_->[1]>$self->{max_commit});
232             $last = $l;
233             $_->[0] => {
234             c => $_->[1], #commits
235             l => $l #label
236             }
237              
238             } splice( @{$response}, $min, ( $max + 1 ) )
239             }
240             : { map {
241             my $w = wday( $_->[0] );
242             my $l = label( $_->[1] );
243             push( @{ $self->{ca} }, $GitInsight::Util::CA_COLOURS{$l} );
244             $last = $l if ( !$last );
245              
246             # $commits_count{ $_->[1] } = 1;
247             $self->{stats}->{$w}->{$l}++
248             if $self->statistics == 1; #filling stats hashref
249             $self->{transition_hash}->{$w}->{$last}
250             ->{$l}++; #filling stats hashref
251             $self->{transition}->{$w}
252             ->slice("$last,$l")++; #filling transition matrix
253             $last = $l;
254             $_->[0] => {
255             c => $_->[1], #commits
256             d => $w, #day in the week
257             l => $l #label
258             }
259              
260             } splice( @{$response}, $min, ( $max + 1 ) )
261             }
262             );
263              
264             return $self->contribs;
265             }
266              
267             sub process {
268             my $self = shift;
269             croak "process() called while you have not specified an username"
270             if !$self->username;
271             $self->contrib_calendar( $self->username )
272             if !$self->contribs and $self->username;
273             $self->_transition_matrix;
274             $self->_markov;
275             $self->_gen_stats if ( $self->statistics );
276             $self->{png} = $self->draw_ca( @{ $self->{ca} } )
277             if ( $self->ca_output == 1 );
278             $self->{steps} = \%GitInsight::Util::LABEL_STEPS;
279             $self->_accuracy if $self->accuracy and $self->accuracy == 1;
280             return $self;
281             }
282              
283             sub _gen_stats {
284             my $self = shift;
285             my $sum = 0;
286             if ( $self->no_day_stats ) {
287             $sum += $_ for values %{ $self->{stats} };
288             foreach my $k ( keys %{ $self->{stats} } ) {
289             info "Calculating probability for label $k $sum / "
290             . $self->{stats}->{$k}
291             if $self->verbose;
292             my $prob = prob( $sum, $self->{stats}->{$k} );
293             info "Is: $prob" if $self->verbose;
294             $self->{stats}->{$k} = sprintf "%.5f", $prob;
295             }
296             }
297             else {
298             foreach my $k ( keys %{ $self->{stats} } ) {
299             $sum = 0;
300             $sum += $_ for values %{ $self->{stats}->{$k} };
301             map {
302             info "Calculating probability for $k -> label $_ $sum / "
303             . $self->{stats}->{$k}->{$_}
304             if $self->verbose;
305             my $prob = prob( $sum, $self->{stats}->{$k}->{$_} );
306             info "Is: $prob" if $self->verbose;
307             $self->{stats}->{$k}->{$_} = sprintf "%.5f", $prob;
308             } ( keys %{ $self->{stats}->{$k} } );
309             }
310             }
311             }
312              
313             sub _markov {
314             my $self = shift;
315             info "Markov chain phase" if $self->verbose;
316             my $dayn = 1;
317             info "Calculating predictions for "
318             . ( scalar( @{ $self->{last_week} } ) ) . " days"
319             if $self->verbose;
320              
321             foreach my $day ( @{ $self->{last_week} } ) { #cycling the last week
322             my $wd = wday( $day->[0] ); #computing the weekday
323             my $ld = $day->[1]; #getting the label
324             my $M = markov_list(
325             gen_m_mat($ld),
326             $self->no_day_stats
327             ? $self->{transition}
328             : $self->{transition}->{$wd},
329             $dayn
330             ); #Computing the markov for the state
331              
332             my $label = 0;
333             $M->[$label] > $M->[$_] or $label = $_ for 1 .. scalar(@$M) - 1;
334             push( @{ $self->{ca} }, $GitInsight::Util::CA_COLOURS{$label} )
335             ; #adding the predictions to ca
336              
337             my ( $mday, $mon, $year )
338             = reverse( split( /-/, $day->[0] ) ); #splitting date
339              
340             push(
341             @{ $self->{result} },
342             [ $wd, $label,
343             $day->[0] = strftime(
344             '%Y-%m-%d',
345             localtime(
346             timelocal( 0, 0, 0, $mday, $mon - 1, $year )
347             + 7 * 86_400
348             )
349             ) #adding 7 days to the date, and adding the result to $self->{result}
350             ,
351             $M
352             ]
353             );
354              
355             if ( $self->verbose ) {
356             info "$wd: "
357             . $label . " has "
358             . ( sprintf "%.2f", $M->[$label] * 100 )
359             . "% of probability to happen";
360             info "\t"
361             . $_
362             . " ---- "
363             . ( sprintf "%.2f", $M->[$_] * 100 ) . "%"
364             for 0 .. scalar(@$M) - 1;
365             }
366              
367             ############# TREEMAP GENERATION #############
368             $self->{'treemap'}->{'name'} = "day";
369             my $hwd = { name => $day->[0], children => [] };
370             push(
371             @{ $hwd->{children} },
372             { name => $_, size => $M->[$_] * 10000 }
373             ) for 0 .. scalar(@$M) - 1;
374             push( @{ $self->{'treemap'}->{"children"} }, $hwd );
375             ################################################
376              
377             $dayn++ if $self->no_day_stats;
378             }
379              
380             return $self->{result};
381              
382             }
383              
384             sub _transition_matrix {
385              
386             #transition matrix, sum all the transitions occourred in each day, and do prob(sumtransitionrow ,current transation occurrance )
387             my $self = shift;
388             info "Going to build transation matrix probabilities" if $self->verbose;
389             if ( $self->no_day_stats ) {
390             my $sum = $self->{transition}->sumover();
391             map {
392             foreach my $c ( 0 .. LABEL_DIM ) {
393             $self->{transition}->slice("$_,$c")
394             .= prob( # slice of the single element of the matrix , calculating bayesian inference
395             $sum->at($c), #contains the transition sum of the row
396             $self->{transition}->at( $_, $c )
397             ); # all the transation occurred, current transation
398             }
399             } ( 0 .. LABEL_DIM );
400             }
401             else {
402             foreach my $k ( keys %{ $self->{transition} } ) {
403             my $sum = $self->{transition}->{$k}->sumover();
404             map {
405             foreach my $c ( 0 .. LABEL_DIM ) {
406             $self->{transition}->{$k}->slice("$_,$c")
407             .= prob( # slice of the single element of the matrix , calculating bayesian inference
408             $sum->at($c)
409             , #contains the transition sum of the row over the day
410             $self->{transition}->{$k}->at( $_, $c )
411             )
412             ; # all the transation occurred in those days, current transation
413             }
414             } ( 0 .. LABEL_DIM );
415             }
416             }
417             }
418              
419             1;
420             __END__