Annotation of loncom/interface/statistics/lonproblemstatistics.pm, revision 1.71

1.1       stredwic    1: # The LearningOnline Network with CAPA
                      2: #
1.71    ! matthew     3: # $Id: lonproblemstatistics.pm,v 1.70 2004/03/07 21:42:19 matthew Exp $
1.1       stredwic    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: #
1.47      matthew    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: ###############################################
1.1       stredwic   49: 
1.36      minaeibi   50: package Apache::lonproblemstatistics;
1.1       stredwic   51: 
                     52: use strict;
                     53: use Apache::lonnet();
1.62      matthew    54: use Apache::loncommon();
1.1       stredwic   55: use Apache::lonhtmlcommon;
                     56: use Apache::loncoursedata;
1.41      matthew    57: use Apache::lonstatistics;
1.59      matthew    58: use Apache::lonlocal;
1.44      matthew    59: use Spreadsheet::WriteExcel;
1.70      matthew    60: use Apache::lonstathelpers();
1.71    ! matthew    61: use Time::HiRes;
1.59      matthew    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: ##
1.49      matthew    69: my @Fields = (
                     70:            { name => 'problem_num',
                     71:              title => 'P#',
                     72:              align => 'right',
                     73:              color => '#FFFFE6' },
                     74:            { name   => 'container',
1.51      matthew    75:              title  => 'Sequence or Folder',
1.49      matthew    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',
1.55      matthew    88:              color  => '#FFFFE6',
                     89:              },
1.49      matthew    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',
1.55      matthew   145:              long_title => 'Degree of Difficulty'.
                    146:                            '[ 1 - ((#YES+#yes) / Tries) ]'},
1.49      matthew   147:            { name   => 'num_solved',
                    148:              title  => '#YES',
                    149:              align  => 'right',
                    150:              color  => '#FFDDDD',
1.63      matthew   151:              format => '%4.1f',#             format => '%d',
1.49      matthew   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',
1.63      matthew   159:              format => '%4.1f',#             format => '%d',
1.49      matthew   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',
1.55      matthew   170:              long_title => 'Percent of students whose final answer is wrong' },
1.71    ! matthew   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: 
1.49      matthew   180: );
                    181: 
1.47      matthew   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
1.1       stredwic  193: 
1.47      matthew   194: ###############################################
                    195: ###############################################
