--- loncom/interface/statistics/lonproblemstatistics.pm 2003/03/27 19:26:33 1.47 +++ loncom/interface/statistics/lonproblemstatistics.pm 2012/05/12 03:17:43 1.122.2.1 @@ -1,6 +1,6 @@ # The LearningOnline Network with CAPA # -# $Id: lonproblemstatistics.pm,v 1.47 2003/03/27 19:26:33 matthew Exp $ +# $Id: lonproblemstatistics.pm,v 1.122.2.1 2012/05/12 03:17:43 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -50,158 +50,544 @@ Excel files, and plots. 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; - -############################################### -############################################### - -=pod - -=item &CreateInterface() - -Create the main intereface for the statistics page. Allows the user to -select sections, maps, and output. - -=cut - -############################################### -############################################### -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 .= ' |
'; + $Str .= &Apache::loncommon::start_data_table(); + $Str .= &Apache::loncommon::start_data_table_header_row(); + $Str .= '
'. + &mt('Press "Generate Statistics" when you are ready.'). + '
'. + ''. + &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("'. + &Apache::lonstatistics::section_and_enrollment_description(). + '
'); + &Excel_output($r); + } else { + $r->print(''.' 'x5); $r->rflush(); - if ($show eq 'grouped') { - &output_html_grouped_by_sequence($r); - } elsif ($show eq 'ungrouped') { - &output_html_ungrouped($r); - } - } elsif ($output_mode eq 'excel') { - $r->print("'. + &Apache::lonstatistics::section_and_enrollment_description(). + '
'); + my $count = 0; + foreach my $seq (@sequences) { + my @resources = + &Apache::lonstathelpers::get_resources($navmap,$seq); + $count += scalar(@resources); + } + if ($count > 10) { + $r->print('".&mt('Compiled on [_1]', + &Apache::lonlocal::locallocaltime(time))."
"; + return $Str; +} -=cut ############################################### ############################################### -sub output_html_grouped_by_sequence { - my ($r) = @_; - my $problem_num = 0; - #$r->print(&ProblemStatisticsLegend()); - my @Header = ("Title","Part","#Stdnts","Tries","Mod", - "Mean","#YES","#yes","%Wrng","DoDiff", - "S.D.","Skew.");#,"D.F.1st","D.F.2nd"); - # #FFFFE6 #EEFFCC #DDFFFF FFDDDD #DDFFDD #FFDDFF - foreach my $sequence (&Apache::lonstatistics::Sequences_with_Assess()) { - next if ($sequence->{'num_assess'}<1); - $r->print("'."\n");
- $r->print('
|
'."\n");
- $r->rflush();
- #
- # 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,
- });
- }
+ $r->print(" ".&Apache::loncommon::DrawBarGraph($title, + 'Problem Number', + $yaxis, + $max, + undef, # colors + \@Labels, + \@Data)." \n"); + return; +} + +sub degrees_plot { + my ($r)=@_; + my $count = scalar(@StatsArray); + my $width = 50 + 10*$count; + $width = 300 if ($width < 300); + my $height = 300; + my $plot = ''; + my $ymax = 0; + my $ymin = 0; + my @Disc; my @Diff; my @Labels; + foreach my $data (@StatsArray) { + push(@Labels,$data->{'problem_num'}); + my $disc = $data->{'deg_of_disc'}; + my $diff = $data->{'deg_of_diff'}; + push(@Disc,$disc); + push(@Diff,$diff); + # + $ymin = $disc if ($ymin > $disc); + $ymin = $diff if ($ymin > $diff); + $ymax = $disc if ($ymax < $disc); + $ymax = $diff if ($ymax < $diff); + } + # + # Make sure we show relevant information. + if ($ymin < 0) { + if (abs($ymin) < 0.05) { + $ymin = 0; + } else { + $ymin = -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; +} + ############################################### ############################################### - -=pod - -=item &output_excel() - -Presents the statistical data in an Excel 95 compatable spreadsheet file. - -=cut - +## +## 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='. + &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]; + my %storestats = + &LONCAPA::lonmetadata::dynamic_metadata_storage($data); + my ($dom,$user) = ($urlres=~m{^($LONCAPA::domain_re)/($LONCAPA::username_re)}); + &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) +# my $duedate = &Apache::lonnet::EXT('resource.'.$part.'.duedate',$symb);; +# my $opendate = &Apache::lonnet::EXT('resource.'.$part.'.opendate',$symb); +# my $maxtries = &Apache::lonnet::EXT('resource.'.$part.'.maxtries',$symb); +# my $hinttries = &Apache::lonnet::EXT('resource.'.$part.'.hinttries',$symb); + my $weight = &Apache::lonnet::EXT('resource.'.$part.'.weight',$symb); + $data->{'weight'} = $weight; +# $data->{'duedate'} = $duedate; +# $data->{'opendate'} = $opendate; +# $data->{'maxtries'} = $maxtries; +# $data->{'hinttries'} = $hinttries; +# $data->{'resptypes'} = join(',',@{$resource->{'partdata'}->{$part}->{'ResponseTypes'}}); + return $data; +} + ############################################### ############################################### -=pod +=pod -=item &DrawGraph() +=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; - } - my @GData = ($title,$xaxis,$yaxis,$Max,$sendCount,$sendValues); - return ''; +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); + } + #&Apache::lonnet::logthis(' '.$top_sum.'/'.$top_max. + # ' - '.$bottom_sum.'/'.$bottom_max); + return $deg_of_disc; } ############################################### ############################################### +## +## 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; + my $part_count; + foreach my $res (&Apache::lonstathelpers::get_resources($navmap,$seq)) { + push (@Resources,$res->symb); + $part_count += scalar(@{$res->parts}); + } + 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::get_selected_sections()], + [&Apache::lonstatistics::get_selected_groups()], + $Apache::lonstatistics::enrollment_status, + \@Resources,$starttime,$endtime,undef); + $SeqStat{$symb}->{'title'} = $seq->compTitle; + $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::get_selected_sections()], + [&Apache::lonstatistics::get_selected_groups()], + $Apache::lonstatistics::enrollment_status, + \@Resources,$starttime,$endtime,undef); + my $K = $part_count; + 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 -=item &ProblemStatisticsLegend() +=item ProblemStatisticsLegend -=cut +=over 4 -############################################### -############################################### -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 .= ' |