# The LearningOnline Network with CAPA # # $Id: lonproblemstatistics.pm,v 1.47 2003/03/27 19:26:33 matthew Exp $ # # Copyright Michigan State University Board of Trustees # # This file is part of the LearningOnline Network with CAPA (LON-CAPA). # # LON-CAPA is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # LON-CAPA is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with LON-CAPA; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # /home/httpd/html/adm/gpl.txt # # http://www.lon-capa.org/ # # (Navigate problems for statistical reports # ############################################### ############################################### =pod =head1 NAME lonproblemstatistics =head1 SYNOPSIS Routines to present problem statistics to instructors via tables, Excel files, and plots. =over 4 =cut ############################################### ############################################### package Apache::lonproblemstatistics; use strict; use Apache::lonnet(); use Apache::lonhtmlcommon; use Apache::loncoursedata; use Apache::lonstatistics; use Spreadsheet::WriteExcel; ############################################### ############################################### =pod =item &CreateInterface() Create the main intereface for the statistics page. Allows the user to select sections, maps, and output. =cut ############################################### ############################################### sub CreateInterface { my $Str = ''; $Str .= ''."\n"; $Str .= ''; $Str .= ''; $Str .= ''; $Str .= ''; $Str .= ''."\n"; # $Str .= ''."\n"; $Str .= '
SectionsSequences and FoldersOutput
'."\n"; $Str .= &Apache::lonstatistics::SectionSelect('Section','multiple',5); $Str .= ''; # my $only_seq_with_assessments = sub { my $s=shift; if ($s->{'num_assess'} < 1) { return 0; } else { return 1; } }; $Str .= &Apache::lonstatistics::MapSelect('Maps','multiple,all',5, $only_seq_with_assessments); $Str .= ''."\n"; $Str .= &CreateAndParseOutputSelector(); $Str .= '
'."\n"; return $Str; } ####################################################### ####################################################### =pod =item &CreateAndParseOutputSelector() Construct a selection list of options for output and parse output selections. The current output selected is indicated by the values of the two package variables $output_mode and $show. @OutputOptions holds the descriptions of the output options and the values for $output_mode and $show. Based on code from lonstudentassessment.pm. =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 OutputDescriptions { my $Str = ''; $Str .= "

Output Modes

\n"; $Str .= "
\n"; foreach my $outputmode (@OutputOptions) { $Str .="
".$outputmode->{'name'}."
\n"; $Str .="
".$outputmode->{'description'}."
\n"; } $Str .= "
\n"; return $Str; } sub CreateAndParseOutputSelector { my $Str = ''; my $elementname = 'statsoutputmode'; # # Format for output options is 'mode, restrictions'; my $selected = 'HTML problem statistics grouped'; if (exists($ENV{'form.'.$elementname})) { if (ref($ENV{'form.'.$elementname} eq 'ARRAY')) { $selected = $ENV{'form.'.$elementname}->[0]; } else { $selected = $ENV{'form.'.$elementname}; } } # # Set package variables describing output mode $output_mode = 'html'; $show = 'all'; foreach my $option (@OutputOptions) { next if ($option->{'value'} ne $selected); $output_mode = $option->{'mode'}; $show = $option->{'show'}; } # # Build the form element $Str = qq/"; return $Str; } ############################################### ############################################### =pod =item &Gather_Student_Data() Ensures all student data is up to date. =cut ############################################### ############################################### 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'); } &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); $r->rflush(); } ############################################### ############################################### =pod =item &BuildProblemStatisticsPage() Main interface to problem statistics. =cut ############################################### ############################################### sub BuildProblemStatisticsPage { my ($r,$c)=@_; # $output_mode = 'html'; $show = 'grouped'; # $r->print(&CreateInterface()); $r->print(''); $r->print(''); if (! exists($ENV{'form.statsfirstcall'})) { $r->print(< Please make your selections in the boxes above and hit the button marked "Update Display".