1.57      matthew   196: my @OutputOptions = 
                    197:     (
1.70      matthew   198:      { name  => 'grouped by sequence',
1.57      matthew   199:        value => 'HTML problem statistics grouped',
                    200:        description => 'Output statistics for the problem parts.',
                    201:        mode => 'html',
                    202:        show => 'grouped',
                    203:      },
1.70      matthew   204:      { name  => 'ungrouped',
1.57      matthew   205:        value => 'HTML problem statistics ungrouped',
                    206:        description => 'Output statistics for the problem parts.',
                    207:        mode => 'html',
                    208:        show => 'ungrouped',
                    209:      },
1.70      matthew   210:      { name  => 'Excel',
1.57      matthew   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: 
1.41      matthew   219: sub CreateInterface {
                    220:     my $Str = '';
1.67      matthew   221:     $Str .= &Apache::lonhtmlcommon::breadcrumbs
1.69      matthew   222:         (undef,'Overall Problem Statistics','Statistics_Overall_Key');
1.41      matthew   223:     $Str .= '<table cellspacing="5">'."\n";
                    224:     $Str .= '<tr>';
1.59      matthew   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>';
1.70      matthew   229:     $Str .= '<td rowspan="2">'.
                    230:         &Apache::lonstathelpers::limit_by_time_form().'</td>';
1.41      matthew   231:     $Str .= '</tr>'."\n";
                    232:     #
                    233:     $Str .= '<tr><td align="center">'."\n";
                    234:     $Str .= &Apache::lonstatistics::SectionSelect('Section','multiple',5);
1.50      matthew   235:     $Str .= '</td><td align="center">';
                    236:     $Str .= &Apache::lonhtmlcommon::StatusOptions(undef,undef,5);
1.41      matthew   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";
1.57      matthew   250:     my ($html,$outputmode,$show) = 
                    251:         &Apache::lonstatistics::CreateAndParseOutputSelector(
                    252:                                             'statsoutputmode',
                    253:                                             'HTML problem statistics grouped',
                    254:                                             @OutputOptions);
                    255:     $Str .= $html;
1.41      matthew   256:     $Str .= '</td></tr>'."\n";
                    257:     $Str .= '</table>'."\n";
1.59      matthew   258:     $Str .= '<input type="submit" name="GenerateStatistics" value="'.
                    259:         &mt('Generate Statistics').'" />';
1.54      matthew   260:     $Str .= '&nbsp;'x5;
1.59      matthew   261:     $Str .= '<input type="submit" name="ClearCache" value="'.
                    262:         &mt('Clear Caches').'" />';
1.54      matthew   263:     $Str .= '&nbsp;'x5;
1.57      matthew   264:     return ($Str,$outputmode,$show);
1.41      matthew   265: }
1.25      stredwic  266: 
1.41      matthew   267: ###############################################
                    268: ###############################################
1.28      stredwic  269: 
1.47      matthew   270: =pod 
                    271: 
                    272: =item &BuildProblemStatisticsPage()
                    273: 
                    274: Main interface to problem statistics.
                    275: 
                    276: =cut
                    277: 
1.41      matthew   278: ###############################################
                    279: ###############################################
                    280: sub BuildProblemStatisticsPage {
                    281:     my ($r,$c)=@_;
1.61      matthew   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();
1.41      matthew   294:     #
1.71    ! matthew   295:     &Apache::loncoursedata::populate_weight_table();
        !           296:     #
1.57      matthew   297:     my ($interface,$output_mode,$show) = &CreateInterface();
                    298:     $r->print($interface);
1.41      matthew   299:     $r->print('<input type="hidden" name="statsfirstcall" value="no" />');
                    300:     $r->print('<input type="hidden" name="sortby" value="'.$ENV{'form.sortby'}.
                    301:               '" />');
1.49      matthew   302:     $r->print('<input type="hidden" name="plot" value="" />');
1.41      matthew   303:     if (! exists($ENV{'form.statsfirstcall'})) {
                    304:         return;
1.28      stredwic  305:     }
1.41      matthew   306:     #
1.56      matthew   307:     &Apache::lonstatistics::Gather_Student_Data($r);
1.41      matthew   308:     #
                    309:     #
                    310:     if ($output_mode eq 'html') {
                    311:         $r->print("<h2>".
                    312:                   $ENV{'course.'.$ENV{'request.course.id'}.'.description'}.
                    313:                   "</h2>\n");
1.70      matthew   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>");
1.41      matthew   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:         }
1.44      matthew   330:     } elsif ($output_mode eq 'excel') {
1.59      matthew   331:         $r->print('<h2>'.&mt('Preparing Excel Spreadsheet').'</h2>');
1.44      matthew   332:         &output_excel($r);
1.41      matthew   333:     } else {
1.59      matthew   334:         $r->print('<h1>'.&mt('Not implemented').'</h1>');
1.28      stredwic  335:     }
1.41      matthew   336:     return;
                    337: }
1.21      stredwic  338: 
1.44      matthew   339: ###############################################
                    340: ###############################################
                    341: 
1.47      matthew   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: 
1.44      matthew   351: ###############################################
                    352: ###############################################
1.41      matthew   353: sub output_html_grouped_by_sequence {
                    354:     my ($r) = @_;
1.45      matthew   355:     my $problem_num = 0;
1.41      matthew   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");
1.49      matthew   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");
1.41      matthew   365:         foreach my $resource (@{$sequence->{'contents'}}) {
                    366:             next if ($resource->{'type'} ne 'assessment');
                    367:             foreach my $part (@{$resource->{'parts'}}) {
1.45      matthew   368:                 $problem_num++;
1.49      matthew   369:                 my $data = &get_statistics($sequence,$resource,$part,
                    370:                                            $problem_num);
1.45      matthew   371:                 my $option = '';
1.49      matthew   372:                 $r->print('<tr>'.&statistics_html_table_data($data,
                    373:                                                              'no container').
1.41      matthew   374:                           "</tr>\n");
                    375:             }
                    376:         }
                    377:         $r->print("</table>\n");
                    378:         $r->print("</td></tr></table>\n");
                    379:         $r->rflush();
1.21      stredwic  380:     }
1.41      matthew   381:     #
                    382:     return;
                    383: }
1.25      stredwic  384: 
1.41      matthew   385: ###############################################
                    386: ###############################################
