# A.I. McLeod (1999,2000), aim@uwo.ca # Date: February 8, 2001 #output exam code to csv file # # Begin added code1 V. Didinchuk put this code require 'keycode.pl'; $TheScanFile = $ARGV[0]; $TheExamKeyText = $ARGV[1]; $NameForCorFile = substr($ARGV[0], 0, index($ARGV[0], ".")); # # &createCorAndStdFiles( TheScanFile, $TheExamKeyText ); # # Input sub createCorAndStdFiles: # $TheScanFile - Name of Scan file # $ExamKeyText - Name of Key file for Exam # ( If $ExamKeyText = "" then $ExamKeyText = "ExamKeyText.txt"; ) # Output sub createCorAndStdFiles: # Program create file ---> ScanFile.cor, ScanFile.std # ( $myCodeError, @MarkingScheme )= &keycode::createCorAndStdFiles( $TheScanFile, $TheExamKeyText ); if( $myCodeError != 0 ){ print " Error = $myCodeError in sub createCorAndStdFiles!\n"; exit; }; ############ End added code1 ##### V. Didinchuk put this code #################################### print "Course Number or Name: "; $CourseNumber = ; chomp($CourseNumber); $CourseNumber =~ s/\s// ; %CombinedGrades = (); $First = 1; $CounterNoNumber = 0; %SIDAnswers = (); %SIDScantron = (); %SIDSolutions = (); %SIDExamCode = (); $ExamNumber = 0; #first we process the original scantron sheet to get the student answers # we index this array with student id since these are unique $TheFile = $TheScanFile; # $TheFile =$NameForCorFile.".std"; open(INFILEDAT, $TheFile); $CounterNoNumber = 0; %OriginalSIDAnswers = (); $TheLine = ; @StudentScores = ( ) ; @SAnswers = (); chomp($TheLine); $StudentID = substr($TheLine, 0, 9); $ExamCode = substr($TheLine, 14, 3); if ( $StudentID ne "999999999" ) { die " 1 First record must have ID 999999999". " and contain the correct answers\n"}; $TheAnswers = substr($TheLine, 25, length($TheLine) ); $FirstBlank = index($TheAnswers, " "); $LengthAnswers = length($TheAnswers); $NumberOfQuestions = ($FirstBlank >= 0) ? $FirstBlank : $LengthAnswers; $EndOfLine = 25+$NumberOfQuestions; if (substr($TheAnswers, $EndOfLine, length($TheLine)) =~ m/\S/) { print "error: no blanks allowed in answer record\n"; print $TheAnswers , " is invalid.\n"; die; } $TotalQuestionsIncluded = $NumberOfQuestions; @QuestionsIndicator = (0) x $NumberOfQuestions; for ($i = 0; $i < $NumberOfQuestions; $i++) { if (substr($TheAnswers, $i, 1) == "A") {$QuestionsIndicator[$i] = 1} elsif (substr($TheAnswers, $i, 1) == "B") {$QuestionsIndicator[$i] = 1} elsif (substr($TheAnswers, $i, 1) == "C") {$QuestionsIndicator[$i] = 1} elsif (substr($TheAnswers, $i, 1) == "D") {$QuestionsIndicator[$i] = 1} elsif (substr($TheAnswers, $i, 1) == "E") {$QuestionsIndicator[$i] = 1} }; $TotalQuestionsIncluded = 0; foreach $i (@QuestionsIndicator) {$TotalQuestionsIncluded+=$i}; if ($TotalQuestionsIncluded == 0) { print $QuestionIncluded, "\n"; print "All zeros - no questions!\n"; die " ";}; $CounterRecord = 1; while() { $TheLine = $_; if ($TheLine !~ m/\S/) { if (eof) {last} else {next} }; $StudentID = substr($TheLine, 0, 9); # start processing student answers chomp($TheLine); $ExamCode = 0+substr($TheLine, 14, 16); $StudentRecord = 0+substr($TheLine, 20, 23); if ((scalar split(/ /, $StudentID) != 1) || ! $StudentID =~ /\S/) { $WarnMissing = 1; $StudentNumber = --$CounterNoNumber; print OUTL "Invalid Student ID. Line # = ",1+$CounterRecord, "\n StudentRecord # = $StudentRecord\n"; } else {$StudentNumber = 0+$StudentID;} ; $StudentAnswers = substr($TheLine, 25, $EndOfLine); $OriginalSIDAnswers{$StudentNumber} = $StudentAnswers; } close(INFILEDAT); #next we process the file containing the correct answers for each exam # and we save by exam code. #VNB: this could cause an error if two students write the same exam # We must fix this!! %ExamCodeCorrectAnswer = (); ############################################### # $TheFile = "s4711.cor"; ################################################ ############ Begin added code2 ##### V. Didinchuk put this code #################################### $TheFile = $NameForCorFile.".cor"; ############ End added code2 ##### V. Didinchuk put this code #################################### open(INFILECOR, $TheFile); while() { $TheLine = $_; chomp($TheLine); $ExamCode = 0+substr($TheLine, 0, 4); $CorrectAnswerCode = substr($TheLine, 7); $ExamCodeCorrectAnswer{$ExamCode} = $CorrectAnswerCode; } close(INFILECOR); ############ Begin added code3 ##### V. Didinchuk put this code #################################### $TheInFile =$NameForCorFile.".std"; if( !open(FILEDAT, $TheInFile) ) { print "The file $TheFile could not be found. \n"; exit; } Start: $TheLine = ; ############ End added code3 ##### V. Didinchuk put this code #################################### $CounterNoNumber = 0; ############################################################# # Start: $TheLine = <>; # if (eof()) {goto End;}; ############################################################ @StudentScores = ( ) ; @SAnswers = (); chomp($TheLine); $StudentID = substr($TheLine, 0, 9); $ExamCode = substr($TheLine, 14, 3); if ( $StudentID ne "999999999" ) { die " 2 First record must have ID 999999999". " and contain the correct answers\n"}; $TheAnswers = substr($TheLine, 25, length($TheLine) ); $FirstBlank = index($TheAnswers, " "); $LengthAnswers = length($TheAnswers); $NumberOfQuestions = ($FirstBlank >= 0) ? $FirstBlank : $LengthAnswers; $EndOfLine = 25+$NumberOfQuestions; if (substr($TheAnswers, $EndOfLine, length($TheLine)) =~ m/\S/) { print "error: no blanks allowed in answer record\n"; print $TheAnswers , " is invalid.\n"; die; } $TotalQuestionsIncluded = $NumberOfQuestions; $Pad = " " x $EndOfLine; @QuestionsIndicator = (0) x $NumberOfQuestions; for ($i = 0; $i < $NumberOfQuestions; $i++) { if (substr($TheAnswers, $i, 1) == "A") {$QuestionsIndicator[$i] = 1} elsif (substr($TheAnswers, $i, 1) == "B") {$QuestionsIndicator[$i] = 1} elsif (substr($TheAnswers, $i, 1) == "C") {$QuestionsIndicator[$i] = 1} elsif (substr($TheAnswers, $i, 1) == "D") {$QuestionsIndicator[$i] = 1} elsif (substr($TheAnswers, $i, 1) == "E") {$QuestionsIndicator[$i] = 1} }; $TotalQuestionsIncluded = 0; foreach $i (@QuestionsIndicator) {$TotalQuestionsIncluded+=$i}; if ($TotalQuestionsIncluded == 0) { print $QuestionIncluded, "\n"; print "All zeros - no questions!\n"; die " ";}; # file extension should be .txt ######################################################################### # $InFileEXT = substr($ARGV, index($ARGV, "."), length($ARGV)); ######################################################################### ############ Begin added code4 ##### V. Didinchuk put this code #################################### $InFileEXT = substr($TheInFile, index($TheInFile, "."), length($TheInFile)); ############ End added code4 ##### V. Didinchuk put this code #################################### if ( (lc($InFileEXT) ne ".txt") && (lc($InFileEXT) ne ".dat") && (lc($InFileEXT) ne ".std") && (lc($InFileEXT) ne ".raw") ) { die "Input file extension not txt or dat!\n". "Input file must be of the form *.txt or *.dat. \n"}; #$OutputFileLOG = "log.".substr($ARGV, 0, index($ARGV, ".")) . ".txt"; $Header = $CourseNumber."-".++$ExamNumber; $HeaderAndFile = "Course: ".$Header. ". Input File: ".$ARGV[0]; $OutputFileLOG = "log_". $Header . ".txt"; $OutputFileQuestions = "ra_". $Header . ".txt"; $OutputFileCA = "cht_". $Header . ".txt"; $OutputFileDIS = "dis_". $CourseNumber . ".txt"; if ($First) { $OutputFileSortedGrades = $CourseNumber . ".txt"; open(OUTTOT, '>'.$OutputFileSortedGrades) or die "Couldn't open the ". "file ". $OutputFileSortedGrades. " for writing \n"; $OutputFileGrades = $CourseNumber . ".csv"; open(OUTGRD, '>'.$OutputFileGrades) or die "Couldn't open the ". "file ". $OutputFileGrades. " for writing \n"; $First = 0; }; open(OUTG, '>'.$OutputFileGrades) or die "Couldn't open the ". "file ". $OutputFileGrades . " for writing \n"; open(OUTRA, '>'.$OutputFileQuestions) or die "Couldn't open the ". "file ". $OutputFileQuestions . " for writing \n"; open(OUTL, '>'.$OutputFileLOG) or die "Couldn't open the ". "file ". $OutputFileLOG . " for writing \n"; open(OUTCH, '>'.$OutputFileCA) or die "Couldn't open the ". "file ". $OutputFileCA . " for writing \n"; open(OUTDIS, '>'.$OutputFileDIS) or die "Couldn't open the ". "file ". $OutputFileDIS . " for writing \n"; print OUTG $HeaderAndFile, "\n"; print OUTRA $HeaderAndFile, "\n"; print OUTL $HeaderAndFile, "\n"; print OUTCH $HeaderAndFile, "\n"; print OUTL "First line: ", substr($TheLine,0,$EndOfLine), "\n"; print OUTL "Number of questions: ", $NumberOfQuestions, "\n"; print OUTL "Correct Answers : ",substr($TheAnswers,0,$NumberOfQuestions), "\n"; @A = (0) x $NumberOfQuestions ; @B = (0) x $NumberOfQuestions ; @C = (0) x $NumberOfQuestions ; @D = (0) x $NumberOfQuestions ; @E = (0) x $NumberOfQuestions ; @Hits = (0) x $NumberOfQuestions ; @Blank = (0) x $NumberOfQuestions ; $CounterRecord = 1; @Weights = (1,0,0,0); if( $#MarkingScheme > 0 ){ @Weights = @MarkingScheme; }; print OUTL "Weights for Correct, Wrong, Blank, Additive-Constant: ", $Weights[0], " ", $Weights[1], " ", $Weights[2], " ", $Weights[3], "\n"; $DECIMALS = 0; ############ Begin added code5 ##### V. Didinchuk put this code #################################### while() { ############ End added code5 ##### V. Didinchuk put this code #################################### $WarnMissing = 0; $Penalty = 0; $CounterCorrect = 0; $CounterBlank = 0; $TheLine = $_; if ($TheLine !~ m/\S/) { if (eof) {last} else {next} }; $StudentID = substr($TheLine, 0, 9); # start processing student answers chomp($TheLine); $SIDScantron{$StudentID} = substr($TheLine.$Pad, 0, $EndOfLine); $StudentSection = 0+substr($TheLine, 10, 12); $ExamCode = 0+substr($TheLine, 14, 16); $StudentRecord = 0+substr($TheLine, 20, 23); $StudentPenalty = substr($TheLine, 9, 9); if ($StudentPenalty =~ /\d/ ) {$Penalty = 0+$StudentPenalty; }; if ((scalar split(/ /, $StudentID) != 1) || ! $StudentID =~ /\S/) { $WarnMissing = 1; $StudentNumber = --$CounterNoNumber; print OUTL "Invalid Student ID. Line # = ",1+$CounterRecord, "\n StudentRecord # = $StudentRecord\n"; } else {$StudentNumber = 0+$StudentID;} ; $StudentAnswers = substr($TheLine, 25, $EndOfLine); $CounterRecord++; #Number of Students for ($i = 0; $i < $NumberOfQuestions; $i++) { unless ($QuestionsIndicator[$i]) {next;}; $StudentResponse = substr($StudentAnswers, $i, 1); if ($StudentResponse eq " " || $StudentResponse eq "") { $CounterBlank++; $Blank[$i]++ ;} elsif ($StudentResponse eq substr($TheAnswers, $i, 1) ) { $CounterCorrect++; $Hits[$i]++; }; if ($StudentResponse eq "A") { $A[$i]++;} elsif ($StudentResponse eq "B") { $B[$i]++;} elsif ($StudentResponse eq "C") { $C[$i]++;} elsif ($StudentResponse eq "D") { $D[$i]++;} elsif ($StudentResponse eq "E") { $E[$i]++;}; } $CounterWrong = $NumberOfQuestions-$CounterCorrect-$CounterBlank; $StudentScore = $Weights[0]*$CounterCorrect + $Weights[1]*$CounterWrong + $Weights[2]*$CounterBlank + $Weights[3] - $Penalty; print "StudentID=$StudentID; ExamCode=$ExamCode;\n"; print "TheAnswers =$TheAnswers:\n"; print "StudentAnswers=$StudentAnswers:\n"; print "CounterWrong=$CounterWrong; NumberOfQuestions=$NumberOfQuestions; CounterCorrect=$CounterCorrect; CounterBlank=$CounterBlank;\n"; print "StudentScore=$StudentScore; Weights[0]=$Weights[0]; Weights[1]=$Weights[1]; Weights[2]=$Weights[2]; Weights[3]=$Weights[3];\n"; print "CounterCorrect=$CounterCorrect; CounterWrong=$CounterWrong; CounterBlank=$CounterBlank; Penalty=$Penalty;\n"; $SSS = ; $FORMATG = ($DECIMALS) ? "%15d%5d%5d%5d%7.1f\n" : "%15d%5d%5d%5d%7d\n"; $SIDExamCode{$StudentNumber} = $ExamCode; $SIDSolutions{$StudentNumber} = $TheAnswers; $CombinedGrades{$StudentNumber} = $StudentScore; $SIDAnswers{$StudentNumber} = $StudentAnswers; push(@SAnswers, $StudentAnswers); push(@StudentScores, $StudentScore); if (eof) {last}; } print "Log file: ", $OutputFileLOG, "\n"; print "Response Analysis: ", $OutputFileQuestions, "\n"; print "Cheating Analysis: ", $OutputFileCA, "\n"; print OUTL "# Records in File: ",$CounterRecord, "\n"; print OUTL "Grades output to: ", $OutputFileGrades, "\n"; print OUTL "Response Analysis: ", $OutputFileQuestions, "\n"; print OUTL "Cheating Analysis: ", $OutputFileCA, "\n"; if ($WarnMissing) { print OUTL "\nWarning: Some Student IDs were not filled in properly.\n"; print OUTL "These students have been assigned unique negative IDS.\n"; print OUTL "Please check the original scantron sheets to see \nit is possible to identify these students.\n"; }; # Pearson correlation analysis @PearsonR = (0) x NumberOfQuestions; if (NotAllEqual(\@StudentScores)) { for ($i=0; $i < $NumberOfQuestions; $i++) { @HitIndicator = (0) x $CounterRecord; for ($j=0; $j < $CounterRecord; $j++) { if ( substr($SAnswers[$j], $i, 1) eq substr($TheAnswers, $i, 1) ) { $HitIndicator[$j] = 1 } }; if (NotAllEqual(\@HitIndicator)) { $PearsonR[$i] = correlation(\@StudentScores, \@HitIndicator);} }; }; # cheat analysis $MinCommon = int(0.1*$NumberOfQuestions); print OUTCH "Cheating Analysis\n\n"; print OUTCH "We find all sets of students whose answers were identical or similar.\n"; print OUTCH "\nThe criterion used is that the number of different answers\n"; print OUTCH " given is less than or equal to 10% of the total number\n"; print OUTCH " questions on the exam.\n\n"; for ($k=0; $k <= $MinCommon; $k++) { if ($k == 0) { print OUTCH "Category: Sets of students whose answers are all identical.\n"} elsif ($k == 1) { print OUTCH "Category: Sets of students whose answers differ by only 1 response.\n" } elsif ($k > 1) { print OUTCH "Category: Sets of students whose answers differ ", $k, " responses.\n" } $NoSuspectsFound = 1; @Indicator = (0) x $CounterRecord; @AllStudents = keys(SIDAnswers); for ($i=0; $i < $CounterRecord; $i++) { if ($Indicator[$i]) {next}; $Indicator[$i] = 1; $StudentNumber1 = $AllStudents[$i] ; $Answer1 = $SIDAnswers{$StudentNumber1} ; $FirstTime = 1; @Suspects = ( ); for ($j=$i; $j < $CounterRecord; $j++) { if ($Indicator[$j]) {next}; $StudentNumber2 = $AllStudents[$j] ; $Answer2 = $SIDAnswers{$StudentNumber2} ; $NumDiff = diff_answers(($Answer1, $Answer2)); if ($NumDiff == $k) { $Indicator[$j] = 1; $NoSuspectsFound = 0; if ($FirstTime==1) { push(@Suspects, ($StudentNumber1, $StudentNumber2)); $FirstTime = 0;} else {push(@Suspects, $StudentNumber2)} }; }; if ($FirstTime==0) {print OUTCH "{ ", join(", ", @Suspects), " }\n";} # if ($FirstTime==0) {print OUTCH "{ ", "@Suspects", " }\n";} }; if ($NoSuspectsFound) {print OUTCH "No suspects found in this category.\n"}; }; printf OUTRA "Analysis of Student Responses\n"; printf OUTRA "%5.1s%7.1s%7.1s%7.1s%7.1s%7.1s%7.5s%7.5s%7.3s\n", "Q", "A", "B", "C", "D", "E", "Blank", "%Hits", "r"; for ($i = 0; $i < $NumberOfQuestions; $i++) { unless ($QuestionsIndicator[$i]) {next;}; $Answer = substr($TheAnswers, $i, 1); if ($Answer eq "A" ) { printf OUTRA "%5.1d%7.1d*%6.1d%7.1d%7.1d%7.1d%7.1d%7.1f%8.2f\n", $i+1, $A[$i], $B[$i], $C[$i], $D[$i], $E[$i], $Blank[$i], 100*$Hits[$i]/$CounterRecord, $PearsonR[$i];} elsif ($Answer eq "B") { printf OUTRA "%5.1d%7.1d%7.1d*%6.1d%7.1d%7.1d%7.1d%7.1f%8.2f\n", $i+1, $A[$i], $B[$i], $C[$i], $D[$i], $E[$i], $Blank[$i], 100*$Hits[$i]/$CounterRecord, $PearsonR[$i];} elsif ($Answer eq "C") { printf OUTRA "%5.1d%7.1d%7.1d%7.1d*%6.1d%7.1d%7.1d%7.1f%8.2f\n", $i+1, $A[$i], $B[$i], $C[$i], $D[$i], $E[$i], $Blank[$i], 100*$Hits[$i]/$CounterRecord, $PearsonR[$i];} elsif ($Answer eq "D") { printf OUTRA "%5.1d%7.1d%7.1d%7.1d%7.1d*%6.1d%7.1d%7.1f%8.2f\n", $i+1, $A[$i], $B[$i], $C[$i], $D[$i], $E[$i], $Blank[$i], 100*$Hits[$i]/$CounterRecord, $PearsonR[$i];} elsif ($Answer eq "E") { printf OUTRA "%5.1d%7.1d%7.1d%7.1d%7.1d%7.1d*%6.1d%7.1f%8.2f\n", $i+1, $A[$i], $B[$i], $C[$i], $D[$i], $E[$i], $Blank[$i], 100*$Hits[$i]/$CounterRecord, $PearsonR[$i];}; }; $NumberOfInputFiles++; close OUTG; close OUTRA; close OUTCH; ################################# # goto Start; ################################# End: @Scores = values(%CombinedGrades); $MeanScore = mean(\@Scores); $SDScore = standard_deviation(\@Scores); $median = median(\@Scores); $Q1 = q1(\@Scores); $Q3 = q3(\@Scores); $Q10 = q10(\@Scores); $Q90 = q90(\@Scores); print OUTDIS "Summary for ",$CourseNumber," Exam/Test.\n"; print OUTDIS "Number of Students who wrote: ", 1+$#Scores, "\n"; print OUTDIS "Mean Score: "; printf OUTDIS "%6.2f\n", $MeanScore; print OUTDIS "SD(Score): "; printf OUTDIS "%6.2f\n", $SDScore; print OUTDIS "lower 10% quantile: ", $Q10, "\n"; print OUTDIS "lower 25% quantile: ", $Q1, "\n"; print OUTDIS "median: ", $median, "\n"; print OUTDIS "upper 25% quantile: ", $Q3, "\n"; print OUTDIS "upper 10% quantile: ", $Q90, "\n"; print "Score Distribution: ", $OutputFileDIS , "\n"; print OUTTOT "Grade Report for ",$CourseNumber," Examination\n"; printf OUTTOT "\n%15s%6s%28s", "ID/Code","Mark","Responses/Solutions\n"; $FORMATTOT1 = ($DECIMALS) ? "%15.9d%6.1f" : "%15.9d%6d"; $FORMATTOT2 = ' %' . $NumberOfQuestions . "s\n" . "%10.3d" . " ". '%' . $NumberOfQuestions . "s\n" ; $FORMATTOT = $FORMATTOT1.$FORMATTOT2; @AllStudentIDS = sort {0+$a <=> 0+$b } keys(%CombinedGrades); foreach $TheKey (@AllStudentIDS) { $ExamCode = 0+$SIDExamCode{$TheKey}; printf OUTTOT $FORMATTOT, $TheKey, $CombinedGrades{$TheKey}, substr($OriginalSIDAnswers{$TheKey},0,$NumberOfQuestions), # $OriginalSIDAnswers{$TheKey}, $ExamCode, substr($ExamCodeCorrectAnswer{$ExamCode},0,$NumberOfQuestions); # $ExamCodeCorrectAnswer{$ExamCode}; print OUTGRD $TheKey, "," , $CombinedGrades{$TheKey}, ",", $ExamCode, "\n" ; }; print OUTTOT "\n\n\n"; print OUTTOT " Summary for ",$CourseNumber," Examination\n"; print OUTTOT " Number of Students who wrote: ", 1+$#Scores, "\n"; print OUTTOT " Mean Score: "; printf OUTTOT " %6.2f\n", $MeanScore; print OUTTOT " SD(Score): "; printf OUTTOT " %6.2f\n", $SDScore; print OUTTOT " lower 10% quantile: ", $Q10, "\n"; print OUTTOT " lower 25% quantile: ", $Q1, "\n"; print OUTTOT " median: ", $median, "\n"; print OUTTOT " upper 25% quantile: ", $Q3, "\n"; print OUTTOT " upper 10% quantile: ", $Q90, "\n"; print "Grades With Answers: ", $OutputFileSortedGrades, "\n"; print "Grades: ", $OutputFileGrades, "\n"; print OUTL "Grades With Answers: ", $OutputFileSortedGrades, "\n"; print OUTL "Grades: ", $OutputFileGrades, "\n"; close OUTL; close OUTTOT; close OUTGRD; sub covariance { my ($array1ref, $array2ref) = @_; my ($i, $result); for ($i = 0; $i < @$array1ref; $i++) { $result += $array1ref->[$i] * $array2ref->[$i]; } $result /= @$array1ref; $result -= mean($array1ref) * mean($array2ref); } sub correlation { my ($array1ref, $array2ref) = @_; my ($sum1, $sum2, $numer, $denom); my ($sum1_squared, $sum2_squared); foreach (@$array1ref) { $sum1 += $_; $sum1_squared += $_ ** 2 } foreach (@$array2ref) { $sum2 += $_; $sum2_squared += $_ ** 2 } $numer = (@$array1ref ** 2) * covariance($array1ref, $array2ref); $denom = sqrt(((@$array1ref * $sum1_squared) - ($sum1 ** 2)) * ((@$array1ref * $sum2_squared) - ($sum2 ** 2))); $r = ($denom == 0) ? 0 : $numer/$denom; return ($r); } sub mean { my ($arrayref) = @_; my $result; foreach (@$arrayref) { $result += $_ } return $result / @$arrayref; } sub NotAllEqual { my ($arrayref) = @_; my $result; foreach (@$arrayref) { $result += ($_== ($arrayref->[1])) } return ($result != @$arrayref) ; } sub diff_answers { my($a1, $a2) = @_; my($count) = 0; my($b1, $b2, $i); @b1 = split(//, $a1); @b2 = split(//, $a2); for ($i = 0; $i $b} @$arrayref; if (@array % 2) { return $array[@array/2]; } else { return ($array[@array/2-1] + $array[@array/2]) / 2; } } sub q1 { my $arrayref = shift; my @array = sort {$a <=> $b} @$arrayref; my $pos = @array/4.0; my $f = $pos - int($pos); return ((1-$f)*$array[@array/4-1] + $f*$array[@array/4]); } sub q3 { my $arrayref = shift; my @array = sort {$a <=> $b} @$arrayref; my $pos = @array/4.0*3.0; my $f = $pos - int($pos); return ((1-$f)*$array[3*@array/4-1] + $f*$array[3*@array/4]); } sub q10 { my $arrayref = shift; my @array = sort {$a <=> $b} @$arrayref; my $pos = @array/10.0; my $f = $pos - int($pos); return ((1-$f)*$array[int($pos)] + $f*$array[1+int($pos)]); } sub q90 { my $arrayref = shift; my @array = sort {$a <=> $b} @$arrayref; my $pos = 9.0*@array/10.0; my $f = $pos - int($pos); return ((1-$f)*$array[int($pos)] + $f*$array[1+int($pos)]); }