ENDMSG return; } # &Gather_Student_Data($r); # # if ($output_mode eq 'html') { $r->print("

". $ENV{'course.'.$ENV{'request.course.id'}.'.description'}. "

\n"); $r->print("

".localtime(time)."

"); $r->rflush(); if ($show eq 'grouped') { &output_html_grouped_by_sequence($r); } elsif ($show eq 'ungrouped') { &output_html_ungrouped($r); } } elsif ($output_mode eq 'excel') { $r->print("

Preparing Excel Spreadsheet

"); &output_excel($r); } elsif ($output_mode eq 'plot') { if ($show eq 'deg of diff') { &plot_statistics($r,'DoDiff'); } elsif ($show eq 'per wrong') { &plot_statistics($r,'%Wrng'); } } else { $r->print("

Not implemented

"); } return; } ############################################### ############################################### =pod =item &output_html_grouped_by_sequence() Presents the statistics data as an html table organized by the order the assessments appear in the course. =cut ############################################### ############################################### sub output_html_grouped_by_sequence { my ($r) = @_; my $problem_num = 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"); # #FFFFE6 #EEFFCC #DDFFFF FFDDDD #DDFFDD #FFDDFF foreach my $sequence (&Apache::lonstatistics::Sequences_with_Assess()) { next if ($sequence->{'num_assess'}<1); $r->print("

".$sequence->{'title'}."

