0.5)) { throw new InvalidArgumentException('Contamination must be' . " between 0 and 0.5, $contamination given."); } $this->k = $k; $this->contamination = $contamination; $this->tree = $tree ?? new KDTree(); } /** * Return the estimator type. * * @internal * * @return EstimatorType */ public function type() : EstimatorType { return EstimatorType::anomalyDetector(); } /** * Return the data types that the estimator is compatible with. * * @internal * * @return list<\Rubix\ML\DataType> */ public function compatibility() : array { return $this->tree->kernel()->compatibility(); } /** * Return the settings of the hyper-parameters in an associative array. * * @internal * * @return mixed[] */ public function params() : array { return [ 'k' => $this->k, 'contamination' => $this->contamination, 'tree' => $this->tree, ]; } /** * Has the learner been trained? * * @return bool */ public function trained() : bool { return !$this->tree->bare() and $this->kdistances and $this->lrds; } /** * Return the base spatial tree instance. * * @return Spatial */ public function tree() : Spatial { return $this->tree; } /** * Train the learner with a dataset. * * @param Dataset $dataset */ public function train(Dataset $dataset) : void { SpecificationChain::with([ new DatasetIsNotEmpty($dataset), new SamplesAreCompatibleWithEstimator($dataset, $this), ])->check(); $labels = range(0, $dataset->numSamples() - 1); $dataset = Labeled::quick($dataset->samples(), $labels); $this->tree->grow($dataset); $this->kdistances = $this->lrds = []; $iHat = $dHat = []; foreach ($dataset->samples() as $sample) { [$samples, $indices, $distances] = $this->tree->nearest($sample, $this->k); $iHat[] = $indices; $dHat[] = $distances; $this->kdistances[] = end($distances) ?: INF; } $this->lrds = array_map([$this, 'localReachabilityDensity'], $iHat, $dHat); if (isset($this->contamination)) { $lofs = array_map([$this, 'localOutlierFactor'], $dataset->samples()); $threshold = Stats::quantile($lofs, 1.0 - $this->contamination); } $this->threshold = $threshold ?? self::DEFAULT_THRESHOLD; $this->featureCount = $dataset->numFeatures(); } /** * Make predictions from a dataset. * * @param Dataset $dataset * @return list */ public function predict(Dataset $dataset) : array { if ($this->tree->bare() or !$this->featureCount) { throw new RuntimeException('Estimator has not been trained.'); } DatasetHasDimensionality::with($dataset, $this->featureCount)->check(); return array_map([$this, 'predictSample'], $dataset->samples()); } /** * Predict a single sample and return the result. * * @internal * * @param list $sample * @return int */ public function predictSample(array $sample) : int { return $this->localOutlierFactor($sample) > $this->threshold ? 1 : 0; } /** * Return the anomaly scores assigned to the samples in a dataset. * * @param Dataset $dataset * @throws RuntimeException * @return list */ public function score(Dataset $dataset) : array { if ($this->tree->bare() or !$this->featureCount) { throw new RuntimeException('Estimator has not been trained.'); } DatasetHasDimensionality::with($dataset, $this->featureCount)->check(); return array_map([$this, 'localOutlierFactor'], $dataset->samples()); } /** * Calculate the local outlier factor of a given sample given its k * nearest neighbors. * * @param list $sample * @return float */ protected function localOutlierFactor(array $sample) : float { [$samples, $indices, $distances] = $this->tree->nearest($sample, $this->k); $lrd = $this->localReachabilityDensity($indices, $distances) ?: EPSILON; $ratios = []; foreach ($indices as $index) { $ratios[] = $this->lrds[$index] / $lrd; } return Stats::mean($ratios); } /** * Calculate the local reachability density of a sample given its * distances to its k nearest neighbors. * * @param list $indices * @param list $distances * @return float */ protected function localReachabilityDensity(array $indices, array $distances) : float { $kdistances = []; foreach ($indices as $index) { $kdistances[] = $this->kdistances[$index]; } $rds = array_map('max', $distances, $kdistances); return 1.0 / (Stats::mean($rds) ?: EPSILON); } /** * Return the string representation of the object. * * @internal * * @return string */ public function __toString() : string { return 'Local Outlier Factor (' . Params::stringify($this->params()) . ')'; } }