Annotation of loncom/interface/statistics/lonproblemstatistics.pm, revision 1.73
1.1 stredwic 1: # The LearningOnline Network with CAPA
2: #
1.73 ! matthew 3: # $Id: lonproblemstatistics.pm,v 1.72 2004/03/23 20:08:58 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.73 ! matthew 62:
! 63: my @StatsArray;
! 64:
1.59 matthew 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: ##
1.49 matthew 72: my @Fields = (
73: { name => 'problem_num',
74: title => 'P#',
75: align => 'right',
76: color => '#FFFFE6' },
77: { name => 'container',
1.51 matthew 78: title => 'Sequence or Folder',
1.49 matthew 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',
1.55 matthew 91: color => '#FFFFE6',
92: },
1.49 matthew 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' },
1.73 ! matthew 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' },
1.49 matthew 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',
1.63 matthew 153: format => '%4.1f',# format => '%d',
1.49 matthew 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',
1.63 matthew 161: format => '%4.1f',# format => '%d',
1.49 matthew 162: sortable => 'yes',
163: graphable => 'yes',
164: long_title => 'Number of Students given Override' },
1.73 ! matthew 165: { name => 'num_wrong',
! 166: title => '#Wrng',
1.49 matthew 167: align => 'right',
1.73 ! matthew 168: color => '#FFDDDD',
1.49 matthew 169: format => '%4.1f',
170: sortable => 'yes',
171: graphable => 'yes',
1.55 matthew 172: long_title => 'Percent of students whose final answer is wrong' },
1.73 ! matthew 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) ]'},
1.71 matthew 182: { name => 'deg_of_disc',
1.73 ! matthew 183: title => 'DoDisc',
1.71 matthew 184: align => 'right',
185: color => '#FFFFE6',
186: format => '%4.2f',
187: sortable => 'yes',
188: graphable => 'yes',
189: long_title => 'Degree of Discrimination' },
1.49 matthew 190: );
191:
1.47 matthew 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
1.1 stredwic 203:
1.47 matthew 204: ###############################################
205: ###############################################
1.41 matthew 206: sub CreateInterface {
207: my $Str = '';
1.67 matthew 208: $Str .= &Apache::lonhtmlcommon::breadcrumbs
1.69 matthew 209: (undef,'Overall Problem Statistics','Statistics_Overall_Key');
1.41 matthew 210: $Str .= '<table cellspacing="5">'."\n";
211: $Str .= '<tr>';
1.59 matthew 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>';
1.70 matthew 215: $Str .= '<td rowspan="2">'.
216: &Apache::lonstathelpers::limit_by_time_form().'</td>';
1.41 matthew 217: $Str .= '</tr>'."\n";
218: #
219: $Str .= '<tr><td align="center">'."\n";
220: $Str .= &Apache::lonstatistics::SectionSelect('Section','multiple',5);
1.50 matthew 221: $Str .= '</td><td align="center">';
222: $Str .= &Apache::lonhtmlcommon::StatusOptions(undef,undef,5);
1.41 matthew 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";
1.59 matthew 237: $Str .= '<input type="submit" name="GenerateStatistics" value="'.
238: &mt('Generate Statistics').'" />';
1.54 matthew 239: $Str .= ' 'x5;
1.73 ! matthew 240: $Str .= 'Plot '.&plot_dropdown().(' 'x10);
1.59 matthew 241: $Str .= '<input type="submit" name="ClearCache" value="'.
242: &mt('Clear Caches').'" />';
1.54 matthew 243: $Str .= ' 'x5;
1.73 ! matthew 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;
1.41 matthew 251: }
1.25 stredwic 252:
1.41 matthew 253: ###############################################
254: ###############################################
1.28 stredwic 255:
1.47 matthew 256: =pod
257:
258: =item &BuildProblemStatisticsPage()
259:
260: Main interface to problem statistics.
261:
262: =cut
263:
1.41 matthew 264: ###############################################
265: ###############################################
266: sub BuildProblemStatisticsPage {
267: my ($r,$c)=@_;
1.61 matthew 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();
1.41 matthew 280: #
1.73 ! matthew 281: # Clear the package variables
! 282: undef(@StatsArray);
1.71 matthew 283: #
1.73 ! matthew 284: # Finally let the user know we are here
! 285: my $interface = &CreateInterface();
1.57 matthew 286: $r->print($interface);
1.41 matthew 287: $r->print('<input type="hidden" name="sortby" value="'.$ENV{'form.sortby'}.
288: '" />');
1.73 ! matthew 289: #
1.41 matthew 290: if (! exists($ENV{'form.statsfirstcall'})) {
1.73 ! matthew 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>');
1.41 matthew 299: return;
1.73 ! matthew 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" />');
1.28 stredwic 307: }
1.73 ! matthew 308: $r->rflush();
1.41 matthew 309: #
1.73 ! matthew 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());
1.41 matthew 344: #
1.73 ! matthew 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");
1.70 matthew 358: }
1.73 ! matthew 359: $r->print('</table>'."\n".'</table>'."\n");
1.41 matthew 360: $r->rflush();
1.28 stredwic 361: }
1.41 matthew 362: return;
363: }
1.21 stredwic 364:
1.73 ! matthew 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: }
1.44 matthew 387:
1.47 matthew 388:
1.73 ! matthew 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: }
1.47 matthew 406:
407:
1.44 matthew 408: ###############################################
409: ###############################################
1.73 ! matthew 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'}.'"';
1.41 matthew 423: }
1.73 ! matthew 424: $row .= '>';
! 425: if (exists($field->{'special'}) && $field->{'special'} eq 'link') {
! 426: $row .= '<a href="'.$data->{$field->{'name'}.'.link'}.'">';
1.41 matthew 427: }
1.73 ! matthew 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>';
1.21 stredwic 437: }
1.73 ! matthew 438: return $row;
1.41 matthew 439: }
1.25 stredwic 440:
1.73 ! matthew 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: }
1.26 stredwic 469:
1.73 ! matthew 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: }
1.47 matthew 488:
1.73 ! matthew 489: sub make_single_stat_plot {
! 490: my ($r,$datafield) = @_;
1.41 matthew 491: #
1.73 ! matthew 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;
1.49 matthew 505: }
506: #
1.73 ! matthew 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;
1.42 matthew 523: }
524: }
1.73 ! matthew 525: if ($max > 20000) {
! 526: $max = 10000*(int($max/10000)+1);
1.42 matthew 527: }
1.73 ! matthew 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;
1.42 matthew 568: }
569: }
1.73 ! matthew 570: if ($ymax > 0) {
! 571: if (abs($ymax) < 0.05) {
! 572: $ymax = 0;
1.42 matthew 573: } else {
1.73 ! matthew 574: $ymax = 1;
1.42 matthew 575: }
1.43 matthew 576: }
1.49 matthew 577: #
1.73 ! matthew 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);
1.49 matthew 586: }
1.26 stredwic 587: }
1.41 matthew 588: #
1.73 ! matthew 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);
1.41 matthew 636: return;
1.42 matthew 637: }
638:
1.73 ! matthew 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:
1.41 matthew 669: ###############################################
670: ###############################################
1.73 ! matthew 671: ##
! 672: ## Excel output routines
! 673: ##
1.41 matthew 674: ###############################################
675: ###############################################
1.73 ! matthew 676: sub Excel_output {
1.44 matthew 677: my ($r) = @_;
1.73 ! matthew 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
1.44 matthew 686: my $filename = '/prtspool/'.
687: $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.
1.73 ! matthew 688: time.'_'.rand(1000000000).'.xls';
1.70 matthew 689: my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
690: #
1.44 matthew 691: # Create sheet
1.73 ! matthew 692: my $excel_workbook = Spreadsheet::WriteExcel->new('/home/httpd'.$filename);
1.44 matthew 693: #
694: # Check for errors
695: if (! defined($excel_workbook)) {
696: $r->log_error("Error creating excel spreadsheet $filename: $!");
1.59 matthew 697: $r->print(&mt("Problems creating new Excel file. ".
1.44 matthew 698: "This error has been logged. ".
1.59 matthew 699: "Please alert your LON-CAPA administrator."));
1.73 ! matthew 700: return 0;
1.44 matthew 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: }
1.73 ! matthew 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);
1.44 matthew 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: #
1.70 matthew 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: #
1.44 matthew 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: #
1.73 ! matthew 768: # Long Headers
1.55 matthew 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
1.49 matthew 781: foreach my $field (@Fields) {
782: next if ($field->{'name'} eq 'problem_num');
1.59 matthew 783: # Use english for excel as I am not sure how well excel handles
784: # other character sets....
1.49 matthew 785: $excel_sheet->write($rows_output,$cols_output++,$field->{'title'});
1.44 matthew 786: }
787: $rows_output++;
1.73 ! matthew 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'}});
1.44 matthew 794: }
1.73 ! matthew 795: $rows_output++;
1.44 matthew 796: }
797: #
798: $excel_workbook->close();
1.73 ! matthew 799: #
1.44 matthew 800: # Tell the user where to get their excel file
801: $r->print('<br />'.
1.59 matthew 802: '<a href="'.$filename.'">'.
803: &mt('Your Excel Spreadsheet').'</a>'."\n");
1.44 matthew 804: $r->rflush();
805: return;
806: }
807:
1.73 ! matthew 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);
1.49 matthew 826: }
1.26 stredwic 827: }
1.73 ! matthew 828: return @Data;
1.41 matthew 829: }
1.26 stredwic 830:
1.73 ! matthew 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);
1.49 matthew 842: }
843: }
844:
1.73 ! matthew 845: sub sort_data {
! 846: my ($sortkey) = @_;
! 847: return if (! @StatsArray);
1.45 matthew 848: #
1.73 ! matthew 849: # Sort the data
! 850: my $sortby = undef;
1.49 matthew 851: foreach my $field (@Fields) {
1.73 ! matthew 852: if ($sortkey eq $field->{'name'}) {
! 853: $sortby = $field->{'name'};
1.45 matthew 854: }
1.26 stredwic 855: }
1.73 ! matthew 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;
1.24 stredwic 868: } else {
1.73 ! matthew 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;
1.24 stredwic 891: }
892: }
1.45 matthew 893: #
1.73 ! matthew 894: # Renumber the data set
! 895: my $count;
! 896: foreach my $data (@StatsArray) {
! 897: $data->{'problem_num'} = ++$count;
! 898: }
1.24 stredwic 899: return;
1.48 matthew 900: }
901:
1.70 matthew 902: ########################################################
903: ########################################################
904:
905: =pod
906:
907: =item &get_statistics()
908:
909: Wrapper routine from the call to loncoursedata::get_problem_statistics.
1.73 ! matthew 910: Calls lonstathelpers::get_time_limits() to limit the data set by time
! 911: and &compute_discrimination_factor
1.70 matthew 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: ########################################################
1.48 matthew 922: sub get_statistics {
1.49 matthew 923: my ($sequence,$resource,$part,$problem_num) = @_;
1.48 matthew 924: #
1.70 matthew 925: my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
1.49 matthew 926: my $symb = $resource->{'symb'};
1.48 matthew 927: my $courseid = $ENV{'request.course.id'};
928: #
1.49 matthew 929: my $data = &Apache::loncoursedata::get_problem_statistics
1.66 matthew 930: (\@Apache::lonstatistics::SelectedSections,
931: $Apache::lonstatistics::enrollment_status,
1.70 matthew 932: $symb,$part,$courseid,$starttime,$endtime);
1.49 matthew 933: $data->{'part'} = $part;
934: $data->{'problem_num'} = $problem_num;
935: $data->{'container'} = $sequence->{'title'};
936: $data->{'title'} = $resource->{'title'};
1.53 matthew 937: $data->{'title.link'} = $resource->{'src'}.'?symb='.
938: &Apache::lonnet::escape($resource->{'symb'});
1.49 matthew 939: #
1.71 matthew 940: $data->{'deg_of_disc'} = &compute_discrimination_factor($resource,$part,$sequence);
1.49 matthew 941: return $data;
1.71 matthew 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;
1.1 stredwic 997: }
1.12 minaeibi 998:
1.45 matthew 999: ###############################################
1000: ###############################################
1.47 matthew 1001:
1002: =pod
1003:
1.73 ! matthew 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.
1.59 matthew 1013:
1.73 ! matthew 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
1.47 matthew 1058:
1059: =cut
1.1 stredwic 1060:
1.73 ! matthew 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;
1.1 stredwic 1162: }
1.24 stredwic 1163:
1.73 ! matthew 1164: } # End of %hooks scope
! 1165:
! 1166: ############################################################
! 1167: ############################################################
1.4 minaeibi 1168:
1.1 stredwic 1169: 1;
1170: __END__
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>