"); $r->print('
'."\n"); $r->print(''."\n"); $r->print('\n"); foreach my $resource (@{$sequence->{'contents'}}) { next if ($resource->{'type'} ne 'assessment'); foreach my $part (@{$resource->{'parts'}}) { $problem_num++; my ($num,$tries,$mod,$mean,$Solved,$solved,$DegOfDiff,$STD, $SKEW) = &Apache::loncoursedata::get_problem_statistics (undef,$resource->{'symb'},$part, $ENV{'request.course.id'}); # $part = ' ' if ($part == 0); # my $wrongpercent = 0; if (defined($num) && $num > 0) { $wrongpercent=int(10*100*($num-$Solved+$solved)/$num)/10; } my $option = ''; $r->print(''.&statistics_html_table_data ($resource,$part,$num,$tries,$mod,$mean,$Solved, $solved,$wrongpercent,$DegOfDiff,$STD,$SKEW, $option). "\n"); } } $r->print("
'. join("",@Header)."
\n"); $r->print("
\n"); $r->rflush(); } # return; } ############################################### ############################################### =pod =item &output_html_ungrouped() Presents the statistics data in a single html table which can be sorted by different columns. =cut ############################################### ############################################### sub output_html_ungrouped { my ($r,$option) = @_; # my $problem_num = 0; 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 = $_; } } if (! defined($sortby) || $sortby eq '') { $sortby = 'Container'; } # If there is more than one sequence, list their titles my @Sequences = &Apache::lonstatistics::Sequences_with_Assess(); if (@Sequences > 1) { unshift(@Header,"Container"); $show_container = 1; } # # If the option for showing the problem number is needed, push that # on the list too if (defined($option) && $option =~ /show probnum/) { unshift(@Header,"P#"); } # $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'}}) { $problem_num++; 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, 'problem_num' => $problem_num, }); } } } # # 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|P\#)$/) { $Str .= ''; } else { $Str .= ''; } } $r->print(''.$Str."\n"); # # Sort the data my @OutputOrder; if ($sortby eq 'Container') { @OutputOrder = @Statsarray; } else { # $sortby is already defined, so we can charge ahead if ($sortby =~ /^(title|part)$/i) { # Alpha comparison @OutputOrder = sort { lc($a->{$sortby}) cmp lc($b->{$sortby}) || lc($a->{'Title'}) cmp lc($b->{'Title'}) || lc($a->{'Part'}) cmp lc($b->{'Part'}); } @Statsarray; } else { # Numerical comparison @OutputOrder = sort { my $retvalue = 0; if ($b->{$sortby} eq 'nan') { if ($a->{$sortby} ne 'nan') { $retvalue = -1; } else { $retvalue = 0; } } if ($a->{$sortby} eq 'nan') { if ($b->{$sortby} ne 'nan') { $retvalue = 1; } } if ($retvalue eq '0') { $retvalue = $b->{$sortby} <=> $a->{$sortby} || lc($a->{'Title'}) <=> lc($b->{'Title'}) || lc($a->{'Part'}) <=> lc($b->{'Part'}); } $retvalue; } @Statsarray; } } $option .= ',no part' if (! $show_part); foreach my $row (@OutputOrder) { $r->print(''); if (defined($option) && $option =~ /show probnum/) { $r->print(''); } if ($show_container) { $r->print(''); } $r->print(&statistics_html_table_data ($row->{'resource'},$row->{'Part'},$row->{'#Stdnts'}, $row->{'Tries'},$row->{'Mod'},$row->{'Mean'}, $row->{'#YES'},$row->{'#yes'},$row->{"\%Wrng"}, $row->{'DoDiff'},$row->{'S.D.'},$row->{'Skew'}, $option)); $r->print("\n"); } $r->print("
'.$_.''. ''. $_.'
'.$row->{'problem_num'}.'' .$row->{'sequence'}->{'title'}.'
\n"); $r->print("
\n"); $r->rflush(); # return; } ############################################### ############################################### =pod =item &output_excel() Presents the statistical data in an Excel 95 compatable spreadsheet file. =cut ############################################### ############################################### sub output_excel { my ($r) = @_; my $filename = '/prtspool/'. $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'. time.'_'.rand(1000000000).'.xls'; # my $excel_workbook = undef; my $excel_sheet = undef; # my $rows_output = 0; my $cols_output = 0; # # Create sheet $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("Problems creating new Excel file. ". "This error has been logged. ". "Please alert your LON-CAPA administrator"); return ; } # # 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); } $excel_sheet = $excel_workbook->addworksheet($sheetname); # # Put the course description in the header $excel_sheet->write($rows_output,$cols_output++, $ENV{'course.'.$ENV{'request.course.id'}.'.description'}); $cols_output += 3; # # Put a description of the sections listed my $sectionstring = ''; my @Sections = @Apache::lonstatistics::SelectedSections; if (scalar(@Sections) > 1) { if (scalar(@Sections) > 2) { my $last = pop(@Sections); $sectionstring = "Sections ".join(', ',@Sections).', and '.$last; } else { $sectionstring = "Sections ".join(' and ',@Sections); } } else { if ($Sections[0] eq 'all') { $sectionstring = "All sections"; } else { $sectionstring = "Section ".$Sections[0]; } } $excel_sheet->write($rows_output,$cols_output++,$sectionstring); $cols_output += scalar(@Sections); # # Put the date in there too $excel_sheet->write($rows_output,$cols_output++, 'Compiled on '.localtime(time)); # $rows_output++; $cols_output=0; # # Add the headers my @Header = ("Container","Title","Part","#Stdnts","Tries","Mod", "Mean","#YES","#yes","%Wrng","DoDiff", "S.D.","Skew.");#,"D.F.1st","D.F.2nd"); foreach (@Header) { $excel_sheet->write($rows_output,$cols_output++,$_); } $rows_output++; # # Write the data foreach my $sequence (&Apache::lonstatistics::Sequences_with_Assess()) { next if ($sequence->{'num_assess'}<1); foreach my $resource (@{$sequence->{'contents'}}) { next if ($resource->{'type'} ne 'assessment'); foreach my $part (@{$resource->{'parts'}}) { $cols_output=0; my ($num,$tries,$mod,$mean,$Solved,$solved,$DegOfDiff,$STD, $SKEW) = &Apache::loncoursedata::get_problem_statistics (undef,$resource->{'symb'},$part, $ENV{'request.course.id'}); # if (!defined($part) || $part eq '') { $part = ' '; } my $wrongpercent = 0; if (defined($num) && $num > 0) { $wrongpercent=int(10*100*($num-$Solved+$solved)/$num)/10; } foreach ($sequence->{'title'},$resource->{'title'},$part, $num,$tries,$mod,$mean,$Solved,$solved,$wrongpercent, $DegOfDiff,$STD,$SKEW) { $excel_sheet->write($rows_output,$cols_output++,$_); } $rows_output++; } } } # # Write the excel file $excel_workbook->close(); # Tell the user where to get their excel file $r->print('
'. 'Your Excel spreadsheet.'."\n"); $r->rflush(); return; } ############################################### ############################################### =pod =item &statistics_html_table_data() Help function used to format the rows for HTML table output. =cut ############################################### ############################################### sub statistics_html_table_data { my ($resource,$part,$num,$tries,$mod,$mean,$Solved,$solved,$wrongpercent, $DegOfDiff,$STD,$SKEW,$options) = @_; my $row = ''; $row .= ''. ''. $resource->{'title'}.''. ''; $row .= ''.$part.'' if ($options !~ /no part/); foreach ($num,$tries) { $row .= ''.$_.''; } foreach ($mod) { $row .= ''.$_.''; } foreach ($mean) { $row .= ''. sprintf("%5.2f",$_).''; } foreach ($Solved,$solved) { $row .= ''.$_.''; } foreach ($wrongpercent) { $row .= ''. sprintf("%5.1f",$_).''; } foreach ($DegOfDiff,$STD,$SKEW) { $row .= ''. sprintf("%5.2f",$_).''; } return $row; } ############################################### ############################################### =pod =item &plot_statistics() =cut ############################################### ############################################### sub plot_statistics { my ($r,$datafield) = @_; my @Data; # my %Fields = ('#Stdnts'=> 0, 'Tries' => 1, 'Mod' => 2, 'Mean' => 3, '#YES' => 4, '#yes' => 5, '%Wrng' => 9, 'DoDiff' => 6, 'S.D.' => 7, 'Skew' => 8,); # my $field = '%Wrng'; foreach (keys(%Fields)) { $field = $_ if ($datafield eq $_); } my $fieldindex = $Fields{$field}; # my $Max = 0; foreach my $sequence (&Apache::lonstatistics::Sequences_with_Assess()) { next if ($sequence->{'num_assess'}<1); foreach my $resource (@{$sequence->{'contents'}}) { next if ($resource->{'type'} ne 'assessment'); foreach my $part (@{$resource->{'parts'}}) { my @Results = &Apache::loncoursedata::get_problem_statistics (undef,$resource->{'symb'},$part, $ENV{'request.course.id'}); my ($num,$Solved,$solved) = @Results[0,4,5]; my $wrongpercent = 0; if (defined($num) && $num > 0) { $wrongpercent=int(10*100*($num-$Solved+$solved)/$num)/10; } push (@Results,$wrongpercent); my $data = $Results[$fieldindex]; $data = 0 if ($data eq 'nan'); $Max = $data if ($Max<$data); push (@Data,$data); } } } # # Print out plot request my $title = 'Percent Wrong'; if ($field eq 'DoDiff') { $title = 'Degree of Difficulty'; } my $yaxis = 'Percent'; if ($field eq 'DoDiff') { $yaxis = ''; } elsif ($field ne '%Wrng') { $yaxis = ''; } # # Determine appropriate value for $Max if ($field eq 'DoDiff') { if ($Max > 0.5) { $Max = 1; } elsif ($Max > 0.2) { $Max = 0.5; } elsif ($Max > 0.1) { $Max = 0.2; } } elsif ($field eq '%Wrng') { if ($Max > 50) { $Max = 100; } elsif ($Max > 25) { $Max = 50; } elsif ($Max > 20) { $Max = 25; } elsif ($Max > 10) { $Max = 20; } elsif ($Max > 5) { $Max = 10; } else { $Max = 5; } } $r->print("