1.26      stredwic  387: 
1.47      matthew   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: 
1.41      matthew   397: ###############################################
                    398: ###############################################
                    399: sub output_html_ungrouped {
1.45      matthew   400:     my ($r,$option) = @_;
1.41      matthew   401:     #
1.49      matthew   402:     if (exists($ENV{'form.plot'}) && $ENV{'form.plot'} ne '') {
                    403:         &plot_statistics($r,$ENV{'form.plot'});
                    404:     }
                    405:     #
1.45      matthew   406:     my $problem_num = 0;
1.41      matthew   407:     my $show_container = 0;
1.43      matthew   408:     my $show_part = 0;
1.41      matthew   409:     #$r->print(&ProblemStatisticsLegend());
1.42      matthew   410:     my $sortby = undef;
1.49      matthew   411:     foreach my $field (@Fields) {
                    412:         if ($ENV{'form.sortby'} eq $field->{'name'}) {
                    413:             $sortby = $field->{'name'};
1.42      matthew   414:         }
                    415:     }
1.49      matthew   416:     if (! defined($sortby) || $sortby eq '' || $sortby eq 'problem_num') {
                    417:         $sortby = 'container';
1.42      matthew   418:     }
1.45      matthew   419:     # If there is more than one sequence, list their titles
1.41      matthew   420:     my @Sequences = &Apache::lonstatistics::Sequences_with_Assess();
1.49      matthew   421:     if (@Sequences < 1) {
                    422:         $option .= ' no container';
1.45      matthew   423:     }
1.41      matthew   424:     #
1.42      matthew   425:     # Compile the data
                    426:     my @Statsarray;
1.41      matthew   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'}}) {
1.45      matthew   432:                 $problem_num++;
1.49      matthew   433:                 my $data = &get_statistics($sequence,$resource,$part,
                    434:                                            $problem_num);
1.43      matthew   435:                 $show_part = 1 if ($part ne '0');
                    436:                 #
1.49      matthew   437:                 push (@Statsarray,$data);
1.42      matthew   438:             }
                    439:         }
                    440:     }
                    441:     #
                    442:     # Sort the data
1.43      matthew   443:     my @OutputOrder;
1.49      matthew   444:     if ($sortby eq 'container') {
1.43      matthew   445:         @OutputOrder = @Statsarray;
1.42      matthew   446:     } else {
                    447:         # $sortby is already defined, so we can charge ahead
                    448:         if ($sortby =~ /^(title|part)$/i) {
                    449:             # Alpha comparison
                    450:             @OutputOrder = sort {
1.43      matthew   451:                 lc($a->{$sortby}) cmp lc($b->{$sortby}) ||
1.49      matthew   452:                     lc($a->{'title'}) cmp lc($b->{'title'}) ||
                    453:                         lc($a->{'part'}) cmp lc($b->{'part'});
1.42      matthew   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} ||
1.49      matthew   473:                                 lc($a->{'title'}) <=> lc($b->{'title'}) ||
                    474:                                 lc($a->{'part'})  <=> lc($b->{'part'});
1.41      matthew   475:                 }
1.42      matthew   476:                 $retvalue;
                    477:             } @Statsarray;
                    478:         }
1.43      matthew   479:     }
1.49      matthew   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");
1.26      stredwic  502:     }
1.41      matthew   503:     $r->print("</table>\n");
                    504:     $r->print("</td></tr></table>\n");
1.26      stredwic  505:     $r->rflush();
1.41      matthew   506:     #
                    507:     return;
1.42      matthew   508: }
                    509: 
1.41      matthew   510: ###############################################
                    511: ###############################################
1.26      stredwic  512: 
1.47      matthew   513: =pod 
                    514: 
                    515: =item &output_excel()
                    516: 
                    517: Presents the statistical data in an Excel 95 compatable spreadsheet file.
                    518: 
                    519: =cut
                    520: 
1.41      matthew   521: ###############################################
                    522: ###############################################
1.44      matthew   523: sub output_excel {
                    524:     my ($r) = @_;
                    525:     my $filename = '/prtspool/'.
                    526:         $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.
                    527:             time.'_'.rand(1000000000).'.xls';
                    528:     #
1.70      matthew   529:     my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
                    530:     #
1.44      matthew   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: $!");
1.59      matthew   543:         $r->print(&mt("Problems creating new Excel file.  ".
1.44      matthew   544:                   "This error has been logged.  ".
1.59      matthew   545:                   "Please alert your LON-CAPA administrator."));
1.44      matthew   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:     }
1.62      matthew   560:     $excel_sheet = $excel_workbook->addworksheet(
                    561:                        &Apache::loncommon::clean_excel_name($sheetname)
                    562:                                                  );
