1: # The LearningOnline Network with CAPA
2: #
3: # $Id: lonproblemstatistics.pm,v 1.73 2004/03/26 22:04:22 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:
65: ##
66: ## Localization notes:
67: ##
68: ## in @Fields[0]->{'long_title'} is placed in Excel files and is used as the
69: ## header for plots created with Graph.pm, both of which more than likely do
70: ## not support localization.
71: ##
72: my @Fields = (
73: { name => 'problem_num',
74: title => 'P#',
75: align => 'right',
76: color => '#FFFFE6' },
77: { name => 'container',
78: title => 'Sequence or Folder',
79: align => 'left',
80: color => '#FFFFE6',
81: sortable => 'yes' },
82: { name => 'title',
83: title => 'Title',
84: align => 'left',
85: color => '#FFFFE6',
86: special => 'link',
87: sortable => 'yes', },
88: { name => 'part',
89: title => 'Part',
90: align => 'left',
91: color => '#FFFFE6',
92: },
93: { name => 'num_students',
94: title => '#Stdnts',
95: align => 'right',
96: color => '#EEFFCC',
97: format => '%d',
98: sortable => 'yes',
99: graphable => 'yes',
100: long_title => 'Number of Students Attempting Problem' },
101: { name => 'tries',
102: title => 'Tries',
103: align => 'right',
104: color => '#EEFFCC',
105: format => '%d',
106: sortable => 'yes',
107: graphable => 'yes',
108: long_title => 'Total Number of Tries' },
109: { name => 'max_tries',
110: title => 'Max Tries',
111: align => 'right',
112: color => '#DDFFFF',
113: format => '%d',
114: sortable => 'yes',
115: graphable => 'yes',
116: long_title => 'Maximum Number of Tries' },
117: { name => 'min_tries',
118: title => 'Min Tries',
119: align => 'right',
120: color => '#DDFFFF',
121: format => '%d',
122: sortable => 'yes',
123: graphable => 'yes',
124: long_title => 'Minumum Number of Tries' },
125: { name => 'mean_tries',
126: title => 'Mean Tries',
127: align => 'right',
128: color => '#DDFFFF',
129: format => '%5.2f',
130: sortable => 'yes',
131: graphable => 'yes',
132: long_title => 'Average Number of Tries' },
133: { name => 'std_tries',
134: title => 'S.D. tries',
135: align => 'right',
136: color => '#DDFFFF',
137: format => '%5.2f',
138: sortable => 'yes',
139: graphable => 'yes',
140: long_title => 'Standard Deviation of Number of Tries' },
141: { name => 'skew_tries',
142: title => 'Skew Tries',
143: align => 'right',
144: color => '#DDFFFF',
145: format => '%5.2f',
146: sortable => 'yes',
147: graphable => 'yes',
148: long_title => 'Skew of Number of Tries' },
149: { name => 'num_solved',
150: title => '#YES',
151: align => 'right',
152: color => '#FFDDDD',
153: format => '%4.1f',# format => '%d',
154: sortable => 'yes',
155: graphable => 'yes',
156: long_title => 'Number of Students able to Solve' },
157: { name => 'num_override',
158: title => '#yes',
159: align => 'right',
160: color => '#FFDDDD',
161: format => '%4.1f',# format => '%d',
162: sortable => 'yes',
163: graphable => 'yes',
164: long_title => 'Number of Students given Override' },
165: { name => 'num_wrong',
166: title => '#Wrng',
167: align => 'right',
168: color => '#FFDDDD',
169: format => '%4.1f',
170: sortable => 'yes',
171: graphable => 'yes',
172: long_title => 'Percent of students whose final answer is wrong' },
173: { name => 'deg_of_diff',
174: title => 'DoDiff',
175: align => 'right',
176: color => '#FFFFE6',
177: format => '%5.2f',
178: sortable => 'yes',
179: graphable => 'yes',
180: long_title => 'Degree of Difficulty'.
181: '[ 1 - ((#YES+#yes) / Tries) ]'},
182: { name => 'deg_of_disc',
183: title => 'DoDisc',
184: align => 'right',
185: color => '#FFFFE6',
186: format => '%4.2f',
187: sortable => 'yes',
188: graphable => 'yes',
189: long_title => 'Degree of Discrimination' },
190: );
191:
192: ###############################################
193: ###############################################
194:
195: =pod
196:
197: =item &CreateInterface()
198:
199: Create the main intereface for the statistics page. Allows the user to
200: select sections, maps, and output.
201:
202: =cut
203:
204: ###############################################
205: ###############################################
206: sub CreateInterface {
207: my $Str = '';
208: $Str .= &Apache::lonhtmlcommon::breadcrumbs
209: (undef,'Overall Problem Statistics','Statistics_Overall_Key');
210: $Str .= '<table cellspacing="5">'."\n";
211: $Str .= '<tr>';
212: $Str .= '<td align="center"><b>'.&mt('Sections').'</b></td>';
213: $Str .= '<td align="center"><b>'.&mt('Enrollment Status').'</b></td>';
214: $Str .= '<td align="center"><b>'.&mt('Sequences and Folders').'</b></td>';
215: $Str .= '<td rowspan="2">'.
216: &Apache::lonstathelpers::limit_by_time_form().'</td>';
217: $Str .= '</tr>'."\n";
218: #
219: $Str .= '<tr><td align="center">'."\n";
220: $Str .= &Apache::lonstatistics::SectionSelect('Section','multiple',5);
221: $Str .= '</td><td align="center">';
222: $Str .= &Apache::lonhtmlcommon::StatusOptions(undef,undef,5);
223: $Str .= '</td><td align="center">';
224: #
225: my $only_seq_with_assessments = sub {
226: my $s=shift;
227: if ($s->{'num_assess'} < 1) {
228: return 0;
229: } else {
230: return 1;
231: }
232: };
233: $Str .= &Apache::lonstatistics::MapSelect('Maps','multiple,all',5,
234: $only_seq_with_assessments);
235: $Str .= '</td></tr>'."\n";
236: $Str .= '</table>'."\n";
237: $Str .= '<input type="submit" name="GenerateStatistics" value="'.
238: &mt('Generate Statistics').'" />';
239: $Str .= ' 'x5;
240: $Str .= 'Plot '.&plot_dropdown().(' 'x10);
241: $Str .= '<input type="submit" name="ClearCache" value="'.
242: &mt('Clear Caches').'" />';
243: $Str .= ' 'x5;
244: $Str .= '<input type="submit" name="UpdateCache" value="'.
245: &mt('Update Student Data').'" />';
246: $Str .= ' 'x5;
247: $Str .= '<input type="submit" name="Excel" value="'.
248: &mt('Produce Excel Output').'" />';
249: $Str .= ' 'x5;
250: return $Str;
251: }
252:
253: ###############################################
254: ###############################################
255:
256: =pod
257:
258: =item &BuildProblemStatisticsPage()
259:
260: Main interface to problem statistics.
261:
262: =cut
263:
264: ###############################################
265: ###############################################
266: sub BuildProblemStatisticsPage {
267: my ($r,$c)=@_;
268: #
269: my %Saveable_Parameters = ('Status' => 'scalar',
270: 'statsoutputmode' => 'scalar',
271: 'Section' => 'array',
272: 'StudentData' => 'array',
273: 'Maps' => 'array');
274: &Apache::loncommon::store_course_settings('statistics',
275: \%Saveable_Parameters);
276: &Apache::loncommon::restore_course_settings('statistics',
277: \%Saveable_Parameters);
278: #
279: &Apache::lonstatistics::PrepareClasslist();
280: #
281: # Clear the package variables
282: undef(@StatsArray);
283: #
284: # Finally let the user know we are here
285: my $interface = &CreateInterface();
286: $r->print($interface);
287: $r->print('<input type="hidden" name="sortby" value="'.$ENV{'form.sortby'}.
288: '" />');
289: #
290: if (! exists($ENV{'form.statsfirstcall'})) {
291: $r->print('<input type="hidden" name="statsfirstcall" value="yes" />');
292: $r->print('<h3>'.
293: &mt('Press "Generate Statistics" when you are ready.').
294: '</h3><p>'.
295: &mt('It may take some time to update the student data '.
296: 'for the first analysis. Future analysis this session '.
297: ' will not have this delay.').
298: '</p>');
299: return;
300: } elsif ($ENV{'form.statsfirstcall'} eq 'yes' ||
301: exists($ENV{'form.UpdateCache'}) ||
302: exists($ENV{'form.ClearCache'}) ) {
303: $r->print('<input type="hidden" name="statsfirstcall" value="no" />');
304: &Apache::lonstatistics::Gather_Student_Data($r);
305: } else {
306: $r->print('<input type="hidden" name="statsfirstcall" value="no" />');
307: }
308: $r->rflush();
309: #
310: # This probably does not need to be done each time we are called, but
311: # it does not slow things down noticably.
312: &Apache::loncoursedata::populate_weight_table();
313: if (exists($ENV{'form.Excel'})) {
314: &Excel_output($r);
315: } else {
316: my $sortby = $ENV{'form.sortby'};
317: $sortby = 'container' if (! defined($sortby) || $sortby =~ /^\s*$/);
318: my $plot = $ENV{'form.plot'};
319: &Apache::lonnet::logthis('form.plot = '.$plot);
320: if ($sortby eq 'container' && ! defined($plot)) {
321: &output_html_by_sequence($r);
322: } else {
323: if (defined($plot)) {
324: &Apache::lonnet::logthis('calling plot routine');
325: &make_plot($r,$plot);
326: }
327: &output_html_stats($r);
328: }
329: }
330: return;
331: }
332:
333: ##########################################################
334: ##########################################################
335: ##
336: ## HTML output routines
337: ##
338: ##########################################################
339: ##########################################################
340: sub output_html_by_sequence {
341: my ($r) = @_;
342: my $c = $r->connection();
343: $r->print(&html_preamble());
344: #
345: foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
346: last if ($c->aborted);
347: next if ($seq->{'num_assess'} < 1);
348: $r->print("<h3>".$seq->{'title'}."</h3>".
349: '<table border="0"><tr><td bgcolor="#777777">'."\n".
350: '<table border="0" cellpadding="3">'."\n".
351: '<tr bgcolor="#FFFFE6">'.
352: &statistics_table_header('no container')."</tr>\n");
353: my @Data = &compute_statistics_on_sequence($seq);
354: foreach my $data (@Data) {
355: $r->print('<tr>'.&statistics_html_table_data($data,
356: 'no container').
357: "</tr>\n");
358: }
359: $r->print('</table>'."\n".'</table>'."\n");
360: $r->rflush();
361: }
362: return;
363: }
364:
365: sub output_html_stats {
366: my ($r)=@_;
367: &compute_all_statistics($r);
368: $r->print(&html_preamble());
369: &sort_data($ENV{'form.sortby'});
370: #
371: my $count=0;
372: foreach my $data (@StatsArray) {
373: if ($count++ % 50 == 0) {
374: $r->print("</table>\n</table>\n");
375: $r->print('<table border="0"><tr><td bgcolor="#777777">'."\n".
376: '<table border="0" cellpadding="3">'."\n".
377: '<tr bgcolor="#FFFFE6">'.
378: '<tr bgcolor="#FFFFE6">'.
379: &statistics_table_header().
380: "</tr>\n");
381: }
382: $r->print('<tr>'.&statistics_html_table_data($data)."</tr>\n");
383: }
384: $r->print("</table>\n</table>\n");
385: return;
386: }
387:
388:
389: sub html_preamble {
390: my $Str='';
391: $Str .= "<h2>".
392: $ENV{'course.'.$ENV{'request.course.id'}.'.description'}.
393: "</h2>\n";
394: my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
395: if (defined($starttime) || defined($endtime)) {
396: # Inform the user what the time limits on the data are.
397: $Str .= '<h3>'.&mt('Statistics on submissions from [_1] to [_2]',
398: &Apache::lonlocal::locallocaltime($starttime),
399: &Apache::lonlocal::locallocaltime($endtime)
400: ).'</h3>';
401: }
402: $Str .= "<h3>".&mt('Compiled on [_1]',
403: &Apache::lonlocal::locallocaltime(time))."</h3>";
404: return $Str;
405: }
406:
407:
408: ###############################################
409: ###############################################
410: ##
411: ## Misc HTML output routines
412: ##
413: ###############################################
414: ###############################################
415: sub statistics_html_table_data {
416: my ($data,$options) = @_;
417: my $row = '';
418: foreach my $field (@Fields) {
419: next if ($options =~ /no $field->{'name'}/);
420: $row .= '<td bgcolor="'.$field->{'color'}.'"';
421: if (exists($field->{'align'})) {
422: $row .= ' align="'.$field->{'align'}.'"';
423: }
424: $row .= '>';
425: if (exists($field->{'special'}) && $field->{'special'} eq 'link') {
426: $row .= '<a href="'.$data->{$field->{'name'}.'.link'}.'">';
427: }
428: if (exists($field->{'format'})) {
429: $row .= sprintf($field->{'format'},$data->{$field->{'name'}});
430: } else {
431: $row .= $data->{$field->{'name'}};
432: }
433: if (exists($field->{'special'}) && $field->{'special'} eq 'link') {
434: $row.= '</a>';
435: }
436: $row .= '</td>';
437: }
438: return $row;
439: }
440:
441: sub statistics_table_header {
442: my ($options) = @_;
443: my $header_row;
444: foreach my $field (@Fields) {
445: next if ($options =~ /no $field->{'name'}/);
446: $header_row .= '<th>';
447: if (exists($field->{'sortable'}) && $field->{'sortable'} eq 'yes') {
448: $header_row .= '<a href="javascript:'.
449: 'document.Statistics.sortby.value='."'".$field->{'name'}."'".
450: ';document.Statistics.submit();">';
451: }
452: $header_row .= &mt($field->{'title'});
453: if ($options =~ /sortable/) {
454: $header_row.= '</a>';
455: }
456: if ($options !~ /no plots/ &&
457: exists($field->{'graphable'}) &&
458: $field->{'graphable'} eq 'yes') {
459: $header_row.=' (';
460: $header_row .= '<a href="javascript:'.
461: "document.Statistics.plot.value='$field->{'name'}'".
462: ';document.Statistics.submit();">';
463: $header_row .= &mt('plot').'</a>)';
464: }
465: $header_row .= '</th>';
466: }
467: return $header_row;
468: }
469:
470: ####################################################
471: ####################################################
472: ##
473: ## Plotting Routines
474: ##
475: ####################################################
476: ####################################################
477: sub make_plot {
478: my ($r,$plot) = @_;
479: &compute_all_statistics($r);
480: &sort_data($ENV{'form.sortby'});
481: if ($plot eq 'degrees') {
482: °rees_plot($r);
483: } else {
484: &make_single_stat_plot($r,$plot);
485: }
486: return;
487: }
488:
489: sub make_single_stat_plot {
490: my ($r,$datafield) = @_;
491: #
492: my $title; my $yaxis;
493: foreach my $field (@Fields) {
494: next if ($field->{'name'} ne $datafield);
495: $title = $field->{'long_title'};
496: $yaxis = $field->{'title'};
497: last;
498: }
499: if ($title eq '' || $yaxis eq '') {
500: # datafield is something we do not know enough about to plot
501: $r->print('<h3>'.
502: &mt('Unable to plot the requested statistic.').
503: '</h3>');
504: return;
505: }
506: #
507: # Build up the data sets to plot
508: my @Labels;
509: my @Data;
510: my $max = 1;
511: foreach my $data (@StatsArray) {
512: push(@Labels,$data->{'problem_num'});
513: push(@Data,$data->{$datafield});
514: if ($data->{$datafield}>$max) {
515: $max = $data->{$datafield};
516: }
517: }
518: foreach (1,2,3,4,5,10,15,20,25,40,50,75,100,150,200,250,300,500,600,750,
519: 1000,1500,2000,2500,3000,3500,4000,5000,7500,10000,15000,20000) {
520: if ($max <= $_) {
521: $max = $_;
522: last;
523: }
524: }
525: if ($max > 20000) {
526: $max = 10000*(int($max/10000)+1);
527: }
528: #
529: $r->print("<p>".&Apache::loncommon::DrawBarGraph($title,
530: 'Problem Number',
531: $yaxis,
532: $max,
533: undef, # colors
534: \@Labels,
535: \@Data)."</p>\n");
536: return;
537: }
538:
539: sub degrees_plot {
540: my ($r)=@_;
541: my $count = scalar(@StatsArray);
542: my $width = 50 + 10*$count;
543: $width = 300 if ($width < 300);
544: my $height = 300;
545: my $plot = '';
546: my $ymax = 0;
547: my $ymin = 0;
548: my @Disc; my @Diff; my @Labels;
549: foreach my $data (@StatsArray) {
550: push(@Labels,$data->{'problem_num'});
551: my $disc = $data->{'deg_of_disc'};
552: my $diff = $data->{'deg_of_diff'};
553: push(@Disc,$disc);
554: push(@Diff,$diff);
555: #
556: $ymin = $disc if ($ymin > $disc);
557: $ymin = $diff if ($ymin > $diff);
558: $ymax = $disc if ($ymax < $disc);
559: $ymax = $diff if ($ymax < $diff);
560: }
561: #
562: # Make sure we show relevant information.
563: if ($ymin < 0) {
564: if (abs($ymin) < 0.05) {
565: $ymin = 0;
566: } else {
567: $ymin = -1;
568: }
569: }
570: if ($ymax > 0) {
571: if (abs($ymax) < 0.05) {
572: $ymax = 0;
573: } else {
574: $ymax = 1;
575: }
576: }
577: #
578: my $xmax = $Labels[-1];
579: if ($xmax > 50) {
580: if ($xmax % 10 != 0) {
581: $xmax = 10 * (int($xmax/10)+1);
582: }
583: } else {
584: if ($xmax % 5 != 0) {
585: $xmax = 5 * (int($xmax/5)+1);
586: }
587: }
588: #
589: my $discdata .= '<data>'.join(',',@Labels).'</data>'.$/.
590: '<data>'.join(',',@Disc).'</data>'.$/;
591: #
592: my $diffdata .= '<data>'.join(',',@Labels).'</data>'.$/.
593: '<data>'.join(',',@Diff).'</data>'.$/;
594: #
595: $plot=<<"END";
596: <gnuplot
597: texfont="10"
598: fgcolor="x000000"
599: plottype="Cartesian"
600: font="large"
601: grid="on"
602: align="center"
603: border="on"
604: transparent="on"
605: alttag="Sample Plot"
606: samples="100"
607: bgcolor="xffffff"
608: height="$height"
609: width="$width">
610: <key
611: pos="top right"
612: title=""
613: box="off" />
614: <title>Degree of Discrmination and Degree of Difficulty</title>
615: <axis xmin="0" ymin="$ymin" xmax="$xmax" ymax="$ymax" color="x000000" />
616: <xlabel>Problem Number</xlabel>
617: <curve
618: linestyle="linespoints"
619: name="DoDisc"
620: pointtype="0"
621: color="x000000">
622: $discdata
623: </curve>
624: <curve
625: linestyle="linespoints"
626: name="DoDiff"
627: pointtype="0"
628: color="xFF0000">
629: $diffdata
630: </curve>
631: </gnuplot>
632: END
633: my $plotresult =
634: '<p>'.&Apache::lonxml::xmlparse($r,'web',$plot).'</p>'.$/;
635: $r->print($plotresult);
636: return;
637: }
638:
639: sub plot_dropdown {
640: my $current = '';
641: #
642: if (defined($ENV{'form.plot'})) {
643: $current = $ENV{'form.plot'};
644: }
645: #
646: my @Additional_Plots = (
647: { graphable=>'yes',
648: name => 'degrees',
649: title => 'DoDisc and DoDiff' });
650: #
651: my $Str= "\n".'<select name="plot" size="1">';
652: $Str .= '<option name="none"></option>'."\n";
653: $Str .= '<option name="none2">none</option>'."\n";
654: foreach my $field (@Fields,@Additional_Plots) {
655: if (! exists($field->{'graphable'}) ||
656: $field->{'graphable'} ne 'yes') {
657: next;
658: }
659: $Str .= '<option value="'.$field->{'name'}.'"';
660: if ($field->{'name'} eq $current) {
661: $Str .= ' selected ';
662: }
663: $Str.= '>'.&mt($field->{'title'}).'</option>'."\n";
664: }
665: $Str .= '</select>'."\n";
666: return $Str;
667: }
668:
669: ###############################################
670: ###############################################
671: ##
672: ## Excel output routines
673: ##
674: ###############################################
675: ###############################################
676: sub Excel_output {
677: my ($r) = @_;
678: $r->print('<h2>'.&mt('Preparing Excel Spreadsheet').'</h2>');
679: ##
680: ## Compute the statistics
681: &compute_all_statistics($r);
682: my $c = $r->connection;
683: return if ($c->aborted());
684: ##
685: ## Create the excel workbook
686: my $filename = '/prtspool/'.
687: $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.
688: time.'_'.rand(1000000000).'.xls';
689: my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
690: #
691: # Create sheet
692: my $excel_workbook = Spreadsheet::WriteExcel->new('/home/httpd'.$filename);
693: #
694: # Check for errors
695: if (! defined($excel_workbook)) {
696: $r->log_error("Error creating excel spreadsheet $filename: $!");
697: $r->print(&mt("Problems creating new Excel file. ".
698: "This error has been logged. ".
699: "Please alert your LON-CAPA administrator."));
700: return 0;
701: }
702: #
703: # The excel spreadsheet stores temporary data in files, then put them
704: # together. If needed we should be able to disable this (memory only).
705: # The temporary directory must be specified before calling 'addworksheet'.
706: # File::Temp is used to determine the temporary directory.
707: $excel_workbook->set_tempdir($Apache::lonnet::tmpdir);
708: #
709: # Add a worksheet
710: my $sheetname = $ENV{'course.'.$ENV{'request.course.id'}.'.description'};
711: if (length($sheetname) > 31) {
712: $sheetname = substr($sheetname,0,31);
713: }
714: my $excel_sheet = $excel_workbook->addworksheet(
715: &Apache::loncommon::clean_excel_name($sheetname));
716: ##
717: ## Begin creating excel sheet
718: ##
719: my ($rows_output,$cols_output) = (0,0);
720: #
721: # Put the course description in the header
722: $excel_sheet->write($rows_output,$cols_output++,
723: $ENV{'course.'.$ENV{'request.course.id'}.'.description'});
724: $cols_output += 3;
725: #
726: # Put a description of the sections listed
727: my $sectionstring = '';
728: my @Sections = @Apache::lonstatistics::SelectedSections;
729: if (scalar(@Sections) > 1) {
730: if (scalar(@Sections) > 2) {
731: my $last = pop(@Sections);
732: $sectionstring = "Sections ".join(', ',@Sections).', and '.$last;
733: } else {
734: $sectionstring = "Sections ".join(' and ',@Sections);
735: }
736: } else {
737: if ($Sections[0] eq 'all') {
738: $sectionstring = "All sections";
739: } else {
740: $sectionstring = "Section ".$Sections[0];
741: }
742: }
743: $excel_sheet->write($rows_output,$cols_output++,$sectionstring);
744: $cols_output += scalar(@Sections);
745: #
746: # Time restrictions
747: my $time_string;
748: if (defined($starttime)) {
749: # call localtime but not lonlocal:locallocaltime because excel probably
750: # cannot handle localized text. Probably.
751: $time_string .= 'Data collected from '.localtime($time_string);
752: if (defined($endtime)) {
753: $time_string .= ' to '.localtime($endtime);
754: }
755: $time_string .= '.';
756: } elsif (defined($endtime)) {
757: # See note above about lonlocal:locallocaltime
758: $time_string .= 'Data collected before '.localtime($endtime).'.';
759: }
760: #
761: # Put the date in there too
762: $excel_sheet->write($rows_output,$cols_output++,
763: 'Compiled on '.localtime(time));
764: #
765: $rows_output++;
766: $cols_output=0;
767: #
768: # Long Headers
769: foreach my $field (@Fields) {
770: next if ($field->{'name'} eq 'problem_num');
771: if (exists($field->{'long_title'})) {
772: $excel_sheet->write($rows_output,$cols_output++,
773: $field->{'long_title'});
774: } else {
775: $excel_sheet->write($rows_output,$cols_output++,'');
776: }
777: }
778: $rows_output++;
779: $cols_output=0;
780: # Brief headers
781: foreach my $field (@Fields) {
782: next if ($field->{'name'} eq 'problem_num');
783: # Use english for excel as I am not sure how well excel handles
784: # other character sets....
785: $excel_sheet->write($rows_output,$cols_output++,$field->{'title'});
786: }
787: $rows_output++;
788: foreach my $data (@StatsArray) {
789: $cols_output=0;
790: foreach my $field (@Fields) {
791: next if ($field->{'name'} eq 'problem_num');
792: $excel_sheet->write($rows_output,$cols_output++,
793: $data->{$field->{'name'}});
794: }
795: $rows_output++;
796: }
797: #
798: $excel_workbook->close();
799: #
800: # Tell the user where to get their excel file
801: $r->print('<br />'.
802: '<a href="'.$filename.'">'.
803: &mt('Your Excel Spreadsheet').'</a>'."\n");
804: $r->rflush();
805: return;
806: }
807:
808: ##################################################
809: ##################################################
810: ##
811: ## Statistics Gathering and Manipulation Routines
812: ##
813: ##################################################
814: ##################################################
815: sub compute_statistics_on_sequence {
816: my ($seq) = @_;
817: my @Data;
818: foreach my $res (@{$seq->{'contents'}}) {
819: next if ($res->{'type'} ne 'assessment');
820: foreach my $part (@{$res->{'parts'}}) {
821: #
822: # This is where all the work happens
823: my $data = &get_statistics($seq,$res,$part,scalar(@StatsArray)+1);
824: push (@Data,$data);
825: push (@StatsArray,$data);
826: }
827: }
828: return @Data;
829: }
830:
831: sub compute_all_statistics {
832: my ($r) = @_;
833: if (@StatsArray > 0) {
834: # Assume we have already computed the statistics
835: return;
836: }
837: my $c = $r->connection;
838: foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
839: last if ($c->aborted);
840: next if ($seq->{'num_assess'} < 1);
841: &compute_statistics_on_sequence($seq);
842: }
843: }
844:
845: sub sort_data {
846: my ($sortkey) = @_;
847: return if (! @StatsArray);
848: #
849: # Sort the data
850: my $sortby = undef;
851: foreach my $field (@Fields) {
852: if ($sortkey eq $field->{'name'}) {
853: $sortby = $field->{'name'};
854: }
855: }
856: if (! defined($sortby) || $sortby eq '' || $sortby eq 'problem_num') {
857: $sortby = 'container';
858: }
859: if ($sortby ne 'container') {
860: # $sortby is already defined, so we can charge ahead
861: if ($sortby =~ /^(title|part)$/i) {
862: # Alpha comparison
863: @StatsArray = sort {
864: lc($a->{$sortby}) cmp lc($b->{$sortby}) ||
865: lc($a->{'title'}) cmp lc($b->{'title'}) ||
866: lc($a->{'part'}) cmp lc($b->{'part'});
867: } @StatsArray;
868: } else {
869: # Numerical comparison
870: @StatsArray = sort {
871: my $retvalue = 0;
872: if ($b->{$sortby} eq 'nan') {
873: if ($a->{$sortby} ne 'nan') {
874: $retvalue = -1;
875: } else {
876: $retvalue = 0;
877: }
878: }
879: if ($a->{$sortby} eq 'nan') {
880: if ($b->{$sortby} ne 'nan') {
881: $retvalue = 1;
882: }
883: }
884: if ($retvalue eq '0') {
885: $retvalue = $b->{$sortby} <=> $a->{$sortby} ||
886: lc($a->{'title'}) <=> lc($b->{'title'}) ||
887: lc($a->{'part'}) <=> lc($b->{'part'});
888: }
889: $retvalue;
890: } @StatsArray;
891: }
892: }
893: #
894: # Renumber the data set
895: my $count;
896: foreach my $data (@StatsArray) {
897: $data->{'problem_num'} = ++$count;
898: }
899: return;
900: }
901:
902: ########################################################
903: ########################################################
904:
905: =pod
906:
907: =item &get_statistics()
908:
909: Wrapper routine from the call to loncoursedata::get_problem_statistics.
910: Calls lonstathelpers::get_time_limits() to limit the data set by time
911: and &compute_discrimination_factor
912:
913: Inputs: $sequence, $resource, $part, $problem_num
914:
915: Returns: Hash reference with statistics data from
916: loncoursedata::get_problem_statistics.
917:
918: =cut
919:
920: ########################################################
921: ########################################################
922: sub get_statistics {
923: my ($sequence,$resource,$part,$problem_num) = @_;
924: #
925: my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
926: my $symb = $resource->{'symb'};
927: my $courseid = $ENV{'request.course.id'};
928: #
929: my $data = &Apache::loncoursedata::get_problem_statistics
930: (\@Apache::lonstatistics::SelectedSections,
931: $Apache::lonstatistics::enrollment_status,
932: $symb,$part,$courseid,$starttime,$endtime);
933: $data->{'part'} = $part;
934: $data->{'problem_num'} = $problem_num;
935: $data->{'container'} = $sequence->{'title'};
936: $data->{'title'} = $resource->{'title'};
937: $data->{'title.link'} = $resource->{'src'}.'?symb='.
938: &Apache::lonnet::escape($resource->{'symb'});
939: #
940: $data->{'deg_of_disc'} = &compute_discrimination_factor($resource,$part,$sequence);
941: return $data;
942: }
943:
944:
945: ###############################################
946: ###############################################
947:
948: =pod
949:
950: =item &compute_discrimination_factor()
951:
952: Inputs: $Resource, $Sequence
953:
954: Returns: integer between -1 and 1
955:
956: =cut
957:
958: ###############################################
959: ###############################################
960: sub compute_discrimination_factor {
961: my ($resource,$part,$sequence) = @_;
962: my @Resources;
963: foreach my $res (@{$sequence->{'contents'}}) {
964: next if ($res->{'symb'} eq $resource->{'symb'});
965: push (@Resources,$res->{'symb'});
966: }
967: #
968: # rank
969: my $ranking =
970: &Apache::loncoursedata::rank_students_by_scores_on_resources
971: (\@Resources,
972: \@Apache::lonstatistics::SelectedSections,
973: $Apache::lonstatistics::enrollment_status,undef);
974: #
975: # compute their percent scores on the problems in the sequence,
976: my $number_to_grab = int(scalar(@{$ranking})/4);
977: my $num_students = scalar(@{$ranking});
978: my @BottomSet = map { $_->[&Apache::loncoursedata::RNK_student()];
979: } @{$ranking}[0..$number_to_grab];
980: my @TopSet =
981: map {
982: $_->[&Apache::loncoursedata::RNK_student()];
983: } @{$ranking}[($num_students-$number_to_grab)..($num_students-1)];
984: my ($bottom_sum,$bottom_max) =
985: &Apache::loncoursedata::get_sum_of_scores($resource,$part,\@BottomSet);
986: my ($top_sum,$top_max) =
987: &Apache::loncoursedata::get_sum_of_scores($resource,$part,\@TopSet);
988: my $deg_of_disc;
989: if ($top_max == 0 || $bottom_max==0) {
990: $deg_of_disc = 'nan';
991: } else {
992: $deg_of_disc = ($top_sum/$top_max) - ($bottom_sum/$bottom_max);
993: }
994: #&Apache::lonnet::logthis(' '.$top_sum.'/'.$top_max.
995: # ' - '.$bottom_sum.'/'.$bottom_max);
996: return $deg_of_disc;
997: }
998:
999: ###############################################
1000: ###############################################
1001:
1002: =pod
1003:
1004: =item ProblemStatisticsLegend
1005:
1006: =over 4
1007:
1008: =item #Stdnts
1009: Total number of students attempted the problem.
1010:
1011: =item Tries
1012: Total number of tries for solving the problem.
1013:
1014: =item Max Tries
1015: Largest number of tries for solving the problem by a student.
1016:
1017: =item Mean
1018: Average number of tries. [ Tries / #Stdnts ]
1019:
1020: =item #YES
1021: Number of students solved the problem correctly.
1022:
1023: =item #yes
1024: Number of students solved the problem by override.
1025:
1026: =item %Wrong
1027: Percentage of students who tried to solve the problem
1028: but is still incorrect. [ 100*((#Stdnts-(#YES+#yes))/#Stdnts) ]
1029:
1030: =item DoDiff
1031: Degree of Difficulty of the problem.
1032: [ 1 - ((#YES+#yes) / Tries) ]
1033:
1034: =item S.D.
1035: Standard Deviation of the tries.
1036: [ sqrt(sum((Xi - Mean)^2)) / (#Stdnts-1)
1037: where Xi denotes every student\'s tries ]
1038:
1039: =item Skew.
1040: Skewness of the students tries.
1041: [(sqrt( sum((Xi - Mean)^3) / #Stdnts)) / (S.D.^3)]
1042:
1043: =item Dis.F.
1044: Discrimination Factor: A Standard for evaluating the
1045: problem according to a Criterion<br>
1046:
1047: =item [Criterion to group students into %27 Upper Students -
1048: and %27 Lower Students]
1049: 1st Criterion for Sorting the Students:
1050: Sum of Partial Credit Awarded / Total Number of Tries
1051: 2nd Criterion for Sorting the Students:
1052: Total number of Correct Answers / Total Number of Tries
1053:
1054: =item Disc.
1055: Number of Students had at least one discussion.
1056:
1057: =back
1058:
1059: =cut
1060:
1061:
1062: ############################################################
1063: ############################################################
1064: ##
1065: ## How this all works:
1066: ## Statistics are computed by calling &get_statistics with the sequence,
1067: ## resource, and part id to run statistics on. At various places within
1068: ## the loops which compute the statistics, as well as before and after
1069: ## the entire process, subroutines can be called. The subroutines are
1070: ## registered to the following hooks:
1071: ##
1072: ## hook subroutine inputs
1073: ## ----------------------------------------------------------
1074: ## pre $r,$count
1075: ## pre_seq $r,$count,$seq
1076: ## pre_res $r,$count,$seq,$res
1077: ## calc $r,$count,$seq,$res,$data
1078: ## post_res $r,$count,$seq,$res
1079: ## post_seq $r,$count,$seq
1080: ## post $r,$count
1081: ##
1082: ## abort $r
1083: ##
1084: ## subroutines will be called in the order in which they are registered.
1085: ##
1086: ############################################################
1087: ############################################################
1088: {
1089:
1090: my %hooks;
1091: my $aborted = 0;
1092:
1093: sub abort_computation {
1094: $aborted = 1;
1095: }
1096:
1097: sub clear_hooks {
1098: $aborted = 0;
1099: undef(%hooks);
1100: }
1101:
1102: sub register_hook {
1103: my ($hookname,$subref)=@_;
1104: if ($hookname !~ /^(pre|pre_seq|pre_res|post|post_seq|post_res|calc)$/){
1105: return;
1106: }
1107: if (ref($subref) ne 'CODE') {
1108: &Apache::lonnet::logthis('attempt to register hook to non-code: '.
1109: $hookname,' = '.$subref);
1110: } else {
1111: if (exists($hooks{$hookname})) {
1112: push(@{$hooks{$hookname}},$subref);
1113: } else {
1114: $hooks{$hookname} = [$subref];
1115: }
1116: }
1117: return;
1118: }
1119:
1120: sub run_hooks {
1121: my $context = shift();
1122: foreach my $hook (@{$hooks{$context}}) {
1123: if ($aborted && $context ne 'abort') {
1124: last;
1125: }
1126: my $retvalue = $hook->(@_);
1127: if (defined($retvalue) && $retvalue eq '0') {
1128: $aborted = 1 if (! $aborted);
1129: }
1130: }
1131: }
1132:
1133: sub run_statistics {
1134: my ($r) = @_;
1135: my $count = 0;
1136: &run_hooks('pre',$r,$count);
1137: foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
1138: last if ($aborted);
1139: next if ($seq->{'num_assess'}<1);
1140: &run_hooks('pre_seq',$r,$count,$seq);
1141: foreach my $res (@{$seq->{'contents'}}) {
1142: last if ($aborted);
1143: next if ($res->{'type'} ne 'assessment');
1144: &run_hooks('pre_res',$r,$count,$seq,$res);
1145: foreach my $part (@{$res->{'parts'}}) {
1146: last if ($aborted);
1147: #
1148: # This is where all the work happens
1149: my $data = &get_statistics($seq,$res,$part,++$count);
1150: &run_hooks('calc',$r,$count,$seq,$res,$part,$data);
1151: }
1152: &run_hooks('post_res',$r,$count,$seq,$res);
1153: }
1154: &run_hooks('post_seq',$r,$count,$seq);
1155: }
1156: if ($aborted) {
1157: &run_hooks('abort',$r);
1158: } else {
1159: &run_hooks('post',$r,$count);
1160: }
1161: return;
1162: }
1163:
1164: } # End of %hooks scope
1165:
1166: ############################################################
1167: ############################################################
1168:
1169: 1;
1170: __END__
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>