# The LearningOnline Network with CAPA # # $Id: lonproblemstatistics.pm,v 1.44 2003/03/26 17:03:41 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 # ### package Apache::lonproblemstatistics; use strict; use Apache::lonnet(); use Apache::lonhtmlcommon; use Apache::loncoursedata; use Apache::lonstatistics; use Spreadsheet::WriteExcel; ####################################################### ####################################################### 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() =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 = 'outputmode'; # # Format for output options is 'mode, restrictions'; my $selected = 'html, with links'; 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; } ############################################### ############################################### ############################################### ############################################### 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(); } ############################################### ############################################### ############################################### ############################################### sub BuildProblemStatisticsPage { my ($r,$c)=@_; # $output_mode = 'html'; $show = 'grouped'; # $r->print(&CreateInterface()); $r->print(''); $r->print(''); if (! exists($ENV{'form.statsfirstcall'})) { 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); } else { $r->print("

Not implemented

"); } return; } ############################################### ############################################### ############################################### ############################################### sub output_html_grouped_by_sequence { my ($r) = @_; #$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()) { my $show_part = 0; 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'}}) { 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; } $r->print(''.&statistics_html_table_data ($resource,$part,$num,$tries,$mod,$mean,$Solved, $solved,$wrongpercent,$DegOfDiff,$STD,$SKEW, $show_part). "\n"); } } $r->print("
'. join("",@Header)."
\n"); $r->print("
\n"); $r->rflush(); } # return; } ############################################### ############################################### ############################################### ############################################### sub output_html_ungrouped { 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 = $_; } } 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, }); } } } # # 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 .= ''; } 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; } } foreach my $row (@OutputOrder) { $r->print(''); if ($show_container) { $r->print(''); } $r->print(&stats_row_from_hash($row,$show_part)); $r->print("\n"); } $r->print("
'.$_.''. ''. $_.'
' .$row->{'sequence'}->{'title'}.'
\n"); $r->print("
\n"); $r->rflush(); # return; } sub stats_row_from_hash { my ($data,$show_part) = @_; return &statistics_html_table_data($data->{'resource'},$data->{'Part'}, $data->{'#Stdnts'}, $data->{'Tries'}, $data->{'Mod'}, $data->{'Mean'}, $data->{'#YES'}, $data->{'#yes'}, $data->{"\%Wrng"}, $data->{'DoDiff'}, $data->{'S.D.'}, $data->{'Skew'}, $show_part); } ############################################### ############################################### ############################################### ############################################### 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; } sub statistics_html_table_data { my ($resource,$part,$num,$tries,$mod,$mean,$Solved,$solved,$wrongpercent, $DegOfDiff,$STD,$SKEW,$show_part) = @_; my $row = ''; $row .= ''. ''. $resource->{'title'}.''. ''; $row .= ''.$part.'' if ($show_part); foreach ($num,$tries) { $row .= ''.$_.''; } foreach ($mod,$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; } ############################################### ############################################### sub BuildGraphicChart { my ($graph,$cacheDB,$courseDescription,$students,$courseID,$r,$c)=@_; my %cache; my $max; my $title = ''; if($graph eq 'DoDiffGraph') { $title = 'Degree-of-Difficulty'; } else { $title = 'Wrong-Percentage'; } my $currentSequence = -1; my $sortProblems = 'Sort Within Sequence'; my ($result, $orderedProblems) = &InitializeProblemStatistics($cacheDB, $students, $courseID, $c, $r); if($result ne 'OK') { return; } my @values = (); unless(tie(%cache,'GDBM_File',$cacheDB,&GDBM_READER(),0640)) { return 'Unable to tie database.7'; } foreach(@$orderedProblems) { my ($sequence,$problem,$part)=split(':', $_); if($cache{'StatisticsMaps'} ne 'All Maps' && $cache{'StatisticsMaps'} ne $cache{$sequence.':title'}) { next; } if( $currentSequence == -1 || ($sortProblems eq 'Sort Within Sequence' && $currentSequence != $sequence)) { if($currentSequence != -1) { &DrawGraph(\@values,$courseDescription,$title,$max,$r); } if($sortProblems eq 'Sort Within Sequence') { $r->print('
'.$cache{$sequence.':title'}.''."\n"); } $currentSequence = $sequence; @values = (); $max=0; } my $data = 0; if($graph eq 'DoDiffGraph') { $data = sprintf("%.2f", $cache{$_.':degreeOfDifficulty'}), } else { $data = sprintf("%.1f", $cache{$_.':percentWrong'}), } if($max < $data) { $max = $data; } push(@values, $data); } untie(%cache); &DrawGraph(\@values,$courseDescription,$title,$max,$r); return; } sub DrawGraph { my ($values,$courseDescription,$title,$Max,$r)=@_; my $sendValues = join(',', @$values); my $sendCount = scalar(@$values); $r->print("
The Maximum Value is: $Max"); if ( $Max > 1 ) { if ($Max % 10) { if ( int($Max) < $Max ) { $Max++; $Max = int($Max); } } #(10 - $Max % 10); } else { $Max = 1; } my @GData = ('','Problem_number',$title,$Max,$sendCount,$sendValues); # $r->print(''."\n"); $r->print('
'."\n"); $r->print(''); # $r->print('
'."\n"); $r->print('
'."\n"); } #---- Problem Statistics Web Page --------------------------------------- sub CreateProblemStatisticsTableHeading { my ($headings,$r)=@_; my $Str=''; $Str .= ''."\n"; $Str .= 'P#'."\n"; foreach(@$headings) { $Str .= ''; $Str .= ''.$_.' '."\n"; } $Str .= "\n".''."\n"; return $Str; } sub BuildStatisticsTable { my ($cache,$displayFormat,$sortProblems,$orderedProblems,$headings, $r,$color)=@_; my $count = 1; my $currentSequence = -1; foreach(@$orderedProblems) { my ($sequence,$problem,$part)=split(':', $_); if($cache->{'StatisticsMaps'} ne 'All Maps' && $cache->{'StatisticsMaps'} ne $cache->{$sequence.':title'}) { next; } if($currentSequence == -1 || ($sortProblems eq 'Sort Within Sequence' && $currentSequence != $sequence)) { if($displayFormat ne 'Display CSV Format') { if($currentSequence ne -1) { $r->print(''); $r->print('
'); } if($sortProblems eq 'Sort Within Sequence') { $r->print(''.$cache->{$sequence.':title'}.''); } $r->print('
'."\n"); $r->print(''."\n"); $r->print(&CreateProblemStatisticsTableHeading($headings, $r)); } else { if($sortProblems eq 'Sort Within Sequence') { $r->print('"'.$cache->{$sequence.':title'}.'"'); } $r->print('
'); } $currentSequence = $sequence; } my $ref = ''.$cache->{$problem.':title'}.''; my $title = $cache->{$problem.':title'}; if($part != 0) { $title .= ' Part '.$part; } my $source = $cache->{$problem.':source'}; my $tableData = join('&', $ref, $title, $source, $cache->{$_.':studentCount'}, $cache->{$_.':totalTries'}, $cache->{$_.':maxTries'}, $cache->{$_.':mean'}, $cache->{$_.':correct'}, $cache->{$_.':correctByOverride'}, $cache->{$_.':percentWrong'}, $cache->{$_.':degreeOfDifficulty'}, $cache->{$_.':standardDeviation'}, $cache->{$_.':skewness'}, $cache->{$_.':discriminationFactor1'}, $cache->{$_.':discriminationFactor2'}); &TableRow($displayFormat,$tableData,$count,$r,$color); $count++; } if($displayFormat ne 'Display CSV Format') { $r->print('
'."\n"); $r->print('
'); } else { $r->print('
'); } return; } sub TableRow { my ($displayFormat,$Str,$RealIdx,$r,$color)=@_; my($ref,$title,$source,$StdNo,$TotalTries,$MxTries,$Avg,$YES,$Override, $Wrng,$DoD,$SD,$Sk,$_D1,$_D2)=split(/\&/,$Str); my $Ptr; if($displayFormat eq 'Display CSV Format') { $Ptr='"'.$RealIdx.'",'."\n". '"'.$title.'",'."\n". '"'.$source.'",'."\n". '"'.$StdNo.'",'."\n". '"'.$TotalTries.'",'."\n". '"'.$MxTries.'",'."\n". '"'.$Avg.'",'."\n". '"'.$YES.'",'."\n". '"'.$Override.'",'."\n". '"'.$Wrng.'",'."\n". '"'.$DoD.'",'."\n". '"'.$SD.'",'."\n". '"'.$Sk.'",'."\n". '"'.$_D1.'",'."\n". '"'.$_D2.'"'."\n". "
\n"; $r->print("\n".$Ptr); } else { $Ptr=''."\n". ''.$RealIdx.''."\n". ''.$ref.''."\n". ' '.$StdNo.''."\n". ''.$TotalTries.''."\n". ''.$MxTries.''."\n". ''.$Avg.''."\n". ' '.$YES.''."\n". ' '.$Override.''."\n". ' '.$Wrng.''."\n". ' '.$DoD.''."\n". ' '.$SD.''."\n". ' '.$Sk.''."\n". ' '.$_D1.''."\n". ' '.$_D2.''."\n"; $r->print($Ptr.''."\n"); } return; } # For loading the colored table for display or un-colored for print sub setbgcolor { my $PrintTable=shift; my %color; if ($PrintTable){ $color{"gb"}="#FFFFFF"; $color{"red"}="#FFFFFF"; $color{"yellow"}="#FFFFFF"; $color{"green"}="#FFFFFF"; $color{"purple"}="#FFFFFF"; } else { $color{"gb"}="#DDFFFF"; $color{"red"}="#FFDDDD"; $color{"yellow"}="#EEFFCC"; $color{"green"}="#DDFFDD"; $color{"purple"}="#FFDDFF"; } return \%color; } sub ProblemStatisticsButtons { my ($displayFormat, $displayLegend, $sortProblems)=@_; my $Ptr = ''; $Ptr .= '