1.44      matthew   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:     #
1.70      matthew   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:     #
1.44      matthew   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:     #
1.55      matthew   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
1.49      matthew   625:     foreach my $field (@Fields) {
                    626:         next if ($field->{'name'} eq 'problem_num');
1.59      matthew   627:         # Use english for excel as I am not sure how well excel handles 
                    628:         # other character sets....
1.49      matthew   629:         $excel_sheet->write($rows_output,$cols_output++,$field->{'title'});
1.44      matthew   630:     }
                    631:     $rows_output++;
                    632:     #
                    633:     # Write the data
1.49      matthew   634:     my $problem_num=0;
1.44      matthew   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;
1.49      matthew   641:                 $problem_num++;
                    642:                 my $data = &get_statistics($sequence,$resource,$part,
                    643:                                            $problem_num);
1.44      matthew   644:                 #
                    645:                 if (!defined($part) || $part eq '') {
                    646:                     $part = ' ';
                    647:                 }
1.49      matthew   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'}});
1.44      matthew   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 />'.
1.59      matthew   662:               '<a href="'.$filename.'">'.
                    663:               &mt('Your Excel Spreadsheet').'</a>'."\n");
1.44      matthew   664:     $r->rflush();
                    665:     return;
                    666: }
                    667: 
1.45      matthew   668: ###############################################
                    669: ###############################################
1.44      matthew   670: 
1.47      matthew   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: 
1.45      matthew   679: ###############################################
                    680: ###############################################
1.41      matthew   681: sub statistics_html_table_data {
1.49      matthew   682:     my ($data,$options) = @_;
1.41      matthew   683:     my $row = '';
1.49      matthew   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') {
1.53      matthew   692:             $row .= '<a href="'.$data->{$field->{'name'}.'.link'}.'">';
1.49      matthew   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>';
1.26      stredwic  703:     }
1.41      matthew   704:     return $row;
                    705: }
1.26      stredwic  706: 
1.49      matthew   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:         }
1.59      matthew   719:         $header_row .= &mt($field->{'title'});
1.49      matthew   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();">';
1.59      matthew   730:             $header_row .= &mt('plot').'</a>)';
1.49      matthew   731:         }
                    732:         $header_row .= '</th>';
                    733:     }
                    734:     return $header_row;
                    735: }
                    736: 
1.41      matthew   737: ###############################################
                    738: ###############################################
1.47      matthew   739: 
                    740: =pod 
                    741: 
                    742: =item &plot_statistics()
                    743: 
                    744: =cut
                    745: 
                    746: ###############################################
                    747: ###############################################
1.45      matthew   748: sub plot_statistics {
                    749:     my ($r,$datafield) = @_;
                    750:     my @Data;
                    751:     #
1.49      matthew   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:         }
1.34      minaeibi  761:     }
1.49      matthew   762:     return if (! defined($sortfield) || $sortfield eq '');
1.45      matthew   763:     #
                    764:     my $Max = 0;
1.49      matthew   765:     my $problem_num = 0;
1.45      matthew   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'}}) {
1.49      matthew   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);
1.45      matthew   777:             }
                    778:         }
1.26      stredwic  779:     }
1.45      matthew   780:     #
                    781:     # Print out plot request
1.49      matthew   782:     my $yaxis = '';
                    783:     if ($sortfield eq 'per_wrong') {
                    784:         $yaxis = 'Percent';
1.45      matthew   785:     }
                    786:     #
                    787:     # Determine appropriate value for $Max
1.49      matthew   788:     if ($sortfield eq 'deg_of_diff') {
1.45      matthew   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:         }
1.49      matthew   796:     } elsif ($sortfield eq 'per_wrong') {
1.45      matthew   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;
1.24      stredwic  807:         } else {
1.45      matthew   808:             $Max = 5;
1.24      stredwic  809:         }
                    810:     }
1.45      matthew   811:     
1.60      matthew   812:     $r->print("<p>".&Apache::loncommon::DrawBarGraph($title,
                    813:                                                      'Problem Number',
                    814:                                                      $yaxis,
                    815:                                                      $Max,
1.65      matthew   816:                                                      undef, # colors
                    817:                                                      undef, # labels
1.60      matthew   818:                                                      \@Data)."</p>\n");
1.45      matthew   819:     #
                    820:     # Print out the data
                    821:     $ENV{'form.sortby'} = 'Contents';
1.49      matthew   822: #    &output_html_ungrouped($r);
1.24      stredwic  823:     return;
1.48      matthew   824: }
                    825: 
