Page MenuHomec4science

lib_amcstats.php
No OneTemporary

File Metadata

Created
Thu, Jun 26, 01:26

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;
public function __construct($filename, $teacher, $max_points) {
if (!file_exists($filename))
throw new Exception('File not found: '.$filename);
$this->filename = $filename;
$this->teacher = $teacher;
$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();
}
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;
//$ticked_col = $this->getColIdsByType('ticked', $this->getQuestionNameByColId($col_id));
$min_points = 0;
$max_points = 0;
foreach($this->raw_data as $line) {
if (preg_match('/\./', $line[$col_id])) return 'open';
if ($line[$col_id] > $max_points) $max_points = $line[$col_id];
}
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 parseStudents() {
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);
$item['points'] = (float)$student[$col];
$item['right'] = (int)($item['points']>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);
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) {
echo "Adding $teacher ($teacher_file) to the dataset ($max_points points).\n";
$AR = new AmcReader($teacher_file, $teacher, $max_points);
$this->addDataSet($AR->getStudents());
}
public function addDataSet($data) {
foreach ($data as $student) $this->dataset[] = $student;
}
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 doStatsOnCommonItems() {
// 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.');
// Computed limits
$nb_students = count($dataset);
$twenty_seven = (int)($nb_students*27.0/100);
$upper_start = 0;
$lower_stop = $nb_students-1;
$upper_stop = $twenty_seven-1;
$lower_start = $lower_stop-$twenty_seven+1;
echo "$nb_students / $twenty_seven / $upper_start -> $upper_stop / $lower_start -> $lower_stop \n";
$stats = array();
foreach ($items as $item_name) {
echo "$item_name\n";
$stats[$item_name] = array();
$stats[$item_name]['27%'] = $twenty_seven;
$stats[$item_name]['upper'] = 0;
$stats[$item_name]['lower'] = 0;
// Stats for upper 27%
for ($i = $upper_start ; $i <= $upper_stop; $i++) {
foreach($dataset[$i]['items'] as $cur_item) {
if ($cur_item['name'] == $item_name) {
if ($cur_item['right']) $stats[$item_name]['upper']++;
}
}
}
// Stats for lower 27%
for ($i = $lower_start ; $i <= $lower_stop; $i++) {
foreach($dataset[$i]['items'] as $cur_item) {
if ($cur_item['name'] == $item_name) {
if ($cur_item['right']) $stats[$item_name]['lower']++;
}
}
}
$stats[$item_name]['DI'] = (float)(($stats[$item_name]['upper']-$stats[$item_name]['lower'])/$twenty_seven);
}
$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' => '?',
'ticked'=> array(),
'ticked_count'=> 0,
'empty_count'=> 0,
'type' => null,
'subtype' => null,
);
$stats[$name]['type'] = $item['type'];
$stats[$name]['subtype'] = $item['subtype'];
if ($item['right']) {
if ($stats[$name]['valid'] == '?')
if ($item['subtype'] = 'tf') {
if ($item['ticked'] == 'A') $stats[$name]['valid'] = 'TRUE'; else $stats[$name]['valid'] = 'FALSE';
} else {
$stats[$name]['valid'] = $item['ticked'];
}
if ($i <= $upper_stop) {
$stats[$name]['upper']++;
}
if ($i >= $lower_start) {
$stats[$name]['lower']++;
}
}
if (empty($item['ticked'])) {
$stats[$name]['empty_count']++;
} else {
$stats[$name]['ticked_count']++;
if (strlen($item['ticked']) > 1) {
$stats[$name]['ticked'] = $this->createAndIncrement($stats[$name]['ticked'], 'multiple');
} else {
if ($item['subtype'] = 'tf') {
if ($item['ticked'] == 'A') $field = 'TRUE';
if ($item['ticked'] == 'B') $field = 'FALSE';
$stats[$name]['ticked'] = $this->createAndIncrement($stats[$name]['ticked'], $field);
} else {
$stats[$name]['ticked'] = $this->createAndIncrement($stats[$name]['ticked'], $item['ticked']);
}
}
}
}
}
// Compute more stats...
$tmp = array();
foreach ($stats as $name => $stat) {
// Discrimination index
$stat['DI'] = ($stat['upper']-$stat['lower'])/(1.0*$stat['27%']);
$tmp[$name] = $stat;
}
$stats = $tmp;
print_r($stats);
}
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 doStats() {
$stats = array();
# Presence
$stats['presence'] = array();
foreach($this->dataset as $student) {
// Create category if need be
$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');
}
}
print_r($stats);
}
}
?>

Event Timeline