--- loncom/interface/statistics/lonproblemstatistics.pm 2003/03/26 21:47:47 1.45 +++ loncom/interface/statistics/lonproblemstatistics.pm 2006/05/05 20:03:43 1.108 @@ -1,6 +1,6 @@ # The LearningOnline Network with CAPA # -# $Id: lonproblemstatistics.pm,v 1.45 2003/03/26 21:47:47 matthew Exp $ +# $Id: lonproblemstatistics.pm,v 1.108 2006/05/05 20:03:43 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -26,509 +26,1314 @@ # # (Navigate problems for statistical reports # -### +############################################### +############################################### + +=pod + +=head1 NAME + +lonproblemstatistics + +=head1 SYNOPSIS + +Routines to present problem statistics to instructors via tables, +Excel files, and plots. + +=over 4 + +=cut + +############################################### +############################################### package Apache::lonproblemstatistics; use strict; -use Apache::lonnet(); +use Apache::lonnet; +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 -sub CreateInterface { - my $Str = ''; - $Str .= '
Sections | '; - $Str .= 'Sequences and Folders | '; - $Str .= 'Output | '; - $Str .= '
'."\n"; - $Str .= &Apache::lonstatistics::SectionSelect('Section','multiple',5); - $Str .= ' | '; + # 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'}}++; + } + } # - my $only_seq_with_assessments = sub { - my $s=shift; - if ($s->{'num_assess'} < 1) { - return 0; - } else { - return 1; - } - }; - $Str .= &Apache::lonstatistics::MapSelect('Maps','multiple,all',5, - $only_seq_with_assessments); - $Str .= ' | '."\n"; - $Str .= &CreateAndParseOutputSelector(); - $Str .= ' |
'.&mt('Sections').' | '; + $Str .= ''.&mt('Groups').' | '; + $Str .= ''.&mt('Enrollment Status').' | '; + $Str .= ''.&mt('Sequences and Folders').' | '; + $Str .= ''.&mt('Statistics').' | '; + $Str .= ''. + &Apache::lonstathelpers::limit_by_time_form().' | '; + $Str .= '
'."\n"; + $Str .= &Apache::lonstatistics::SectionSelect('Section','multiple',5); + $Str .= ' | '; + $Str .= &Apache::lonstatistics::GroupSelect('Group','multiple',5); + $Str .= ' | '; + $Str .= &Apache::lonhtmlcommon::StatusOptions(undef,undef,5); + $Str .= ' | '; + # + $Str .= &Apache::lonstatistics::map_select('Maps','multiple,all',5); + $Str .= ' | '.&field_selection_input(); + $Str .= ' |
'.&mt('Status: [_1]', + '' + ). + '
'; + # + $Str .= ''; + $Str .= ' 'x5; + $Str .= 'Plot '.&plot_dropdown().(' 'x10); + # + return $Str; } ############################################### ############################################### +=pod + +=item &BuildProblemStatisticsPage() + +Main interface to problem statistics. + +=cut + ############################################### ############################################### +my $navmap; +my @sequences; + +sub clean_up { + undef($navmap); + undef(@sequences); +} + sub BuildProblemStatisticsPage { my ($r,$c)=@_; + undef($navmap); + undef(@sequences); # - $output_mode = 'html'; - $show = 'grouped'; - # - $r->print(&CreateInterface()); - $r->print(''); - $r->print(''); - if (! exists($ENV{'form.statsfirstcall'})) { + # + 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('It may take some time to update the student data '. + 'for the first analysis. Future analysis this session '. + ' will not have this delay.'). + '
'); + &clean_up(); return; } + $r->rflush(); # - &Gather_Student_Data($r); - # - # - if ($output_mode eq 'html') { - $r->print("'."\n".
+ ' |
'."\n");
- $r->print('
|
'."\n".
+ ' |
'."\n");
- $r->rflush();
+ my $title; my $yaxis;
+ foreach my $field (@Fields) {
+ next if ($field->{'name'} ne $datafield);
+ $title = $field->{'long_title'};
+ $yaxis = $field->{'title'};
+ last;
+ }
+ if ($title eq '' || $yaxis eq '') {
+ # datafield is something we do not know enough about to plot
+ $r->print(''. + &mt('Unable to plot the requested statistic.'). + ''); + return; + } # - # Compile the data - my @Statsarray; - foreach my $sequence (@Sequences) { - next if ($sequence->{'num_assess'}<1); - foreach my $resource (@{$sequence->{'contents'}}) { - next if ($resource->{'type'} ne 'assessment'); - foreach my $part (@{$resource->{'parts'}}) { - $problem_num++; - my ($num,$tries,$mod,$mean,$Solved,$solved,$DegOfDiff,$STD, - $SKEW) = &Apache::loncoursedata::get_problem_statistics - (undef,$resource->{'symb'},$part, - $ENV{'request.course.id'}); - # - $show_part = 1 if ($part ne '0'); - $part = ' ' if ($part == 0); - # - my $wrongpercent = 0; - if (defined($num) && $num > 0) { - $wrongpercent=int(10*100*($num-$Solved+$solved)/$num)/10; - } - push (@Statsarray, - { 'sequence' => $sequence, - 'resource' => $resource, - 'Title' => $resource->{'title'}, - 'Part' => $part, - '#Stdnts' => $num, - 'Tries' => $tries, - 'Mod' => $mod, - 'Mean' => $mean, - '#YES' => $Solved, - '#yes' => $solved, - '%Wrng' => $wrongpercent, - 'DoDiff' => $DegOfDiff, - 'S.D.' => $STD, - 'Skew' => $SKEW, - 'problem_num' => $problem_num, - }); - } + # Build up the data sets to plot + my @Labels; + my @Data; + my $max = 1; + foreach my $data (@StatsArray) { + push(@Labels,$data->{'problem_num'}); + push(@Data,$data->{$datafield}); + if ($data->{$datafield}>$max) { + $max = $data->{$datafield}; + } + } + foreach (1,2,3,4,5,10,15,20,25,40,50,75,100,150,200,250,300,500,600,750, + 1000,1500,2000,2500,3000,3500,4000,5000,7500,10000,15000,20000) { + if ($max <= $_) { + $max = $_; + last; } } + if ($max > 20000) { + $max = 10000*(int($max/10000)+1); + } # - # Table Headers - $r->print('
|
'.&Apache::lonxml::xmlparse($r,'web',$plot).'
'.$/; + $r->print($plotresult); return; } +sub plot_dropdown { + my $current = ''; + # + if (defined($env{'form.plot'})) { + $current = $env{'form.plot'}; + } + # + my @Additional_Plots = ( + { graphable=>'yes', + name => 'degrees', + title => 'Difficulty Indexes' }, + { graphable=>'yes', + name => 'tries statistics', + title => 'Tries Statistics' }); + # + my $Str= "\n".''."\n"; + return $Str; +} ############################################### ############################################### - +## +## Excel output routines +## ############################################### ############################################### -sub output_excel { +sub Excel_output { my ($r) = @_; - my $filename = '/prtspool/'. - $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'. - time.'_'.rand(1000000000).'.xls'; - # - my $excel_workbook = undef; - my $excel_sheet = undef; - # - my $rows_output = 0; - my $cols_output = 0; - # - # Create sheet - $excel_workbook = Spreadsheet::WriteExcel->new('/home/httpd'.$filename); - # - # Check for errors - if (! defined($excel_workbook)) { - $r->log_error("Error creating excel spreadsheet $filename: $!"); - $r->print("Problems creating new Excel file. ". - "This error has been logged. ". - "Please alert your LON-CAPA administrator"); - return ; - } - # - # The excel spreadsheet stores temporary data in files, then put them - # together. If needed we should be able to disable this (memory only). - # The temporary directory must be specified before calling 'addworksheet'. - # File::Temp is used to determine the temporary directory. - $excel_workbook->set_tempdir($Apache::lonnet::tmpdir); + $r->print('".&DrawGraph(\@Data,$title,'Problem Number',$yaxis, - $Max)."
\n"); # - # Print out the data - $ENV{'form.sortby'} = 'Contents'; - &output_html_ungrouped($r,'show probnum'); + # Renumber the data set + my $count; + foreach my $data (@StatsArray) { + $data->{'problem_num'} = ++$count; + } return; } +######################################################## +######################################################## + +=pod + +=item &get_statistics() + +Wrapper routine from the call to loncoursedata::get_problem_statistics. +Calls lonstathelpers::get_time_limits() to limit the data set by time +and &compute_discrimination_factor + +Inputs: $sequence, $resource, $part, $problem_num + +Returns: Hash reference with statistics data from +loncoursedata::get_problem_statistics. + +=cut + +######################################################## +######################################################## +sub get_statistics { + my ($sequence,$resource,$part,$problem_num) = @_; + # + my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits(); + my $symb = $resource->symb; + my $courseid = $env{'request.course.id'}; + # + my $data = &Apache::loncoursedata::get_problem_statistics + ([&Apache::lonstatistics::get_selected_sections()], + [&Apache::lonstatistics::get_selected_groups()], + $Apache::lonstatistics::enrollment_status, + $symb,$part,$courseid,$starttime,$endtime); + $data->{'symb'} = $symb; + $data->{'part'} = $part; + $data->{'problem_num'} = $problem_num; + $data->{'container'} = $sequence->compTitle; + $data->{'title'} = $resource->compTitle; + $data->{'title.link'} = $resource->src.'?symb='. + &Apache::lonnet::escape($resource->symb); + # + if ($SelectedFields{'deg_of_disc'}) { + $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::get_selected_sections(); + 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; +} + ############################################### ############################################### +=pod + +=item &compute_discrimination_factor() + +Inputs: $Resource, $Sequence + +Returns: integer between -1 and 1 + +=cut + ############################################### ############################################### -sub DrawGraph { - my ($values,$title,$xaxis,$yaxis,$Max)=@_; - $title = '' if (! defined($title)); - $xaxis = '' if (! defined($xaxis)); - $yaxis = '' if (! defined($yaxis)); - # - my $sendValues = join(',', @$values); - my $sendCount = scalar(@$values); - if ( $Max > 1 ) { - if ($Max % 10) { - if ( int($Max) < $Max ) { - $Max++; - $Max = int($Max); - } - } - } else { - $Max = 1; +sub compute_discrimination_factor { + my ($resource,$part,$seq) = @_; + my $symb = $resource->symb; + my @Resources; + foreach my $res (&Apache::lonstathelpers::get_resources($navmap,$seq)){ + next if ($res->symb eq $symb); + push (@Resources,$res->symb); + } + # + # rank + my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits(); + my $ranking = + &Apache::loncoursedata::rank_students_by_scores_on_resources + (\@Resources, + [&Apache::lonstatistics::get_selected_sections()], + [&Apache::lonstatistics::get_selected_groups()], + $Apache::lonstatistics::enrollment_status,undef, + $starttime,$endtime, $symb); + # + # compute their percent scores on the problems in the sequence, + my $number_to_grab = int(scalar(@{$ranking})/4); + my $num_students = scalar(@{$ranking}); + my @BottomSet = map { $_->[&Apache::loncoursedata::RNK_student()]; + } @{$ranking}[0..$number_to_grab]; + my @TopSet = + map { + $_->[&Apache::loncoursedata::RNK_student()]; + } @{$ranking}[-$number_to_grab..0]; + 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($symb,$part,\@BottomSet, + undef,$starttime,$endtime); + my ($top_sum,$top_max) = + &Apache::loncoursedata::get_sum_of_scores($symb,$part,\@TopSet, + undef,$starttime,$endtime); + my $deg_of_disc; + if ($top_max == 0 || $bottom_max==0) { + $deg_of_disc = 'nan'; + } else { + $deg_of_disc = ($top_sum/$top_max) - ($bottom_sum/$bottom_max); } - my @GData = ($title,$xaxis,$yaxis,$Max,$sendCount,$sendValues); - return ''; + #&Apache::lonnet::logthis(' '.$top_sum.'/'.$top_max. + # ' - '.$bottom_sum.'/'.$bottom_max); + return $deg_of_disc; } ############################################### ############################################### - -############################################### -############################################### -sub ProblemStatisticsLegend { - my $Ptr = ''; - $Ptr = ''; - $Ptr .= '#Stdnts | '; - $Ptr .= 'Total number of students attempted the problem.'; - $Ptr .= ' |
'; - $Ptr .= 'Tries | '; - $Ptr .= 'Total number of tries for solving the problem.'; - $Ptr .= ' |
'; - $Ptr .= 'Mod | '; - $Ptr .= 'Largest number of tries for solving the problem by a student.'; - $Ptr .= ' |
'; - $Ptr .= 'Mean | '; - $Ptr .= 'Average number of tries. [ Tries / #Stdnts ]'; - $Ptr .= ' |
'; - $Ptr .= '#YES | '; - $Ptr .= 'Number of students solved the problem correctly.'; - $Ptr .= ' |
'; - $Ptr .= '#yes | '; - $Ptr .= 'Number of students solved the problem by override.'; - $Ptr .= ' |
'; - $Ptr .= '%Wrong | '; - $Ptr .= 'Percentage of students who tried to solve the problem '; - $Ptr .= 'but is still incorrect. [ 100*((#Stdnts-(#YES+#yes))/#Stdnts) ]'; - $Ptr .= ' |
'; - $Ptr .= 'DoDiff | '; - $Ptr .= 'Degree of Difficulty of the problem. '; - $Ptr .= '[ 1 - ((#YES+#yes) / Tries) ]'; - $Ptr .= ' |
'; - $Ptr .= 'S.D. | '; - $Ptr .= 'Standard Deviation of the tries. '; - $Ptr .= '[ sqrt(sum((Xi - Mean)^2)) / (#Stdnts-1) '; - $Ptr .= 'where Xi denotes every student\'s tries ]'; - $Ptr .= ' |
'; - $Ptr .= 'Skew. | '; - $Ptr .= 'Skewness of the students tries.'; - $Ptr .= '[(sqrt( sum((Xi - Mean)^3) / #Stdnts)) / (S.D.^3)]'; - $Ptr .= ' |
'; - $Ptr .= 'Dis.F. | '; - $Ptr .= 'Discrimination Factor: A Standard for evaluating the ';
- $Ptr .= 'problem according to a Criterion '; - $Ptr .= '[Criterion to group students into %27 Upper Students - '; - $Ptr .= 'and %27 Lower Students] '; - $Ptr .= '1st Criterion for Sorting the Students: '; - $Ptr .= 'Sum of Partial Credit Awarded / Total Number of Tries '; - $Ptr .= '2nd Criterion for Sorting the Students: '; - $Ptr .= 'Total number of Correct Answers / Total Number of Tries'; - $Ptr .= ' |
Disc. | '; - $Ptr .= 'Number of Students had at least one discussion.'; - $Ptr .= ' |