';
+ #
+ $Str .= '';
+ $Str .= ' 'x5;
+ $Str .= 'Plot '.&plot_dropdown().(' 'x10);
+ #
return $Str;
}
-#######################################################
-#######################################################
+###############################################
+###############################################
+
+=pod
-=pod
+=item &BuildProblemStatisticsPage()
-=item &CreateAndParseOutputSelector()
+Main interface to problem statistics.
=cut
-#######################################################
-#######################################################
-my $output_mode;
-my $show;
-
-my @OutputOptions =
- (
- { name => 'problem statistics grouped by sequence',
- value => 'HTML problem statistics grouped',
- description => 'Output statistics for the problem parts.',
- mode => 'html',
- show => 'grouped',
- },
- { name => 'problem statistics ungrouped',
- value => 'HTML problem statistics ungrouped',
- description => 'Output statistics for the problem parts.',
- mode => 'html',
- show => 'ungrouped',
- },
- { name => 'problem statistics, Excel',
- value => 'Excel problem statistics',
- description => 'Output statistics for the problem parts '.
- 'in an Excel workbook',
- mode => 'excel',
- show => 'all',
- },
- { name => 'Degree of Difficulty Plot',
- value => 'plot deg diff',
- description => 'Generate a plot of the degree of difficulty of each '.
- 'problem part.',
- mode => 'plot',
- show => 'deg of diff',
- },
- { name => 'Percent Wrong Plot',
- value => 'plot per wrong',
- description => 'Generate a plot showing the percent of students who '.
- 'were unable to complete each problem part',
- mode => 'plot',
- show => 'per wrong',
- },
- );
+###############################################
+###############################################
+sub BuildProblemStatisticsPage {
+ my ($r,$c)=@_;
+ #
+ my %Saveable_Parameters = ('Status' => 'scalar',
+ 'statsoutputmode' => 'scalar',
+ 'Section' => 'array',
+ 'StudentData' => 'array',
+ 'Maps' => 'array',
+ 'fieldselections'=> 'array');
+ &Apache::loncommon::store_course_settings('statistics',
+ \%Saveable_Parameters);
+ &Apache::loncommon::restore_course_settings('statistics',
+ \%Saveable_Parameters);
+ #
+ &Apache::lonstatistics::PrepareClasslist();
+ #
+ # Clear the package variables
+ undef(@StatsArray);
+ undef(%SeqStat);
+ #
+ # Finally let the user know we are here
+ my $interface = &CreateInterface($r);
+ $r->print($interface);
+ $r->print('');
+ #
+ my @CacheButtonHTML =
+ &Apache::lonstathelpers::manage_caches($r,'Statistics','stats_status');
+ my $Str;
+ foreach my $html (@CacheButtonHTML) {
+ $Str.=$html.(' 'x5);
+ }
+ #
+ $r->print($Str);
+ if (! exists($ENV{'form.firstrun'})) {
+ $r->print('
'.
+ &mt('Press "Generate Statistics" when you are ready.').
+ '
'.
+ &mt('It may take some time to update the student data '.
+ 'for the first analysis. Future analysis this session '.
+ ' will not have this delay.').
+ '
');
+ return;
+ }
+ $r->rflush();
+ #
+ # This probably does not need to be done each time we are called, but
+ # it does not slow things down noticably.
+ &Apache::loncoursedata::populate_weight_table();
+ #
+ if (exists($ENV{'form.Excel'})) {
+ &Excel_output($r);
+ } else {
+ $r->print(''.' 'x5);
+ $r->rflush();
+ my $count = 0;
+ foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
+ $count += $seq->{'num_assess_parts'};
+ }
+ if ($count > 10) {
+ $r->print('
'.
+ &mt('Compiling statistics for [_1] problems',$count).
+ '
\n";
+ my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
+ if (defined($starttime) || defined($endtime)) {
+ # Inform the user what the time limits on the data are.
+ $Str .= '
'.&mt('Statistics on submissions from [_1] to [_2]',
+ &Apache::lonlocal::locallocaltime($starttime),
+ &Apache::lonlocal::locallocaltime($endtime)
+ ).'
';
}
- $Str .= "\n";
+ $Str .= "
".&mt('Compiled on [_1]',
+ &Apache::lonlocal::locallocaltime(time))."
";
return $Str;
}
+
###############################################
###############################################
-
+##
+## Misc HTML output routines
+##
###############################################
###############################################
-sub Gather_Student_Data {
- my ($r) = @_;
- my $c = $r->connection();
- #
- my @Sequences = &Apache::lonstatistics::Sequences_with_Assess();
- #
- my @Students = @Apache::lonstatistics::Students;
- #
- # Open the progress window
- my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin
- ($r,'Statistics Compilation Status',
- 'Statistics Compilation Progress', scalar(@Students));
- #
- while (my $student = shift @Students) {
- return if ($c->aborted());
- my ($status,undef) = &Apache::loncoursedata::ensure_current_data
- ($student->{'username'},$student->{'domain'},
- $ENV{'request.course.id'});
- &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state,
- 'last student');
+sub statistics_html_table_data {
+ my ($data,$options) = @_;
+ my $row = '';
+ foreach my $field (@Fields) {
+ next if ($options =~ /no $field->{'name'}/);
+ next if ($field->{'selected'} ne 'yes');
+ $row .= '
\n");
- }
+sub tries_data_plot {
+ my ($r)=@_;
+ my $count = scalar(@StatsArray);
+ my $width = 50 + 10*$count;
+ $width = 300 if ($width < 300);
+ my $height = 300;
+ my $plot = '';
+ my @STD; my @Mean; my @Max; my @Min;
+ my @Labels;
+ my $ymax = 5;
+ foreach my $data (@StatsArray) {
+ my $max = $data->{'mean_tries'} + $data->{'std_tries'};
+ $ymax = $max if ($ymax < $max);
+ $ymax = $max if ($ymax < $max);
+ push(@Labels,$data->{'problem_num'});
+ push(@STD,$data->{'std_tries'});
+ push(@Mean,$data->{'mean_tries'});
+ }
+ #
+ # Make sure we show relevant information.
+ my $xmax = $Labels[-1];
+ if ($xmax > 50) {
+ if ($xmax % 10 != 0) {
+ $xmax = 10 * (int($xmax/10)+1);
+ }
+ } else {
+ if ($xmax % 5 != 0) {
+ $xmax = 5 * (int($xmax/5)+1);
}
- $r->print("
\n");
- $r->print("
\n");
- $r->rflush();
}
+ $ymax = int($ymax)+1+2;
#
+ my $std_data .= ''.join(',',@Labels).''.$/.
+ ''.join(',',@Mean).''.$/;
+ #
+ my $std_error_data .= ''.join(',',@Labels).''.$/.
+ ''.join(',',@Mean).''.$/.
+ ''.join(',',@STD).''.$/;
+ #
+ my $title = 'Mean and S.D. of Tries';
+ if ($xmax > 25) {
+ $title = 'Mean and Standard Deviation of Tries';
+ }
+ #
+ $plot=<<"END";
+
+ $title
+
+ Problem Number
+ Number of Tries
+
+ $std_error_data
+
+
+ $std_data
+
+
+END
+ my $plotresult =
+ '
'.&Apache::lonxml::xmlparse($r,'web',$plot).'
'.$/;
+ $r->print($plotresult);
return;
}
+sub plot_dropdown {
+ my $current = '';
+ #
+ if (defined($ENV{'form.plot'})) {
+ $current = $ENV{'form.plot'};
+ }
+ #
+ my @Additional_Plots = (
+ { graphable=>'yes',
+ name => 'degrees',
+ title => 'Difficulty Indexes' },
+ { graphable=>'yes',
+ name => 'tries statistics',
+ title => 'Tries Statistics' });
+ #
+ my $Str= "\n".''."\n";
+ return $Str;
+}
###############################################
###############################################
-
+##
+## Excel output routines
+##
###############################################
###############################################
-sub output_html_ungrouped {
+sub Excel_output {
my ($r) = @_;
- #
- my $show_container = 0;
- my $show_part = 0;
- #$r->print(&ProblemStatisticsLegend());
- my @Header = ("Title","Part","#Stdnts","Tries","Mod",
- "Mean","#YES","#yes","%Wrng","DoDiff",
- "S.D.","Skew");#,"D.F.1st","D.F.2nd");
- #
- my $sortby = undef;
- foreach (@Header) {
- if ($ENV{'form.sortby'} eq $_) {
- $sortby = $_;
+ $r->print('
'.&mt('Preparing Excel Spreadsheet').'
');
+ ##
+ ## Compute the statistics
+ &compute_all_statistics($r);
+ my $c = $r->connection;
+ return if ($c->aborted());
+ ##
+ ## Create the excel workbook
+ my $filename = '/prtspool/'.
+ $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.
+ time.'_'.rand(1000000000).'.xls';
+ my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
+ #
+ # Create sheet
+ my $excel_workbook = Spreadsheet::WriteExcel->new('/home/httpd'.$filename);
+ #
+ # Check for errors
+ if (! defined($excel_workbook)) {
+ $r->log_error("Error creating excel spreadsheet $filename: $!");
+ $r->print(&mt("Problems creating new Excel file. ".
+ "This error has been logged. ".
+ "Please alert your LON-CAPA administrator."));
+ return 0;
+ }
+ #
+ # The excel spreadsheet stores temporary data in files, then put them
+ # together. If needed we should be able to disable this (memory only).
+ # The temporary directory must be specified before calling 'addworksheet'.
+ # File::Temp is used to determine the temporary directory.
+ $excel_workbook->set_tempdir($Apache::lonnet::tmpdir);
+ #
+ # Add a worksheet
+ my $sheetname = $ENV{'course.'.$ENV{'request.course.id'}.'.description'};
+ if (length($sheetname) > 31) {
+ $sheetname = substr($sheetname,0,31);
+ }
+ my $excel_sheet = $excel_workbook->addworksheet(
+ &Apache::loncommon::clean_excel_name($sheetname));
+ #
+ my $format = &Apache::loncommon::define_excel_formats($excel_workbook);
+ ##
+ ## Begin creating excel sheet
+ ##
+ my ($rows_output,$cols_output) = (0,0);
+ #
+ # Put the course description in the header
+ $excel_sheet->write($rows_output,$cols_output++,
+ $ENV{'course.'.$ENV{'request.course.id'}.'.description'},
+ $format->{'h1'});
+ $cols_output += 3;
+ #
+ # Put a description of the sections listed
+ my $sectionstring = '';
+ $excel_sheet->write($rows_output,$cols_output++,
+ &Apache::lonstathelpers::sections_description
+ (@Apache::lonstatistics::SelectedSections),
+ $format->{'h3'});
+ $cols_output += scalar(@Apache::lonstatistics::SelectedSections);
+ #
+ # Time restrictions
+ my $time_string;
+ if (defined($starttime)) {
+ # call localtime but not lonlocal:locallocaltime because excel probably
+ # cannot handle localized text. Probably.
+ $time_string .= 'Data collected from '.localtime($time_string);
+ if (defined($endtime)) {
+ $time_string .= ' to '.localtime($endtime);
}
+ $time_string .= '.';
+ } elsif (defined($endtime)) {
+ # See note above about lonlocal:locallocaltime
+ $time_string .= 'Data collected before '.localtime($endtime).'.';
+ }
+ if (defined($time_string)) {
+ $excel_sheet->write($rows_output,$cols_output++,$time_string);
+ $cols_output+= 5;
+ }
+ #
+ # Put the date in there too
+ $excel_sheet->write($rows_output,$cols_output++,
+ 'Compiled on '.localtime(time));
+ #
+ $rows_output++;
+ $cols_output=0;
+ ##
+ ## Sequence Statistics
+ ##
+ &write_headers($excel_sheet,$format,\$rows_output,\$cols_output,
+ \@SeqFields);
+ foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
+ next if ($seq->{'num_assess'} < 1);
+ my $data = $SeqStat{$seq->{'symb'}};
+ $cols_output=0;
+ foreach my $field (@SeqFields) {
+ next if ($field->{'selected'} ne 'yes');
+ my $fieldformat = undef;
+ if (exists($field->{'excel_format'})) {
+ $fieldformat = $format->{$field->{'excel_format'}};
+ }
+ $excel_sheet->write($rows_output,$cols_output++,
+ $data->{$field->{'name'}},$fieldformat);
+ }
+ $rows_output++;
+ $cols_output=0;
}
- if (! defined($sortby) || $sortby eq '') {
- $sortby = 'Container';
- }
- # #FFFFE6 #EEFFCC #DDFFFF FFDDDD #DDFFDD #FFDDFF
- my @Sequences = &Apache::lonstatistics::Sequences_with_Assess();
- if (@Sequences > 1) {
- unshift(@Header,"Container");
- $show_container = 1;
- }
- #
- $r->print('
'."\n");
- $r->rflush();
- #
- # Compile the data
- my @Statsarray;
- foreach my $sequence (@Sequences) {
- next if ($sequence->{'num_assess'}<1);
- foreach my $resource (@{$sequence->{'contents'}}) {
- next if ($resource->{'type'} ne 'assessment');
- foreach my $part (@{$resource->{'parts'}}) {
- my ($num,$tries,$mod,$mean,$Solved,$solved,$DegOfDiff,$STD,
- $SKEW) = &Apache::loncoursedata::get_problem_statistics
- (undef,$resource->{'symb'},$part,
- $ENV{'request.course.id'});
- #
- $show_part = 1 if ($part ne '0');
- $part = ' ' if ($part == 0);
- #
- my $wrongpercent = 0;
- if (defined($num) && $num > 0) {
- $wrongpercent=int(10*100*($num-$Solved+$solved)/$num)/10;
- }
- push (@Statsarray,
- { 'sequence' => $sequence,
- 'resource' => $resource,
- 'Title' => $resource->{'title'},
- 'Part' => $part,
- '#Stdnts' => $num,
- 'Tries' => $tries,
- 'Mod' => $mod,
- 'Mean' => $mean,
- '#YES' => $Solved,
- '#yes' => $solved,
- '%Wrng' => $wrongpercent,
- 'DoDiff' => $DegOfDiff,
- 'S.D.' => $STD,
- 'Skew' => $SKEW,
- });
+ ##
+ ## Resource Statistics
+ ##
+ $rows_output++;
+ $cols_output=0;
+ &write_headers($excel_sheet,$format,\$rows_output,\$cols_output,
+ \@Fields);
+ #
+ foreach my $data (@StatsArray) {
+ $cols_output=0;
+ foreach my $field (@Fields) {
+ next if ($field->{'selected'} ne 'yes');
+ next if ($field->{'name'} eq 'problem_num');
+ my $fieldformat = undef;
+ if (exists($field->{'excel_format'})) {
+ $fieldformat = $format->{$field->{'excel_format'}};
}
+ $excel_sheet->write($rows_output,$cols_output++,
+ $data->{$field->{'name'}},$fieldformat);
}
+ $rows_output++;
+ $cols_output=0;
}
#
- # Table Headers
- $r->print('
'."\n");
- my $Str = '';
- foreach (@Header) {
- next if ($_ eq 'Part' && !$show_part);
- # Do not allow sorting on some fields
- if ($_ eq $sortby || /^(Part)$/) {
- $Str .= '
'.$_.'
';
+ $excel_workbook->close();
+ #
+ # Tell the user where to get their excel file
+ $r->print(' '.
+ ''.
+ &mt('Your Excel Spreadsheet').''."\n");
+ $r->rflush();
+ return;
+}
+
+##
+## &write_headers
+##
+sub write_headers {
+ my ($excel_sheet,$format,$rows_output,$cols_output,$Fields) = @_;
+ ##
+ ## First the long titles
+ foreach my $field (@{$Fields}) {
+ next if ($field->{'name'} eq 'problem_num');
+ next if ($field->{'selected'} ne 'yes');
+ if (exists($field->{'long_title'})) {
+ $excel_sheet->write($$rows_output,${$cols_output},
+ $field->{'long_title'},
+ $format->{'bold'});
} else {
- $Str .= '
';
+ $excel_sheet->write($$rows_output,${$cols_output},'');
}
+ ${$cols_output}+= 1;
+ }
+ ${$cols_output} =0;
+ ${$rows_output}+=1;
+ ##
+ ## Then the short titles
+ foreach my $field (@{$Fields}) {
+ next if ($field->{'selected'} ne 'yes');
+ next if ($field->{'name'} eq 'problem_num');
+ # Use english for excel as I am not sure how well excel handles
+ # other character sets....
+ $excel_sheet->write($$rows_output,$$cols_output,
+ $field->{'title'},
+ $format->{'bold'});
+ $$cols_output+=1;
}
- $r->print('
'.$Str."
\n");
+ ${$cols_output} =0;
+ ${$rows_output}+=1;
+ return;
+}
+
+##################################################
+##################################################
+##
+## Statistics Gathering and Manipulation Routines
+##
+##################################################
+##################################################
+sub compute_statistics_on_sequence {
+ my ($seq) = @_;
+ my @Data;
+ foreach my $res (@{$seq->{'contents'}}) {
+ next if ($res->{'type'} ne 'assessment');
+ foreach my $part (@{$res->{'parts'}}) {
+ next if ($res->{'partdata'}->{$part}->{'Survey'});
+ #
+ # This is where all the work happens
+ my $data = &get_statistics($seq,$res,$part,scalar(@StatsArray)+1);
+ push (@Data,$data);
+ push (@StatsArray,$data);
+ }
+ }
+ return @Data;
+}
+
+sub compute_all_statistics {
+ my ($r) = @_;
+ if (@StatsArray > 0) {
+ # Assume we have already computed the statistics
+ return;
+ }
+ my $c = $r->connection;
+ foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
+ last if ($c->aborted);
+ next if ($seq->{'num_assess'} < 1);
+ &compute_sequence_statistics($seq);
+ &compute_statistics_on_sequence($seq);
+ }
+}
+
+sub sort_data {
+ my ($sortkey) = @_;
+ return if (! @StatsArray);
#
# Sort the data
- my @OutputOrder;
- if ($sortby eq 'Container') {
- @OutputOrder = @Statsarray;
- } else {
+ my $sortby = undef;
+ foreach my $field (@Fields) {
+ if ($sortkey eq $field->{'name'}) {
+ $sortby = $field->{'name'};
+ }
+ }
+ if (! defined($sortby) || $sortby eq '' || $sortby eq 'problem_num') {
+ $sortby = 'container';
+ }
+ if ($sortby ne 'container') {
# $sortby is already defined, so we can charge ahead
if ($sortby =~ /^(title|part)$/i) {
# Alpha comparison
- @OutputOrder = sort {
+ @StatsArray = sort {
lc($a->{$sortby}) cmp lc($b->{$sortby}) ||
- lc($a->{'Title'}) cmp lc($b->{'Title'}) ||
- lc($a->{'Part'}) cmp lc($b->{'Part'});
- } @Statsarray;
+ lc($a->{'title'}) cmp lc($b->{'title'}) ||
+ lc($a->{'part'}) cmp lc($b->{'part'});
+ } @StatsArray;
} else {
# Numerical comparison
- @OutputOrder = sort {
+ @StatsArray = sort {
my $retvalue = 0;
if ($b->{$sortby} eq 'nan') {
if ($a->{$sortby} ne 'nan') {
@@ -401,394 +1488,285 @@ sub output_html_ungrouped {
}
}
if ($retvalue eq '0') {
- $retvalue = $b->{$sortby} <=> $a->{$sortby} ||
- lc($a->{'Title'}) <=> lc($b->{'Title'}) ||
- lc($a->{'Part'}) <=> lc($b->{'Part'});
+ $retvalue = $b->{$sortby} <=> $a->{$sortby} ||
+ lc($a->{'title'}) <=> lc($b->{'title'}) ||
+ lc($a->{'part'}) <=> lc($b->{'part'});
}
$retvalue;
- } @Statsarray;
+ } @StatsArray;
}
}
- foreach my $row (@OutputOrder) {
- $r->print('