Page MenuHomec4science

lib_amcstats.php
No OneTemporary

File Metadata

Created
Sun, Dec 22, 04:57

lib_amcstats.php

<?php
# some functions for array_maps
function clean_array($string) { return preg_replace('/"/', '', $string); }
function decimal_conversion($string) { return preg_replace('/,/', '.', $string); }
function square($n) { return($n*$n); }
# standard deviation function (if PECL stats not available)
if (!function_exists('stats_standard_deviation')) {
function stats_standard_deviation(array $a, $sample = false) {
$n = count($a);
if ($n === 0) {
trigger_error("The array has zero elements", E_USER_WARNING);
return false;
}
if ($sample && $n === 1) {
trigger_error("The array has only 1 element", E_USER_WARNING);
return false;
}
$mean = array_sum($a)/$n;
$carry = 0.0;
foreach ($a as $val) {
$d = ((double) $val) - $mean;
$carry += $d * $d;
}
if ($sample) --$n;
return sqrt($carry / $n);
}
}
class AmcReader {
protected $filename = null;
protected $teacher = null;
protected $raw_data = null;
protected $columns = null;
protected $students = null;
protected $exam_points = null;
public function __construct($filename, $teacher, $exam_points, $only_questions = null, $inverse_filter = false, $external = null) {
if (!file_exists($filename))
throw new Exception('File not found: '.$filename);
$this->filename = $filename;
$this->teacher = $teacher;
$this->exam_points = $exam_points;
$raw_data = file($this->filename, FILE_IGNORE_NEW_LINES);
$this->raw_data = array();
foreach($raw_data as $line) {
$line = array_map("clean_array", explode(';', $line));
$line = array_map("decimal_conversion", $line);
$this->raw_data[] = $line;
}
$this->parseHeader();
$this->parseStudents($only_questions, $inverse_filter, $external);
}
public function getStudents() {
return $this->students;
}
protected function parseHeader() {
foreach($this->raw_data[0] as $col_id => $value) {
// Analyse header from CSV file, based on content
$item = array();
switch ($value) {
case "ID":
case "NAME":
case "EMAIL":
case "SECTION":
case "Mark":
$item['name'] = $value;
$item['type'] = "info";
break;
case "SCIPER":
$item['name'] = $value;
$item['type'] = "unique_id";
break;
default:
$item['name'] = $value;
if (preg_match('/^TICKED:/', $value)) {
$item['name'] = preg_replace('/^TICKED:/', '', $value);
$item['type'] = "ticked";
} else {
$item['type'] = "question";
$item['subtype'] = $this->guessSubtype($col_id);
}
}
// Stats will be computed at a later stage
$item['stats'] = null;
$this->columns[] = $item;
}
}
protected function guessSubtype($col_id) {
$subtype = null;
$min_points = 0;
$max_points = 0;
$decimal = false;
foreach($this->raw_data as $line) {
if (preg_match('/\./', $line[$col_id])) $decimal = true;
if ($line[$col_id] > $max_points) $max_points = $line[$col_id];
}
// This only works for MATHS exams...
if ($decimal or $max_points > 3) {
// Only open questions have decimal points
return 'open';
}
if ($max_points == 3) return 'mc';
if ($max_points == 1) return 'tf';
return 'unknown';
}
protected function getColIdByName($name) {
foreach ($this->columns as $id => $col) if ($col['name'] == $name) return $id;
throw new Exception('Column not found: '.$name);
}
protected function getColIdsByType($type, $name = null) {
$ids = array();
if (is_null($name)) {
foreach ($this->columns as $id => $col) if ($col['type'] == $type) $ids[] = $id;
} else {
foreach ($this->columns as $id => $col) if ($col['type'] == $type and $col['name'] == $name) $ids[] = $id;
}
if (count($ids) == 0) {
if (is_null($name)) throw new Exception('Column type not found: '.$type);
throw new Exception('Column type not found: '.$type.'/'.$name);
}
if (count($ids) == 1) return $ids[0];
return $ids;
}
protected function getQuestionNameByColId($id) {
if (array_key_exists($id, $this->columns) and $this->columns[$id]['type'] == 'question')
return $this->columns[$id]['name'];
throw new Exception('Column not found, or is not a question: '.$id);
}
protected function getMaximumPointsByColId($id) {
$maximum = 0.0;
foreach($this->raw_data as $student) if ((float)$student[$id] > $maximum) $maximum = (float)$student[$id];
return $maximum;
}
protected function parseStudents($only_questions, $inverse_filter, $external) {
foreach($this->raw_data as $line => $student) {
if ($line == 0) continue; // skip header
$data = array('teacher' => $this->teacher);
foreach(array('ID', 'SCIPER', 'NAME', 'EMAIL', 'SECTION') as $key) {
$data[$key] = $student[$this->getColIdByName($key)];
}
// Get points
$points = array();
$data['items'] = array();
foreach($this->getColIdsByType('question') as $col) {
$item = array();
$item['name'] = $this->getQuestionNameByColId($col);
// Should we filter this question ?
if (is_null($only_questions)
or (!$inverse_filter and in_array($item['name'], $only_questions))
or ($inverse_filter and !in_array($item['name'], $only_questions))
) {
// Take this question into account
$item['points'] = (float)$student[$col];
$item['max_points'] = $this->getMaximumPointsByColId($col);
if ($item['max_points'] == 0) {
// Cancelled question ? Count it right for everyone
$item['right'] = 1;
} else {
$item['right'] = max((float)($item['points']/$item['max_points']), 0.0);
}
$points[] = $item['points'];
$item['ticked'] = $student[$this->getColIdsByType('ticked', $item['name'])];
$item['type'] = $this->columns[$col]['type'];
$item['subtype'] = $this->columns[$col]['subtype'];
$data['items'][] = $item;
}
}
$data['total'] = array_sum($points);
$data['present'] = (int)(array_sum(array_map("square", $points))>0);
$data['exam_points'] = $this->exam_points;
// Compute marks
if ($data['present']) {
$data['positive_total'] = (float)max($data['total'], 0.0);
if (is_null($external)) {
$data['mark6'] = (float)min($data['positive_total']/($this->exam_points)*5.0+1, 6.0);
} else {
$output = array();
exec("./".$external." ".$data['total'], $output);
$data['mark6'] = (float)trim($output[0]);
}
$data['quarter_mark6'] = (float)round($data['mark6']*4.0, 0)/4.0;
} else {
$data['positive_total'] = 'n/a';
$data['mark6'] = 'abs';
$data['quarter_mark6'] = 'abs';
}
if (preg_match('/^FAKE/', $data['SCIPER'])) {
if ($data['present']) $data['type'] = 'unregistered';
else $data['type'] = 'unused';
} else $data['type'] = 'student';
$this->students[] = $data;
}
}
}
// Compare students on total (higher to lower)
function cmp_total($a, $b)
{
if ($a['total'] == $b['total']) {
return 0;
}
return ($a['total'] < $b['total']) ? 1 : -1;
}
class ExamCalcs {
protected $dataset = null;
protected $tmp_dataset = null;
public function __construct($dataset = null) {
$this->dataset = array();
if (!is_null($dataset)) $this->dataset = $dataset;
}
public function addFile($teacher, $teacher_file, $max_points, $only_questions = null, $inverse_filter = false, $external = null) {
#echo "Adding $teacher ($teacher_file) to the dataset ($max_points points).\n";
$AR = new AmcReader($teacher_file, $teacher, $max_points, $only_questions, $inverse_filter, $external);
$this->addDataSet($AR->getStudents());
}
public function addDataSet($data) {
foreach ($data as $student) $this->dataset[] = $student;
}
public function filterBySections($sections, $update = true) {
$dataset = array();
foreach ($this->dataset as $student) {
if (is_array($sections)) {
if (in_array($student['SECTION'], $sections))
$dataset[] = $student;
} else {
if ($student['SECTION'] == $sections)
$dataset[] = $student;
}
}
if ($update) $this->dataset = $dataset;
return $dataset;
}
public function filterByTeachers($teachers, $update = true) {
$dataset = array();
foreach ($this->dataset as $student) {
if (is_array($teachers)) {
if (in_array($student['teacher'], $teachers))
$dataset[] = $student;
} else {
if ($student['teacher'] == $teachers)
$dataset[] = $student;
}
}
if ($update) $this->dataset = $dataset;
return $dataset;
}
public function getTeachers() {
$teachers = array();
foreach ($this->dataset as $student) {
if (!in_array($student['teacher'], $teachers)) $teachers[] = $student['teacher'];
}
return $teachers;
}
public function getSections() {
$sections = array();
foreach ($this->dataset as $student) {
$section = $student['SECTION'];
if (!in_array($section, $sections) and $section != 'XXX') $sections[] = $section;
}
return $sections;
}
public function printStatsOnCommonItems() {
// Sort dataset by points
$dataset = $this->sortByTotalPoints(false);
if (count($dataset) < 3) throw new Exception('Dataset is too small.');
// Get items from the first student
$items = array();
foreach ($this->dataset[0]['items'] as $item) $items[] = $item['name'];
foreach ($dataset as $student) {
// Get items for current student
$tmp_items = array();
foreach ($student['items'] as $item) $tmp_items[] = $item['name'];
// Keep only items in both '$items' AND '$tmp_items'
$items = array_intersect($items, $tmp_items);
}
// Now, filter items in the dataset
$filtered_dataset = array();
foreach ($dataset as $student) {
if (!$student['present']) continue;
$filtered_items = array();
foreach ($student['items'] as $item) {
if (in_array($item['name'], $items)) $filtered_items[] = $item;
}
if (count($filtered_items)) {
$student['items'] = $filtered_items;
$filtered_dataset[] = $student;
}
}
$dataset = $filtered_dataset;
if (count($dataset) < 3) throw new Exception('Dataset is too small.');
// Compute limits
$nb_students = count($dataset);
$twenty_seven = (int)($nb_students*27.0/100);
$upper_stop = $twenty_seven-1;
$lower_start = $nb_students-$twenty_seven+1;
#echo "$nb_students / $twenty_seven / 0 -> $upper_stop / $lower_start -> $nb_students \n";
$stats = array();
foreach ($dataset as $i => $student) {
foreach ($student['items'] as $item) {
$name = $item['name'];
if (!array_key_exists($name, $stats))
$stats[$name] = array( '27%' => $twenty_seven,
'upper' => 0,
'lower' => 0,
'valid' => null,
'ticked'=> null,
'ticked_count'=> 0,
'empty_count'=> 0,
'type' => null,
'subtype' => null,
'max_points' => null,
);
$stats[$name]['max_points'] = $item['max_points'];
$stats[$name]['type'] = $item['type'];
$stats[$name]['subtype'] = $item['subtype'];
// Initialise 'ticked' table
if (is_null($stats[$name]['ticked'])) {
switch ($stats[$name]['subtype']) {
case 'mc':
$stats[$name]['ticked'] = array( 'A' => 0, 'B' => 0, 'C' => 0, 'D' => 0, 'multiple' => 0);
break;
case 'tf':
$stats[$name]['ticked'] = array( 'TRUE' => 0, 'FALSE' => 0, 'multiple' => 0);
break;
default:
$stats[$name]['ticked'] = array();
break;
}
}
// Count right answers
if ($item['right'] > 0) {
// Save valid answer
if (is_null($stats[$name]['valid'])) {
switch ($stats[$name]['subtype']) {
case 'tf':
if ($item['ticked'] == 'A')
$stats[$name]['valid'] = 'TRUE';
else
$stats[$name]['valid'] = 'FALSE';
break;
case 'mc':
$stats[$name]['valid'] = $item['ticked'];
break;
case 'open':
$stats[$name]['valid'] = 'n/a';
break;
default:
$stats[$name]['valid'] = 'n/a';
break;
}
}
// 'upper 27%' and 'lower 27%' counters
switch ($stats[$name]['subtype']) {
case 'mc':
case 'tf':
if ($i <= $upper_stop) $stats[$name]['upper']++;
if ($i >= $lower_start) $stats[$name]['lower']++;
break;
case 'open':
if ($i <= $upper_stop) $stats[$name]['upper'] += $item['points'];
if ($i >= $lower_start) $stats[$name]['lower'] += $item['points'];
break;
}
}
// Count empty answers
if (empty($item['ticked'])) {
$stats[$name]['empty_count']++;
} else {
// Stats on non-empty answers
$stats[$name]['ticked_count']++;
if (strlen($item['ticked']) > 1) {
$stats[$name]['ticked'] = $this->createAndIncrement($stats[$name]['ticked'], 'multiple');
} else {
switch ($item['subtype']) {
case 'tf':
if ($item['ticked'] == 'A') $field = 'TRUE';
if ($item['ticked'] == 'B') $field = 'FALSE';
$stats[$name]['ticked'] = $this->createAndIncrement($stats[$name]['ticked'], $field);
break;
case 'mc':
$stats[$name]['ticked'] = $this->createAndIncrement($stats[$name]['ticked'], $item['ticked']);
break;
default:
$stats[$name]['ticked'] = $this->createAndIncrement($stats[$name]['ticked'], $item['ticked']);
break;
}
}
}
}
}
// Compute more stats
$tmp = array();
foreach ($stats as $name => $stat) {
// Discrimination index
// For open questions, change the '27%' value.
if ($stat['subtype'] == 'open') $stat['27%'] = $stat['27%']*$stat['max_points'];
$stat['DI'] = ($stat['upper']-$stat['lower'])/(1.0*$stat['27%']);
// Calculate percentages
$ticked_percentage = array();
foreach ($stat['ticked'] as $t => $n) {
$ticked_percentage[$t] = array( 'n' => $n, '%' => (float)(100.0*$n/$stat['ticked_count']), 'valid' => (int)($t == $stat['valid']));
}
$stat['ticked'] = $ticked_percentage;
$tmp[$name] = $stat;
}
$stats = $tmp;
// Print CSV
$previous_subtype = null;
$header = '"question_id","subtype","27 %","upper","lower","DI","count","valid"';
foreach ($stats as $name => $stat) {
if ($stat['subtype'] != $previous_subtype) {
#if (!is_null($previous_subtype)) echo "\n";
echo $header;
if ($stat['subtype'] != 'open') {
foreach ($stat['ticked'] as $answer => $data) echo ",\"[$answer] count\"";
foreach ($stat['ticked'] as $answer => $data) echo ",\"[$answer] %\"";
}
echo "\n";
$previous_subtype = $stat['subtype'];
}
echo "$name,{$stat['subtype']},{$stat['27%']},{$stat['upper']},{$stat['lower']},{$stat['DI']},{$stat['ticked_count']}";
if ($stat['subtype'] == 'open') {
echo ",\"n/a\"";
} else {
foreach ($stat['ticked'] as $answer => $data) if ($data['valid'] == 1) echo ",\"$answer\"";
foreach ($stat['ticked'] as $answer => $data) echo ",{$data['n']}";
foreach ($stat['ticked'] as $answer => $data) echo ",{$data['%']}";
}
echo "\n";
}
}
public function sortByTotalPoints($update = true) {
if ($update) {
usort($this->dataset, "cmp_total");
return $this->dataset;
} else {
$dataset = $this->dataset;
usort($dataset, "cmp_total");
return $dataset;
}
}
public function getDataSet() {
return $this->dataset;
}
protected function createAndIncrement($table, $field, $increment = 1) {
if (!array_key_exists($field, $table)) {
$table[$field] = 0;
}
$table[$field] += $increment;
return $table;
}
public function getMarks() {
$marks = array();
foreach($this->dataset as $student) {
$tmp = array();
$tmp['teacher'] = $student['teacher'];
$tmp['ID'] = $student['ID'];
$tmp['SECTION'] = $student['SECTION'];
$tmp['exam_points'] = $student['exam_points'];
$tmp['total'] = $student['total'];
$tmp['present'] = $student['present'];
$tmp['SCIPER'] = $student['SCIPER'];
$tmp['quarter_mark6'] = $student['quarter_mark6'];
$marks[] = $tmp;
}
return $marks;
}
public function getStats() {
$stats = array();
# Presence
$stats['presence'] = array();
$marks = array();
$stats['quarter_mark6'] = array( 'n' => 0, 'tot' => 0, 'average' => null, 'stddev' => null, 'median' => null);
foreach($this->dataset as $student) {
// Presence
$stats['presence'] = $this->createAndIncrement($stats['presence'], 'total');
switch ($student['type']) {
case 'student':
if ($student['present']) {
$stats['presence'] = $this->createAndIncrement($stats['presence'], 'present');
} else {
$stats['presence'] = $this->createAndIncrement($stats['presence'], 'absent');
}
break;
case 'unused':
$stats['presence'] = $this->createAndIncrement($stats['presence'], 'unsused');
break;
default:
$stats['presence'] = $this->createAndIncrement($stats['presence'], 'unknown');
}
// Average
if ($student['present']) $marks[] = $student['quarter_mark6'];
}
$stats['quarter_mark6']['n'] = count($marks);
if ($stats['quarter_mark6']['n'] > 0) {
$stats['quarter_mark6']['tot'] = array_sum($marks);
$stats['quarter_mark6']['average'] = $stats['quarter_mark6']['tot']/$stats['quarter_mark6']['n'];
$stats['quarter_mark6']['stddev'] = stats_standard_deviation($marks);
if (count($marks) >=3) {
sort($marks);
$stats['quarter_mark6']['median'] = $marks[round(count($marks)/2)];
} else {
$stats['quarter_mark6']['median'] = 'n/a';
}
} else {
$stats['quarter_mark6']['tot'] = 0;
$stats['quarter_mark6']['average'] = 0;
$stats['quarter_mark6']['stddev'] = 0;
$stats['quarter_mark6']['median'] = 0;
}
// Distribution (of marks)
$distribution = array();
for ($m = 1.0 ; $m <= 6.0 ; $m += 0.25) $distribution[(string)$m] = 0;
$stats['distribution_total'] = 0;
foreach ($marks as $mark) {
$distribution[(string)$mark]++;
$stats['distribution_total']++;
}
$stats['distribution'] = $distribution;
$stats['distribution_percentage'] = array();
if ($stats['quarter_mark6']['n']) {
foreach ($stats['distribution'] as $mark => $count) $stats['distribution_percentage'][$mark] = $count*100.0/$stats['distribution_total'];
} else {
$stats['distribution_percentage'] = $stats['distribution'];
}
return($stats);
}
}
?>

Event Timeline