2016-07-19 19:58:59 +00:00
|
|
|
<?php
|
2016-07-19 19:59:23 +00:00
|
|
|
|
2016-11-20 21:53:17 +00:00
|
|
|
declare(strict_types=1);
|
2016-07-19 19:58:59 +00:00
|
|
|
|
|
|
|
namespace Phpml\Metric;
|
|
|
|
|
|
|
|
class ClassificationReport
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
private $precision = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
private $recall = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
private $f1score = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
private $support = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
private $average = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array $actualLabels
|
|
|
|
* @param array $predictedLabels
|
|
|
|
*/
|
|
|
|
public function __construct(array $actualLabels, array $predictedLabels)
|
|
|
|
{
|
2016-09-27 18:07:21 +00:00
|
|
|
$truePositive = $falsePositive = $falseNegative = $this->support = self::getLabelIndexedArray($actualLabels, $predictedLabels);
|
2016-07-19 19:58:59 +00:00
|
|
|
|
|
|
|
foreach ($actualLabels as $index => $actual) {
|
|
|
|
$predicted = $predictedLabels[$index];
|
2016-07-19 19:59:23 +00:00
|
|
|
++$this->support[$actual];
|
2016-07-19 19:58:59 +00:00
|
|
|
|
2016-07-19 19:59:23 +00:00
|
|
|
if ($actual === $predicted) {
|
|
|
|
++$truePositive[$actual];
|
2016-07-19 19:58:59 +00:00
|
|
|
} else {
|
2016-07-19 19:59:23 +00:00
|
|
|
++$falsePositive[$predicted];
|
|
|
|
++$falseNegative[$actual];
|
2016-07-19 19:58:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->computeMetrics($truePositive, $falsePositive, $falseNegative);
|
|
|
|
$this->computeAverage();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getPrecision()
|
|
|
|
{
|
|
|
|
return $this->precision;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getRecall()
|
|
|
|
{
|
|
|
|
return $this->recall;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getF1score()
|
|
|
|
{
|
|
|
|
return $this->f1score;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getSupport()
|
|
|
|
{
|
|
|
|
return $this->support;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getAverage()
|
|
|
|
{
|
|
|
|
return $this->average;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array $truePositive
|
|
|
|
* @param array $falsePositive
|
|
|
|
* @param array $falseNegative
|
|
|
|
*/
|
|
|
|
private function computeMetrics(array $truePositive, array $falsePositive, array $falseNegative)
|
|
|
|
{
|
|
|
|
foreach ($truePositive as $label => $tp) {
|
2016-09-27 18:07:21 +00:00
|
|
|
$this->precision[$label] = $this->computePrecision($tp, $falsePositive[$label]);
|
|
|
|
$this->recall[$label] = $this->computeRecall($tp, $falseNegative[$label]);
|
2016-07-19 19:59:23 +00:00
|
|
|
$this->f1score[$label] = $this->computeF1Score((float) $this->precision[$label], (float) $this->recall[$label]);
|
2016-07-19 19:58:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private function computeAverage()
|
|
|
|
{
|
|
|
|
foreach (['precision', 'recall', 'f1score'] as $metric) {
|
|
|
|
$values = array_filter($this->$metric);
|
2016-11-20 21:53:17 +00:00
|
|
|
if (0 == count($values)) {
|
2016-11-20 21:49:26 +00:00
|
|
|
$this->average[$metric] = 0.0;
|
|
|
|
continue;
|
|
|
|
}
|
2016-07-19 19:58:59 +00:00
|
|
|
$this->average[$metric] = array_sum($values) / count($values);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-27 18:07:21 +00:00
|
|
|
/**
|
|
|
|
* @param int $truePositive
|
|
|
|
* @param int $falsePositive
|
|
|
|
*
|
|
|
|
* @return float|string
|
|
|
|
*/
|
|
|
|
private function computePrecision(int $truePositive, int $falsePositive)
|
|
|
|
{
|
|
|
|
if (0 == ($divider = $truePositive + $falsePositive)) {
|
|
|
|
return 0.0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $truePositive / $divider;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param int $truePositive
|
|
|
|
* @param int $falseNegative
|
|
|
|
*
|
|
|
|
* @return float|string
|
|
|
|
*/
|
|
|
|
private function computeRecall(int $truePositive, int $falseNegative)
|
|
|
|
{
|
|
|
|
if (0 == ($divider = $truePositive + $falseNegative)) {
|
|
|
|
return 0.0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $truePositive / $divider;
|
|
|
|
}
|
|
|
|
|
2016-07-19 19:58:59 +00:00
|
|
|
/**
|
|
|
|
* @param float $precision
|
|
|
|
* @param float $recall
|
|
|
|
*
|
|
|
|
* @return float
|
|
|
|
*/
|
|
|
|
private function computeF1Score(float $precision, float $recall): float
|
|
|
|
{
|
2016-07-19 19:59:23 +00:00
|
|
|
if (0 == ($divider = $precision + $recall)) {
|
2016-07-19 19:58:59 +00:00
|
|
|
return 0.0;
|
|
|
|
}
|
|
|
|
|
2016-12-07 23:45:42 +00:00
|
|
|
return 2.0 * (($precision * $recall) / $divider);
|
2016-07-19 19:58:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-09-27 18:07:21 +00:00
|
|
|
* @param array $actualLabels
|
|
|
|
* @param array $predictedLabels
|
2016-07-19 19:58:59 +00:00
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
2016-09-27 18:07:21 +00:00
|
|
|
private static function getLabelIndexedArray(array $actualLabels, array $predictedLabels): array
|
2016-07-19 19:58:59 +00:00
|
|
|
{
|
2016-09-27 18:07:21 +00:00
|
|
|
$labels = array_values(array_unique(array_merge($actualLabels, $predictedLabels)));
|
2016-07-19 19:58:59 +00:00
|
|
|
sort($labels);
|
|
|
|
$labels = array_combine($labels, array_fill(0, count($labels), 0));
|
|
|
|
|
|
|
|
return $labels;
|
|
|
|
}
|
|
|
|
}
|