--- loncom/interface/statistics/lonproblemstatistics.pm 2004/04/01 21:35:52 1.81 +++ loncom/interface/statistics/lonproblemstatistics.pm 2005/01/11 19:45:18 1.96 @@ -1,6 +1,6 @@ # The LearningOnline Network with CAPA # -# $Id: lonproblemstatistics.pm,v 1.81 2004/04/01 21:35:52 matthew Exp $ +# $Id: lonproblemstatistics.pm,v 1.96 2005/01/11 19:45:18 matthew Exp $ # # Copyright Michigan State University Board of Trustees # @@ -55,6 +55,7 @@ use Apache::loncommon(); use Apache::lonhtmlcommon; use Apache::loncoursedata; use Apache::lonstatistics; +use LONCAPA::lonmetadata(); use Apache::lonlocal; use Spreadsheet::WriteExcel; use Apache::lonstathelpers(); @@ -95,6 +96,10 @@ my %SeqStat; # keys are symbs, values ## 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#', @@ -226,6 +231,17 @@ my @Fields = ( selectable => 'yes', defaultselected => 'yes', }, + { name => 'tries_per_correct', + title => 'tries/correct', + align => 'right', + color => '#FFDDDD', + format => '%4.1f', + sortable => 'yes', + graphable => 'yes', + long_title => 'Tries per Correct Answer', + selectable => 'yes', + defaultselected => 'yes', + }, { name => 'num_wrong', title => '#Wrng', align => 'right', @@ -233,6 +249,17 @@ 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', defaultselected => 'yes', @@ -258,8 +285,85 @@ my @Fields = ( graphable => 'yes', long_title => 'Degree of Discrimination', selectable => 'yes', - defaultselected => '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', +# }, +## weight included for research purposes. Commented out most of the time. +# { name => 'weight', +# title => 'weight', +# align => 'right', +# color => '#FFFFFF', +# sortable => 'no', +# graphable => 'no', +# long_title => 'Problem weight (for instructor)', +# selectable => 'no', +# defaultselected => 'yes', +# }, ); my @SeqFields = ( @@ -461,6 +565,11 @@ sub parse_field_selection { } } } + # + # Always show all the sequence statistics (for now) + foreach my $field (@SeqFields) { + $field->{'selected'} = 'yes'; + } return; } @@ -493,6 +602,7 @@ select sections, maps, and output. ############################################### ############################################### sub CreateInterface { + my ($r) = @_; # &parse_field_selection(); # @@ -528,19 +638,18 @@ sub CreateInterface { $Str .= ''.&field_selection_input(); $Str .= ''."\n"; $Str .= ''."\n"; + # + $Str .= '

'.&mt('Status: [_1]', + '' + ). + '

'; + # $Str .= ''; $Str .= ' 'x5; $Str .= 'Plot '.&plot_dropdown().(' 'x10); - $Str .= ''; - $Str .= ' 'x5; - $Str .= ''; - $Str .= ' 'x5; - $Str .= ''; - $Str .= ' 'x5; + # return $Str; } @@ -578,13 +687,20 @@ sub BuildProblemStatisticsPage { undef(%SeqStat); # # Finally let the user know we are here - my $interface = &CreateInterface(); + my $interface = &CreateInterface($r); $r->print($interface); $r->print(''); # - if (! exists($ENV{'form.statsfirstcall'})) { - $r->print(''); + 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('

'. &mt('Press "Generate Statistics" when you are ready.'). '

'. @@ -593,13 +709,6 @@ sub BuildProblemStatisticsPage { ' will not have this delay.'). '

'); return; - } elsif ($ENV{'form.statsfirstcall'} eq 'yes' || - exists($ENV{'form.UpdateCache'}) || - exists($ENV{'form.ClearCache'}) ) { - $r->print(''); - &Apache::lonstatistics::Gather_Student_Data($r); - } else { - $r->print(''); } $r->rflush(); # @@ -609,7 +718,10 @@ sub BuildProblemStatisticsPage { # if (exists($ENV{'form.Excel'})) { &Excel_output($r); - } else { + } else { + $r->print(''.' 'x5); + $r->rflush(); my $count = 0; foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) { $count += $seq->{'num_assess_parts'}; @@ -647,7 +759,9 @@ sub BuildProblemStatisticsPage { sub output_sequence_statistics { my ($r) = @_; my $c=$r->connection(); - $r->print('

'.&mt('Sequence Statistics').'

'); + $r->print('

'.&mt('Sequence Statistics'). + &Apache::loncommon::help_open_topic('Statistics_Sequence'). + '

'); $r->print('
'."\n". ''."\n". ''); @@ -760,7 +874,7 @@ sub statistics_html_table_data { if (exists($field->{'special'}) && $field->{'special'} eq 'link') { $row .= ''; } - 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'}}; @@ -818,15 +932,9 @@ sub sequence_html_header { sub sequence_html_output { my ($seq) = @_; my $data = $SeqStat{$seq->{'symb'}}; -# $SeqStat{$symb}->{'max'} -# $SeqStat{$symb}->{'min'} -# $SeqStat{$symb}->{'mean'} -# $SeqStat{$symb}->{'std'} -# $SeqStat{$symb}->{'count'} -# $SeqStat{$symb}->{'max_possible'} my $row = ''; foreach my $field (@SeqFields) { -# next if ($field->{'selected'} ne 'yes'); + next if ($field->{'selected'} ne 'yes'); $row .= '
{'align'})) { $row .= ' align="'.$field->{'align'}.'"'; @@ -1184,6 +1292,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 ## @@ -1191,28 +1301,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; @@ -1228,6 +1327,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++, @@ -1235,38 +1338,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(); @@ -1279,6 +1393,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; +} + ################################################## ################################################## ## @@ -1292,6 +1444,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); @@ -1312,6 +1465,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); } } @@ -1404,6 +1558,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'}; @@ -1415,6 +1570,37 @@ 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); + } + # + $data->{'tries_per_correct'} = $data->{'tries'} / + ($data->{'num_solved'}+0.1); + # + # 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->{'weight'} = +# &Apache::lonnet::EXT('resource.'.$part.'.weight',$symb); +# $data->{'resptypes'} = join(',',@{$resource->{'partdata'}->{$part}->{'ResponseTypes'}}); return $data; } @@ -1442,11 +1628,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); @@ -1457,10 +1645,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'; @@ -1530,10 +1724,10 @@ sub compute_sequence_statistics { $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; }