".&DrawGraph(\@Data,$title,'Problem Number',$yaxis, $Max)."

\n"); # # Print out the data $ENV{'form.sortby'} = 'Contents'; &output_html_ungrouped($r,'show probnum'); return; } ############################################### ############################################### =pod =item &DrawGraph() =cut ############################################### ############################################### sub DrawGraph { my ($values,$title,$xaxis,$yaxis,$Max)=@_; $title = '' if (! defined($title)); $xaxis = '' if (! defined($xaxis)); $yaxis = '' if (! defined($yaxis)); # my $sendValues = join(',', @$values); my $sendCount = scalar(@$values); if ( $Max > 1 ) { if ($Max % 10) { if ( int($Max) < $Max ) { $Max++; $Max = int($Max); } } } else { $Max = 1; } my @GData = ($title,$xaxis,$yaxis,$Max,$sendCount,$sendValues); return ''; } ############################################### ############################################### =pod =item &ProblemStatisticsLegend() =cut ############################################### ############################################### sub ProblemStatisticsLegend { my $Ptr = ''; $Ptr = ''; $Ptr .= ''; $Ptr .= ''; $Ptr .= ''; $Ptr .= ''; $Ptr .= ''; $Ptr .= ''; $Ptr .= ''; $Ptr .= ''; $Ptr .= ''; $Ptr .= ''; $Ptr .= ''; $Ptr .= ''; $Ptr .= ''; $Ptr .= '
'; $Ptr .= '#StdntsTotal number of students attempted the problem.'; $Ptr .= '
'; $Ptr .= 'TriesTotal number of tries for solving the problem.'; $Ptr .= '
'; $Ptr .= 'ModLargest number of tries for solving the problem by a student.'; $Ptr .= '
'; $Ptr .= 'MeanAverage number of tries. [ Tries / #Stdnts ]'; $Ptr .= '
'; $Ptr .= '#YESNumber of students solved the problem correctly.'; $Ptr .= '
'; $Ptr .= '#yesNumber of students solved the problem by override.'; $Ptr .= '
'; $Ptr .= '%WrongPercentage of students who tried to solve the problem '; $Ptr .= 'but is still incorrect. [ 100*((#Stdnts-(#YES+#yes))/#Stdnts) ]'; $Ptr .= '
'; $Ptr .= 'DoDiffDegree of Difficulty of the problem. '; $Ptr .= '[ 1 - ((#YES+#yes) / Tries) ]'; $Ptr .= '
'; $Ptr .= 'S.D.Standard Deviation of the tries. '; $Ptr .= '[ sqrt(sum((Xi - Mean)^2)) / (#Stdnts-1) '; $Ptr .= 'where Xi denotes every student\'s tries ]'; $Ptr .= '
'; $Ptr .= 'Skew.Skewness of the students tries.'; $Ptr .= '[(sqrt( sum((Xi - Mean)^3) / #Stdnts)) / (S.D.^3)]'; $Ptr .= '
'; $Ptr .= 'Dis.F.Discrimination Factor: A Standard for evaluating the '; $Ptr .= 'problem according to a Criterion
'; $Ptr .= '[Criterion to group students into %27 Upper Students - '; $Ptr .= 'and %27 Lower Students]
'; $Ptr .= '1st Criterion for Sorting the Students: '; $Ptr .= 'Sum of Partial Credit Awarded / Total Number of Tries
'; $Ptr .= '2nd Criterion for Sorting the Students: '; $Ptr .= 'Total number of Correct Answers / Total Number of Tries'; $Ptr .= '
Disc.Number of Students had at least one discussion.'; $Ptr .= '
'; return $Ptr; } #---- END Problem Statistics Web Page ---------------------------------------- 1; __END__