1.70      matthew   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: ########################################################
1.48      matthew   845: sub get_statistics {
1.49      matthew   846:     my ($sequence,$resource,$part,$problem_num) = @_;
1.48      matthew   847:     #
1.70      matthew   848:     my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
1.49      matthew   849:     my $symb = $resource->{'symb'};
1.48      matthew   850:     my $courseid = $ENV{'request.course.id'};
                    851:     #
1.49      matthew   852:     my $data = &Apache::loncoursedata::get_problem_statistics
1.66      matthew   853:                         (\@Apache::lonstatistics::SelectedSections,
                    854:                          $Apache::lonstatistics::enrollment_status,
1.70      matthew   855:                          $symb,$part,$courseid,$starttime,$endtime);
1.49      matthew   856:     $data->{'part'}        = $part;
                    857:     $data->{'problem_num'} = $problem_num;
                    858:     $data->{'container'}   = $sequence->{'title'};
                    859:     $data->{'title'}       = $resource->{'title'};
1.53      matthew   860:     $data->{'title.link'}  = $resource->{'src'}.'?symb='.
                    861:         &Apache::lonnet::escape($resource->{'symb'});
1.49      matthew   862:     #
1.71    ! matthew   863:     $data->{'deg_of_disc'} = &compute_discrimination_factor($resource,$part,$sequence);
1.49      matthew   864:     return $data;
1.71    ! matthew   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:     &Apache::lonnet::logthis($sequence->{'title'}.' '.$resource->{'title'});
        !           886:     my @Resources;
        !           887:     foreach my $res (@{$sequence->{'contents'}}) {
        !           888:         next if ($res->{'symb'} eq $resource->{'symb'});
        !           889:         push (@Resources,$res->{'symb'});
        !           890:     }
        !           891:     #
        !           892:     # rank
        !           893:     my $ranking = 
        !           894:         &Apache::loncoursedata::rank_students_by_scores_on_resources
        !           895:         (\@Resources,
        !           896:          \@Apache::lonstatistics::SelectedSections,
        !           897:          $Apache::lonstatistics::enrollment_status,undef);
        !           898:     #
        !           899:     # compute their percent scores on the problems in the sequence,
        !           900:     my $number_to_grab = int(scalar(@{$ranking})/4);
        !           901:     my $num_students = scalar(@{$ranking});
        !           902:     my @BottomSet = map { $_->[&Apache::loncoursedata::RNK_student()]; 
        !           903:                       } @{$ranking}[0..$number_to_grab];
        !           904:     my @TopSet    = 
        !           905:         map { 
        !           906:             $_->[&Apache::loncoursedata::RNK_student()]; 
        !           907:           } @{$ranking}[($num_students-$number_to_grab)..($num_students-1)];
        !           908:     my ($bottom_sum,$bottom_max) = 
        !           909:         &Apache::loncoursedata::get_sum_of_scores($resource,$part,\@BottomSet);
        !           910:     my ($top_sum,$top_max) = 
        !           911:         &Apache::loncoursedata::get_sum_of_scores($resource,$part,\@TopSet);
        !           912:     my $deg_of_disc;
        !           913:     if ($top_max == 0 || $bottom_max==0) {
        !           914:         $deg_of_disc = 'nan';
        !           915:     } else {
        !           916:         $deg_of_disc = ($top_sum/$top_max) - ($bottom_sum/$bottom_max);
        !           917:     }
        !           918:     #&Apache::lonnet::logthis('    '.$top_sum.'/'.$top_max.
        !           919:     #                         ' - '.$bottom_sum.'/'.$bottom_max);
        !           920:     return $deg_of_disc;
1.1       stredwic  921: }
1.12      minaeibi  922: 
1.45      matthew   923: ###############################################
                    924: ###############################################
1.47      matthew   925: 
                    926: =pod 
                    927: 
                    928: =item &ProblemStatisticsLegend()
1.59      matthew   929: 
                    930: HELP  This needs to be localized, or at least generated automatically.
1.47      matthew   931: 
                    932: =cut
1.1       stredwic  933: 
1.45      matthew   934: ###############################################
                    935: ###############################################
