';
- if($cache{'DisplayLegend'} eq 'Show Legend') {
- $Ptr .= &ProblemStatisticsLegend();
- }
- $r->print($Ptr);
- $r->rflush();
- untie(%cache);
+=cut
- my ($result, $orderedProblems) =
- &InitializeProblemStatistics($cacheDB, $students, $courseID, $c, $r);
- if($result ne 'OK') {
+###############################################
+###############################################
+sub BuildProblemStatisticsPage {
+ my ($r,$c)=@_;
+ #
+ my %Saveable_Parameters = ('Status' => 'scalar',
+ 'statsoutputmode' => 'scalar',
+ 'Section' => 'array',
+ 'StudentData' => 'array',
+ 'Maps' => 'array',
+ 'fieldselections'=> 'array');
+ &Apache::loncommon::store_course_settings('statistics',
+ \%Saveable_Parameters);
+ &Apache::loncommon::restore_course_settings('statistics',
+ \%Saveable_Parameters);
+ #
+ &Apache::lonstatistics::PrepareClasslist();
+ #
+ # Clear the package variables
+ undef(@StatsArray);
+ undef(%SeqStat);
+ #
+ # Finally let the user know we are here
+ my $interface = &CreateInterface();
+ $r->print($interface);
+ $r->print('');
+ #
+ if (! exists($ENV{'form.statsfirstcall'})) {
+ $r->print('');
+ $r->print('
'.
+ &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.').
+ '
');
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('');
}
-
- unless(tie(%cache,'GDBM_File',$cacheDB,&GDBM_READER(),0640)) {
- $r->print('Unable to tie database.6');
- return;
+ $r->rflush();
+ #
+ # This probably does not need to be done each time we are called, but
+ # it does not slow things down noticably.
+ &Apache::loncoursedata::populate_weight_table();
+ #
+ if (exists($ENV{'form.Excel'})) {
+ &Excel_output($r);
+ } else {
+ my $count = 0;
+ foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
+ $count += $seq->{'num_assess_parts'};
+ }
+ if ($count > 10) {
+ $r->print('
'.
+ &mt('Compiling statistics for [_1] problems',$count).
+ '
');
+ if ($count > 30) {
+ $r->print('
'.&mt('This will take some time.').'
');
+ }
+ $r->rflush();
+ }
+ #
+ my $sortby = $ENV{'form.sortby'};
+ $sortby = 'container' if (! defined($sortby) || $sortby =~ /^\s*$/);
+ my $plot = $ENV{'form.plot'};
+ if ($plot eq '' || $plot eq 'none') {
+ 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);
+ }
}
- &BuildStatisticsTable(\%cache, $cache{'DisplayFormat'},
- $cache{'SortProblems'}, $orderedProblems,
- \@Header, $r, $color);
- untie(%cache);
-
return;
}
-sub BuildGraphicChart {
- my ($graph,$cacheDB,$courseDescription,$students,$courseID,$r,$c)=@_;
- my %cache;
- my $max = 0;
- unless(tie(%cache,'GDBM_File',$cacheDB,&GDBM_READER(),0640)) {
- return 'Unable to tie database.7';
+sub output_sequence_statistics {
+ my ($r) = @_;
+ my $c=$r->connection();
+ $r->print('
'.&mt('Sequence Statistics').'
');
+ $r->print('
'."\n".
+ '
'."\n".
+ '
');
+ $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('
');
+ $r->print('
');
+ $r->rflush();
+ return;
+}
-# my @problems = split(':::', $cache{'problemList'});
- my $title = '';
- if($graph eq 'DoDiffGraph') {
- $title = 'Degree-of-Difficulty';
- } else {
- $title = 'Wrong-Percentage';
+##########################################################
+##########################################################
+##
+## HTML output routines
+##
+##########################################################
+##########################################################
+sub output_html_by_sequence {
+ my ($r) = @_;
+ my $c = $r->connection();
+ $r->print(&html_preamble());
+ #
+ foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
+ last if ($c->aborted);
+ next if ($seq->{'num_assess'} < 1);
+ $r->print("
".$seq->{'title'}."
".
+ '
'."\n".
+ '
'."\n".
+ '
'.
+ &statistics_table_header('no container')."
\n");
+ my @Data = &compute_statistics_on_sequence($seq);
+ foreach my $data (@Data) {
+ $r->print('
\n";
+ my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
+ if (defined($starttime) || defined($endtime)) {
+ # Inform the user what the time limits on the data are.
+ $Str .= '
'.&mt('Statistics on submissions from [_1] to [_2]',
+ &Apache::lonlocal::locallocaltime($starttime),
+ &Apache::lonlocal::locallocaltime($endtime)
+ ).'
';
}
+ $Str .= "
".&mt('Compiled on [_1]',
+ &Apache::lonlocal::locallocaltime(time))."
";
+ return $Str;
+}
- my @values = ();
- foreach(@$orderedProblems) {
- my ($sequence,$problem,$part)=split(':', $_);
- if($cache{'StatisticsMaps'} ne 'All Maps' &&
- $cache{'StatisticsMaps'} ne $cache{$sequence.':title'}) {
- next;
- }
-
- if($currentSequence == -1 ||
- ($sortProblems eq 'Sort Within Sequence' &&
- $currentSequence != $sequence)) {
- if($currentSequence ne -1) {
- #$r->print(' finish a graph ');
- }
- if($sortProblems eq 'Sort Within Sequence') {
- $r->print(''.$cache{$sequence.':title'}.'');
+###############################################
+###############################################
+##
+## Misc HTML output routines
+##
+###############################################
+###############################################
+sub statistics_html_table_data {
+ my ($data,$options) = @_;
+ my $row = '';
+ foreach my $field (@Fields) {
+ next if ($options =~ /no $field->{'name'}/);
+ next if ($field->{'selected'} ne 'yes');
+ $row .= '
'."\n";
+sub statistics_table_header {
+ my ($options) = @_;
+ my $header_row;
+ foreach my $field (@Fields) {
+ next if ($options =~ /no $field->{'name'}/);
+ next if ($field->{'selected'} ne 'yes');
+ $header_row .= '
\n");
+ return;
+}
- $r->print("\n".$Ptr);
+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;
+ }
+ }
+ if ($ymax > 0) {
+ if (abs($ymax) < 0.05) {
+ $ymax = 0;
+ } else {
+ $ymax = 1;
+ }
+ }
+ #
+ my $xmax = $Labels[-1];
+ if ($xmax > 50) {
+ if ($xmax % 10 != 0) {
+ $xmax = 10 * (int($xmax/10)+1);
+ }
} else {
- $Ptr='
'."\n".
- '
'.$RealIdx.'
'."\n".
- '
'.$ref.'
'."\n".
- '
'.$StdNo.'
'."\n".
- '
'.$TotalTries.'
'."\n".
- '
'.$MxTries.'
'."\n".
- '
'.$Avg.'
'."\n".
- '
'.$YES.'
'."\n".
- '
'.$Override.'
'."\n".
- '
'.$Wrng.'
'."\n".
- '
'.$DoD.'
'."\n".
- '
'.$SD.'
'."\n".
- '
'.$Sk.'
'."\n".
- '
'.$_D1.'
'."\n".
- '
'.$_D2.'
'."\n";
- $r->print($Ptr.'
'."\n");
+ if ($xmax % 5 != 0) {
+ $xmax = 5 * (int($xmax/5)+1);
+ }
}
-
+ #
+ my $discdata .= ''.join(',',@Labels).''.$/.
+ ''.join(',',@Disc).''.$/;
+ #
+ my $diffdata .= ''.join(',',@Labels).''.$/.
+ ''.join(',',@Diff).''.$/;
+ #
+ my $title = 'Degree of Discrimination\nand Degree of Difficulty';
+ if ($xmax > 50) {
+ $title = 'Degree of Discrimination and Degree of Difficulty';
+ }
+ #
+ $plot=<<"END";
+
+
+ $title
+
+ Problem Number
+
+ $discdata
+
+
+ $diffdata
+
+
+END
+ my $plotresult =
+ '
'.&Apache::lonxml::xmlparse($r,'web',$plot).'
'.$/;
+ $r->print($plotresult);
return;
}
-# For loading the colored table for display or un-colored for print
-sub setbgcolor {
- my $PrintTable=shift;
- my %color;
- if ($PrintTable){
- $color{"gb"}="#FFFFFF";
- $color{"red"}="#FFFFFF";
- $color{"yellow"}="#FFFFFF";
- $color{"green"}="#FFFFFF";
- $color{"purple"}="#FFFFFF";
- } else {
- $color{"gb"}="#DDFFFF";
- $color{"red"}="#FFDDDD";
- $color{"yellow"}="#EEFFCC";
- $color{"green"}="#DDFFDD";
- $color{"purple"}="#FFDDFF";
- }
-
- return \%color;
-}
-
-sub ProblemStatisticsButtons {
- my ($displayFormat, $displayLegend, $sortProblems)=@_;
-
- my $Ptr = '
';
- $Ptr .= '{'mean_tries'} + $data->{'std_tries'};
+ $ymax = $max if ($ymax < $max);
+ $ymax = $max if ($ymax < $max);
+ push(@Labels,$data->{'problem_num'});
+ push(@STD,$data->{'std_tries'});
+ push(@Mean,$data->{'mean_tries'});
+ }
+ #
+ # Make sure we show relevant information.
+ my $xmax = $Labels[-1];
+ if ($xmax > 50) {
+ if ($xmax % 10 != 0) {
+ $xmax = 10 * (int($xmax/10)+1);
+ }
} else {
- $Ptr .= 'value="Sort All Problems" />'."\n";
+ if ($xmax % 5 != 0) {
+ $xmax = 5 * (int($xmax/5)+1);
+ }
}
- $Ptr .= '
';
- $Ptr .= ''.$/.
+ ''.join(',',@Mean).''.$/;
+ #
+ my $std_error_data .= ''.join(',',@Labels).''.$/.
+ ''.join(',',@Mean).''.$/.
+ ''.join(',',@STD).''.$/;
+ #
+ my $title = 'Mean and S.D. of Tries';
+ if ($xmax > 25) {
+ $title = 'Mean and Standard Deviation of Tries';
+ }
+ #
+ $plot=<<"END";
+
+ $title
+
+ Problem Number
+ Number of Tries
+
+ $std_error_data
+
+
+ $std_data
+
+
+END
+ my $plotresult =
+ '
'.&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".'
';
- $Ptr .= 'print('
'.&mt('Preparing Excel Spreadsheet').'
');
+ ##
+ ## Compute the statistics
+ &compute_all_statistics($r);
+ my $c = $r->connection;
+ return if ($c->aborted());
+ ##
+ ## Create the excel workbook
+ my $filename = '/prtspool/'.
+ $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.
+ time.'_'.rand(1000000000).'.xls';
+ my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
+ #
+ # Create sheet
+ my $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(&mt("Problems creating new Excel file. ".
+ "This error has been logged. ".
+ "Please alert your LON-CAPA administrator."));
+ return 0;
+ }
+ #
+ # 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);
+ #
+ # Add a worksheet
+ my $sheetname = $ENV{'course.'.$ENV{'request.course.id'}.'.description'};
+ if (length($sheetname) > 31) {
+ $sheetname = substr($sheetname,0,31);
+ }
+ my $excel_sheet = $excel_workbook->addworksheet(
+ &Apache::loncommon::clean_excel_name($sheetname));
+ ##
+ ## Begin creating excel sheet
+ ##
+ my ($rows_output,$cols_output) = (0,0);
+ #
+ # Put the course description in the header
+ $excel_sheet->write($rows_output,$cols_output++,
+ $ENV{'course.'.$ENV{'request.course.id'}.'.description'});
+ $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 {
- $Ptr .= 'value="Display CSV Format" />'."\n";
+ 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);
+ #
+ # Time restrictions
+ my $time_string;
+ if (defined($starttime)) {
+ # call localtime but not lonlocal:locallocaltime because excel probably
+ # cannot handle localized text. Probably.
+ $time_string .= 'Data collected from '.localtime($time_string);
+ if (defined($endtime)) {
+ $time_string .= ' to '.localtime($endtime);
+ }
+ $time_string .= '.';
+ } elsif (defined($endtime)) {
+ # See note above about lonlocal:locallocaltime
+ $time_string .= 'Data collected before '.localtime($endtime).'.';
+ }
+ #
+ # Put the date in there too
+ $excel_sheet->write($rows_output,$cols_output++,
+ 'Compiled on '.localtime(time));
+ #
+ $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'})) {
+ $excel_sheet->write($rows_output,$cols_output++,
+ $field->{'long_title'});
+ } else {
+ $excel_sheet->write($rows_output,$cols_output++,'');
+ }
}
- $Ptr .= '
';
+ $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++;
+ foreach my $data (@StatsArray) {
+ $cols_output=0;
+ foreach my $field (@Fields) {
+ next if ($field->{'selected'} ne 'yes');
+ next if ($field->{'name'} eq 'problem_num');
+ $excel_sheet->write($rows_output,$cols_output++,
+ $data->{$field->{'name'}});
+ }
+ $rows_output++;
+ }
+ #
+ $excel_workbook->close();
+ #
+ # Tell the user where to get their excel file
+ $r->print(' '.
+ ''.
+ &mt('Your Excel Spreadsheet').''."\n");
+ $r->rflush();
+ return;
+}
- return $Ptr;
+##################################################
+##################################################
+##
+## Statistics Gathering and Manipulation Routines
+##
+##################################################
+##################################################
+sub compute_statistics_on_sequence {
+ my ($seq) = @_;
+ my @Data;
+ foreach my $res (@{$seq->{'contents'}}) {
+ next if ($res->{'type'} ne 'assessment');
+ foreach my $part (@{$res->{'parts'}}) {
+ #
+ # This is where all the work happens
+ my $data = &get_statistics($seq,$res,$part,scalar(@StatsArray)+1);
+ push (@Data,$data);
+ push (@StatsArray,$data);
+ }
+ }
+ return @Data;
}
-sub ProblemStatisticsLegend {
- my $Ptr = '';
- $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 .= '
';
- $Ptr .= '
Disc.
';
- $Ptr .= '
Number of Students had at least one discussion.';
- $Ptr .= '
';
-
- return $Ptr;
-}
-
-sub ExtractStudentData {
- my ($cache, $students)=@_;
-
- my @problemList=();
- my %problemData;
- foreach my $sequence (split(':', $cache->{'orderedSequences'})) {
- foreach my $problemID (split(':', $cache->{$sequence.':problems'})) {
- foreach my $part (split(/\:/,$cache->{$sequence.':'.
- $problemID.
- ':parts'})) {
- my $id = $sequence.':'.$problemID.':'.$part;
- push(@problemList, $id);
- my $totalTries = 0;
- my $totalAwarded = 0;
- my $correct = 0;
- my $correctByOverride = 0;
- my $studentCount = 0;
- my $maxTries = 0;
- my $totalFirst = 0;
- my @studentTries=();
- foreach(@$students) {
- my $code = $cache->{"$_:$problemID:$part:code"};
-
- if(defined($cache->{$_.':error'}) || $code eq ' ' ||
- $cache->{"$_:$problemID:NoVersion"} eq 'true') {
- next;
- }
+sub compute_all_statistics {
+ my ($r) = @_;
+ if (@StatsArray > 0) {
+ # Assume we have already computed the statistics
+ return;
+ }
+ my $c = $r->connection;
+ foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
+ last if ($c->aborted);
+ next if ($seq->{'num_assess'} < 1);
+ &compute_statistics_on_sequence($seq);
+ }
+}
- $studentCount++;
- my $tries = $cache->{"$_:$problemID:$part:tries"};
- if($maxTries < $tries) {
- $maxTries = $tries;
+sub sort_data {
+ my ($sortkey) = @_;
+ return if (! @StatsArray);
+ #
+ # Sort the data
+ my $sortby = undef;
+ foreach my $field (@Fields) {
+ if ($sortkey eq $field->{'name'}) {
+ $sortby = $field->{'name'};
+ }
+ }
+ if (! defined($sortby) || $sortby eq '' || $sortby eq 'problem_num') {
+ $sortby = 'container';
+ }
+ if ($sortby ne 'container') {
+ # $sortby is already defined, so we can charge ahead
+ if ($sortby =~ /^(title|part)$/i) {
+ # Alpha comparison
+ @StatsArray = sort {
+ lc($a->{$sortby}) cmp lc($b->{$sortby}) ||
+ lc($a->{'title'}) cmp lc($b->{'title'}) ||
+ lc($a->{'part'}) cmp lc($b->{'part'});
+ } @StatsArray;
+ } else {
+ # Numerical comparison
+ @StatsArray = sort {
+ my $retvalue = 0;
+ if ($b->{$sortby} eq 'nan') {
+ if ($a->{$sortby} ne 'nan') {
+ $retvalue = -1;
+ } else {
+ $retvalue = 0;
}
- $totalTries += $tries;
- push(@studentTries, $tries);
-
- my $awarded = $cache->{"$_:$problemID:$part:awarded"};
- $totalAwarded += $awarded;
-
- if($code eq '*') {
- $correct++;
- if($tries == 1) {
- $totalFirst++;
- }
- } elsif($code eq '+') {
- $correctByOverride++;
+ }
+ if ($a->{$sortby} eq 'nan') {
+ if ($b->{$sortby} ne 'nan') {
+ $retvalue = 1;
}
}
-
- my $studentTriesJoined = join(':::', @studentTries);
- $problemData{$id.':sequenceTitle'} =
- $cache->{$sequence.':title'};
- $problemData{$id.':studentCount'} = $studentCount;
- $problemData{$id.':totalTries'} = $totalTries;
- $problemData{$id.':studentTries'} = $studentTriesJoined;
- $problemData{$id.':totalAwarded'} = $totalAwarded;
- $problemData{$id.':correct'} = $correct;
- $problemData{$id.':correctByOverride'} = $correctByOverride;
- $problemData{$id.':wrong'} = $studentCount -
- ($correct + $correctByOverride);
- $problemData{$id.':maxTries'} = $maxTries;
- $problemData{$id.':totalFirst'} = $totalFirst;
- }
+ if ($retvalue eq '0') {
+ $retvalue = $b->{$sortby} <=> $a->{$sortby} ||
+ lc($a->{'title'}) <=> lc($b->{'title'}) ||
+ lc($a->{'part'}) <=> lc($b->{'part'});
+ }
+ $retvalue;
+ } @StatsArray;
}
}
-
- my @upperStudents1=();
- my @lowerStudents1=();
- my @upperStudents2=();
- my @lowerStudents2=();
- my $upperCount = int(0.27*scalar(@$students));
- # Discriminant Factor criterion 1
- my $sortedStudents = &SortDivideByTries($students,$cache,':totalAwarded');
-
- for(my $i=0; $i<$upperCount; $i++) {
- push(@lowerStudents1, $sortedStudents->[$i]);
- push(@upperStudents1, $sortedStudents->[(scalar(@$students)-$i-1)]);
- }
-
- $problemData{'studentsUpperListCriterion1'}=join(':::', @upperStudents1);
- $problemData{'studentsLowerListCriterion1'}=join(':::', @lowerStudents1);
-
- # Discriminant Factor criterion 2
- $sortedStudents = &SortDivideByTries($students, $cache, ':totalSolved');
-
- for(my $i=0; $i<$upperCount; $i++) {
- push(@lowerStudents2, $sortedStudents->[$i]);
- push(@upperStudents2, $sortedStudents->[(scalar(@$students)-$i-1)]);
+ #
+ # Renumber the data set
+ my $count;
+ foreach my $data (@StatsArray) {
+ $data->{'problem_num'} = ++$count;
}
- $problemData{'studentsUpperListCriterion2'}=join(':::', @upperStudents2);
- $problemData{'studentsLowerListCriterion2'}=join(':::', @lowerStudents2);
-
- $problemData{'problemList'} = join(':::', @problemList);
-
- return \%problemData;
+ return;
}
-sub SortDivideByTries {
- my ($toSort, $data, $sortOn)=@_;
- my @orderedData = sort { ($data->{$a.':totalTries'}) ?
- ($data->{$a.$sortOn}/$data->{$a.':totalTries'}):0
- <=>
- ($data->{$b.':totalTries'}) ?
- ($data->{$b.$sortOn}/$data->{$b.':totalTries'}):0
- } @$toSort;
+########################################################
+########################################################
- return \@orderedData;
-}
+=pod
-sub SortProblems {
- my ($problemData,$sortBy,$sortProblems,$ascend)=@_;
+=item &get_statistics()
- my @problems = split(':::', $problemData->{'problemList'});
- if($sortBy eq "Homework Sets Order") {
- return \@problems;
+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::SelectedSections,
+ $Apache::lonstatistics::enrollment_status,
+ $symb,$part,$courseid,$starttime,$endtime);
+ $data->{'part'} = $part;
+ $data->{'problem_num'} = $problem_num;
+ $data->{'container'} = $sequence->{'title'};
+ $data->{'title'} = $resource->{'title'};
+ $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);
}
+ return $data;
+}
- my $data;
+###############################################
+###############################################
- if ($sortBy eq "#Stdnts") { $data = ':studentCount'; }
- elsif($sortBy eq "Tries") { $data = ':totalTries'; }
- elsif($sortBy eq "Mod") { $data = ':maxTries'; }
- elsif($sortBy eq "Mean") { $data = ':mean'; }
- elsif($sortBy eq "#YES") { $data = ':correct'; }
- elsif($sortBy eq "#yes") { $data = ':correctByOverride'; }
- elsif($sortBy eq "%Wrng") { $data = ':percentWrong'; }
- elsif($sortBy eq "DoDiff") { $data = ':degreeOfDifficulty'; }
- elsif($sortBy eq "S.D.") { $data = ':standardDeviation'; }
- elsif($sortBy eq "Skew.") { $data = ':skewness'; }
- elsif($sortBy eq "D.F.1st") { $data = ':discriminationFactor1'; }
- elsif($sortBy eq "D.F.2nd") { $data = ':discriminationFactor2'; }
- else { return \@problems; }
+=pod
- my %temp;
- my @sequenceList=();
- foreach(@problems) {
- my ($sequence) = split(':', $_);
+=item &compute_discrimination_factor()
- my @array=();
- my $tempArray;
- if(defined($temp{$sequence})) {
- $tempArray = $temp{$sequence};
- } else {
- push(@sequenceList, $sequence);
- $tempArray = \@array;
- $temp{$sequence} = $tempArray;
- }
-
- push(@$tempArray, $_);
- }
+Inputs: $Resource, $Sequence
- my @orderedProblems;
- if($sortProblems eq "Sort Within Sequence") {
- foreach(keys(%temp)) {
- my $tempArray = $temp{$_};
- my @tempOrder =
- sort { $problemData->{$a.$data} <=> $problemData->{$b.$data} }
- @$tempArray;
- $temp{$_} = \@tempOrder;
- }
- foreach(@sequenceList) {
- my $tempArray = $temp{$_};
- @orderedProblems = (@orderedProblems, @$tempArray);
- }
+Returns: integer between -1 and 1
+
+=cut
+
+###############################################
+###############################################
+sub compute_discrimination_factor {
+ my ($resource,$part,$sequence) = @_;
+ my @Resources;
+ foreach my $res (@{$sequence->{'contents'}}) {
+ next if ($res->{'symb'} eq $resource->{'symb'});
+ push (@Resources,$res->{'symb'});
+ }
+ #
+ # rank
+ my $ranking =
+ &Apache::loncoursedata::rank_students_by_scores_on_resources
+ (\@Resources,
+ \@Apache::lonstatistics::SelectedSections,
+ $Apache::lonstatistics::enrollment_status,undef);
+ #
+ # 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}[($num_students-$number_to_grab)..($num_students-1)];
+ my ($bottom_sum,$bottom_max) =
+ &Apache::loncoursedata::get_sum_of_scores($resource,$part,\@BottomSet);
+ my ($top_sum,$top_max) =
+ &Apache::loncoursedata::get_sum_of_scores($resource,$part,\@TopSet);
+ my $deg_of_disc;
+ if ($top_max == 0 || $bottom_max==0) {
+ $deg_of_disc = 'nan';
} else {
- @orderedProblems =
- sort { $problemData->{$a.$data} <=> $problemData->{$b.$data} }
- @problems;
+ $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;
+}
- if($ascend eq 'Descending') {
- @orderedProblems = reverse(@orderedProblems);
+###############################################
+###############################################
+##
+## 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}->{'count'} = $ccount;
+ $SeqStat{$symb}->{'items'} = $K;
+ $SeqStat{$symb}->{'KR-21'}=$kr_21;
- return \@orderedProblems;
+ return;
}
-sub CalculateStatistics {
- my ($data, $cache, $courseID)=@_;
- my @problems = split(':::', $data->{'problemList'});
- foreach(@problems) {
- # Mean
- my $mean = ($data->{$_.':studentCount'}) ?
- ($data->{$_.':totalTries'} / $data->{$_.':studentCount'}) : 0;
- $data->{$_.':mean'} = sprintf("%.2f", $mean);
- # %Wrong
- my $pw = ($data->{$_.':studentCount'}) ?
- (($data->{$_.':wrong'} / $data->{$_.':studentCount'}) * 100.0) :
- 100.0;
- $data->{$_.':percentWrong'} = sprintf("%.1f", $pw);
+=pod
- # Degree of Difficulty
- my $dod = ($data->{$_.':totalTries'}) ?
- (1 - (($data->{$_.':correct'} + $data->{$_.':correctByOverride'}) /
- $data->{$_.':totalTries'})) : 0;
+=item ProblemStatisticsLegend
- $data->{$_.':degreeOfDifficulty'} = sprintf("%.2f", $dod);
+=over 4
- # Factor in mean
- my @studentTries = split(':::', $data->{$_.':studentTries'});
- foreach(my $index=0; $index < scalar(@studentTries); $index++) {
- $studentTries[$index] -= $mean;
- }
- my $sumSquared = 0;
- my $sumCubed = 0;
- foreach(@studentTries) {
- my $squared = ($_ * $_);
- my $cubed = ($squared * $_);
- $sumSquared += $squared;
- $sumCubed += $cubed;
- }
+=item #Stdnts
+Total number of students attempted the problem.
- # Standard deviation
- my $standardDeviation;
- if($data->{$_.':studentCount'} - 1 > 0) {
- $standardDeviation = (sqrt($sumSquared)) /
- ($data->{$_.':studentCount'} - 1);
- } else {
- $standardDeviation = 0.0;
- }
- $data->{$_.':standardDeviation'} = sprintf("%.1f", $standardDeviation);
+=item Tries
+Total number of tries for solving the problem.
- # Skewness
- my $skew;
- if($standardDeviation > 0.0999 && $data->{$_.':studentCount'} > 0) {
- $skew = (((sqrt($sumSquared)) / $data->{$_.':studentCount'}) /
- ($standardDeviation *
- $standardDeviation *
- $standardDeviation));
- } else {
- $skew = 0.0;
- }
+=item Max Tries
+Largest number of tries for solving the problem by a student.
- $data->{$_.':skewness'} = sprintf("%.1f", $skew);
+=item Mean
+Average number of tries. [ Tries / #Stdnts ]
- # Discrimination Factor 1
- my ($sequence, $problem, $part) = split(':', $_);
+=item #YES
+Number of students solved the problem correctly.
- my @upper1 = split(':::', $data->{'studentsUpperListCriterion1'});
- my @lower1 = split(':::', $data->{'studentsLowerListCriterion1'});
+=item #yes
+Number of students solved the problem by override.
- my $upper1Sum=0;
- foreach my $name (@upper1) {
- $upper1Sum += $cache->{"$name:$problem:$part:awarded"};
- }
- $upper1Sum = (scalar(@upper1)) ? ($upper1Sum/(scalar(@upper1))) : 0;
+=item %Wrong
+Percentage of students who tried to solve the problem
+but is still incorrect. [ 100*((#Stdnts-(#YES+#yes))/#Stdnts) ]
- my $lower1Sum=0;
- foreach my $name (@lower1) {
- $lower1Sum += $cache->{"$name:$problem:$part:awarded"};
- }
- $lower1Sum = (scalar(@lower1)) ? ($lower1Sum/(scalar(@lower1))) : 0;
+=item DoDiff
+Degree of Difficulty of the problem.
+[ 1 - ((#YES+#yes) / Tries) ]
- my $df1 = $upper1Sum - $lower1Sum;
- $data->{$_.':discriminationFactor1'} = sprintf("%.2f", $df1);
+=item S.D.
+Standard Deviation of the tries.
+[ sqrt(sum((Xi - Mean)^2)) / (#Stdnts-1)
+where Xi denotes every student\'s tries ]
- # Discrimination Factor 2
- my @upper2 = split(':::', $data->{'studentsUpperListCriterion2'});
- my @lower2 = split(':::', $data->{'studentsLowerListCriterion2'});
+=item Skew.
+Skewness of the students tries.
+[(sqrt( sum((Xi - Mean)^3) / #Stdnts)) / (S.D.^3)]
- my $upper2Sum=0;
- foreach my $name (@upper2) {
- $upper2Sum += $cache->{"$name:$problem:$part:awarded"};
- }
- $upper2Sum = (scalar(@upper2)) ? ($upper2Sum/(scalar(@upper2))) : 0;
+=item Dis.F.
+Discrimination Factor: A Standard for evaluating the
+problem according to a Criterion
- my $lower2Sum=0;
- foreach my $name (@lower2) {
- $lower2Sum += $cache->{"$name:$problem:$part:awarded"};
- }
- $lower2Sum = (scalar(@lower2)) ? ($lower2Sum/(scalar(@lower2))) : 0;
+=item [Criterion to group students into %27 Upper Students -
+and %27 Lower Students]
+1st Criterion for Sorting the Students:
+Sum of Partial Credit Awarded / Total Number of Tries
+2nd Criterion for Sorting the Students:
+Total number of Correct Answers / Total Number of Tries
- my $df2 = $upper2Sum - $lower2Sum;
- $data->{$_.':discriminationFactor2'} = sprintf("%.2f", $df2);
+=item Disc.
+Number of Students had at least one discussion.
- my %storestats;
- my $Average = ($data->{$_.':studentCount'}) ?
- $data->{$_.':totalTries'}/$data->{$_.':studentCount'} : 0;
- $storestats{$courseID.'___'.$cache->{$sequence.':source'}.
- '___timestamp'}=time;
- $storestats{$courseID.'___'.$cache->{$sequence.':source'}.
- '___stdno'}=$data->{$_.':studentCount'};
- $storestats{$courseID.'___'.$cache->{$sequence.':source'}.
- '___avetries'}=$Average;
- $storestats{$courseID.'___'.$cache->{$sequence.':source'}.
- '___difficulty'}=$data->{$_.':degreeOfDifficulty'};
- $cache->{$sequence.':source'} =~ /^(\w+)\/(\w+)/;
- if($data->{$_.':studentCount'}) {
- &Apache::lonnet::put('nohist_resevaldata',\%storestats,$1,$2);
- }
- }
+=back
- return;
-}
+=cut
-#---- END Problem Statistics Web Page ----------------------------------------
+############################################################
+############################################################
1;
__END__