--- loncom/interface/statistics/lonproblemstatistics.pm 2004/03/29 19:41:24 1.76 +++ loncom/interface/statistics/lonproblemstatistics.pm 2004/10/06 15:37:59 1.94 @@ -1,6 +1,6 @@ # The LearningOnline Network with CAPA # -# $Id: lonproblemstatistics.pm,v 1.76 2004/03/29 19:41:24 matthew Exp $ +# $Id: lonproblemstatistics.pm,v 1.94 2004/10/06 15:37:59 matthew Exp $ # # Copyright Michigan State University Board of Trustees # @@ -55,12 +55,14 @@ use Apache::loncommon(); use Apache::lonhtmlcommon; use Apache::loncoursedata; use Apache::lonstatistics; +use LONCAPA::lonmetadata(); use Apache::lonlocal; use Spreadsheet::WriteExcel; use Apache::lonstathelpers(); use Time::HiRes; my @StatsArray; +my %SeqStat; # keys are symbs, values are hash refs ## ## Localization notes: @@ -69,13 +71,42 @@ my @StatsArray; ## header for plots created with Graph.pm, both of which more than likely do ## not support localization. ## +# +# +## +## Description of Field attributes +## +## Attribute Required Value Meaning or Use +## +## name yes any scalar Used to uniquely identify field +## title yes any scalar This is what the user sees to identify +## the field. Passed through &mt(). +## long_title yes any scalar Used as graph heading and in excel +## output. NOT translated +## align no (left|right|center) HTML cell contents alignment +## color yes html color HTML cell background color +## used to visually group statistics +## special no (link) Indicates a link, target is name.link +## Currently set in &get_statistics() +## graphable no (yes|no) Can a bar graph of the field be +## produced? +## sortable no (yes|no) Should a sort link be put in the +## column header? +## selectable yes (yes|no) Can the column be removed from the +## statistics display? +## selected yes (yes|no) Is the column selected by default? +## +## format no sprintf format string +## +## excel_format no excel format type +## (see &Apache::loncommon::define_excel_formats my @Fields = ( { name => 'problem_num', title => 'P#', align => 'right', color => '#FFFFE6', selectable => 'no', - selected => 'yes', + defaultselected => 'yes', }, { name => 'container', title => 'Sequence or Folder', @@ -83,7 +114,7 @@ my @Fields = ( color => '#FFFFE6', sortable => 'yes', selectable => 'no', - selected => 'yes', + defaultselected => 'yes', }, { name => 'title', title => 'Title', @@ -92,14 +123,14 @@ my @Fields = ( special => 'link', sortable => 'yes', selectable => 'no', - selected => 'yes', + defaultselected => 'yes', }, { name => 'part', title => 'Part', align => 'left', color => '#FFFFE6', selectable => 'no', - selected => 'yes', + defaultselected => 'yes', }, { name => 'num_students', title => '#Stdnts', @@ -110,7 +141,7 @@ my @Fields = ( graphable => 'yes', long_title => 'Number of Students Attempting Problem', selectable => 'yes', - selected => 'yes', + defaultselected => 'yes', }, { name => 'tries', title => 'Tries', @@ -121,7 +152,7 @@ my @Fields = ( graphable => 'yes', long_title => 'Total Number of Tries', selectable => 'yes', - selected => 'yes', + defaultselected => 'yes', }, { name => 'max_tries', title => 'Max Tries', @@ -132,7 +163,7 @@ my @Fields = ( graphable => 'yes', long_title => 'Maximum Number of Tries', selectable => 'yes', - selected => 'yes', + defaultselected => 'yes', }, { name => 'min_tries', title => 'Min Tries', @@ -143,7 +174,7 @@ my @Fields = ( graphable => 'yes', long_title => 'Minumum Number of Tries', selectable => 'yes', - selected => 'yes', + defaultselected => 'yes', }, { name => 'mean_tries', title => 'Mean Tries', @@ -154,7 +185,7 @@ my @Fields = ( graphable => 'yes', long_title => 'Average Number of Tries', selectable => 'yes', - selected => 'yes', + defaultselected => 'yes', }, { name => 'std_tries', title => 'S.D. tries', @@ -165,7 +196,7 @@ my @Fields = ( graphable => 'yes', long_title => 'Standard Deviation of Number of Tries', selectable => 'yes', - selected => 'yes', + defaultselected => 'yes', }, { name => 'skew_tries', title => 'Skew Tries', @@ -176,7 +207,7 @@ my @Fields = ( graphable => 'yes', long_title => 'Skew of Number of Tries', selectable => 'yes', - selected => 'no', + defaultselected => 'no', }, { name => 'num_solved', title => '#YES', @@ -186,8 +217,8 @@ my @Fields = ( sortable => 'yes', graphable => 'yes', long_title => 'Number of Students able to Solve', - selectable => 'no', - selected => 'yes', + selectable => 'yes', + defaultselected => 'yes', }, { name => 'num_override', title => '#yes', @@ -198,7 +229,7 @@ my @Fields = ( graphable => 'yes', long_title => 'Number of Students given Override', selectable => 'yes', - selected => 'yes', + defaultselected => 'yes', }, { name => 'num_wrong', title => '#Wrng', @@ -207,9 +238,20 @@ my @Fields = ( format => '%4.1f', sortable => 'yes', graphable => 'yes', + long_title => 'Number of students whose final answer is wrong', + selectable => 'yes', + defaultselected => 'yes', + }, + { name => 'per_wrong', + title => '%Wrng', + align => 'right', + color => '#FFDDDD', + format => '%4.1f', + sortable => 'yes', + graphable => 'yes', long_title => 'Percent of students whose final answer is wrong', selectable => 'yes', - selected => 'yes', + defaultselected => 'yes', }, { name => 'deg_of_diff', title => 'DoDiff', @@ -221,7 +263,7 @@ my @Fields = ( long_title => 'Degree of Difficulty'. '[ 1 - ((#YES+#yes) / Tries) ]', selectable => 'yes', - selected => 'yes', + defaultselected => 'yes', }, { name => 'deg_of_disc', title => 'DoDisc', @@ -232,8 +274,218 @@ my @Fields = ( graphable => 'yes', long_title => 'Degree of Discrimination', selectable => 'yes', - selected => 'no', + defaultselected => 'yes', }, +## duedate included for research purposes. Commented out most of the time. +# { name => 'duedate', +# title => 'Due Date', +# align => 'left', +# color => '#FFFFFF', +# sortable => 'yes', +# graphable => 'no', +# long_title => 'Due date of resource for instructor', +# selectable => 'no', +# defaultselected => 'yes', +# }, +## opendate included for research purposes. Commented out most of the time. +# { name => 'opendate', +# title => 'Open Date', +# align => 'left', +# color => '#FFFFFF', +# sortable => 'yes', +# graphable => 'no', +# long_title => 'date resource became answerable', +# selectable => 'no', +# defaultselected => 'yes', +# }, +## symb included for research purposes. Commented out most of the time. +# { name => 'symb', +# title => 'Symb', +# align => 'left', +# color => '#FFFFFF', +# sortable => 'yes', +# graphable => 'no', +# long_title => 'Unique LON-CAPA identifier for problem', +# selectable => 'no', +# defaultselected => 'yes', +# }, +## resptypes included for research purposes. Commented out most of the time. +# { name => 'resptypes', +# title => 'Response Types', +# align => 'left', +# color => '#FFFFFF', +# sortable => 'no', +# graphable => 'no', +# long_title => 'Response Types used in this problem', +# selectable => 'no', +# defaultselected => 'yes', +# }, +## maxtries included for research purposes. Commented out most of the time. +# { name => 'maxtries', +# title => 'Maxtries', +# align => 'left', +# color => '#FFFFFF', +# sortable => 'no', +# graphable => 'no', +# long_title => 'Maximum number of tries', +# selectable => 'no', +# defaultselected => 'yes', +# }, +## hinttries included for research purposes. Commented out most of the time. +# { name => 'hinttries', +# title => 'hinttries', +# align => 'left', +# color => '#FFFFFF', +# sortable => 'no', +# graphable => 'no', +# long_title => 'Number of tries before a hint appears', +# selectable => 'no', +# defaultselected => 'yes', +# }, +); + +my @SeqFields = ( + { name => 'title', + title => 'Sequence', + align => 'left', + color => '#FFFFE6', + special => 'no', + sortable => 'no', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'items', + title => '#Items', + align => 'right', + color => '#FFFFE6', + format => '%4d', + sortable => 'no', + graphable => 'no', + long_title => 'Number of Items in Sequence', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'scoremean', + title => 'Score Mean', + align => 'right', + color => '#FFFFE6', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'Mean Sequence Score', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'scorestd', + title => 'Score STD', + align => 'right', + color => '#FFFFE6', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'Standard Deviation of Sequence Scores', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'scoremax', + title => 'Score Max', + align => 'right', + color => '#FFFFE6', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'Maximum Sequence Score', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'scoremin', + title => 'Score Min', + align => 'right', + color => '#FFFFE6', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'Minumum Sequence Score', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'scorecount', + title => 'Score N', + align => 'right', + color => '#FFFFE6', + format => '%4d', + sortable => 'no', + graphable => 'no', + long_title => 'Number of Students in score computations', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'countmean', + title => 'Count Mean', + align => 'right', + color => '#FFFFFF', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'Mean Sequence Score', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'countstd', + title => 'Count STD', + align => 'right', + color => '#FFFFFF', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'Standard Deviation of Sequence Scores', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'countmax', + title => 'Count Max', + align => 'right', + color => '#FFFFFF', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'Maximum Number of Correct Problems', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'countmin', + title => 'Count Min', + align => 'right', + color => '#FFFFFF', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'Minumum Number of Correct Problems', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'count', + title => 'Count N', + align => 'right', + color => '#FFFFFF', + format => '%4d', + sortable => 'no', + graphable => 'no', + long_title => 'Number of Students in score computations', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'KR-21', + title => 'KR-21', + align => 'right', + color => '#FFAAAA', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'KR-21 reliability statistic', + selectable => 'yes', + defaultselected => 'no', + }, ); my %SelectedFields; @@ -245,12 +497,28 @@ sub parse_field_selection { $ENV{'form.fieldselections'} = []; foreach my $field (@Fields) { next if ($field->{'selectable'} ne 'yes'); - if ($field->{'selected'} eq 'yes') { + if ($field->{'defaultselected'} eq 'yes') { push(@{$ENV{'form.fieldselections'}},$field->{'name'}); } } } # + # Make sure the data we are plotting is there + my %NeededFields; + if (exists($ENV{'form.plot'}) && $ENV{'form.plot'} ne '' && + $ENV{'form.plot'} ne 'none') { + if ($ENV{'form.plot'} eq 'degrees') { + $NeededFields{'deg_of_diff'}++; + $NeededFields{'deg_of_disc'}++; + } elsif ($ENV{'form.plot'} eq 'tries statistics') { + $NeededFields{'mean_tries'}++; + $NeededFields{'std_tries'}++; + $NeededFields{'problem_num'}++; + } else { + $NeededFields{$ENV{'form.plot'}}++; + } + } + # # This should not happen, but in case it does... if (ref($ENV{'form.fieldselections'}) ne 'ARRAY') { $ENV{'form.fieldselections'} = [$ENV{'form.fieldselections'}]; @@ -259,8 +527,15 @@ sub parse_field_selection { # Set the field data and the selected fields (for easier checking) undef(%SelectedFields); foreach my $field (@Fields) { - next if ($field->{'selectable'} ne 'yes'); - $field->{'selected'} = 'no'; + if ($field->{'selectable'} ne 'yes') { + $field->{'selected'} = 'yes'; + } else { + $field->{'selected'} = 'no'; + } + if (exists($NeededFields{$field->{'name'}})) { + $field->{'selected'} = 'yes'; + $SelectedFields{$field->{'name'}}++; + } foreach my $selection (@{$ENV{'form.fieldselections'}}) { if ($selection eq $field->{'name'} || $selection eq 'all') { $field->{'selected'} = 'yes'; @@ -268,6 +543,11 @@ sub parse_field_selection { } } } + # + # Always show all the sequence statistics (for now) + foreach my $field (@SeqFields) { + $field->{'selected'} = 'yes'; + } return; } @@ -300,7 +580,10 @@ select sections, maps, and output. ############################################### ############################################### sub CreateInterface { + my ($r) = @_; + # &parse_field_selection(); + # my $Str = ''; $Str .= &Apache::lonhtmlcommon::breadcrumbs (undef,'Overall Problem Statistics','Statistics_Overall_Key'); @@ -333,19 +616,18 @@ sub CreateInterface { $Str .= '</td><td>'.&field_selection_input(); $Str .= '</td></tr>'."\n"; $Str .= '</table>'."\n"; + # + $Str .= '<p>'.&mt('Status: [_1]', + '<input type="text" '. + 'name="stats_status" size="60" value="" />' + ). + '</nobr></p>'; + # $Str .= '<input type="submit" name="GenerateStatistics" value="'. &mt('Generate Statistics').'" />'; $Str .= ' 'x5; $Str .= 'Plot '.&plot_dropdown().(' 'x10); - $Str .= '<input type="submit" name="ClearCache" value="'. - &mt('Clear Caches').'" />'; - $Str .= ' 'x5; - $Str .= '<input type="submit" name="UpdateCache" value="'. - &mt('Update Student Data').'" />'; - $Str .= ' 'x5; - $Str .= '<input type="submit" name="Excel" value="'. - &mt('Produce Excel Output').'" />'; - $Str .= ' 'x5; + # return $Str; } @@ -369,7 +651,8 @@ sub BuildProblemStatisticsPage { 'statsoutputmode' => 'scalar', 'Section' => 'array', 'StudentData' => 'array', - 'Maps' => 'array'); + 'Maps' => 'array', + 'fieldselections'=> 'array'); &Apache::loncommon::store_course_settings('statistics', \%Saveable_Parameters); &Apache::loncommon::restore_course_settings('statistics', @@ -379,15 +662,23 @@ sub BuildProblemStatisticsPage { # # Clear the package variables undef(@StatsArray); + undef(%SeqStat); # # Finally let the user know we are here - my $interface = &CreateInterface(); + my $interface = &CreateInterface($r); $r->print($interface); $r->print('<input type="hidden" name="sortby" value="'.$ENV{'form.sortby'}. '" />'); # - if (! exists($ENV{'form.statsfirstcall'})) { - $r->print('<input type="hidden" name="statsfirstcall" value="yes" />'); + my @CacheButtonHTML = + &Apache::lonstathelpers::manage_caches($r,'Statistics','stats_status'); + my $Str; + foreach my $html (@CacheButtonHTML) { + $Str.=$html.(' 'x5); + } + # + $r->print($Str); + if (! exists($ENV{'form.firstrun'})) { $r->print('<h3>'. &mt('Press "Generate Statistics" when you are ready.'). '</h3><p>'. @@ -396,13 +687,6 @@ sub BuildProblemStatisticsPage { ' will not have this delay.'). '</p>'); return; - } elsif ($ENV{'form.statsfirstcall'} eq 'yes' || - exists($ENV{'form.UpdateCache'}) || - exists($ENV{'form.ClearCache'}) ) { - $r->print('<input type="hidden" name="statsfirstcall" value="no" />'); - &Apache::lonstatistics::Gather_Student_Data($r); - } else { - $r->print('<input type="hidden" name="statsfirstcall" value="no" />'); } $r->rflush(); # @@ -412,10 +696,13 @@ sub BuildProblemStatisticsPage { # if (exists($ENV{'form.Excel'})) { &Excel_output($r); - } else { + } else { + $r->print('<input type="submit" name="Excel" value="'. + &mt('Produce Excel Output').'" />'.' 'x5); + $r->rflush(); my $count = 0; foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) { - $count += $seq->{'num_assess'}; + $count += $seq->{'num_assess_parts'}; } if ($count > 10) { $r->print('<h2>'. @@ -434,17 +721,42 @@ sub BuildProblemStatisticsPage { undef($plot); } if ($sortby eq 'container' && ! defined($plot)) { + &output_sequence_statistics($r); &output_html_by_sequence($r); } else { if (defined($plot)) { &make_plot($r,$plot); } &output_html_stats($r); + &output_sequence_statistics($r); } } return; } +sub output_sequence_statistics { + my ($r) = @_; + my $c=$r->connection(); + $r->print('<h2>'.&mt('Sequence Statistics'). + &Apache::loncommon::help_open_topic('Statistics_Sequence'). + '</h2>'); + $r->print('<table border="0"><tr><td bgcolor="#777777">'."\n". + '<table border="0" cellpadding="3">'."\n". + '<tr bgcolor="#FFFFE6">'); + $r->print(&sequence_html_header()); + foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) { + last if ($c->aborted); + next if ($seq->{'num_assess'} < 1); + &compute_sequence_statistics($seq); + $r->print(&sequence_html_output($seq)); + } + $r->print('</table>'); + $r->print('</table>'); + $r->rflush(); + return; +} + + ########################################################## ########################################################## ## @@ -540,7 +852,7 @@ sub statistics_html_table_data { if (exists($field->{'special'}) && $field->{'special'} eq 'link') { $row .= '<a href="'.$data->{$field->{'name'}.'.link'}.'">'; } - if (exists($field->{'format'})) { + if (exists($field->{'format'}) && $data->{$field->{'name'}} !~ /[A-Z]/i) { $row .= sprintf($field->{'format'},$data->{$field->{'name'}}); } else { $row .= $data->{$field->{'name'}}; @@ -583,6 +895,40 @@ sub statistics_table_header { return $header_row; } +sub sequence_html_header { + my $Str .= '<tr>'; + foreach my $field (@SeqFields) { +# next if ($field->{'selected'} ne 'yes'); + $Str .= '<th bgcolor="'.$field->{'color'}.'"'; + $Str .= '>'.$field->{'title'}.'</th>'; + } + $Str .= '</tr>'; + return $Str; +} + + +sub sequence_html_output { + my ($seq) = @_; + my $data = $SeqStat{$seq->{'symb'}}; + my $row = '<tr>'; + foreach my $field (@SeqFields) { + next if ($field->{'selected'} ne 'yes'); + $row .= '<td bgcolor="'.$field->{'color'}.'"'; + if (exists($field->{'align'})) { + $row .= ' align="'.$field->{'align'}.'"'; + } + $row .= '>'; + if (exists($field->{'format'})) { + $row .= sprintf($field->{'format'},$data->{$field->{'name'}}); + } else { + $row .= $data->{$field->{'name'}}; + } + $row .= '</td>'; + } + $row .= '</tr>'."\n"; + return $row; +} + #################################################### #################################################### ## @@ -710,6 +1056,11 @@ sub degrees_plot { my $diffdata .= '<data>'.join(',',@Labels).'</data>'.$/. '<data>'.join(',',@Diff).'</data>'.$/; # + my $title = 'Degree of Discrimination\nand Degree of Difficulty'; + if ($xmax > 50) { + $title = 'Degree of Discrimination and Degree of Difficulty'; + } + # $plot=<<"END"; <gnuplot texfont="10" @@ -720,7 +1071,7 @@ sub degrees_plot { align="center" border="on" transparent="on" - alttag="Sample Plot" + alttag="Degree of Discrimination and Degree of Difficulty Plot" samples="100" bgcolor="xffffff" height="$height" @@ -729,7 +1080,7 @@ sub degrees_plot { pos="top right" title="" box="off" /> - <title>Degree of Discrmination and Degree of Difficulty</title> + <title>$title</title> <axis xmin="0" ymin="$ymin" xmax="$xmax" ymax="$ymax" color="x000000" /> <xlabel>Problem Number</xlabel> <curve @@ -793,6 +1144,11 @@ sub tries_data_plot { '<data>'.join(',',@Mean).'</data>'.$/. '<data>'.join(',',@STD).'</data>'.$/; # + my $title = 'Mean and S.D. of Tries'; + if ($xmax > 25) { + $title = 'Mean and Standard Deviation of Tries'; + } + # $plot=<<"END"; <gnuplot texfont="10" @@ -803,14 +1159,15 @@ sub tries_data_plot { align="center" border="on" transparent="on" - alttag="Sample Plot" + alttag="Mean and S.D of Tries Plot" samples="100" bgcolor="xffffff" height="$height" width="$width"> - <title>Mean and S.D. of Tries</title> + <title>$title</title> <axis xmin="0" ymin="0" xmax="$xmax" ymax="$ymax" color="x000000" /> <xlabel>Problem Number</xlabel> + <ylabel>Number of Tries</ylabel> <curve linestyle="yerrorbars" name="S.D. Tries" @@ -843,15 +1200,15 @@ sub plot_dropdown { my @Additional_Plots = ( { graphable=>'yes', name => 'degrees', - title => 'DoDisc and DoDiff' }, + title => 'Difficulty Indexes' }, { graphable=>'yes', name => 'tries statistics', - title => 'Mean and S.D. of Tries' }); + title => 'Tries Statistics' }); # my $Str= "\n".'<select name="plot" size="1">'; $Str .= '<option name="none"></option>'."\n"; $Str .= '<option name="none2">none</option>'."\n"; - foreach my $field (@Fields,@Additional_Plots) { + foreach my $field (@Additional_Plots,@Fields) { if (! exists($field->{'graphable'}) || $field->{'graphable'} ne 'yes') { next; @@ -913,6 +1270,8 @@ sub Excel_output { } my $excel_sheet = $excel_workbook->addworksheet( &Apache::loncommon::clean_excel_name($sheetname)); + # + my $format = &Apache::loncommon::define_excel_formats($excel_workbook); ## ## Begin creating excel sheet ## @@ -920,28 +1279,17 @@ sub Excel_output { # # Put the course description in the header $excel_sheet->write($rows_output,$cols_output++, - $ENV{'course.'.$ENV{'request.course.id'}.'.description'}); + $ENV{'course.'.$ENV{'request.course.id'}.'.description'}, + $format->{'h1'}); $cols_output += 3; # # Put a description of the sections listed my $sectionstring = ''; - my @Sections = @Apache::lonstatistics::SelectedSections; - if (scalar(@Sections) > 1) { - if (scalar(@Sections) > 2) { - my $last = pop(@Sections); - $sectionstring = "Sections ".join(', ',@Sections).', and '.$last; - } else { - $sectionstring = "Sections ".join(' and ',@Sections); - } - } else { - if ($Sections[0] eq 'all') { - $sectionstring = "All sections"; - } else { - $sectionstring = "Section ".$Sections[0]; - } - } - $excel_sheet->write($rows_output,$cols_output++,$sectionstring); - $cols_output += scalar(@Sections); + $excel_sheet->write($rows_output,$cols_output++, + &Apache::lonstathelpers::sections_description + (@Apache::lonstatistics::SelectedSections), + $format->{'h3'}); + $cols_output += scalar(@Apache::lonstatistics::SelectedSections); # # Time restrictions my $time_string; @@ -957,6 +1305,10 @@ sub Excel_output { # See note above about lonlocal:locallocaltime $time_string .= 'Data collected before '.localtime($endtime).'.'; } + if (defined($time_string)) { + $excel_sheet->write($rows_output,$cols_output++,$time_string); + $cols_output+= 5; + } # # Put the date in there too $excel_sheet->write($rows_output,$cols_output++, @@ -964,38 +1316,49 @@ sub Excel_output { # $rows_output++; $cols_output=0; - # - # Long Headers - foreach my $field (@Fields) { - next if ($field->{'name'} eq 'problem_num'); - next if ($field->{'selected'} ne 'yes'); - if (exists($field->{'long_title'})) { + ## + ## Sequence Statistics + ## + &write_headers($excel_sheet,$format,\$rows_output,\$cols_output, + \@SeqFields); + foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) { + next if ($seq->{'num_assess'} < 1); + my $data = $SeqStat{$seq->{'symb'}}; + $cols_output=0; + foreach my $field (@SeqFields) { + next if ($field->{'selected'} ne 'yes'); + my $fieldformat = undef; + if (exists($field->{'excel_format'})) { + $fieldformat = $format->{$field->{'excel_format'}}; + } $excel_sheet->write($rows_output,$cols_output++, - $field->{'long_title'}); - } else { - $excel_sheet->write($rows_output,$cols_output++,''); + $data->{$field->{'name'}},$fieldformat); } + $rows_output++; + $cols_output=0; } + ## + ## Resource Statistics + ## $rows_output++; $cols_output=0; - # Brief headers - foreach my $field (@Fields) { - next if ($field->{'selected'} ne 'yes'); - next if ($field->{'name'} eq 'problem_num'); - # Use english for excel as I am not sure how well excel handles - # other character sets.... - $excel_sheet->write($rows_output,$cols_output++,$field->{'title'}); - } - $rows_output++; + &write_headers($excel_sheet,$format,\$rows_output,\$cols_output, + \@Fields); + # foreach my $data (@StatsArray) { $cols_output=0; foreach my $field (@Fields) { next if ($field->{'selected'} ne 'yes'); next if ($field->{'name'} eq 'problem_num'); + my $fieldformat = undef; + if (exists($field->{'excel_format'})) { + $fieldformat = $format->{$field->{'excel_format'}}; + } $excel_sheet->write($rows_output,$cols_output++, - $data->{$field->{'name'}}); + $data->{$field->{'name'}},$fieldformat); } $rows_output++; + $cols_output=0; } # $excel_workbook->close(); @@ -1008,6 +1371,44 @@ sub Excel_output { return; } +## +## &write_headers +## +sub write_headers { + my ($excel_sheet,$format,$rows_output,$cols_output,$Fields) = @_; + ## + ## First the long titles + foreach my $field (@{$Fields}) { + next if ($field->{'name'} eq 'problem_num'); + next if ($field->{'selected'} ne 'yes'); + if (exists($field->{'long_title'})) { + $excel_sheet->write($$rows_output,${$cols_output}, + $field->{'long_title'}, + $format->{'bold'}); + } else { + $excel_sheet->write($$rows_output,${$cols_output},''); + } + ${$cols_output}+= 1; + } + ${$cols_output} =0; + ${$rows_output}+=1; + ## + ## Then the short titles + foreach my $field (@{$Fields}) { + next if ($field->{'selected'} ne 'yes'); + next if ($field->{'name'} eq 'problem_num'); + # Use english for excel as I am not sure how well excel handles + # other character sets.... + $excel_sheet->write($$rows_output,$$cols_output, + $field->{'title'}, + $format->{'bold'}); + $$cols_output+=1; + } + ${$cols_output} =0; + ${$rows_output}+=1; + return; +} + ################################################## ################################################## ## @@ -1021,6 +1422,7 @@ sub compute_statistics_on_sequence { foreach my $res (@{$seq->{'contents'}}) { next if ($res->{'type'} ne 'assessment'); foreach my $part (@{$res->{'parts'}}) { + next if ($res->{'partdata'}->{$part}->{'Survey'}); # # This is where all the work happens my $data = &get_statistics($seq,$res,$part,scalar(@StatsArray)+1); @@ -1041,6 +1443,7 @@ sub compute_all_statistics { foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) { last if ($c->aborted); next if ($seq->{'num_assess'} < 1); + &compute_sequence_statistics($seq); &compute_statistics_on_sequence($seq); } } @@ -1133,6 +1536,7 @@ sub get_statistics { (\@Apache::lonstatistics::SelectedSections, $Apache::lonstatistics::enrollment_status, $symb,$part,$courseid,$starttime,$endtime); + $data->{'symb'} = $symb; $data->{'part'} = $part; $data->{'problem_num'} = $problem_num; $data->{'container'} = $sequence->{'title'}; @@ -1144,10 +1548,35 @@ sub get_statistics { $data->{'deg_of_disc'} = &compute_discrimination_factor($resource,$part,$sequence); } + # + # Store in metadata if computations were done for all students + if ($data->{'num_students'} > 1) { + my @Sections = @Apache::lonstatistics::SelectedSections; + my $sections = '"'.join(' ',@Sections).'"'; + $sections =~ s/&+/_/g; # Ensure no special characters + $data->{'sections'}=$sections; + $data->{'course'} = $ENV{'request.course.id'}; + my $urlres=(&Apache::lonnet::decode_symb($resource->{'symb'}))[2]; + $data->{'urlres'}=$urlres; + my %storestats = + &LONCAPA::lonmetadata::dynamic_metadata_storage($data); + my ($dom,$user) = $urlres=~/^(\w+)\/(\w+)/; + &Apache::lonnet::put('nohist_resevaldata',\%storestats,$dom,$user); + } + # + # Get the due date for research purposes (commented out most of the time) +# $data->{'duedate'} = +# &Apache::lonnet::EXT('resource.'.$part.'.duedate',$symb); +# $data->{'opendate'} = +# &Apache::lonnet::EXT('resource.'.$part.'.opendate',$symb); +# $data->{'maxtries'} = +# &Apache::lonnet::EXT('resource.'.$part.'.maxtries',$symb); +# $data->{'hinttries'} = +# &Apache::lonnet::EXT('resource.'.$part.'.hinttries',$symb); +# $data->{'resptypes'} = join(',',@{$resource->{'partdata'}->{$part}->{'ResponseTypes'}}); return $data; } - ############################################### ############################################### @@ -1172,11 +1601,13 @@ sub compute_discrimination_factor { } # # rank + my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits(); my $ranking = &Apache::loncoursedata::rank_students_by_scores_on_resources (\@Resources, \@Apache::lonstatistics::SelectedSections, - $Apache::lonstatistics::enrollment_status,undef); + $Apache::lonstatistics::enrollment_status,undef, + $starttime,$endtime); # # compute their percent scores on the problems in the sequence, my $number_to_grab = int(scalar(@{$ranking})/4); @@ -1187,10 +1618,16 @@ sub compute_discrimination_factor { map { $_->[&Apache::loncoursedata::RNK_student()]; } @{$ranking}[($num_students-$number_to_grab)..($num_students-1)]; + if (! @BottomSet || (@BottomSet == 1 && $BottomSet[0] eq '') || + ! @TopSet || (@TopSet == 1 && $TopSet[0] eq '')) { + return 'nan'; + } my ($bottom_sum,$bottom_max) = - &Apache::loncoursedata::get_sum_of_scores($resource,$part,\@BottomSet); + &Apache::loncoursedata::get_sum_of_scores($resource,$part,\@BottomSet, + undef,$starttime,$endtime); my ($top_sum,$top_max) = - &Apache::loncoursedata::get_sum_of_scores($resource,$part,\@TopSet); + &Apache::loncoursedata::get_sum_of_scores($resource,$part,\@TopSet, + undef,$starttime,$endtime); my $deg_of_disc; if ($top_max == 0 || $bottom_max==0) { $deg_of_disc = 'nan'; @@ -1204,6 +1641,70 @@ sub compute_discrimination_factor { ############################################### ############################################### +## +## Compute KR-21 +## +## To compute KR-21, you need the following information: +## +## K=the number of items in your test +## M=the mean score on the test +## s=the standard deviation of the scores on your test +## +## then: +## +## KR-21 rk= [K/(K-1)] * [1- (M*(K-M))/(K*s^2))] +## +############################################### +############################################### +sub compute_sequence_statistics { + my ($seq) = @_; + my $symb = $seq->{'symb'}; + my @Resources; + foreach my $res (@{$seq->{'contents'}}) { + next if ($res->{'type'} ne 'assessment'); + push (@Resources,$res->{'symb'}); + } + my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits(); + # + # First compute statistics based on student scores + my ($smin,$smax,$sMean,$sSTD,$scount,$sMAX) = + &Apache::loncoursedata::score_stats + (\@Apache::lonstatistics::SelectedSections, + $Apache::lonstatistics::enrollment_status, + \@Resources,$starttime,$endtime,undef); + $SeqStat{$symb}->{'title'} = $seq->{'title'}; + $SeqStat{$symb}->{'scoremax'} = $smax; + $SeqStat{$symb}->{'scoremin'} = $smin; + $SeqStat{$symb}->{'scoremean'} = $sMean; + $SeqStat{$symb}->{'scorestd'} = $sSTD; + $SeqStat{$symb}->{'scorecount'} = $scount; + $SeqStat{$symb}->{'max_possible'} = $sMAX; + # + # Compute statistics based on the number of correct problems + # 'correct' is taken to mean + my ($cmin,$cmax,$cMean,$cSTD,$ccount)= + &Apache::loncoursedata::count_stats + (\@Apache::lonstatistics::SelectedSections, + $Apache::lonstatistics::enrollment_status, + \@Resources,$starttime,$endtime,undef); + my $K = $seq->{'num_assess_parts'}; + my $kr_21; + if ($K > 1 && $cSTD > 0) { + $kr_21 = ($K/($K-1)) * (1 - $cMean*($K-$cMean)/($K*$cSTD**2)); + } else { + $kr_21 = 'nan'; + } + $SeqStat{$symb}->{'countmax'} = $cmax; + $SeqStat{$symb}->{'countmin'} = $cmin; + $SeqStat{$symb}->{'countstd'} = $cSTD; + $SeqStat{$symb}->{'countmean'} = $cMean; + $SeqStat{$symb}->{'count'} = $ccount; + $SeqStat{$symb}->{'items'} = $K; + $SeqStat{$symb}->{'KR-21'}=$kr_21; + return; +} + + =pod @@ -1264,111 +1765,6 @@ Number of Students had at least one disc =cut - -############################################################ -############################################################ -## -## How this all works: -## Statistics are computed by calling &get_statistics with the sequence, -## resource, and part id to run statistics on. At various places within -## the loops which compute the statistics, as well as before and after -## the entire process, subroutines can be called. The subroutines are -## registered to the following hooks: -## -## hook subroutine inputs -## ---------------------------------------------------------- -## pre $r,$count -## pre_seq $r,$count,$seq -## pre_res $r,$count,$seq,$res -## calc $r,$count,$seq,$res,$data -## post_res $r,$count,$seq,$res -## post_seq $r,$count,$seq -## post $r,$count -## -## abort $r -## -## subroutines will be called in the order in which they are registered. -## -############################################################ -############################################################ -{ - -my %hooks; -my $aborted = 0; - -sub abort_computation { - $aborted = 1; -} - -sub clear_hooks { - $aborted = 0; - undef(%hooks); -} - -sub register_hook { - my ($hookname,$subref)=@_; - if ($hookname !~ /^(pre|pre_seq|pre_res|post|post_seq|post_res|calc)$/){ - return; - } - if (ref($subref) ne 'CODE') { - &Apache::lonnet::logthis('attempt to register hook to non-code: '. - $hookname,' = '.$subref); - } else { - if (exists($hooks{$hookname})) { - push(@{$hooks{$hookname}},$subref); - } else { - $hooks{$hookname} = [$subref]; - } - } - return; -} - -sub run_hooks { - my $context = shift(); - foreach my $hook (@{$hooks{$context}}) { - if ($aborted && $context ne 'abort') { - last; - } - my $retvalue = $hook->(@_); - if (defined($retvalue) && $retvalue eq '0') { - $aborted = 1 if (! $aborted); - } - } -} - -sub run_statistics { - my ($r) = @_; - my $count = 0; - &run_hooks('pre',$r,$count); - foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) { - last if ($aborted); - next if ($seq->{'num_assess'}<1); - &run_hooks('pre_seq',$r,$count,$seq); - foreach my $res (@{$seq->{'contents'}}) { - last if ($aborted); - next if ($res->{'type'} ne 'assessment'); - &run_hooks('pre_res',$r,$count,$seq,$res); - foreach my $part (@{$res->{'parts'}}) { - last if ($aborted); - # - # This is where all the work happens - my $data = &get_statistics($seq,$res,$part,++$count); - &run_hooks('calc',$r,$count,$seq,$res,$part,$data); - } - &run_hooks('post_res',$r,$count,$seq,$res); - } - &run_hooks('post_seq',$r,$count,$seq); - } - if ($aborted) { - &run_hooks('abort',$r); - } else { - &run_hooks('post',$r,$count); - } - return; -} - -} # End of %hooks scope - ############################################################ ############################################################