File:  [LON-CAPA] / loncom / interface / statistics / lonproblemstatistics.pm
Revision 1.83: download - view: text, annotated - select for diffs
Tue Apr 6 15:45:13 2004 UTC (20 years, 3 months ago) by matthew
Branches: MAIN
CVS tags: HEAD
Moved storage of dynamic metadata from loncoursedata to lonproblemstatistics.
lonproblemstatistics now only stores the dynamic metadata when the stats are
computed for all sections.
Discrimination factor computations now respect start and end times.
Modified loncoursedata::rank_students_by_scores_on_resources and
loncoursedata::get_sum_of_scores to support time constraints.

    1: # The LearningOnline Network with CAPA
    2: #
    3: # $Id: lonproblemstatistics.pm,v 1.83 2004/04/06 15:45:13 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: my @StatsArray;
   64: my %SeqStat;    # keys are symbs, values are hash refs
   65: 
   66: ##
   67: ## Localization notes:
   68: ##
   69: ## in @Fields[0]->{'long_title'} is placed in Excel files and is used as the
   70: ## header for plots created with Graph.pm, both of which more than likely do
   71: ## not support localization.
   72: ##
   73: #
   74: #
   75: ##
   76: ## Description of Field attributes
   77: ##
   78: ## Attribute     Required   Value       Meaning or Use
   79: ##
   80: ## name            yes      any scalar  Used to uniquely identify field
   81: ## title           yes      any scalar  This is what the user sees to identify
   82: ##                                      the field.  Passed through &mt().
   83: ## long_title      yes      any scalar  Used as graph heading and in excel
   84: ##                                      output.  NOT translated
   85: ## align           no    (left|right|center)  HTML cell contents alignment
   86: ## color           yes      html color  HTML cell background color
   87: ##                                      used to visually group statistics
   88: ## special         no          (link)   Indicates a link, target is name.link
   89: ##                                      Currently set in &get_statistics()
   90: ## graphable       no      (yes|no)     Can a bar graph of the field be 
   91: ##                                      produced?
   92: ## sortable        no      (yes|no)     Should a sort link be put in the
   93: ##                                      column header?
   94: ## selectable      yes     (yes|no)     Can the column be removed from the
   95: ##                                      statistics display?
   96: ## selected        yes     (yes|no)     Is the column selected by default?
   97: ##
   98: my @Fields = (
   99:            { name => 'problem_num',
  100:              title => 'P#',
  101:              align => 'right',
  102:              color => '#FFFFE6',
  103:              selectable => 'no',
  104:              defaultselected => 'yes',
  105:            },
  106:            { name   => 'container',
  107:              title  => 'Sequence or Folder',
  108:              align  => 'left',
  109:              color  => '#FFFFE6',
  110:              sortable => 'yes',
  111:              selectable => 'no',
  112:              defaultselected => 'yes',
  113:            },
  114:            { name   => 'title',
  115:              title  => 'Title',
  116:              align  => 'left',
  117:              color  => '#FFFFE6',
  118:              special  => 'link',
  119:              sortable => 'yes', 
  120:              selectable => 'no',
  121:              defaultselected => 'yes',
  122:            },
  123:            { name   => 'part', 
  124:              title  => 'Part',
  125:              align  => 'left',
  126:              color  => '#FFFFE6',
  127:              selectable => 'no',
  128:              defaultselected => 'yes',
  129:            },
  130:            { name   => 'num_students',
  131:              title  => '#Stdnts',
  132:              align  => 'right',
  133:              color  => '#EEFFCC',
  134:              format => '%d',
  135:              sortable  => 'yes',
  136:              graphable => 'yes',
  137:              long_title => 'Number of Students Attempting Problem',
  138:              selectable => 'yes',
  139:              defaultselected => 'yes',
  140:            },
  141:            { name   => 'tries',
  142:              title  => 'Tries',
  143:              align  => 'right',
  144:              color  => '#EEFFCC',
  145:              format => '%d',
  146:              sortable  => 'yes',
  147:              graphable => 'yes',
  148:              long_title => 'Total Number of Tries',
  149:              selectable => 'yes',
  150:              defaultselected => 'yes',
  151:            },
  152:            { name   => 'max_tries',
  153:              title  => 'Max Tries',
  154:              align  => 'right',
  155:              color  => '#DDFFFF',
  156:              format => '%d',
  157:              sortable  => 'yes',
  158:              graphable => 'yes',
  159:              long_title => 'Maximum Number of Tries',
  160:              selectable => 'yes',
  161:              defaultselected => 'yes',
  162:            },
  163:            { name   => 'min_tries',
  164:              title  => 'Min Tries',
  165:              align  => 'right',
  166:              color  => '#DDFFFF',
  167:              format => '%d',
  168:              sortable  => 'yes',
  169:              graphable => 'yes',
  170:              long_title => 'Minumum Number of Tries',
  171:              selectable => 'yes',
  172:              defaultselected => 'yes',
  173:            },
  174:            { name   => 'mean_tries',
  175:              title  => 'Mean Tries',
  176:              align  => 'right',
  177:              color  => '#DDFFFF',
  178:              format => '%5.2f',
  179:              sortable  => 'yes',
  180:              graphable => 'yes',
  181:              long_title => 'Average Number of Tries',
  182:              selectable => 'yes',
  183:              defaultselected => 'yes',
  184:            },
  185:            { name   => 'std_tries',
  186:              title  => 'S.D. tries',
  187:              align  => 'right',
  188:              color  => '#DDFFFF',
  189:              format => '%5.2f',
  190:              sortable  => 'yes',
  191:              graphable => 'yes',
  192:              long_title => 'Standard Deviation of Number of Tries',
  193:              selectable => 'yes',
  194:              defaultselected => 'yes',
  195:            },
  196:            { name   => 'skew_tries',
  197:              title  => 'Skew Tries',
  198:              align  => 'right',
  199:              color  => '#DDFFFF',
  200:              format => '%5.2f',
  201:              sortable  => 'yes',
  202:              graphable => 'yes',
  203:              long_title => 'Skew of Number of Tries',
  204:              selectable => 'yes',
  205:              defaultselected => 'no',
  206:            },
  207:            { name   => 'num_solved',
  208:              title  => '#YES',
  209:              align  => 'right',
  210:              color  => '#FFDDDD',
  211:              format => '%4.1f',#             format => '%d',
  212:              sortable  => 'yes',
  213:              graphable => 'yes',
  214:              long_title => 'Number of Students able to Solve',
  215:              selectable => 'yes',
  216:              defaultselected => 'yes',
  217:            },
  218:            { name   => 'num_override',
  219:              title  => '#yes',
  220:              align  => 'right',
  221:              color  => '#FFDDDD',
  222:              format => '%4.1f',#             format => '%d',
  223:              sortable  => 'yes',
  224:              graphable => 'yes',
  225:              long_title => 'Number of Students given Override',
  226:              selectable => 'yes',
  227:              defaultselected => 'yes',
  228:            },
  229:            { name   => 'num_wrong',
  230:              title  => '#Wrng',
  231:              align  => 'right',
  232:              color  => '#FFDDDD',
  233:              format => '%4.1f',
  234:              sortable  => 'yes',
  235:              graphable => 'yes',
  236:              long_title => 'Percent of students whose final answer is wrong',
  237:              selectable => 'yes',
  238:              defaultselected => 'yes',
  239:            },
  240:            { name   => 'deg_of_diff',
  241:              title  => 'DoDiff',
  242:              align  => 'right',
  243:              color  => '#FFFFE6',
  244:              format => '%5.2f',
  245:              sortable  => 'yes',
  246:              graphable => 'yes',
  247:              long_title => 'Degree of Difficulty'.
  248:                            '[ 1 - ((#YES+#yes) / Tries) ]',
  249:              selectable => 'yes',
  250:              defaultselected => 'yes',
  251:            },
  252:            { name   => 'deg_of_disc',
  253:              title  => 'DoDisc',
  254:              align  => 'right',
  255:              color  => '#FFFFE6',
  256:              format => '%4.2f',
  257:              sortable  => 'yes',
  258:              graphable => 'yes',
  259:              long_title => 'Degree of Discrimination',
  260:              selectable => 'yes',
  261:              defaultselected => 'no',
  262:            },
  263: );
  264: 
  265: my @SeqFields = (
  266:            { name   => 'title',
  267:              title  => 'Sequence',
  268:              align  => 'left',
  269:              color  => '#FFFFE6',
  270:              special  => 'no',
  271:              sortable => 'no', 
  272:              selectable => 'yes',
  273:              defaultselected => 'no',
  274:            },
  275:            { name   => 'items',
  276:              title  => '#Items',
  277:              align  => 'right',
  278:              color  => '#FFFFE6',
  279:              format => '%4d',
  280:              sortable  => 'no',
  281:              graphable => 'no',
  282:              long_title => 'Number of Items in Sequence',
  283:              selectable => 'yes',
  284:              defaultselected => 'no',
  285:            },
  286:            { name   => 'scoremean',
  287:              title  => 'Score Mean',
  288:              align  => 'right',
  289:              color  => '#FFFFE6',
  290:              format => '%4.2f',
  291:              sortable  => 'no',
  292:              graphable => 'no',
  293:              long_title => 'Mean Sequence Score',
  294:              selectable => 'yes',
  295:              defaultselected => 'no',
  296:            },
  297:            { name   => 'scorestd',
  298:              title  => 'Score STD',
  299:              align  => 'right',
  300:              color  => '#FFFFE6',
  301:              format => '%4.2f',
  302:              sortable  => 'no',
  303:              graphable => 'no',
  304:              long_title => 'Standard Deviation of Sequence Scores',
  305:              selectable => 'yes',
  306:              defaultselected => 'no',
  307:            },
  308:            { name   => 'scoremax',
  309:              title  => 'Score Max',
  310:              align  => 'right',
  311:              color  => '#FFFFE6',
  312:              format => '%4.2f',
  313:              sortable  => 'no',
  314:              graphable => 'no',
  315:              long_title => 'Maximum Sequence Score',
  316:              selectable => 'yes',
  317:              defaultselected => 'no',
  318:            },
  319:            { name   => 'scoremin',
  320:              title  => 'Score Min',
  321:              align  => 'right',
  322:              color  => '#FFFFE6',
  323:              format => '%4.2f',
  324:              sortable  => 'no',
  325:              graphable => 'no',
  326:              long_title => 'Minumum Sequence Score',
  327:              selectable => 'yes',
  328:              defaultselected => 'no',
  329:            },
  330:            { name   => 'scorecount',
  331:              title  => 'Score N',
  332:              align  => 'right',
  333:              color  => '#FFFFE6',
  334:              format => '%4d',
  335:              sortable  => 'no',
  336:              graphable => 'no',
  337:              long_title => 'Number of Students in score computations',
  338:              selectable => 'yes',
  339:              defaultselected => 'no',
  340:            },
  341:            { name   => 'countmean',
  342:              title  => 'Count Mean',
  343:              align  => 'right',
  344:              color  => '#FFFFFF',
  345:              format => '%4.2f',
  346:              sortable  => 'no',
  347:              graphable => 'no',
  348:              long_title => 'Mean Sequence Score',
  349:              selectable => 'yes',
  350:              defaultselected => 'no',
  351:            },
  352:            { name   => 'countstd',
  353:              title  => 'Count STD',
  354:              align  => 'right',
  355:              color  => '#FFFFFF',
  356:              format => '%4.2f',
  357:              sortable  => 'no',
  358:              graphable => 'no',
  359:              long_title => 'Standard Deviation of Sequence Scores',
  360:              selectable => 'yes',
  361:              defaultselected => 'no',
  362:            },
  363:            { name   => 'countmax',
  364:              title  => 'Count Max',
  365:              align  => 'right',
  366:              color  => '#FFFFFF',
  367:              format => '%4.2f',
  368:              sortable  => 'no',
  369:              graphable => 'no',
  370:              long_title => 'Maximum Number of Correct Problems',
  371:              selectable => 'yes',
  372:              defaultselected => 'no',
  373:            },
  374:            { name   => 'countmin',
  375:              title  => 'Count Min',
  376:              align  => 'right',
  377:              color  => '#FFFFFF',
  378:              format => '%4.2f',
  379:              sortable  => 'no',
  380:              graphable => 'no',
  381:              long_title => 'Minumum Number of Correct Problems',
  382:              selectable => 'yes',
  383:              defaultselected => 'no',
  384:            },
  385:            { name   => 'count',
  386:              title  => 'Count N',
  387:              align  => 'right',
  388:              color  => '#FFFFFF',
  389:              format => '%4d',
  390:              sortable  => 'no',
  391:              graphable => 'no',
  392:              long_title => 'Number of Students in score computations',
  393:              selectable => 'yes',
  394:              defaultselected => 'no',
  395:            },
  396:            { name   => 'KR-21',
  397:              title  => 'KR-21',
  398:              align  => 'right',
  399:              color  => '#FFAAAA',
  400:              format => '%4.2f',
  401:              sortable  => 'no',
  402:              graphable => 'no',
  403:              long_title => 'KR-21 reliability statistic',
  404:              selectable => 'yes',
  405:              defaultselected => 'no',
  406:            },           
  407: );
  408: 
  409: my %SelectedFields;
  410: 
  411: sub parse_field_selection {
  412:     #
  413:     # Pull out the defaults
  414:     if (! defined($ENV{'form.fieldselections'})) {
  415:         $ENV{'form.fieldselections'} = [];
  416:         foreach my $field (@Fields) {
  417:             next if ($field->{'selectable'} ne 'yes');
  418:             if ($field->{'defaultselected'} eq 'yes') {
  419:                 push(@{$ENV{'form.fieldselections'}},$field->{'name'});
  420:             }
  421:         }
  422:     }
  423:     #
  424:     # Make sure the data we are plotting is there
  425:     my %NeededFields;
  426:     if (exists($ENV{'form.plot'}) && $ENV{'form.plot'} ne '' &&
  427:         $ENV{'form.plot'} ne 'none') {
  428:         if ($ENV{'form.plot'} eq 'degrees') {
  429:             $NeededFields{'deg_of_diff'}++;
  430:             $NeededFields{'deg_of_disc'}++;
  431:         } elsif ($ENV{'form.plot'} eq 'tries statistics') {
  432:             $NeededFields{'mean_tries'}++;
  433:             $NeededFields{'std_tries'}++;
  434:             $NeededFields{'problem_num'}++;
  435:         } else {
  436:             $NeededFields{$ENV{'form.plot'}}++;
  437:         }
  438:     }
  439:     #
  440:     # This should not happen, but in case it does...
  441:     if (ref($ENV{'form.fieldselections'}) ne 'ARRAY') {
  442:         $ENV{'form.fieldselections'} = [$ENV{'form.fieldselections'}];
  443:     }
  444:     #
  445:     # Set the field data and the selected fields (for easier checking)
  446:     undef(%SelectedFields);
  447:     foreach my $field (@Fields) {
  448:         if ($field->{'selectable'} ne 'yes') {
  449:             $field->{'selected'} = 'yes';
  450:         } else {
  451:             $field->{'selected'} = 'no';
  452:         }
  453:         if (exists($NeededFields{$field->{'name'}})) {
  454:             $field->{'selected'} = 'yes';
  455:             $SelectedFields{$field->{'name'}}++;
  456:         }
  457:         foreach my $selection (@{$ENV{'form.fieldselections'}}) {
  458:             if ($selection eq $field->{'name'} || $selection eq 'all') {
  459:                 $field->{'selected'} = 'yes';
  460:                 $SelectedFields{$field->{'name'}}++;
  461:             }
  462:         }
  463:     }
  464:     #
  465:     # Always show all the sequence statistics (for now)
  466:     foreach my $field (@SeqFields) {
  467:         $field->{'selected'} = 'yes';
  468:     }
  469:     return;
  470: }
  471: 
  472: sub field_selection_input {
  473:     my $Str = '<select name="fieldselections" multiple size="5">'."\n";
  474:     $Str .= '<option value="all">all</option>'."\n";
  475:     foreach my $field (@Fields) {
  476:         next if ($field->{'selectable'} ne 'yes');
  477:         $Str .= '    <option value="'.$field->{'name'}.'" ';
  478:         if ($field->{'selected'} eq 'yes') {
  479:             $Str .= 'selected ';
  480:         }
  481:         $Str .= '>'.$field->{'title'}.'</option>'."\n";
  482:     }
  483:     $Str .= "</select>\n";
  484: }
  485: 
  486: ###############################################
  487: ###############################################
  488: 
  489: =pod 
  490: 
  491: =item &CreateInterface()
  492: 
  493: Create the main intereface for the statistics page.  Allows the user to
  494: select sections, maps, and output.
  495: 
  496: =cut
  497: 
  498: ###############################################
  499: ###############################################
  500: sub CreateInterface {
  501:     #
  502:     &parse_field_selection();
  503:     #
  504:     my $Str = '';
  505:     $Str .= &Apache::lonhtmlcommon::breadcrumbs
  506:         (undef,'Overall Problem Statistics','Statistics_Overall_Key');
  507:     $Str .= '<table cellspacing="5">'."\n";
  508:     $Str .= '<tr>';
  509:     $Str .= '<td align="center"><b>'.&mt('Sections').'</b></td>';
  510:     $Str .= '<td align="center"><b>'.&mt('Enrollment Status').'</b></td>';
  511:     $Str .= '<td align="center"><b>'.&mt('Sequences and Folders').'</b></td>';
  512:     $Str .= '<td align="center"><b>'.&mt('Statistics').'</b></td>';
  513:     $Str .= '<td rowspan="2">'.
  514:         &Apache::lonstathelpers::limit_by_time_form().'</td>';
  515:     $Str .= '</tr>'."\n";
  516:     #
  517:     $Str .= '<tr><td align="center">'."\n";
  518:     $Str .= &Apache::lonstatistics::SectionSelect('Section','multiple',5);
  519:     $Str .= '</td><td align="center">';
  520:     $Str .= &Apache::lonhtmlcommon::StatusOptions(undef,undef,5);
  521:     $Str .= '</td><td align="center">';
  522:     #
  523:     my $only_seq_with_assessments = sub { 
  524:         my $s=shift;
  525:         if ($s->{'num_assess'} < 1) { 
  526:             return 0;
  527:         } else { 
  528:             return 1;
  529:         }
  530:     };
  531:     $Str .= &Apache::lonstatistics::MapSelect('Maps','multiple,all',5,
  532:                                               $only_seq_with_assessments);
  533:     $Str .= '</td><td>'.&field_selection_input();
  534:     $Str .= '</td></tr>'."\n";
  535:     $Str .= '</table>'."\n";
  536:     $Str .= '<input type="submit" name="GenerateStatistics" value="'.
  537:         &mt('Generate Statistics').'" />';
  538:     $Str .= '&nbsp;'x5;
  539:     $Str .= 'Plot '.&plot_dropdown().('&nbsp;'x10);
  540:     $Str .= '<input type="submit" name="ClearCache" value="'.
  541:         &mt('Clear Caches').'" />';
  542:     $Str .= '&nbsp;'x5;
  543:     $Str .= '<input type="submit" name="UpdateCache" value="'.
  544:         &mt('Update Student Data').'" />';
  545:     $Str .= '&nbsp;'x5;
  546:     $Str .= '<input type="submit" name="Excel" value="'.
  547:         &mt('Produce Excel Output').'" />';
  548:     $Str .= '&nbsp;'x5;
  549:     return $Str;
  550: }
  551: 
  552: ###############################################
  553: ###############################################
  554: 
  555: =pod 
  556: 
  557: =item &BuildProblemStatisticsPage()
  558: 
  559: Main interface to problem statistics.
  560: 
  561: =cut
  562: 
  563: ###############################################
  564: ###############################################
  565: sub BuildProblemStatisticsPage {
  566:     my ($r,$c)=@_;
  567:     #
  568:     my %Saveable_Parameters = ('Status' => 'scalar',
  569:                                'statsoutputmode' => 'scalar',
  570:                                'Section' => 'array',
  571:                                'StudentData' => 'array',
  572:                                'Maps' => 'array',
  573:                                'fieldselections'=> 'array');
  574:     &Apache::loncommon::store_course_settings('statistics',
  575:                                               \%Saveable_Parameters);
  576:     &Apache::loncommon::restore_course_settings('statistics',
  577:                                                 \%Saveable_Parameters);
  578:     #
  579:     &Apache::lonstatistics::PrepareClasslist();
  580:     #
  581:     # Clear the package variables
  582:     undef(@StatsArray);
  583:     undef(%SeqStat);
  584:     #
  585:     # Finally let the user know we are here
  586:     my $interface = &CreateInterface();
  587:     $r->print($interface);
  588:     $r->print('<input type="hidden" name="sortby" value="'.$ENV{'form.sortby'}.
  589:               '" />');
  590:     #
  591:     if (! exists($ENV{'form.statsfirstcall'})) {
  592:         $r->print('<input type="hidden" name="statsfirstcall" value="yes" />');
  593:         $r->print('<h3>'.
  594:                   &mt('Press "Generate Statistics" when you are ready.').
  595:                   '</h3><p>'.
  596:                   &mt('It may take some time to update the student data '.
  597:                       'for the first analysis.  Future analysis this session '.
  598:                       ' will not have this delay.').
  599:                   '</p>');
  600:         return;
  601:     } elsif ($ENV{'form.statsfirstcall'} eq 'yes' || 
  602:              exists($ENV{'form.UpdateCache'}) ||
  603:              exists($ENV{'form.ClearCache'}) ) {
  604:         $r->print('<input type="hidden" name="statsfirstcall" value="no" />');
  605:         &Apache::lonstatistics::Gather_Student_Data($r);
  606:     } else {
  607:         $r->print('<input type="hidden" name="statsfirstcall" value="no" />');
  608:     }
  609:     $r->rflush();
  610:     #
  611:     # This probably does not need to be done each time we are called, but
  612:     # it does not slow things down noticably.
  613:     &Apache::loncoursedata::populate_weight_table();
  614:     #
  615:     if (exists($ENV{'form.Excel'})) {
  616:         &Excel_output($r);
  617:     } else {
  618:         my $count = 0;
  619:         foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
  620:             $count += $seq->{'num_assess_parts'};
  621:         }
  622:         if ($count > 10) {
  623:             $r->print('<h2>'.
  624:                       &mt('Compiling statistics for [_1] problems',$count).
  625:                       '</h2>');
  626:             if ($count > 30) {
  627:                 $r->print('<h3>'.&mt('This will take some time.').'</h3>');
  628:             }
  629:             $r->rflush();
  630:         }
  631:         #
  632:         my $sortby = $ENV{'form.sortby'};
  633:         $sortby = 'container' if (! defined($sortby) || $sortby =~ /^\s*$/);
  634:         my $plot = $ENV{'form.plot'};
  635:         if ($plot eq '' || $plot eq 'none') {
  636:             undef($plot);
  637:         }
  638:         if ($sortby eq 'container' && ! defined($plot)) {
  639:             &output_sequence_statistics($r);
  640:             &output_html_by_sequence($r);
  641:         } else {
  642:             if (defined($plot)) {
  643:                 &make_plot($r,$plot);
  644:             }
  645:             &output_html_stats($r);
  646:             &output_sequence_statistics($r);
  647:         }
  648:     }
  649:     return;
  650: }
  651: 
  652: sub output_sequence_statistics {
  653:     my ($r) = @_;
  654:     my $c=$r->connection();
  655:     $r->print('<h2>'.&mt('Sequence Statistics').'</h2>');
  656:     $r->print('<table border="0"><tr><td bgcolor="#777777">'."\n".
  657:               '<table border="0" cellpadding="3">'."\n".
  658:               '<tr bgcolor="#FFFFE6">');
  659:     $r->print(&sequence_html_header());
  660:     foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
  661:         last if ($c->aborted);
  662:         next if ($seq->{'num_assess'} < 1);
  663:         &compute_sequence_statistics($seq);
  664:         $r->print(&sequence_html_output($seq));
  665:     }
  666:     $r->print('</table>');
  667:     $r->print('</table>');
  668:     $r->rflush();
  669:     return;
  670: }
  671: 
  672: 
  673: ##########################################################
  674: ##########################################################
  675: ##
  676: ## HTML output routines
  677: ##
  678: ##########################################################
  679: ##########################################################
  680: sub output_html_by_sequence {
  681:     my ($r) = @_;
  682:     my $c = $r->connection();
  683:     $r->print(&html_preamble());
  684:     #
  685:     foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
  686:         last if ($c->aborted);
  687:         next if ($seq->{'num_assess'} < 1);
  688:         $r->print("<h3>".$seq->{'title'}."</h3>".
  689:                   '<table border="0"><tr><td bgcolor="#777777">'."\n".
  690:                   '<table border="0" cellpadding="3">'."\n".
  691:                   '<tr bgcolor="#FFFFE6">'.
  692:                   &statistics_table_header('no container')."</tr>\n");
  693:         my @Data = &compute_statistics_on_sequence($seq);
  694:         foreach my $data (@Data) {
  695:             $r->print('<tr>'.&statistics_html_table_data($data,
  696:                                                          'no container').
  697:                       "</tr>\n");
  698:         }
  699:         $r->print('</table>'."\n".'</table>'."\n");
  700:         $r->rflush();
  701:     }
  702:     return;
  703: }
  704: 
  705: sub output_html_stats {
  706:     my ($r)=@_;
  707:     &compute_all_statistics($r);
  708:     $r->print(&html_preamble());
  709:     &sort_data($ENV{'form.sortby'});
  710:     #
  711:     my $count=0;
  712:     foreach my $data (@StatsArray) {
  713:         if ($count++ % 50 == 0) {
  714:             $r->print("</table>\n</table>\n");
  715:             $r->print('<table border="0"><tr><td bgcolor="#777777">'."\n".
  716:                       '<table border="0" cellpadding="3">'."\n".
  717:                       '<tr bgcolor="#FFFFE6">'.
  718:                       '<tr bgcolor="#FFFFE6">'.
  719:                       &statistics_table_header().
  720:                       "</tr>\n");
  721:         }
  722:         $r->print('<tr>'.&statistics_html_table_data($data)."</tr>\n");
  723:     }
  724:     $r->print("</table>\n</table>\n");
  725:     return;
  726: }
  727: 
  728: sub html_preamble {
  729:     my $Str='';
  730:     $Str .= "<h2>".
  731:         $ENV{'course.'.$ENV{'request.course.id'}.'.description'}.
  732:         "</h2>\n";
  733:     my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
  734:     if (defined($starttime) || defined($endtime)) {
  735:         # Inform the user what the time limits on the data are.
  736:         $Str .= '<h3>'.&mt('Statistics on submissions from [_1] to [_2]',
  737:                            &Apache::lonlocal::locallocaltime($starttime),
  738:                            &Apache::lonlocal::locallocaltime($endtime)
  739:                            ).'</h3>';
  740:     }
  741:     $Str .= "<h3>".&mt('Compiled on [_1]',
  742:                        &Apache::lonlocal::locallocaltime(time))."</h3>";
  743:     return $Str;
  744: }
  745: 
  746: 
  747: ###############################################
  748: ###############################################
  749: ##
  750: ## Misc HTML output routines
  751: ##
  752: ###############################################
  753: ###############################################
  754: sub statistics_html_table_data {
  755:     my ($data,$options) = @_;
  756:     my $row = '';
  757:     foreach my $field (@Fields) {
  758:         next if ($options =~ /no $field->{'name'}/);
  759:         next if ($field->{'selected'} ne 'yes');
  760:         $row .= '<td bgcolor="'.$field->{'color'}.'"';
  761:         if (exists($field->{'align'})) {
  762:             $row .= ' align="'.$field->{'align'}.'"';
  763:             }
  764:         $row .= '>';
  765:         if (exists($field->{'special'}) && $field->{'special'} eq 'link') {
  766:             $row .= '<a href="'.$data->{$field->{'name'}.'.link'}.'">';
  767:         }
  768:         if (exists($field->{'format'})) {
  769:             $row .= sprintf($field->{'format'},$data->{$field->{'name'}});
  770:         } else {
  771:             $row .= $data->{$field->{'name'}};
  772:         }
  773:         if (exists($field->{'special'}) && $field->{'special'} eq 'link') {
  774:             $row.= '</a>';
  775:         }
  776:         $row .= '</td>';
  777:     }
  778:     return $row;
  779: }
  780: 
  781: sub statistics_table_header {
  782:     my ($options) = @_;
  783:     my $header_row;
  784:     foreach my $field (@Fields) {
  785:         next if ($options =~ /no $field->{'name'}/);
  786:         next if ($field->{'selected'} ne 'yes');
  787:         $header_row .= '<th>';
  788:         if (exists($field->{'sortable'}) && $field->{'sortable'} eq 'yes') {
  789:             $header_row .= '<a href="javascript:'.
  790:                 'document.Statistics.sortby.value='."'".$field->{'name'}."'".
  791:                     ';document.Statistics.submit();">';
  792:         }
  793:         $header_row .= &mt($field->{'title'});
  794:         if ($options =~ /sortable/) {
  795:             $header_row.= '</a>';
  796:         }
  797:         if ($options !~ /no plots/        && 
  798:             exists($field->{'graphable'}) && 
  799:             $field->{'graphable'} eq 'yes') {
  800:             $header_row.=' (';
  801:             $header_row .= '<a href="javascript:'.
  802:                 "document.Statistics.plot.value='$field->{'name'}'".
  803:                     ';document.Statistics.submit();">';
  804:             $header_row .= &mt('plot').'</a>)';
  805:         }
  806:         $header_row .= '</th>';
  807:     }
  808:     return $header_row;
  809: }
  810: 
  811: sub sequence_html_header {
  812:     my $Str .= '<tr>';
  813:     foreach my $field (@SeqFields) {
  814: #        next if ($field->{'selected'} ne 'yes');
  815:         $Str .= '<th bgcolor="'.$field->{'color'}.'"';
  816:         $Str .= '>'.$field->{'title'}.'</th>';
  817:     }
  818:     $Str .= '</tr>';
  819:     return $Str;
  820: }
  821: 
  822: 
  823: sub sequence_html_output {
  824:     my ($seq) = @_;
  825:     my $data = $SeqStat{$seq->{'symb'}};
  826:     my $row = '<tr>';
  827:     foreach my $field (@SeqFields) {
  828:         next if ($field->{'selected'} ne 'yes');
  829:         $row .= '<td bgcolor="'.$field->{'color'}.'"';
  830:         if (exists($field->{'align'})) {
  831:             $row .= ' align="'.$field->{'align'}.'"';
  832:         }
  833:         $row .= '>';
  834:         if (exists($field->{'format'})) {
  835:             $row .= sprintf($field->{'format'},$data->{$field->{'name'}});
  836:         } else {
  837:             $row .= $data->{$field->{'name'}};
  838:         }
  839:         $row .= '</td>';
  840:     }
  841:     $row .= '</tr>'."\n";
  842:     return $row;
  843: }
  844: 
  845: ####################################################
  846: ####################################################
  847: ##
  848: ##    Plotting Routines
  849: ##
  850: ####################################################
  851: ####################################################
  852: sub make_plot {
  853:     my ($r,$plot) = @_;
  854:     &compute_all_statistics($r);
  855:     &sort_data($ENV{'form.sortby'});
  856:     if ($plot eq 'degrees') {
  857:         &degrees_plot($r);
  858:     } elsif ($plot eq 'tries statistics') {
  859:         &tries_data_plot($r);
  860:     } else {
  861:         &make_single_stat_plot($r,$plot);
  862:     }
  863:     return;
  864: }
  865: 
  866: sub make_single_stat_plot {
  867:     my ($r,$datafield) = @_;
  868:     #
  869:     my $title; my $yaxis;
  870:     foreach my $field (@Fields) {
  871:         next if ($field->{'name'} ne $datafield);
  872:         $title = $field->{'long_title'};
  873:         $yaxis = $field->{'title'};
  874:         last;
  875:     }
  876:     if ($title eq '' || $yaxis eq '') {
  877:         # datafield is something we do not know enough about to plot
  878:         $r->print('<h3>'.
  879:                   &mt('Unable to plot the requested statistic.').
  880:                   '</h3>');
  881:         return;
  882:     }
  883:     #
  884:     # Build up the data sets to plot
  885:     my @Labels; 
  886:     my @Data;
  887:     my $max = 1;
  888:     foreach my $data (@StatsArray) {
  889:         push(@Labels,$data->{'problem_num'});
  890:         push(@Data,$data->{$datafield});
  891:         if ($data->{$datafield}>$max) {
  892:             $max = $data->{$datafield};
  893:         }
  894:     }
  895:     foreach (1,2,3,4,5,10,15,20,25,40,50,75,100,150,200,250,300,500,600,750,
  896:              1000,1500,2000,2500,3000,3500,4000,5000,7500,10000,15000,20000) {
  897:         if ($max <= $_) {
  898:             $max = $_;
  899:             last;
  900:         }
  901:     }
  902:     if ($max > 20000) {
  903:         $max = 10000*(int($max/10000)+1);
  904:     }
  905:     #
  906:     $r->print("<p>".&Apache::loncommon::DrawBarGraph($title,
  907:                                                      'Problem Number',
  908:                                                      $yaxis,
  909:                                                      $max,
  910:                                                      undef, # colors
  911:                                                      \@Labels,
  912:                                                      \@Data)."</p>\n");
  913:     return;
  914: }
  915: 
  916: sub degrees_plot {
  917:     my ($r)=@_;
  918:     my $count = scalar(@StatsArray);
  919:     my $width = 50 + 10*$count;
  920:     $width = 300 if ($width < 300);
  921:     my $height = 300;
  922:     my $plot = '';
  923:     my $ymax = 0;
  924:     my $ymin = 0;
  925:     my @Disc; my @Diff; my @Labels;    
  926:     foreach my $data (@StatsArray) {
  927:         push(@Labels,$data->{'problem_num'});
  928:         my $disc = $data->{'deg_of_disc'};
  929:         my $diff = $data->{'deg_of_diff'};
  930:         push(@Disc,$disc);
  931:         push(@Diff,$diff);
  932:         #
  933:         $ymin = $disc if ($ymin > $disc);
  934:         $ymin = $diff if ($ymin > $diff);
  935:         $ymax = $disc if ($ymax < $disc);
  936:         $ymax = $diff if ($ymax < $diff);
  937:     }
  938:     #
  939:     # Make sure we show relevant information.
  940:     if ($ymin < 0) {
  941:         if (abs($ymin) < 0.05) {
  942:             $ymin = 0;
  943:         } else {
  944:             $ymin = -1;
  945:         }
  946:     }
  947:     if ($ymax > 0) {
  948:         if (abs($ymax) < 0.05) {
  949:             $ymax = 0;
  950:         } else {
  951:             $ymax = 1;
  952:         }
  953:     }
  954:     #
  955:     my $xmax = $Labels[-1];
  956:     if ($xmax > 50) {
  957:         if ($xmax % 10 != 0) {
  958:             $xmax = 10 * (int($xmax/10)+1);
  959:         }
  960:     } else {
  961:         if ($xmax % 5 != 0) {
  962:             $xmax = 5 * (int($xmax/5)+1);
  963:         }
  964:     }
  965:     #
  966:     my $discdata .= '<data>'.join(',',@Labels).'</data>'.$/.
  967:                     '<data>'.join(',',@Disc).'</data>'.$/;
  968:     #
  969:     my $diffdata .= '<data>'.join(',',@Labels).'</data>'.$/.
  970:                     '<data>'.join(',',@Diff).'</data>'.$/;
  971:     #
  972:     my $title = 'Degree of Discrimination\nand Degree of Difficulty';
  973:     if ($xmax > 50) {
  974:         $title = 'Degree of Discrimination and Degree of Difficulty';
  975:     }
  976:     #
  977:     $plot=<<"END";
  978: <gnuplot 
  979:     texfont="10"
  980:     fgcolor="x000000"
  981:     plottype="Cartesian"
  982:     font="large"
  983:     grid="on"
  984:     align="center"
  985:     border="on"
  986:     transparent="on"
  987:     alttag="Degree of Discrimination and Degree of Difficulty Plot"
  988:     samples="100"
  989:     bgcolor="xffffff"
  990:     height="$height"
  991:     width="$width">
  992:     <key 
  993:         pos="top right"
  994:         title=""
  995:         box="off" />
  996:     <title>$title</title>
  997:     <axis xmin="0" ymin="$ymin" xmax="$xmax" ymax="$ymax" color="x000000" />
  998:     <xlabel>Problem Number</xlabel>
  999:     <curve 
 1000:         linestyle="linespoints" 
 1001:         name="DoDisc" 
 1002:         pointtype="0" 
 1003:         color="x000000">
 1004:         $discdata
 1005:     </curve>
 1006:     <curve 
 1007:         linestyle="linespoints" 
 1008:         name="DoDiff" 
 1009:         pointtype="0" 
 1010:         color="xFF0000">
 1011:         $diffdata
 1012:     </curve>
 1013: </gnuplot>
 1014: END
 1015:     my $plotresult = 
 1016:         '<p>'.&Apache::lonxml::xmlparse($r,'web',$plot).'</p>'.$/;
 1017:     $r->print($plotresult);
 1018:     return;
 1019: }
 1020: 
 1021: sub tries_data_plot {
 1022:     my ($r)=@_;
 1023:     my $count = scalar(@StatsArray);
 1024:     my $width = 50 + 10*$count;
 1025:     $width = 300 if ($width < 300);
 1026:     my $height = 300;
 1027:     my $plot = '';
 1028:     my @STD;  my @Mean; my @Max; my @Min;
 1029:     my @Labels;
 1030:     my $ymax = 5;
 1031:     foreach my $data (@StatsArray) {
 1032:         my $max = $data->{'mean_tries'} + $data->{'std_tries'};
 1033:         $ymax = $max if ($ymax < $max);
 1034:         $ymax = $max if ($ymax < $max);
 1035:         push(@Labels,$data->{'problem_num'});
 1036:         push(@STD,$data->{'std_tries'});
 1037:         push(@Mean,$data->{'mean_tries'});
 1038:     }
 1039:     #
 1040:     # Make sure we show relevant information.
 1041:     my $xmax = $Labels[-1];
 1042:     if ($xmax > 50) {
 1043:         if ($xmax % 10 != 0) {
 1044:             $xmax = 10 * (int($xmax/10)+1);
 1045:         }
 1046:     } else {
 1047:         if ($xmax % 5 != 0) {
 1048:             $xmax = 5 * (int($xmax/5)+1);
 1049:         }
 1050:     }
 1051:     $ymax = int($ymax)+1+2;
 1052:     #
 1053:     my $std_data .= '<data>'.join(',',@Labels).'</data>'.$/.
 1054:                     '<data>'.join(',',@Mean).'</data>'.$/;
 1055:     #
 1056:     my $std_error_data .= '<data>'.join(',',@Labels).'</data>'.$/.
 1057:                           '<data>'.join(',',@Mean).'</data>'.$/.
 1058:                           '<data>'.join(',',@STD).'</data>'.$/;
 1059:     #
 1060:     my $title = 'Mean and S.D. of Tries';
 1061:     if ($xmax > 25) {
 1062:         $title = 'Mean and Standard Deviation of Tries';
 1063:     }
 1064:     #
 1065:     $plot=<<"END";
 1066: <gnuplot 
 1067:     texfont="10"
 1068:     fgcolor="x000000"
 1069:     plottype="Cartesian"
 1070:     font="large"
 1071:     grid="on"
 1072:     align="center"
 1073:     border="on"
 1074:     transparent="on"
 1075:     alttag="Mean and S.D of Tries Plot"
 1076:     samples="100"
 1077:     bgcolor="xffffff"
 1078:     height="$height"
 1079:     width="$width">
 1080:     <title>$title</title>
 1081:     <axis xmin="0" ymin="0" xmax="$xmax" ymax="$ymax" color="x000000" />
 1082:     <xlabel>Problem Number</xlabel>
 1083:     <ylabel>Number of Tries</ylabel>
 1084:     <curve 
 1085:         linestyle="yerrorbars"
 1086:         name="S.D. Tries" 
 1087:         pointtype="1" 
 1088:         color="x666666">
 1089:         $std_error_data
 1090:     </curve>
 1091:     <curve 
 1092:         linestyle="points"
 1093:         name="Mean Tries" 
 1094:         pointtype="1" 
 1095:         color="xCC4444">
 1096:         $std_data
 1097:     </curve>
 1098: </gnuplot>
 1099: END
 1100:     my $plotresult = 
 1101:         '<p>'.&Apache::lonxml::xmlparse($r,'web',$plot).'</p>'.$/;
 1102:     $r->print($plotresult);
 1103:     return;
 1104: }
 1105: 
 1106: sub plot_dropdown {
 1107:     my $current = '';
 1108:     #
 1109:     if (defined($ENV{'form.plot'})) {
 1110:         $current = $ENV{'form.plot'};
 1111:     }
 1112:     #
 1113:     my @Additional_Plots = (
 1114:                             { graphable=>'yes',
 1115:                               name => 'degrees',
 1116:                               title => 'Difficulty Indexes' },
 1117:                             { graphable=>'yes',
 1118:                               name => 'tries statistics',
 1119:                               title => 'Tries Statistics' });
 1120:     #
 1121:     my $Str= "\n".'<select name="plot" size="1">';
 1122:     $Str .= '<option name="none"></option>'."\n";
 1123:     $Str .= '<option name="none2">none</option>'."\n";
 1124:     foreach my $field (@Additional_Plots,@Fields) {
 1125:         if (! exists($field->{'graphable'}) ||
 1126:             $field->{'graphable'} ne 'yes') {
 1127:             next;
 1128:         }
 1129:         $Str .= '<option value="'.$field->{'name'}.'"';
 1130:         if ($field->{'name'} eq $current) {
 1131:             $Str .= ' selected ';
 1132:         }
 1133:         $Str.= '>'.&mt($field->{'title'}).'</option>'."\n";
 1134:     }
 1135:     $Str .= '</select>'."\n";
 1136:     return $Str;
 1137: }
 1138: 
 1139: ###############################################
 1140: ###############################################
 1141: ##
 1142: ## Excel output routines
 1143: ##
 1144: ###############################################
 1145: ###############################################
 1146: sub Excel_output {
 1147:     my ($r) = @_;
 1148:     $r->print('<h2>'.&mt('Preparing Excel Spreadsheet').'</h2>');
 1149:     ##
 1150:     ## Compute the statistics
 1151:     &compute_all_statistics($r);
 1152:     my $c = $r->connection;
 1153:     return if ($c->aborted());
 1154:     ##
 1155:     ## Create the excel workbook
 1156:     my $filename = '/prtspool/'.
 1157:         $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.
 1158:         time.'_'.rand(1000000000).'.xls';
 1159:     my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
 1160:     #
 1161:     # Create sheet
 1162:     my $excel_workbook = Spreadsheet::WriteExcel->new('/home/httpd'.$filename);
 1163:     #
 1164:     # Check for errors
 1165:     if (! defined($excel_workbook)) {
 1166:         $r->log_error("Error creating excel spreadsheet $filename: $!");
 1167:         $r->print(&mt("Problems creating new Excel file.  ".
 1168:                   "This error has been logged.  ".
 1169:                   "Please alert your LON-CAPA administrator."));
 1170:         return 0;
 1171:     }
 1172:     #
 1173:     # The excel spreadsheet stores temporary data in files, then put them
 1174:     # together.  If needed we should be able to disable this (memory only).
 1175:     # The temporary directory must be specified before calling 'addworksheet'.
 1176:     # File::Temp is used to determine the temporary directory.
 1177:     $excel_workbook->set_tempdir($Apache::lonnet::tmpdir);
 1178:     #
 1179:     # Add a worksheet
 1180:     my $sheetname = $ENV{'course.'.$ENV{'request.course.id'}.'.description'};
 1181:     if (length($sheetname) > 31) {
 1182:         $sheetname = substr($sheetname,0,31);
 1183:     }
 1184:     my $excel_sheet = $excel_workbook->addworksheet(
 1185:         &Apache::loncommon::clean_excel_name($sheetname));
 1186:     #
 1187:     my $format = &Apache::loncommon::define_excel_formats($excel_workbook);
 1188:     ##
 1189:     ## Begin creating excel sheet
 1190:     ##
 1191:     my ($rows_output,$cols_output) = (0,0);
 1192:     #
 1193:     # Put the course description in the header
 1194:     $excel_sheet->write($rows_output,$cols_output++,
 1195:                    $ENV{'course.'.$ENV{'request.course.id'}.'.description'},
 1196:                         $format->{'h1'});
 1197:     $cols_output += 3;
 1198:     #
 1199:     # Put a description of the sections listed
 1200:     my $sectionstring = '';
 1201:     $excel_sheet->write($rows_output,$cols_output++,
 1202:                         &Apache::lonstathelpers::sections_description
 1203:                             (@Apache::lonstatistics::SelectedSections),
 1204:                         $format->{'h3'});
 1205:     $cols_output += scalar(@Apache::lonstatistics::SelectedSections);
 1206:     #
 1207:     # Time restrictions
 1208:     my $time_string;
 1209:     if (defined($starttime)) {
 1210:         # call localtime but not lonlocal:locallocaltime because excel probably
 1211:         # cannot handle localized text.  Probably.
 1212:         $time_string .= 'Data collected from '.localtime($time_string);
 1213:         if (defined($endtime)) {
 1214:             $time_string .= ' to '.localtime($endtime);
 1215:         }
 1216:         $time_string .= '.';
 1217:     } elsif (defined($endtime)) {
 1218:         # See note above about lonlocal:locallocaltime
 1219:         $time_string .= 'Data collected before '.localtime($endtime).'.';
 1220:     }
 1221:     if (defined($time_string)) {
 1222:         $excel_sheet->write($rows_output,$cols_output++,$time_string);
 1223:         $cols_output+= 5;
 1224:     }
 1225:     #
 1226:     # Put the date in there too
 1227:     $excel_sheet->write($rows_output,$cols_output++,
 1228:                         'Compiled on '.localtime(time));
 1229:     #
 1230:     $rows_output++; 
 1231:     $cols_output=0;
 1232:     ##
 1233:     ## Sequence Statistics
 1234:     ## 
 1235:     &write_headers($excel_sheet,$format,\$rows_output,\$cols_output,
 1236:                    \@SeqFields);
 1237:     foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
 1238:         next if ($seq->{'num_assess'} < 1);
 1239:         my $data = $SeqStat{$seq->{'symb'}};
 1240:         $cols_output=0;
 1241:         foreach my $field (@SeqFields) {
 1242:             next if ($field->{'selected'} ne 'yes');
 1243:             $excel_sheet->write($rows_output,$cols_output++,
 1244:                                 $data->{$field->{'name'}});
 1245:         }
 1246:         $rows_output++;
 1247:         $cols_output=0;
 1248:     }
 1249:     ##
 1250:     ## Resource Statistics
 1251:     ##
 1252:     $rows_output++;
 1253:     $cols_output=0;
 1254:     &write_headers($excel_sheet,$format,\$rows_output,\$cols_output,
 1255:                    \@Fields);
 1256:     #
 1257:     foreach my $data (@StatsArray) {
 1258:         $cols_output=0;
 1259:         foreach my $field (@Fields) {
 1260:             next if ($field->{'selected'} ne 'yes');
 1261:             next if ($field->{'name'} eq 'problem_num');
 1262:             $excel_sheet->write($rows_output,$cols_output++,
 1263:                                 $data->{$field->{'name'}});
 1264:         }
 1265:         $rows_output++;
 1266:         $cols_output=0;
 1267:     }
 1268:     #
 1269:     $excel_workbook->close();
 1270:     #
 1271:     # Tell the user where to get their excel file
 1272:     $r->print('<br />'.
 1273:               '<a href="'.$filename.'">'.
 1274:               &mt('Your Excel Spreadsheet').'</a>'."\n");
 1275:     $r->rflush();
 1276:     return;
 1277: }
 1278: 
 1279: ##
 1280: ## &write_headers
 1281: ##
 1282: sub write_headers {
 1283:     my ($excel_sheet,$format,$rows_output,$cols_output,$Fields) = @_;
 1284:     ##
 1285:     ## First the long titles
 1286:     foreach my $field (@{$Fields}) {
 1287:         next if ($field->{'name'} eq 'problem_num');
 1288:         next if ($field->{'selected'} ne 'yes');
 1289:         if (exists($field->{'long_title'})) {
 1290:             $excel_sheet->write($$rows_output,${$cols_output},
 1291:                                 $field->{'long_title'},
 1292:                                 $format->{'bold'});
 1293:         } else {
 1294:             $excel_sheet->write($$rows_output,${$cols_output},'');
 1295:         }
 1296:         ${$cols_output}+= 1;
 1297:     }
 1298:     ${$cols_output} =0;
 1299:     ${$rows_output}+=1;
 1300:     ##
 1301:     ## Then the short titles
 1302:     foreach my $field (@{$Fields}) {
 1303:         next if ($field->{'selected'} ne 'yes');
 1304:         next if ($field->{'name'} eq 'problem_num');
 1305:         # Use english for excel as I am not sure how well excel handles 
 1306:         # other character sets....
 1307:         $excel_sheet->write($$rows_output,$$cols_output,
 1308:                             $field->{'title'},
 1309:                             $format->{'bold'});
 1310:         $$cols_output+=1;
 1311:     }
 1312:     ${$cols_output} =0;
 1313:     ${$rows_output}+=1;
 1314:     return;
 1315: }
 1316: 
 1317: ##################################################
 1318: ##################################################
 1319: ##
 1320: ## Statistics Gathering and Manipulation Routines
 1321: ##
 1322: ##################################################
 1323: ##################################################
 1324: sub compute_statistics_on_sequence {
 1325:     my ($seq) = @_;
 1326:     my @Data;
 1327:     foreach my $res (@{$seq->{'contents'}}) {
 1328:         next if ($res->{'type'} ne 'assessment');
 1329:         foreach my $part (@{$res->{'parts'}}) {
 1330:             #
 1331:             # This is where all the work happens
 1332:             my $data = &get_statistics($seq,$res,$part,scalar(@StatsArray)+1);
 1333:             push (@Data,$data);
 1334:             push (@StatsArray,$data);
 1335:         }
 1336:     }
 1337:     return @Data;
 1338: }
 1339: 
 1340: sub compute_all_statistics {
 1341:     my ($r) = @_;
 1342:     if (@StatsArray > 0) {
 1343:         # Assume we have already computed the statistics
 1344:         return;
 1345:     }
 1346:     my $c = $r->connection;
 1347:     foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
 1348:         last if ($c->aborted);
 1349:         next if ($seq->{'num_assess'} < 1);
 1350:         &compute_sequence_statistics($seq);
 1351:         &compute_statistics_on_sequence($seq);
 1352:     }
 1353: }
 1354: 
 1355: sub sort_data {
 1356:     my ($sortkey) = @_;
 1357:     return if (! @StatsArray);
 1358:     #
 1359:     # Sort the data
 1360:     my $sortby = undef;
 1361:     foreach my $field (@Fields) {
 1362:         if ($sortkey eq $field->{'name'}) {
 1363:             $sortby = $field->{'name'};
 1364:         }
 1365:     }
 1366:     if (! defined($sortby) || $sortby eq '' || $sortby eq 'problem_num') {
 1367:         $sortby = 'container';
 1368:     }
 1369:     if ($sortby ne 'container') {
 1370:         # $sortby is already defined, so we can charge ahead
 1371:         if ($sortby =~ /^(title|part)$/i) {
 1372:             # Alpha comparison
 1373:             @StatsArray = sort {
 1374:                 lc($a->{$sortby}) cmp lc($b->{$sortby}) ||
 1375:                 lc($a->{'title'}) cmp lc($b->{'title'}) ||
 1376:                 lc($a->{'part'}) cmp lc($b->{'part'});
 1377:             } @StatsArray;
 1378:         } else {
 1379:             # Numerical comparison
 1380:             @StatsArray = sort {
 1381:                 my $retvalue = 0;
 1382:                 if ($b->{$sortby} eq 'nan') {
 1383:                     if ($a->{$sortby} ne 'nan') {
 1384:                         $retvalue = -1;
 1385:                     } else {
 1386:                         $retvalue = 0;
 1387:                     }
 1388:                 }
 1389:                 if ($a->{$sortby} eq 'nan') {
 1390:                     if ($b->{$sortby} ne 'nan') {
 1391:                         $retvalue = 1;
 1392:                     }
 1393:                 }
 1394:                 if ($retvalue eq '0') {
 1395:                     $retvalue = $b->{$sortby} <=> $a->{$sortby}     ||
 1396:                             lc($a->{'title'}) <=> lc($b->{'title'}) ||
 1397:                             lc($a->{'part'})  <=> lc($b->{'part'});
 1398:                 }
 1399:                 $retvalue;
 1400:             } @StatsArray;
 1401:         }
 1402:     }
 1403:     #
 1404:     # Renumber the data set
 1405:     my $count;
 1406:     foreach my $data (@StatsArray) {
 1407:         $data->{'problem_num'} = ++$count;
 1408:     }
 1409:     return;
 1410: }
 1411: 
 1412: ########################################################
 1413: ########################################################
 1414: 
 1415: =pod
 1416: 
 1417: =item &get_statistics()
 1418: 
 1419: Wrapper routine from the call to loncoursedata::get_problem_statistics.  
 1420: Calls lonstathelpers::get_time_limits() to limit the data set by time
 1421: and &compute_discrimination_factor
 1422: 
 1423: Inputs: $sequence, $resource, $part, $problem_num
 1424: 
 1425: Returns: Hash reference with statistics data from 
 1426: loncoursedata::get_problem_statistics.
 1427: 
 1428: =cut
 1429: 
 1430: ########################################################
 1431: ########################################################
 1432: sub get_statistics {
 1433:     my ($sequence,$resource,$part,$problem_num) = @_;
 1434:     #
 1435:     my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
 1436:     my $symb = $resource->{'symb'};
 1437:     my $courseid = $ENV{'request.course.id'};
 1438:     #
 1439:     my $data = &Apache::loncoursedata::get_problem_statistics
 1440:                         (\@Apache::lonstatistics::SelectedSections,
 1441:                          $Apache::lonstatistics::enrollment_status,
 1442:                          $symb,$part,$courseid,$starttime,$endtime);
 1443:     $data->{'part'}        = $part;
 1444:     $data->{'problem_num'} = $problem_num;
 1445:     $data->{'container'}   = $sequence->{'title'};
 1446:     $data->{'title'}       = $resource->{'title'};
 1447:     $data->{'title.link'}  = $resource->{'src'}.'?symb='.
 1448:         &Apache::lonnet::escape($resource->{'symb'});
 1449:     #
 1450:     if ($SelectedFields{'deg_of_disc'}) {
 1451:         $data->{'deg_of_disc'} = 
 1452:             &compute_discrimination_factor($resource,$part,$sequence);
 1453:     }
 1454:     #
 1455:     # Store in metadata if computations were done for all students
 1456:     if ($Apache::lonstatistics::SelectedSections[0] eq 'all' &&
 1457:         $data->{'num_students'} > 1) {
 1458:         my %storestats;
 1459:         #
 1460:         my $urlres=(&Apache::lonnet::decode_symb($resource->{'symb'}))[2];
 1461:         my ($dom,$user) = $urlres=~/^(\w+)\/(\w+)/; 
 1462:         my $preamble = $courseid.'___'.$urlres.'___';
 1463:         #
 1464:         $storestats{$preamble.'timestamp'}  = time;
 1465:         $storestats{$preamble.'stdno'}      = $data->{'num_students'};
 1466:         $storestats{$preamble.'avetries'}   = $data->{'mean_tries'};
 1467:         $storestats{$preamble.'difficulty'} = $data->{'deg_of_diff'};
 1468:         if ($SelectedFields{'deg_of_disc'}) {
 1469:             $storestats{$preamble.'discrimination'} = $data->{'deg_of_disc'};
 1470:         }
 1471:         #
 1472:         &Apache::lonnet::put('nohist_resevaldata',\%storestats,$dom,$user);
 1473:     }
 1474:     return $data;
 1475: }
 1476: 
 1477: ###############################################
 1478: ###############################################
 1479: 
 1480: =pod
 1481: 
 1482: =item &compute_discrimination_factor()
 1483: 
 1484: Inputs: $Resource, $Sequence
 1485: 
 1486: Returns: integer between -1 and 1
 1487: 
 1488: =cut
 1489: 
 1490: ###############################################
 1491: ###############################################
 1492: sub compute_discrimination_factor {
 1493:     my ($resource,$part,$sequence) = @_;
 1494:     my @Resources;
 1495:     foreach my $res (@{$sequence->{'contents'}}) {
 1496:         next if ($res->{'symb'} eq $resource->{'symb'});
 1497:         push (@Resources,$res->{'symb'});
 1498:     }
 1499:     #
 1500:     # rank
 1501:     my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
 1502:     my $ranking = 
 1503:         &Apache::loncoursedata::rank_students_by_scores_on_resources
 1504:         (\@Resources,
 1505:          \@Apache::lonstatistics::SelectedSections,
 1506:          $Apache::lonstatistics::enrollment_status,undef,
 1507:          $starttime,$endtime);
 1508:     #
 1509:     # compute their percent scores on the problems in the sequence,
 1510:     my $number_to_grab = int(scalar(@{$ranking})/4);
 1511:     my $num_students = scalar(@{$ranking});
 1512:     my @BottomSet = map { $_->[&Apache::loncoursedata::RNK_student()]; 
 1513:                       } @{$ranking}[0..$number_to_grab];
 1514:     my @TopSet    = 
 1515:         map { 
 1516:             $_->[&Apache::loncoursedata::RNK_student()]; 
 1517:           } @{$ranking}[($num_students-$number_to_grab)..($num_students-1)];
 1518:     my ($bottom_sum,$bottom_max) = 
 1519:         &Apache::loncoursedata::get_sum_of_scores($resource,$part,\@BottomSet,
 1520:                                                   undef,$starttime,$endtime);
 1521:     my ($top_sum,$top_max) = 
 1522:         &Apache::loncoursedata::get_sum_of_scores($resource,$part,\@TopSet,
 1523:                                                   undef,$starttime,$endtime);
 1524:     my $deg_of_disc;
 1525:     if ($top_max == 0 || $bottom_max==0) {
 1526:         $deg_of_disc = 'nan';
 1527:     } else {
 1528:         $deg_of_disc = ($top_sum/$top_max) - ($bottom_sum/$bottom_max);
 1529:     }
 1530:     #&Apache::lonnet::logthis('    '.$top_sum.'/'.$top_max.
 1531:     #                         ' - '.$bottom_sum.'/'.$bottom_max);
 1532:     return $deg_of_disc;
 1533: }
 1534: 
 1535: ###############################################
 1536: ###############################################
 1537: ##
 1538: ## Compute KR-21
 1539: ##
 1540: ## To compute KR-21, you need the following information:
 1541: ##
 1542: ## K=the number of items in your test
 1543: ## M=the mean score on the test
 1544: ## s=the standard deviation of the scores on your test 
 1545: ##
 1546: ## then:
 1547: ## 
 1548: ## KR-21 rk= [K/(K-1)] * [1- (M*(K-M))/(K*s^2))]
 1549: ##
 1550: ###############################################
 1551: ###############################################
 1552: sub compute_sequence_statistics {
 1553:     my ($seq) = @_;
 1554:     my $symb = $seq->{'symb'};
 1555:     my @Resources;
 1556:     foreach my $res (@{$seq->{'contents'}}) {
 1557:         next if ($res->{'type'} ne 'assessment');
 1558:         push (@Resources,$res->{'symb'});
 1559:     }
 1560:     my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
 1561:     #
 1562:     # First compute statistics based on student scores
 1563:     my ($smin,$smax,$sMean,$sSTD,$scount,$sMAX) = 
 1564:         &Apache::loncoursedata::score_stats
 1565:                     (\@Apache::lonstatistics::SelectedSections,
 1566:                      $Apache::lonstatistics::enrollment_status,
 1567:                      \@Resources,$starttime,$endtime,undef);
 1568:     $SeqStat{$symb}->{'title'}  = $seq->{'title'};
 1569:     $SeqStat{$symb}->{'scoremax'}  = $smax;
 1570:     $SeqStat{$symb}->{'scoremin'}  = $smin;
 1571:     $SeqStat{$symb}->{'scoremean'} = $sMean;
 1572:     $SeqStat{$symb}->{'scorestd'}  = $sSTD;
 1573:     $SeqStat{$symb}->{'scorecount'} = $scount;
 1574:     $SeqStat{$symb}->{'max_possible'} = $sMAX;
 1575:     #
 1576:     # Compute statistics based on the number of correct problems
 1577:     # 'correct' is taken to mean 
 1578:     my ($cmin,$cmax,$cMean,$cSTD,$ccount)=
 1579:         &Apache::loncoursedata::count_stats
 1580:         (\@Apache::lonstatistics::SelectedSections,
 1581:          $Apache::lonstatistics::enrollment_status,
 1582:          \@Resources,$starttime,$endtime,undef);
 1583:     my $K = $seq->{'num_assess_parts'};
 1584:     my $kr_21;
 1585:     if ($K > 1 && $cSTD > 0) {
 1586:         $kr_21 =  ($K/($K-1)) * (1 - $cMean*($K-$cMean)/($K*$cSTD**2));
 1587:     } else {
 1588:         $kr_21 = 'nan';
 1589:     }
 1590:     $SeqStat{$symb}->{'countmax'} = $cmax;
 1591:     $SeqStat{$symb}->{'countmin'} = $cmin;
 1592:     $SeqStat{$symb}->{'countstd'} = $cSTD;
 1593:     $SeqStat{$symb}->{'countmean'} = $cMean;
 1594:     $SeqStat{$symb}->{'count'} = $ccount;
 1595:     $SeqStat{$symb}->{'items'} = $K;
 1596:     $SeqStat{$symb}->{'KR-21'}=$kr_21;
 1597:     return;
 1598: }
 1599: 
 1600: 
 1601: 
 1602: =pod 
 1603: 
 1604: =item ProblemStatisticsLegend
 1605: 
 1606: =over 4
 1607: 
 1608: =item #Stdnts
 1609: Total number of students attempted the problem.
 1610: 
 1611: =item Tries
 1612: Total number of tries for solving the problem.
 1613: 
 1614: =item Max Tries
 1615: Largest number of tries for solving the problem by a student.
 1616: 
 1617: =item Mean
 1618: Average number of tries. [ Tries / #Stdnts ]
 1619: 
 1620: =item #YES
 1621: Number of students solved the problem correctly.
 1622: 
 1623: =item #yes
 1624: Number of students solved the problem by override.
 1625: 
 1626: =item %Wrong
 1627: Percentage of students who tried to solve the problem 
 1628: but is still incorrect. [ 100*((#Stdnts-(#YES+#yes))/#Stdnts) ]
 1629: 
 1630: =item DoDiff
 1631: Degree of Difficulty of the problem.  
 1632: [ 1 - ((#YES+#yes) / Tries) ]
 1633: 
 1634: =item S.D.
 1635: Standard Deviation of the tries.  
 1636: [ sqrt(sum((Xi - Mean)^2)) / (#Stdnts-1) 
 1637: where Xi denotes every student\'s tries ]
 1638: 
 1639: =item Skew.
 1640: Skewness of the students tries.
 1641: [(sqrt( sum((Xi - Mean)^3) / #Stdnts)) / (S.D.^3)]
 1642: 
 1643: =item Dis.F.
 1644: Discrimination Factor: A Standard for evaluating the 
 1645: problem according to a Criterion<br>
 1646: 
 1647: =item [Criterion to group students into %27 Upper Students - 
 1648: and %27 Lower Students]
 1649: 1st Criterion for Sorting the Students: 
 1650: Sum of Partial Credit Awarded / Total Number of Tries
 1651: 2nd Criterion for Sorting the Students: 
 1652: Total number of Correct Answers / Total Number of Tries
 1653: 
 1654: =item Disc.
 1655: Number of Students had at least one discussion.
 1656: 
 1657: =back
 1658: 
 1659: =cut
 1660: 
 1661: ############################################################
 1662: ############################################################
 1663: 
 1664: 1;
 1665: __END__

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