1.1       stredwic  936: sub ProblemStatisticsLegend {
                    937:     my $Ptr = '';
                    938:     $Ptr = '<table border="0">';
                    939:     $Ptr .= '<tr><td>';
1.6       minaeibi  940:     $Ptr .= '<b>#Stdnts</b></td>';
1.19      stredwic  941:     $Ptr .= '<td>Total number of students attempted the problem.';
1.1       stredwic  942:     $Ptr .= '</td></tr><tr><td>';
1.6       minaeibi  943:     $Ptr .= '<b>Tries</b></td>';
1.19      stredwic  944:     $Ptr .= '<td>Total number of tries for solving the problem.';
1.1       stredwic  945:     $Ptr .= '</td></tr><tr><td>';
1.49      matthew   946:     $Ptr .= '<b>Max Tries</b></td>';
1.19      stredwic  947:     $Ptr .= '<td>Largest number of tries for solving the problem by a student.';
1.1       stredwic  948:     $Ptr .= '</td></tr><tr><td>';
1.6       minaeibi  949:     $Ptr .= '<b>Mean</b></td>';
1.19      stredwic  950:     $Ptr .= '<td>Average number of tries. [ Tries / #Stdnts ]';
1.1       stredwic  951:     $Ptr .= '</td></tr><tr><td>';
1.6       minaeibi  952:     $Ptr .= '<b>#YES</b></td>';
1.1       stredwic  953:     $Ptr .= '<td>Number of students solved the problem correctly.';
                    954:     $Ptr .= '</td></tr><tr><td>';
1.6       minaeibi  955:     $Ptr .= '<b>#yes</b></td>';
1.1       stredwic  956:     $Ptr .= '<td>Number of students solved the problem by override.';
                    957:     $Ptr .= '</td></tr><tr><td>';
1.19      stredwic  958:     $Ptr .= '<b>%Wrong</b></td>';
                    959:     $Ptr .= '<td>Percentage of students who tried to solve the problem ';
                    960:     $Ptr .= 'but is still incorrect. [ 100*((#Stdnts-(#YES+#yes))/#Stdnts) ]';
1.1       stredwic  961:     $Ptr .= '</td></tr><tr><td>';
1.6       minaeibi  962:     $Ptr .= '<b>DoDiff</b></td>';
1.1       stredwic  963:     $Ptr .= '<td>Degree of Difficulty of the problem.  ';
                    964:     $Ptr .= '[ 1 - ((#YES+#yes) / Tries) ]';
                    965:     $Ptr .= '</td></tr><tr><td>';
1.6       minaeibi  966:     $Ptr .= '<b>S.D.</b></td>';
1.1       stredwic  967:     $Ptr .= '<td>Standard Deviation of the tries.  ';
                    968:     $Ptr .= '[ sqrt(sum((Xi - Mean)^2)) / (#Stdnts-1) ';
                    969:     $Ptr .= 'where Xi denotes every student\'s tries ]';
                    970:     $Ptr .= '</td></tr><tr><td>';
1.6       minaeibi  971:     $Ptr .= '<b>Skew.</b></td>';
1.1       stredwic  972:     $Ptr .= '<td>Skewness of the students tries.';
                    973:     $Ptr .= '[(sqrt( sum((Xi - Mean)^3) / #Stdnts)) / (S.D.^3)]';
                    974:     $Ptr .= '</td></tr><tr><td>';
1.6       minaeibi  975:     $Ptr .= '<b>Dis.F.</b></td>';
1.1       stredwic  976:     $Ptr .= '<td>Discrimination Factor: A Standard for evaluating the ';
                    977:     $Ptr .= 'problem according to a Criterion<br>';
1.31      stredwic  978:     $Ptr .= '<b>[Criterion to group students into %27 Upper Students - ';
                    979:     $Ptr .= 'and %27 Lower Students]</b><br>';
1.1       stredwic  980:     $Ptr .= '<b>1st Criterion</b> for Sorting the Students: ';
                    981:     $Ptr .= '<b>Sum of Partial Credit Awarded / Total Number of Tries</b><br>';
                    982:     $Ptr .= '<b>2nd Criterion</b> for Sorting the Students: ';
                    983:     $Ptr .= '<b>Total number of Correct Answers / Total Number of Tries</b>';
                    984:     $Ptr .= '</td></tr>';
                    985:     $Ptr .= '<tr><td><b>Disc.</b></td>';
                    986:     $Ptr .= '<td>Number of Students had at least one discussion.';
                    987:     $Ptr .= '</td></tr></table>';
                    988:     return $Ptr;
                    989: }
1.24      stredwic  990: 
                    991: #---- END Problem Statistics Web Page ----------------------------------------
1.4       minaeibi  992: 
1.1       stredwic  993: 1;
                    994: __END__

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