File:  [LON-CAPA] / loncom / interface / statistics / lonproblemstatistics.pm
Revision 1.72: download - view: text, annotated - select for diffs
Tue Mar 23 20:08:58 2004 UTC (20 years, 3 months ago) by matthew
Branches: MAIN
CVS tags: HEAD
Remove debug spew.

    1: # The LearningOnline Network with CAPA
    2: #
    3: # $Id: lonproblemstatistics.pm,v 1.72 2004/03/23 20:08:58 matthew Exp $
    4: #
    5: # Copyright Michigan State University Board of Trustees
    6: #
    7: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
    8: #
    9: # LON-CAPA is free software; you can redistribute it and/or modify
   10: # it under the terms of the GNU General Public License as published by
   11: # the Free Software Foundation; either version 2 of the License, or
   12: # (at your option) any later version.
   13: #
   14: # LON-CAPA is distributed in the hope that it will be useful,
   15: # but WITHOUT ANY WARRANTY; without even the implied warranty of
   16: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   17: # GNU General Public License for more details.
   18: #
   19: # You should have received a copy of the GNU General Public License
   20: # along with LON-CAPA; if not, write to the Free Software
   21: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   22: #
   23: # /home/httpd/html/adm/gpl.txt
   24: #
   25: # http://www.lon-capa.org/
   26: #
   27: # (Navigate problems for statistical reports
   28: #
   29: ###############################################
   30: ###############################################
   31: 
   32: =pod
   33: 
   34: =head1 NAME
   35: 
   36: lonproblemstatistics
   37: 
   38: =head1 SYNOPSIS
   39: 
   40: Routines to present problem statistics to instructors via tables,
   41: Excel files, and plots.
   42: 
   43: =over 4
   44: 
   45: =cut
   46: 
   47: ###############################################
   48: ###############################################
   49: 
   50: package Apache::lonproblemstatistics;
   51: 
   52: use strict;
   53: use Apache::lonnet();
   54: use Apache::loncommon();
   55: use Apache::lonhtmlcommon;
   56: use Apache::loncoursedata;
   57: use Apache::lonstatistics;
   58: use Apache::lonlocal;
   59: use Spreadsheet::WriteExcel;
   60: use Apache::lonstathelpers();
   61: use Time::HiRes;
   62: ##
   63: ## Localization notes:
   64: ##
   65: ## in @Fields[0]->{'long_title'} is placed in Excel files and is used as the
   66: ## header for plots created with Graph.pm, both of which more than likely do
   67: ## not support localization.
   68: ##
   69: my @Fields = (
   70:            { name => 'problem_num',
   71:              title => 'P#',
   72:              align => 'right',
   73:              color => '#FFFFE6' },
   74:            { name   => 'container',
   75:              title  => 'Sequence or Folder',
   76:              align  => 'left',
   77:              color  => '#FFFFE6',
   78:              sortable => 'yes' },
   79:            { name   => 'title',
   80:              title  => 'Title',
   81:              align  => 'left',
   82:              color  => '#FFFFE6',
   83:              special  => 'link',
   84:              sortable => 'yes', },
   85:            { name   => 'part', 
   86:              title  => 'Part',
   87:              align  => 'left',
   88:              color  => '#FFFFE6',
   89:              },
   90:            { name   => 'num_students',
   91:              title  => '#Stdnts',
   92:              align  => 'right',
   93:              color  => '#EEFFCC',
   94:              format => '%d',
   95:              sortable  => 'yes',
   96:              graphable => 'yes',
   97:              long_title => 'Number of Students Attempting Problem' },
   98:            { name   => 'tries',
   99:              title  => 'Tries',
  100:              align  => 'right',
  101:              color  => '#EEFFCC',
  102:              format => '%d',
  103:              sortable  => 'yes',
  104:              graphable => 'yes',
  105:              long_title => 'Total Number of Tries' },
  106:            { name   => 'max_tries',
  107:              title  => 'Max Tries',
  108:              align  => 'right',
  109:              color  => '#DDFFFF',
  110:              format => '%d',
  111:              sortable  => 'yes',
  112:              graphable => 'yes',
  113:              long_title => 'Maximum Number of Tries' },
  114:            { name   => 'mean_tries',
  115:              title  => 'Mean Tries',
  116:              align  => 'right',
  117:              color  => '#DDFFFF',
  118:              format => '%5.2f',
  119:              sortable  => 'yes',
  120:              graphable => 'yes',
  121:              long_title => 'Average Number of Tries' },
  122:            { name   => 'std_tries',
  123:              title  => 'S.D. tries',
  124:              align  => 'right',
  125:              color  => '#DDFFFF',
  126:              format => '%5.2f',
  127:              sortable  => 'yes',
  128:              graphable => 'yes',
  129:              long_title => 'Standard Deviation of Number of Tries' },
  130:            { name   => 'skew_tries',
  131:              title  => 'Skew Tries',
  132:              align  => 'right',
  133:              color  => '#DDFFFF',
  134:              format => '%5.2f',
  135:              sortable  => 'yes',
  136:              graphable => 'yes',
  137:              long_title => 'Skew of Number of Tries' },
  138:            { name   => 'deg_of_diff',
  139:              title  => 'DoDiff',
  140:              align  => 'right',
  141:              color  => '#DDFFFF',
  142:              format => '%5.2f',
  143:              sortable  => 'yes',
  144:              graphable => 'yes',
  145:              long_title => 'Degree of Difficulty'.
  146:                            '[ 1 - ((#YES+#yes) / Tries) ]'},
  147:            { name   => 'num_solved',
  148:              title  => '#YES',
  149:              align  => 'right',
  150:              color  => '#FFDDDD',
  151:              format => '%4.1f',#             format => '%d',
  152:              sortable  => 'yes',
  153:              graphable => 'yes',
  154:              long_title => 'Number of Students able to Solve' },
  155:            { name   => 'num_override',
  156:              title  => '#yes',
  157:              align  => 'right',
  158:              color  => '#FFDDDD',
  159:              format => '%4.1f',#             format => '%d',
  160:              sortable  => 'yes',
  161:              graphable => 'yes',
  162:              long_title => 'Number of Students given Override' },
  163:            { name   => 'per_wrong',
  164:              title  => '%Wrng',
  165:              align  => 'right',
  166:              color  => '#FFFFE6',
  167:              format => '%4.1f',
  168:              sortable  => 'yes',
  169:              graphable => 'yes',
  170:              long_title => 'Percent of students whose final answer is wrong' },
  171:            { name   => 'deg_of_disc',
  172:              title  => 'Deg of Disc',
  173:              align  => 'right',
  174:              color  => '#FFFFE6',
  175:              format => '%4.2f',
  176:              sortable  => 'yes',
  177:              graphable => 'yes',
  178:              long_title => 'Degree of Discrimination' },
  179: 
  180: );
  181: 
  182: ###############################################
  183: ###############################################
  184: 
  185: =pod 
  186: 
  187: =item &CreateInterface()
  188: 
  189: Create the main intereface for the statistics page.  Allows the user to
  190: select sections, maps, and output.
  191: 
  192: =cut
  193: 
  194: ###############################################
  195: ###############################################
  196: my @OutputOptions = 
  197:     (
  198:      { name  => 'grouped by sequence',
  199:        value => 'HTML problem statistics grouped',
  200:        description => 'Output statistics for the problem parts.',
  201:        mode => 'html',
  202:        show => 'grouped',
  203:      },
  204:      { name  => 'ungrouped',
  205:        value => 'HTML problem statistics ungrouped',
  206:        description => 'Output statistics for the problem parts.',
  207:        mode => 'html',
  208:        show => 'ungrouped',
  209:      },
  210:      { name  => 'Excel',
  211:        value => 'Excel problem statistics',
  212:        description => 'Output statistics for the problem parts '.
  213:            'in an Excel workbook',
  214:        mode => 'excel',
  215:        show => 'all',
  216:      },
  217:      );
  218: 
  219: sub CreateInterface {
  220:     my $Str = '';
  221:     $Str .= &Apache::lonhtmlcommon::breadcrumbs
  222:         (undef,'Overall Problem Statistics','Statistics_Overall_Key');
  223:     $Str .= '<table cellspacing="5">'."\n";
  224:     $Str .= '<tr>';
  225:     $Str .= '<td align="center"><b>'.&mt('Sections').'</b></td>';
  226:     $Str .= '<td align="center"><b>'.&mt('Enrollment Status').'</b></td>';
  227:     $Str .= '<td align="center"><b>'.&mt('Sequences and Folders').'</b></td>';
  228:     $Str .= '<td align="center"><b>'.&mt('Output').'</b></td>';
  229:     $Str .= '<td rowspan="2">'.
  230:         &Apache::lonstathelpers::limit_by_time_form().'</td>';
  231:     $Str .= '</tr>'."\n";
  232:     #
  233:     $Str .= '<tr><td align="center">'."\n";
  234:     $Str .= &Apache::lonstatistics::SectionSelect('Section','multiple',5);
  235:     $Str .= '</td><td align="center">';
  236:     $Str .= &Apache::lonhtmlcommon::StatusOptions(undef,undef,5);
  237:     $Str .= '</td><td align="center">';
  238:     #
  239:     my $only_seq_with_assessments = sub { 
  240:         my $s=shift;
  241:         if ($s->{'num_assess'} < 1) { 
  242:             return 0;
  243:         } else { 
  244:             return 1;
  245:         }
  246:     };
  247:     $Str .= &Apache::lonstatistics::MapSelect('Maps','multiple,all',5,
  248:                                               $only_seq_with_assessments);
  249:     $Str .= '</td><td>'."\n";
  250:     my ($html,$outputmode,$show) = 
  251:         &Apache::lonstatistics::CreateAndParseOutputSelector(
  252:                                             'statsoutputmode',
  253:                                             'HTML problem statistics grouped',
  254:                                             @OutputOptions);
  255:     $Str .= $html;
  256:     $Str .= '</td></tr>'."\n";
  257:     $Str .= '</table>'."\n";
  258:     $Str .= '<input type="submit" name="GenerateStatistics" value="'.
  259:         &mt('Generate Statistics').'" />';
  260:     $Str .= '&nbsp;'x5;
  261:     $Str .= '<input type="submit" name="ClearCache" value="'.
  262:         &mt('Clear Caches').'" />';
  263:     $Str .= '&nbsp;'x5;
  264:     return ($Str,$outputmode,$show);
  265: }
  266: 
  267: ###############################################
  268: ###############################################
  269: 
  270: =pod 
  271: 
  272: =item &BuildProblemStatisticsPage()
  273: 
  274: Main interface to problem statistics.
  275: 
  276: =cut
  277: 
  278: ###############################################
  279: ###############################################
  280: sub BuildProblemStatisticsPage {
  281:     my ($r,$c)=@_;
  282:     #
  283:     my %Saveable_Parameters = ('Status' => 'scalar',
  284:                                'statsoutputmode' => 'scalar',
  285:                                'Section' => 'array',
  286:                                'StudentData' => 'array',
  287:                                'Maps' => 'array');
  288:     &Apache::loncommon::store_course_settings('statistics',
  289:                                               \%Saveable_Parameters);
  290:     &Apache::loncommon::restore_course_settings('statistics',
  291:                                                 \%Saveable_Parameters);
  292:     #
  293:     &Apache::lonstatistics::PrepareClasslist();
  294:     #
  295:     &Apache::loncoursedata::populate_weight_table();
  296:     #
  297:     my ($interface,$output_mode,$show) = &CreateInterface();
  298:     $r->print($interface);
  299:     $r->print('<input type="hidden" name="statsfirstcall" value="no" />');
  300:     $r->print('<input type="hidden" name="sortby" value="'.$ENV{'form.sortby'}.
  301:               '" />');
  302:     $r->print('<input type="hidden" name="plot" value="" />');
  303:     if (! exists($ENV{'form.statsfirstcall'})) {
  304:         return;
  305:     }
  306:     #
  307:     &Apache::lonstatistics::Gather_Student_Data($r);
  308:     #
  309:     #
  310:     if ($output_mode eq 'html') {
  311:         $r->print("<h2>".
  312:                   $ENV{'course.'.$ENV{'request.course.id'}.'.description'}.
  313:                   "</h2>\n");
  314:         my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
  315:         if (defined($starttime) || defined($endtime)) {
  316:             # Inform the user what the time limits on the data are.
  317:             $r->print('<h3>'.&mt('Statistics on submissions from [_1] to [_2]',
  318:                                  &Apache::lonlocal::locallocaltime($starttime),
  319:                                  &Apache::lonlocal::locallocaltime($endtime)).
  320:                       '</h3>');
  321:         }
  322:         $r->print("<h3>".&mt('Compiled on [_1]',
  323:                              &Apache::lonlocal::locallocaltime(time))."</h3>");
  324:         $r->rflush();
  325:         if ($show eq 'grouped') {
  326:             &output_html_grouped_by_sequence($r);
  327:         } elsif ($show eq 'ungrouped') {
  328:             &output_html_ungrouped($r);
  329:         }
  330:     } elsif ($output_mode eq 'excel') {
  331:         $r->print('<h2>'.&mt('Preparing Excel Spreadsheet').'</h2>');
  332:         &output_excel($r);
  333:     } else {
  334:         $r->print('<h1>'.&mt('Not implemented').'</h1>');
  335:     }
  336:     return;
  337: }
  338: 
  339: ###############################################
  340: ###############################################
  341: 
  342: =pod 
  343: 
  344: =item &output_html_grouped_by_sequence()
  345: 
  346: Presents the statistics data as an html table organized by the order
  347: the assessments appear in the course.
  348: 
  349: =cut
  350: 
  351: ###############################################
  352: ###############################################
  353: sub output_html_grouped_by_sequence {
  354:     my ($r) = @_;
  355:     my $problem_num = 0;
  356:     #$r->print(&ProblemStatisticsLegend());
  357:     foreach my $sequence (&Apache::lonstatistics::Sequences_with_Assess()) {
  358:         next if ($sequence->{'num_assess'}<1);
  359:         $r->print("<h3>".$sequence->{'title'}."</h3>");
  360:         $r->print('<table border="0"><tr><td bgcolor="#777777">'."\n");
  361:         $r->print('<table border="0" cellpadding="3">'."\n");
  362:         $r->print('<tr bgcolor="#FFFFE6">');
  363:         my $Str = &statistics_table_header('no container no plots');
  364:         $r->print('<tr bgcolor="#FFFFE6">'.$Str."</tr>\n");
  365:         foreach my $resource (@{$sequence->{'contents'}}) {
  366:             next if ($resource->{'type'} ne 'assessment');
  367:             foreach my $part (@{$resource->{'parts'}}) {
  368:                 $problem_num++;
  369:                 my $data = &get_statistics($sequence,$resource,$part,
  370:                                            $problem_num);
  371:                 my $option = '';
  372:                 $r->print('<tr>'.&statistics_html_table_data($data,
  373:                                                              'no container').
  374:                           "</tr>\n");
  375:             }
  376:         }
  377:         $r->print("</table>\n");
  378:         $r->print("</td></tr></table>\n");
  379:         $r->rflush();
  380:     }
  381:     #
  382:     return;
  383: }
  384: 
  385: ###############################################
  386: ###############################################
  387: 
  388: =pod 
  389: 
  390: =item &output_html_ungrouped()
  391: 
  392: Presents the statistics data in a single html table which can be sorted by
  393: different columns.
  394: 
  395: =cut
  396: 
  397: ###############################################
  398: ###############################################
  399: sub output_html_ungrouped {
  400:     my ($r,$option) = @_;
  401:     #
  402:     if (exists($ENV{'form.plot'}) && $ENV{'form.plot'} ne '') {
  403:         &plot_statistics($r,$ENV{'form.plot'});
  404:     }
  405:     #
  406:     my $problem_num = 0;
  407:     my $show_container = 0;
  408:     my $show_part = 0;
  409:     #$r->print(&ProblemStatisticsLegend());
  410:     my $sortby = undef;
  411:     foreach my $field (@Fields) {
  412:         if ($ENV{'form.sortby'} eq $field->{'name'}) {
  413:             $sortby = $field->{'name'};
  414:         }
  415:     }
  416:     if (! defined($sortby) || $sortby eq '' || $sortby eq 'problem_num') {
  417:         $sortby = 'container';
  418:     }
  419:     # If there is more than one sequence, list their titles
  420:     my @Sequences = &Apache::lonstatistics::Sequences_with_Assess();
  421:     if (@Sequences < 1) {
  422:         $option .= ' no container';
  423:     }
  424:     #
  425:     # Compile the data
  426:     my @Statsarray;
  427:     foreach my $sequence (@Sequences) {
  428:         next if ($sequence->{'num_assess'}<1);
  429:         foreach my $resource (@{$sequence->{'contents'}}) {
  430:             next if ($resource->{'type'} ne 'assessment');
  431:             foreach my $part (@{$resource->{'parts'}}) {
  432:                 $problem_num++;
  433:                 my $data = &get_statistics($sequence,$resource,$part,
  434:                                            $problem_num);
  435:                 $show_part = 1 if ($part ne '0');
  436:                 #
  437:                 push (@Statsarray,$data);
  438:             }
  439:         }
  440:     }
  441:     #
  442:     # Sort the data
  443:     my @OutputOrder;
  444:     if ($sortby eq 'container') {
  445:         @OutputOrder = @Statsarray;
  446:     } else {
  447:         # $sortby is already defined, so we can charge ahead
  448:         if ($sortby =~ /^(title|part)$/i) {
  449:             # Alpha comparison
  450:             @OutputOrder = sort {
  451:                 lc($a->{$sortby}) cmp lc($b->{$sortby}) ||
  452:                     lc($a->{'title'}) cmp lc($b->{'title'}) ||
  453:                         lc($a->{'part'}) cmp lc($b->{'part'});
  454:             } @Statsarray;
  455:         } else {
  456:             # Numerical comparison
  457:             @OutputOrder = sort {
  458:                 my $retvalue = 0;
  459:                 if ($b->{$sortby} eq 'nan') {
  460:                     if ($a->{$sortby} ne 'nan') {
  461:                         $retvalue = -1;
  462:                     } else {
  463:                         $retvalue = 0;
  464:                     }
  465:                 }
  466:                 if ($a->{$sortby} eq 'nan') {
  467:                     if ($b->{$sortby} ne 'nan') {
  468:                         $retvalue = 1;
  469:                     }
  470:                 }
  471:                 if ($retvalue eq '0') {
  472:                     $retvalue = $b->{$sortby} <=> $a->{$sortby} ||
  473:                                 lc($a->{'title'}) <=> lc($b->{'title'}) ||
  474:                                 lc($a->{'part'})  <=> lc($b->{'part'});
  475:                 }
  476:                 $retvalue;
  477:             } @Statsarray;
  478:         }
  479:     }
  480:     $option .= 'no part' if (! $show_part);
  481:     my $num_output = 0;
  482:     #
  483:     # output the headers
  484:     $r->print('<table border="0"><tr><td bgcolor="#777777">'."\n");
  485:     $r->print('<table border="0" cellpadding="3">'."\n");
  486:     my $Str = &statistics_table_header($option.' sortable');
  487:     $r->print('<tr bgcolor="#FFFFE6">'.$Str."</tr>\n");
  488:     #
  489:     foreach my $rowdata (@OutputOrder) {
  490:         $num_output++;
  491:         if ($num_output % 25 == 0) {
  492:             $r->print("</table>\n</td></tr></table>\n");
  493:             #
  494:             $r->print('<table border="0"><tr><td bgcolor="#777777">'."\n");
  495:             $r->print('<table border="0" cellpadding="3">'."\n");
  496:             my $Str = &statistics_table_header($option.' sortable');
  497:             $r->print('<tr bgcolor="#FFFFE6">'.$Str."</tr>\n");
  498:             $r->rflush();
  499:         }
  500:         $r->print('<tr>'.&statistics_html_table_data($rowdata,$option).
  501:                   "</tr>\n");
  502:     }
  503:     $r->print("</table>\n");
  504:     $r->print("</td></tr></table>\n");
  505:     $r->rflush();
  506:     #
  507:     return;
  508: }
  509: 
  510: ###############################################
  511: ###############################################
  512: 
  513: =pod 
  514: 
  515: =item &output_excel()
  516: 
  517: Presents the statistical data in an Excel 95 compatable spreadsheet file.
  518: 
  519: =cut
  520: 
  521: ###############################################
  522: ###############################################
  523: sub output_excel {
  524:     my ($r) = @_;
  525:     my $filename = '/prtspool/'.
  526:         $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.
  527:             time.'_'.rand(1000000000).'.xls';
  528:     #
  529:     my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
  530:     #
  531:     my $excel_workbook = undef;
  532:     my $excel_sheet = undef;
  533:     #
  534:     my $rows_output = 0;
  535:     my $cols_output = 0;
  536:     #
  537:     # Create sheet
  538:     $excel_workbook = Spreadsheet::WriteExcel->new('/home/httpd'.$filename);
  539:     #
  540:     # Check for errors
  541:     if (! defined($excel_workbook)) {
  542:         $r->log_error("Error creating excel spreadsheet $filename: $!");
  543:         $r->print(&mt("Problems creating new Excel file.  ".
  544:                   "This error has been logged.  ".
  545:                   "Please alert your LON-CAPA administrator."));
  546:         return ;
  547:     }
  548:     #
  549:     # The excel spreadsheet stores temporary data in files, then put them
  550:     # together.  If needed we should be able to disable this (memory only).
  551:     # The temporary directory must be specified before calling 'addworksheet'.
  552:     # File::Temp is used to determine the temporary directory.
  553:     $excel_workbook->set_tempdir($Apache::lonnet::tmpdir);
  554:     #
  555:     # Add a worksheet
  556:     my $sheetname = $ENV{'course.'.$ENV{'request.course.id'}.'.description'};
  557:     if (length($sheetname) > 31) {
  558:         $sheetname = substr($sheetname,0,31);
  559:     }
  560:     $excel_sheet = $excel_workbook->addworksheet(
  561:                        &Apache::loncommon::clean_excel_name($sheetname)
  562:                                                  );
  563:     #
  564:     # Put the course description in the header
  565:     $excel_sheet->write($rows_output,$cols_output++,
  566:                    $ENV{'course.'.$ENV{'request.course.id'}.'.description'});
  567:     $cols_output += 3;
  568:     #
  569:     # Put a description of the sections listed
  570:     my $sectionstring = '';
  571:     my @Sections = @Apache::lonstatistics::SelectedSections;
  572:     if (scalar(@Sections) > 1) {
  573:         if (scalar(@Sections) > 2) {
  574:             my $last = pop(@Sections);
  575:             $sectionstring = "Sections ".join(', ',@Sections).', and '.$last;
  576:         } else {
  577:             $sectionstring = "Sections ".join(' and ',@Sections);
  578:         }
  579:     } else {
  580:         if ($Sections[0] eq 'all') {
  581:             $sectionstring = "All sections";
  582:         } else {
  583:             $sectionstring = "Section ".$Sections[0];
  584:         }
  585:     }
  586:     $excel_sheet->write($rows_output,$cols_output++,$sectionstring);
  587:     $cols_output += scalar(@Sections);
  588:     #
  589:     # Time restrictions
  590:     my $time_string;
  591:     if (defined($starttime)) {
  592:         # call localtime but not lonlocal:locallocaltime because excel probably
  593:         # cannot handle localized text.  Probably.
  594:         $time_string .= 'Data collected from '.localtime($time_string);
  595:         if (defined($endtime)) {
  596:             $time_string .= ' to '.localtime($endtime);
  597:         }
  598:         $time_string .= '.';
  599:     } elsif (defined($endtime)) {
  600:         # See note above about lonlocal:locallocaltime
  601:         $time_string .= 'Data collected before '.localtime($endtime).'.';
  602:     }
  603:     
  604:     #
  605:     # Put the date in there too
  606:     $excel_sheet->write($rows_output,$cols_output++,
  607:                         'Compiled on '.localtime(time));
  608:     #
  609:     $rows_output++; 
  610:     $cols_output=0;
  611:     #
  612:     # Long Headersheaders
  613:     foreach my $field (@Fields) {
  614:         next if ($field->{'name'} eq 'problem_num');
  615:         if (exists($field->{'long_title'})) {
  616:             $excel_sheet->write($rows_output,$cols_output++,
  617:                                 $field->{'long_title'});
  618:         } else {
  619:             $excel_sheet->write($rows_output,$cols_output++,'');
  620:         }
  621:     }
  622:     $rows_output++;
  623:     $cols_output=0;
  624:     # Brief headers
  625:     foreach my $field (@Fields) {
  626:         next if ($field->{'name'} eq 'problem_num');
  627:         # Use english for excel as I am not sure how well excel handles 
  628:         # other character sets....
  629:         $excel_sheet->write($rows_output,$cols_output++,$field->{'title'});
  630:     }
  631:     $rows_output++;
  632:     #
  633:     # Write the data
  634:     my $problem_num=0;
  635:     foreach my $sequence (&Apache::lonstatistics::Sequences_with_Assess()) {
  636:         next if ($sequence->{'num_assess'}<1);
  637:         foreach my $resource (@{$sequence->{'contents'}}) {
  638:             next if ($resource->{'type'} ne 'assessment');
  639:             foreach my $part (@{$resource->{'parts'}}) {
  640:                 $cols_output=0;
  641:                 $problem_num++;
  642:                 my $data = &get_statistics($sequence,$resource,$part,
  643:                                            $problem_num);
  644:                 #
  645:                 if (!defined($part) || $part eq '') {
  646:                     $part = ' ';
  647:                 }
  648:                 foreach my $field (@Fields) {
  649:                     next if ($field->{'name'} eq 'problem_num');
  650:                     $excel_sheet->write($rows_output,$cols_output++,
  651:                                         $data->{$field->{'name'}});
  652:                 }
  653:                 $rows_output++;
  654:             }
  655:         }
  656:     }
  657:     #
  658:     # Write the excel file
  659:     $excel_workbook->close();
  660:     # Tell the user where to get their excel file
  661:     $r->print('<br />'.
  662:               '<a href="'.$filename.'">'.
  663:               &mt('Your Excel Spreadsheet').'</a>'."\n");
  664:     $r->rflush();
  665:     return;
  666: }
  667: 
  668: ###############################################
  669: ###############################################
  670: 
  671: =pod 
  672: 
  673: =item &statistics_html_table_data()
  674: 
  675: Help function used to format the rows for HTML table output.
  676: 
  677: =cut
  678: 
  679: ###############################################
  680: ###############################################
  681: sub statistics_html_table_data {
  682:     my ($data,$options) = @_;
  683:     my $row = '';
  684:     foreach my $field (@Fields) {
  685:         next if ($options =~ /no $field->{'name'}/);
  686:         $row .= '<td bgcolor="'.$field->{'color'}.'"';
  687:         if (exists($field->{'align'})) {
  688:             $row .= ' align="'.$field->{'align'}.'"';
  689:             }
  690:         $row .= '>';
  691:         if (exists($field->{'special'}) && $field->{'special'} eq 'link') {
  692:             $row .= '<a href="'.$data->{$field->{'name'}.'.link'}.'">';
  693:         }
  694:         if (exists($field->{'format'})) {
  695:             $row .= sprintf($field->{'format'},$data->{$field->{'name'}});
  696:         } else {
  697:             $row .= $data->{$field->{'name'}};
  698:         }
  699:         if (exists($field->{'special'}) && $field->{'special'} eq 'link') {
  700:             $row.= '</a>';
  701:         }
  702:         $row .= '</td>';
  703:     }
  704:     return $row;
  705: }
  706: 
  707: sub statistics_table_header {
  708:     my ($options) = @_;
  709:     my $header_row;
  710:     foreach my $field (@Fields) {
  711:         next if ($options =~ /no $field->{'name'}/);
  712:         $header_row .= '<th>';
  713:         if ($options =~ /sortable/ && 
  714:             exists($field->{'sortable'}) && $field->{'sortable'} eq 'yes') {
  715:             $header_row .= '<a href="javascript:'.
  716:                 'document.Statistics.sortby.value='."'".$field->{'name'}."'".
  717:                     ';document.Statistics.submit();">';
  718:         }
  719:         $header_row .= &mt($field->{'title'});
  720:         if ($options =~ /sortable/) {
  721:             $header_row.= '</a>';
  722:         }
  723:         if ($options !~ /no plots/        && 
  724:             exists($field->{'graphable'}) && 
  725:             $field->{'graphable'} eq 'yes') {
  726:             $header_row.=' (';
  727:             $header_row .= '<a href="javascript:'.
  728:                 "document.Statistics.plot.value='$field->{'name'}'".
  729:                     ';document.Statistics.submit();">';
  730:             $header_row .= &mt('plot').'</a>)';
  731:         }
  732:         $header_row .= '</th>';
  733:     }
  734:     return $header_row;
  735: }
  736: 
  737: ###############################################
  738: ###############################################
  739: 
  740: =pod 
  741: 
  742: =item &plot_statistics()
  743: 
  744: =cut
  745: 
  746: ###############################################
  747: ###############################################
  748: sub plot_statistics {
  749:     my ($r,$datafield) = @_;
  750:     my @Data;
  751:     #
  752:     #
  753:     my $sortfield = undef;
  754:     my $title = undef;
  755:     foreach my $field (@Fields) {
  756:         if ($datafield eq $field->{'name'} &&
  757:             exists($field->{'graphable'}) && $field->{'graphable'} eq 'yes') {
  758:             $sortfield = $field->{'name'};
  759:             $title = $field->{'long_title'};
  760:         }
  761:     }
  762:     return if (! defined($sortfield) || $sortfield eq '');
  763:     #
  764:     my $Max = 0;
  765:     my $problem_num = 0;
  766:     foreach my $sequence (&Apache::lonstatistics::Sequences_with_Assess()) {
  767:         next if ($sequence->{'num_assess'}<1);
  768:         foreach my $resource (@{$sequence->{'contents'}}) {
  769:             next if ($resource->{'type'} ne 'assessment');
  770:             foreach my $part (@{$resource->{'parts'}}) {
  771:                 my $problem_number++;
  772:                 my $data = &get_statistics($sequence,$resource,$part,
  773:                                            $problem_num);
  774:                 my $value = $data->{$sortfield};
  775:                 $Max = $value if ($Max < $value);
  776:                 push (@Data,$value);
  777:             }
  778:         }
  779:     }
  780:     #
  781:     # Print out plot request
  782:     my $yaxis = '';
  783:     if ($sortfield eq 'per_wrong') {
  784:         $yaxis = 'Percent';
  785:     }
  786:     #
  787:     # Determine appropriate value for $Max
  788:     if ($sortfield eq 'deg_of_diff') {
  789:         if ($Max > 0.5) {
  790:             $Max = 1;
  791:         } elsif ($Max > 0.2) {
  792:             $Max = 0.5;
  793:         } elsif ($Max > 0.1) {
  794:             $Max = 0.2;
  795:         }
  796:     } elsif ($sortfield eq 'per_wrong') {
  797:         if ($Max > 50) {
  798:             $Max = 100;
  799:         } elsif ($Max > 25) {
  800:             $Max = 50;
  801:         } elsif ($Max > 20) {
  802:             $Max = 25;
  803:         } elsif ($Max > 10) {
  804:             $Max = 20;
  805:         } elsif ($Max > 5) {
  806:             $Max = 10;
  807:         } else {
  808:             $Max = 5;
  809:         }
  810:     }
  811:     
  812:     $r->print("<p>".&Apache::loncommon::DrawBarGraph($title,
  813:                                                      'Problem Number',
  814:                                                      $yaxis,
  815:                                                      $Max,
  816:                                                      undef, # colors
  817:                                                      undef, # labels
  818:                                                      \@Data)."</p>\n");
  819:     #
  820:     # Print out the data
  821:     $ENV{'form.sortby'} = 'Contents';
  822: #    &output_html_ungrouped($r);
  823:     return;
  824: }
  825: 
  826: ########################################################
  827: ########################################################
  828: 
  829: =pod
  830: 
  831: =item &get_statistics()
  832: 
  833: Wrapper routine from the call to loncoursedata::get_problem_statistics.  
  834: Calls lonstathelpers::get_time_limits() to limit the data set by time.
  835: 
  836: Inputs: $sequence, $resource, $part, $problem_num
  837: 
  838: Returns: Hash reference with statistics data from 
  839: loncoursedata::get_problem_statistics.
  840: 
  841: =cut
  842: 
  843: ########################################################
  844: ########################################################
  845: sub get_statistics {
  846:     my ($sequence,$resource,$part,$problem_num) = @_;
  847:     #
  848:     my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
  849:     my $symb = $resource->{'symb'};
  850:     my $courseid = $ENV{'request.course.id'};
  851:     #
  852:     my $data = &Apache::loncoursedata::get_problem_statistics
  853:                         (\@Apache::lonstatistics::SelectedSections,
  854:                          $Apache::lonstatistics::enrollment_status,
  855:                          $symb,$part,$courseid,$starttime,$endtime);
  856:     $data->{'part'}        = $part;
  857:     $data->{'problem_num'} = $problem_num;
  858:     $data->{'container'}   = $sequence->{'title'};
  859:     $data->{'title'}       = $resource->{'title'};
  860:     $data->{'title.link'}  = $resource->{'src'}.'?symb='.
  861:         &Apache::lonnet::escape($resource->{'symb'});
  862:     #
  863:     $data->{'deg_of_disc'} = &compute_discrimination_factor($resource,$part,$sequence);
  864:     return $data;
  865: }
  866: 
  867: 
  868: ###############################################
  869: ###############################################
  870: 
  871: =pod
  872: 
  873: =item &compute_discrimination_factor()
  874: 
  875: Inputs: $Resource, $Sequence
  876: 
  877: Returns: integer between -1 and 1
  878: 
  879: =cut
  880: 
  881: ###############################################
  882: ###############################################
  883: sub compute_discrimination_factor {
  884:     my ($resource,$part,$sequence) = @_;
  885:     my @Resources;
  886:     foreach my $res (@{$sequence->{'contents'}}) {
  887:         next if ($res->{'symb'} eq $resource->{'symb'});
  888:         push (@Resources,$res->{'symb'});
  889:     }
  890:     #
  891:     # rank
  892:     my $ranking = 
  893:         &Apache::loncoursedata::rank_students_by_scores_on_resources
  894:         (\@Resources,
  895:          \@Apache::lonstatistics::SelectedSections,
  896:          $Apache::lonstatistics::enrollment_status,undef);
  897:     #
  898:     # compute their percent scores on the problems in the sequence,
  899:     my $number_to_grab = int(scalar(@{$ranking})/4);
  900:     my $num_students = scalar(@{$ranking});
  901:     my @BottomSet = map { $_->[&Apache::loncoursedata::RNK_student()]; 
  902:                       } @{$ranking}[0..$number_to_grab];
  903:     my @TopSet    = 
  904:         map { 
  905:             $_->[&Apache::loncoursedata::RNK_student()]; 
  906:           } @{$ranking}[($num_students-$number_to_grab)..($num_students-1)];
  907:     my ($bottom_sum,$bottom_max) = 
  908:         &Apache::loncoursedata::get_sum_of_scores($resource,$part,\@BottomSet);
  909:     my ($top_sum,$top_max) = 
  910:         &Apache::loncoursedata::get_sum_of_scores($resource,$part,\@TopSet);
  911:     my $deg_of_disc;
  912:     if ($top_max == 0 || $bottom_max==0) {
  913:         $deg_of_disc = 'nan';
  914:     } else {
  915:         $deg_of_disc = ($top_sum/$top_max) - ($bottom_sum/$bottom_max);
  916:     }
  917:     #&Apache::lonnet::logthis('    '.$top_sum.'/'.$top_max.
  918:     #                         ' - '.$bottom_sum.'/'.$bottom_max);
  919:     return $deg_of_disc;
  920: }
  921: 
  922: ###############################################
  923: ###############################################
  924: 
  925: =pod 
  926: 
  927: =item &ProblemStatisticsLegend()
  928: 
  929: HELP  This needs to be localized, or at least generated automatically.
  930: 
  931: =cut
  932: 
  933: ###############################################
  934: ###############################################
  935: sub ProblemStatisticsLegend {
  936:     my $Ptr = '';
  937:     $Ptr = '<table border="0">';
  938:     $Ptr .= '<tr><td>';
  939:     $Ptr .= '<b>#Stdnts</b></td>';
  940:     $Ptr .= '<td>Total number of students attempted the problem.';
  941:     $Ptr .= '</td></tr><tr><td>';
  942:     $Ptr .= '<b>Tries</b></td>';
  943:     $Ptr .= '<td>Total number of tries for solving the problem.';
  944:     $Ptr .= '</td></tr><tr><td>';
  945:     $Ptr .= '<b>Max Tries</b></td>';
  946:     $Ptr .= '<td>Largest number of tries for solving the problem by a student.';
  947:     $Ptr .= '</td></tr><tr><td>';
  948:     $Ptr .= '<b>Mean</b></td>';
  949:     $Ptr .= '<td>Average number of tries. [ Tries / #Stdnts ]';
  950:     $Ptr .= '</td></tr><tr><td>';
  951:     $Ptr .= '<b>#YES</b></td>';
  952:     $Ptr .= '<td>Number of students solved the problem correctly.';
  953:     $Ptr .= '</td></tr><tr><td>';
  954:     $Ptr .= '<b>#yes</b></td>';
  955:     $Ptr .= '<td>Number of students solved the problem by override.';
  956:     $Ptr .= '</td></tr><tr><td>';
  957:     $Ptr .= '<b>%Wrong</b></td>';
  958:     $Ptr .= '<td>Percentage of students who tried to solve the problem ';
  959:     $Ptr .= 'but is still incorrect. [ 100*((#Stdnts-(#YES+#yes))/#Stdnts) ]';
  960:     $Ptr .= '</td></tr><tr><td>';
  961:     $Ptr .= '<b>DoDiff</b></td>';
  962:     $Ptr .= '<td>Degree of Difficulty of the problem.  ';
  963:     $Ptr .= '[ 1 - ((#YES+#yes) / Tries) ]';
  964:     $Ptr .= '</td></tr><tr><td>';
  965:     $Ptr .= '<b>S.D.</b></td>';
  966:     $Ptr .= '<td>Standard Deviation of the tries.  ';
  967:     $Ptr .= '[ sqrt(sum((Xi - Mean)^2)) / (#Stdnts-1) ';
  968:     $Ptr .= 'where Xi denotes every student\'s tries ]';
  969:     $Ptr .= '</td></tr><tr><td>';
  970:     $Ptr .= '<b>Skew.</b></td>';
  971:     $Ptr .= '<td>Skewness of the students tries.';
  972:     $Ptr .= '[(sqrt( sum((Xi - Mean)^3) / #Stdnts)) / (S.D.^3)]';
  973:     $Ptr .= '</td></tr><tr><td>';
  974:     $Ptr .= '<b>Dis.F.</b></td>';
  975:     $Ptr .= '<td>Discrimination Factor: A Standard for evaluating the ';
  976:     $Ptr .= 'problem according to a Criterion<br>';
  977:     $Ptr .= '<b>[Criterion to group students into %27 Upper Students - ';
  978:     $Ptr .= 'and %27 Lower Students]</b><br>';
  979:     $Ptr .= '<b>1st Criterion</b> for Sorting the Students: ';
  980:     $Ptr .= '<b>Sum of Partial Credit Awarded / Total Number of Tries</b><br>';
  981:     $Ptr .= '<b>2nd Criterion</b> for Sorting the Students: ';
  982:     $Ptr .= '<b>Total number of Correct Answers / Total Number of Tries</b>';
  983:     $Ptr .= '</td></tr>';
  984:     $Ptr .= '<tr><td><b>Disc.</b></td>';
  985:     $Ptr .= '<td>Number of Students had at least one discussion.';
  986:     $Ptr .= '</td></tr></table>';
  987:     return $Ptr;
  988: }
  989: 
  990: #---- END Problem Statistics Web Page ----------------------------------------
  991: 
  992: 1;
  993: __END__

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>