--- loncom/interface/statistics/lonproblemanalysis.pm 2004/10/29 16:39:42 1.101
+++ loncom/interface/statistics/lonproblemanalysis.pm 2004/11/10 21:23:10 1.104
@@ -1,6 +1,6 @@
# The LearningOnline Network with CAPA
#
-# $Id: lonproblemanalysis.pm,v 1.101 2004/10/29 16:39:42 matthew Exp $
+# $Id: lonproblemanalysis.pm,v 1.104 2004/11/10 21:23:10 matthew Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -152,19 +152,10 @@ sub BuildProblemAnalysisPage {
$problem_data,
\@Students);
} elsif ($current_problem->{'resptype'} eq 'numerical') {
- ##
- ## analyze all responses of a problem at once
- my $res = $current_problem->{'resource'};
- foreach my $partid (@{$res->{'parts'}}) {
- $current_problem->{'part'} = $partid;
- foreach my $respid (@{$res->{'partdata'}->{$partid}->{'ResponseIds'}}) {
- $current_problem->{'respid'}=$respid;
- &NumericalResponseAnalysis($r,$current_problem,
- $problem_data,\@Students);
- }
- }
+ &numerical_response_analysis($r,$current_problem,
+ $problem_data,\@Students);
} else {
- $r->print('
This analysis is not supported
');
+ $r->print('Analysis of '.$current_problem->{'resptype'}.' is not supported
');
}
}
$r->print('
');
@@ -178,7 +169,6 @@ sub BuildProblemAnalysisPage {
}
}
-
#########################################################
#########################################################
##
@@ -186,79 +176,403 @@ sub BuildProblemAnalysisPage {
##
#########################################################
#########################################################
-sub NumericalResponseAnalysis {
- my ($r,$problem,$problem_data,$Students) = @_;
+sub numerical_response_analysis {
+ my ($r,$problem,$problem_analysis,$students) = @_;
my $c = $r->connection();
- my ($resource,$partid,$respid) = ($problem->{'resource'},
- $problem->{'part'},
- $problem->{'respid'});
#
- if (scalar(@{$resource->{'parts'}})>1) {
- if (@{$resource->{'partdata'}->{$partid}->{'ResponseIds'}}>1) {
- $r->print(''.
- &mt('Part [_1], response [_2].',$partid,$respid).
- '
');
- } else {
- $r->print(''.
- &mt('Part [_1]',$partid,$respid).
- '
');
- }
- } elsif (@{$resource->{'partdata'}->{$partid}->{'ResponseIds'}}>1) {
- $r->print(''.&mt('Response [_1]',$respid).'
');
+ if ($ENV{'form.AnalyzeOver'} !~ /^(tries|time)$/) {
+ $r->print('Bad request');
}
#
- my $analysis_html;
- my $PerformanceData = &Apache::loncoursedata::get_response_data
+ my ($resource,$partid,$respid) = ($problem->{'resource'},
+ $problem->{'part'},
+ $problem->{'respid'});
+ # Gather student data
+ my $response_data = &Apache::loncoursedata::get_response_data
(\@Apache::lonstatistics::SelectedSections,
$Apache::lonstatistics::enrollment_status,
$resource->{'symb'},$respid);
- if (! defined($PerformanceData) ||
- ref($PerformanceData) ne 'ARRAY' ) {
- $analysis_html = ''.
- &mt('There is no submission data for this resource').
- '
';
- $r->print($analysis_html);
- return;
+ #
+ $problem_analysis->{'answercomputed'} = 1;
+ if ($problem_analysis->{'answercomputed'}) {
+ my $answers =
+ &Apache::lonstathelpers::GetStudentAnswers($r,$problem,$students,
+ 'Statistics',
+ 'stats_status');
+ $r->print(&numerical_one_dimensional_plot($r,600,150,$answers));
}
+ my $analysis_html = '';
+ for (my $plot_num = 1;$plot_num<=$ENV{'form.NumPlots'};$plot_num++) {
+ my $restriction_function;
+ my $header_message;
+ my $stats_message;
+ my $post_message; # passed through &mt sooner rather than later
+ my $no_data_message;
+ my @extra_data;
+ if ($ENV{'form.AnalyzeOver'} eq 'tries') {
+ $restriction_function = sub {($_[0]->{'tries'} == $plot_num?1:0)};
+ $header_message = 'Attempt [_1]';
+ $stats_message =
+ '[_1] submissions, [_2] correct, [_3] incorrect';
+ $post_message = '';
+ $no_data_message = 'No data exists for attempt [_1]';
+ } else {
+ my $starttime = &Apache::lonhtmlcommon::get_date_from_form
+ ('startdate_'.$plot_num);
+ my $endtime = &Apache::lonhtmlcommon::get_date_from_form
+ ('enddate_'.$plot_num);
+ ($starttime,$endtime) = &ensure_start_end_times
+ ($starttime,$endtime,
+ &get_time_from_row($response_data->[0]),
+ &get_time_from_row($response_data->[-1]),
+ $plot_num);
+ $header_message = 'Data from [_2] to [_3]';
+ $extra_data[0] = &Apache::lonlocal::locallocaltime($starttime);
+ $extra_data[1] = &Apache::lonlocal::locallocaltime($endtime);
+ #
+ $stats_message =
+ '[_1] submissions from [_4] students, [_2] correct, [_3] incorrect';
+ #
+ $post_message =
+ &mt('Start time: [_1]',
+ &Apache::lonhtmlcommon::date_setter
+ ('Statistics','startdate_'.$plot_num,$starttime)).
+ '
'.
+ &mt('End time: [_1]',
+ &Apache::lonhtmlcommon::date_setter
+ ('Statistics','enddate_'.$plot_num,$endtime));
+ $restriction_function =
+ sub {
+ my $t = $_[0]->{'timestamp'};
+ if ($t >= $starttime && $t < $endtime) {
+ return 1;
+ } else {
+ return 0;
+ }
+ };
+ $no_data_message = 'No data for [_2] to [_3]';
+ }
+ #
+ my ($correct,$intervals,$answers) =
+ &numerical_response_determine_intervals($r,$resource,$partid,
+ $respid,$students);
+ if ($c->aborted()) { return; };
+ #
+ my $responses = &numerical_classify_responses($response_data,
+ $correct,
+ $restriction_function);
+ if ($responses->{'_count'} == 0) {
+ $analysis_html.=
+ ''.
+ &mt($no_data_message,$plot_num,@extra_data).
+ ' |
';
+ } else {
+ $analysis_html.=
+ ''.
+ &mt($header_message,$plot_num,@extra_data).
+ ' |
'.
+ ''.
+ &mt($stats_message,
+ $responses->{'_count'},
+ $responses->{'_correct'},
+ $responses->{'_count'}-$responses->{'_correct'},
+ $responses->{'_students'},
+ @extra_data).
+ ' |
'.
+ ''.''.
+ &numerical_plot_percent($r,$responses).' | '.
+ ''.
+ &numerical_plot_differences($r,$responses).' | '.
+ '
';
+ }
+ if ($post_message ne '') {
+ $analysis_html .=
+ ''.$post_message.' |
';
+ }
+ }
+ $analysis_html.='
';
+ $r->print($analysis_html);
#
- # This next call causes all the waiting around that people complain about
- &Apache::lonstathelpers::GetStudentAnswers($r,$problem,$Students,
- 'Statistics',
- 'stats_status');
- return if ($c->aborted());
- #
- # Collate the data
- my %Data;
- foreach my $student (@$Students) {
- my $answer = $student->{'answer'};
- $Data{$answer}++;
- }
- my @Labels = sort {$a <=> $b } keys(%Data);
- my @PlotData = @Data{@Labels};
- #
- my $width = 500;
- my $height = 100;
- my $plot = &one_dimensional_plot($r,500,100,scalar(@$Students),
- \@Labels,\@PlotData);
- $r->print($plot);
return;
}
-sub one_dimensional_plot {
- my ($r,$width,$height,$N,$Labels,$Data)=@_;
+sub numerical_plot_percent {
+ my ($r,$responses) = @_;
#
- # Compute data -> image scaling factors
- my $min = $Labels->[0];
- my $max = $Labels->[-1];
- if ($max == $min) {
- $max =$min+1;
+ my $total = $responses->{'_count'};
+ return '' if ($total == 0);
+ my $minbin = 0.5;
+ while (my ($interval,$submissions) = each(%$responses)) {
+ next if ($interval =~ /^_/);
+ my ($ans,$ans_low,$ans_high) = split(" ",$interval);
+ my $low_percent = abs(($ans-$ans_low)/$ans);
+ my $high_percent = abs(($ans_high-$ans)/$ans);
+ if ($minbin > $high_percent) { $minbin = $high_percent; }
+ if ($minbin > $low_percent) { $minbin = $low_percent; }
+ }
+ #
+ my @bins;
+ if ($minbin < 1) {
+ @bins = ('0.1','0.5','1.0','1.5','2.0','2.5','3.0','4.0','5.0',10,20,50,100);
+ } elsif ($minbin < 2) {
+ @bins = ('0.5','1.0','1.5','2.0','2.5','3.0','4.0','5.0',10,20,50,100);
+ } elsif ($minbin < 5) {
+ @bins = (1,2,3,4,5,10,25,50,75,100,200);
+ } elsif ($minbin < 10) {
+ @bins = (2,4,6,8,10,12,15,20,25,30,50,75,100,200);
+ } else {
+ @bins = (5,10,15,20,25,30,50,75,100,200);
+ }
+ my @labels = (1..scalar(@bins));
+ #
+ my @correct;
+ my @incorrect;
+ my @count;
+ while (my ($interval,$submissions) = each(%$responses)) {
+ next if ($interval =~ /^_/);
+ my ($ans,$ans_low,$ans_high) = split(" ",$interval);
+ while (my ($submission,$counts) = each(%$submissions)) {
+ my ($correct_count,$incorrect_count) = @$counts;
+ my $scaled_value = abs(($submission-$ans)/$ans);
+ my $bin=0;
+ for ($bin=0;$bin<$#bins;$bin++) { # not <= for a reason
+ last if ($bins[$bin]>$scaled_value);
+ }
+ $correct[$bin]+=$correct_count;
+ $incorrect[$bin]+=$incorrect_count;
+ $count[$bin]+=$correct_count+$incorrect_count;
+ }
+ }
+ #
+ my @plot_correct;
+ my @plot_incorrect;
+ for (my $i=0;$i<=$#bins;$i++) {
+ $plot_correct[$i] = $correct[$i]*100/$total;
+ $plot_incorrect[$i] = $incorrect[$i]*100/$total;
+ }
+ my $title = &mt('Distribution by Percent');
+ my $graph = &Apache::loncommon::DrawBarGraph
+ ($title,'Percent difference from correct','Number of answers',
+ 100,['#33FF00','#FF3300'],\@labels,\@plot_correct,\@plot_incorrect,
+ {xskip=>1});
+ #
+ my $table = $graph.$/.
+ &numerical_bin_table(\@bins,\@labels,\@incorrect,\@correct,\@count).$/;
+ return $table;
+}
+
+sub numerical_plot_differences {
+ my ($r,$responses) = @_;
+ #
+ my $total = $responses->{'_count'};
+ return '' if ($total == 0);
+ my $minbin = undef;
+ my $maxbin = undef;
+ while (my ($interval,$submissions) = each(%$responses)) {
+ next if ($interval =~ /^_/);
+ my ($ans,$ans_low,$ans_high) = split(" ",$interval);
+ my $low_diff = abs($ans-$ans_low);
+ my $high_diff = abs($ans_high-$ans);
+ if (! defined($maxbin)) { $maxbin = $low_diff;}
+ if (! defined($minbin)) { $minbin = $low_diff;}
+ #
+ if ($minbin > $high_diff) { $minbin = $high_diff; }
+ if ($minbin > $low_diff ) { $minbin = $low_diff; }
+ #
+ if ($maxbin < $high_diff) { $maxbin = $high_diff; }
+ if ($maxbin < $low_diff ) { $maxbin = $low_diff; }
+ }
+ #
+ my @bins;
+ my @labels;
+ # Hmmmm, should switch to absolute difference
+ for (my $i=1;$i<=20;$i++) {
+ push(@bins,$i*$minbin/2);
+ push(@labels,$i);
+ }
+ #
+ my @correct;
+ my @incorrect;
+ my @count;
+ while (my ($interval,$submissions) = each(%$responses)) {
+ next if ($interval =~ /^_/);
+ my ($ans,$ans_low,$ans_high) = split(" ",$interval);
+ while (my ($submission,$counts) = each(%$submissions)) {
+ my ($correct_count,$incorrect_count) = @$counts;
+ my $value = abs($submission-$ans);
+ my $bin=0;
+ for ($bin=0;$bin<$#bins;$bin++) { # not <= for a reason
+ last if ($bins[$bin]>$value);
+ }
+ $correct[$bin]+=$correct_count;
+ $incorrect[$bin]+=$incorrect_count;
+ $count[$bin]+=$correct_count+$incorrect_count;
+ }
+ }
+ #
+ my @plot_correct;
+ my @plot_incorrect;
+ for (my $i=0;$i<=$#bins;$i++) {
+ $plot_correct[$i] = $correct[$i]*100/$total;
+ $plot_incorrect[$i] = $incorrect[$i]*100/$total;
}
- my $h_scale = ($width-10)/($max-$min);
+ my $title = &mt('Distribution by Magnitude');
+ my $graph = &Apache::loncommon::DrawBarGraph
+ ($title,'magnitude difference from correct','Number of answers',
+ 100,['#33FF00','#FF3300'],\@labels,\@plot_correct,\@plot_incorrect,
+ {xskip=>1});
#
+ my $table = $graph.$/.
+ &numerical_bin_table(\@bins,\@labels,\@incorrect,\@correct,\@count).$/;
+ return $table;
+}
+
+sub numerical_classify_responses {
+ my ($full_row_data,$correct,$function) = @_;
+ my %submission_data;
+ my %students;
+ my $max=0;
+ foreach my $row (@$full_row_data) {
+ my %subm = &hashify_attempt($row);
+ if (ref($correct) eq 'HASH') {
+ $subm{'correct'} = $correct->{$subm{'student'}};
+ } else {
+ $subm{'correct'} = $correct;
+ }
+ $subm{'submission'} =~ s/=\d+\s*$//;
+ if (&$function(\%subm)) {
+ my $submission = $subm{'submission'};
+ $students{$subm{'student'}}++;
+ if (&numerical_submission_is_correct($subm{'award'})) {
+ $submission_data{'_correct'}++;
+ $submission_data{'_count'}++;
+ $submission_data{$subm{'correct'}}->{$submission}->[0]++;
+ } elsif (&numerical_submission_is_incorrect($subm{'award'})) {
+ $submission_data{'_count'}++;
+ $submission_data{$subm{'correct'}}->{$submission}->[1]++;
+ }
+ my $value =
+ $submission_data{$subm{'correct'}}->{$submission}->[0]+
+ $submission_data{$subm{'correct'}}->{$submission}->[1];
+ if ($max < $value) { $max = $value; }
+ }
+ }
+ $submission_data{'_max'} = $max;
+ $submission_data{'_students'}=scalar(keys(%students));
+ return \%submission_data;
+}
+
+sub numerical_submission_is_correct {
+ my ($award) = @_;
+ if ($award =~ /^(APPROX_ANS|EXACT_ANS)$/) {
+ return 1;
+ } else {
+ return 0;
+ }
+}
+
+sub numerical_submission_is_incorrect {
+ my ($award) = @_;
+ if ($award =~ /^(INCORRECT)$/) {
+ return 1;
+ } else {
+ return 0;
+ }
+}
+
+sub numerical_bin_table {
+ my ($bins,$labels,$incorrect,$correct,$count)=@_;
+ my $table =
+ ''.&mt('Bar').' | '.
+ ''.&mt('Range').' | '.
+ ''.&mt('Incorrect').' | '.
+ ''.&mt('Correct').' | '.
+ ''.&mt('Count').' | '.
+ '
'.$/;
+ for (my $i=0;$i[$i-1];
+ }
+ my $highnum = $bins->[$i];
+ $table .=
+ ''.
+ ''.$labels->[$i].' | '.
+ ''.$lownum.' | '.
+ ' - | '.
+ ''.$highnum.' | '.
+ ''.$incorrect->[$i].' | '.
+ ''.$correct->[$i].' | '.
+ ''.$count->[$i].' | '.
+ '
'.$/;
+ }
+ $table.= '
';
+ return $table;
+}
+
+sub numerical_response_determine_intervals {
+ my ($r,$resource,$partid,$respid,$students)=@_;
+ my $c = $r->connection();
+ #
+ # FIX ME: May need progress dialog updates
+ #
+ # Read in the cache (if it exists) before we start timing things.
+ &Apache::lonstathelpers::ensure_proper_cache($resource->{'symb'});
+ #
+ my $correct;
+ # %intervals differs from %answers because it may be possible for two
+ # students to have the same correct answer but different intervals.
+ my %intervals;
+ my %answers;
+ foreach my $student (@$students) {
+ last if ($c->aborted());
+ my $sname = $student->{'username'};
+ my $sdom = $student->{'domain'};
+ # analyze problem
+ my $analysis =
+ &Apache::lonstathelpers::analyze_problem_as_student($resource,
+ $sname,
+ $sdom);
+ # make the key
+ my $key = $partid.'.'.$respid;
+ my $interval = join(' ',($analysis->{$key.'.answer'}->[0],
+ $analysis->{$key.'.ans_low'}->[0],
+ $analysis->{$key.'.ans_high'}->[0]));
+ $correct->{$sname.':'.$sdom} = $interval;
+ $intervals{$interval}++;
+ $answers{$analysis->{$key.'.answer'}->[0]}++;
+ }
+ &Apache::lonstathelpers::write_analysis_cache();
+ return ($correct,\%intervals,\%answers);
+}
+
+#
+# Inputs: $r, $width, $height, $data
+# $n = number of students
+# $data = hashref of $answer => $frequency pairs
+sub numerical_one_dimensional_plot {
+ my ($r,$width,$height,$data)=@_;
+ #
+ # Compute data -> image scaling factors
my $max_y = 0;
- foreach (@$Data) {
- $max_y = $_ if ($max_y < $_);
+ my $min_x = undef;
+ my $max_x = undef;
+ my $n = 0;
+ while (my ($answer,$count) = each(%$data)) {
+ $n+=$count;
+ $max_y = $count if ($max_y < $count);
+ if (! defined($min_x) || $answer < $min_x) {
+ $min_x = $answer;
+ }
+ if (! defined($max_x) || $answer > $max_x) {
+ $max_x = $answer;
+ }
}
+ #
+ my $h_scale = ($width-10)/($max_x-$min_x);
+ #
my $ticscale = 5;
if ($max_y * $ticscale > $height/2) {
$ticscale = int($height/2/$max_y);
@@ -268,17 +582,16 @@ sub one_dimensional_plot {
# Create the plot
my $plot =
qq{};
- for (my $idx=0;$idx[$idx] - $min);
- my $top = $height/2-$Data->[$idx]*$ticscale;
- my $bottom = $height/2+$Data->[$idx]*$ticscale;
- $plot .=
- &line($xloc,$top,$xloc,$bottom,'888888',1);
+ while (my ($answer,$count) = each(%$data)) {
+ my $xloc = 5+$h_scale*($answer - $min_x);
+ my $top = $height/2-$count*$ticscale;
+ my $bottom = $height/2+$count*$ticscale;
+ $plot .= &line($xloc,$top,$xloc,$bottom,'888888',1);
}
#
# Put the scale on last to ensure it is on top of the data.
- if ($min < 0 && $max > 0) {
- my $circle_x = 5+$h_scale*abs($min); # '0' in data coordinates
+ if ($min_x < 0 && $max_x > 0) {
+ my $circle_x = 5+$h_scale*abs($min_x); # '0' in data coordinates
my $r = 4;
$plot .= &line(5,$height/2,$circle_x-$r,$height/2,'000000',1);
$plot .= &circle($circle_x,$height/2,$r+1,'000000');
@@ -289,20 +602,18 @@ sub one_dimensional_plot {
$plot .= '';
my $plotresult = &Apache::lonxml::xmlparse($r,'web',$plot);
- my $title = 'Distribution of correct answers';
my $result = ''.
''.
- ''.$title.' (N='.$N.')'.
- ''.
+ ''.&mt('Distribution of correct answers').''.
+ ' '.&mt('[_1] students, [_2] distinct correct answers',
+ $n,scalar(keys(%$data))).
+ ' '.&mt('Maximum number of coinciding values: [_1]',$max_y).
' |
'.
''.
- ''.$min.' | '.
+ ''.$min_x.' | '.
''.$plotresult.' | '.
- ''.$max.' | '.
+ ''.$max_x.' | '.
'
'.
- ''.
- 'Maximum Number of Coinciding Values: '.$max_y.
- ' |
'.
'
';
return $result;
}
@@ -312,7 +623,7 @@ sub one_dimensional_plot {
## These should probably go somewhere more suitable soon.
sub line {
my ($x1,$y1,$x2,$y2,$color,$thickness) = @_;
- return qq{$/};
+ return qq{};
}
sub text {
@@ -461,7 +772,7 @@ sub radio_response_analysis {
&get_time_from_row($response_data->[-1]),
$plot_num);
$pre_graph_text =
- 'Data from [_5] to [_6], [_2] submissions, [_3] correct, [_4] incorrect';
+ 'Data from [_6] to [_7]
[_2] submissions from [_5] students, [_3] correct, [_4] incorrect';
$extra_data[0] = &Apache::lonlocal::locallocaltime($starttime);
$extra_data[1] = &Apache::lonlocal::locallocaltime($endtime);
#
@@ -485,8 +796,8 @@ sub radio_response_analysis {
$no_data_text = 'No data for [_5] to [_6]';
}
my $foil_choice_data =
- &RR_classify_response_data($response_data,$correct,
- $restriction_function);
+ &classify_response_data($response_data,$correct,
+ $restriction_function);
# &Apache::lonstathelpers::log_hash_ref($foil_choice_data);
my $answers;
if (ref($correct)) {
@@ -528,8 +839,9 @@ sub radio_response_analysis {
''.
&mt($pre_graph_text,
$plot_num,$foil_choice_data->{'_count'},
- $correct,
+ $correct,
$foil_choice_data->{'_count'}-$correct,
+ $foil_choice_data->{'_students'},
@extra_data).
''.$/;
$analysis_html.=
@@ -756,11 +1068,22 @@ sub RR_create_stacked_selection_plot {
return ($graph,\%count_per_foil);
}
+
+#########################################################
+#########################################################
+##
+## Misc routines
+##
+#########################################################
+#########################################################
+
# if $correct is a hash ref, it is assumed to be indexed by student names.
# the values are assumed to be hash refs with a key of 'answer'.
-sub RR_classify_response_data {
+sub classify_response_data {
my ($full_row_data,$correct,$function) = @_;
my %submission_data;
+ my %students;
+ my $max=0;
foreach my $row (@$full_row_data) {
my %subm = &hashify_attempt($row);
if (ref($correct) eq 'HASH') {
@@ -770,13 +1093,19 @@ sub RR_classify_response_data {
}
$subm{'submission'} =~ s/=\d+\s*$//;
if (&$function(\%subm)) {
+ $students{$subm{'student'}}++;
$submission_data{'_count'}++;
if (&submission_is_correct($subm{'award'})) {
$submission_data{'_correct'}++;
}
- $submission_data{$subm{'correct'}}->{$subm{'submission'}}++;
+
+ if($max<++$submission_data{$subm{'correct'}}->{$subm{'submission'}}) {
+ $max=$submission_data{$subm{'correct'}}->{$subm{'submission'}};
+ }
}
}
+ $submission_data{'_max'} = $max;
+ $submission_data{'_students'}=scalar(keys(%students));
return \%submission_data;
}
@@ -1099,7 +1428,7 @@ sub OR_time_analysis {
&OR_time_process_data($performance_data,$begin_index,$end_index);
##
$table .= ''.
- &mt('[_1] submissions from [_2] students submitting, [_3] correct, [_4] incorrect',
+ &mt('[_1] submissions from [_2] students, [_3] correct, [_4] incorrect',
$data_count,$student_count,$correct,$data_count-$correct).
' |
'.$/;
my $concept_correct_plot = '';