kernel()->compatibility())) { $kernel = $tree->kernel(); if (!$kernel instanceof NaNSafe) { throw new InvalidArgumentException('Continuous distance kernels' . ' must implement the NaNSafe interface.'); } } if (empty($categoricalPlaceholder)) { throw new InvalidArgumentException('Categorical placeholder cannot be empty.'); } $this->k = $k; $this->weighted = $weighted; $this->categoricalPlaceholder = $categoricalPlaceholder; $this->tree = $tree ?? new BallTree(30, new SafeEuclidean()); } /** * Return the data types that this transformer is compatible with. * * @internal * * @return list */ public function compatibility() : array { return $this->tree->kernel()->compatibility(); } /** * Is the transformer fitted? * * @return bool */ public function fitted() : bool { return !$this->tree->bare(); } /** * Fit the transformer to a dataset. * * @param Dataset $dataset * @throws RuntimeException */ public function fit(Dataset $dataset) : void { SamplesAreCompatibleWithTransformer::with($dataset, $this)->check(); $donors = []; foreach ($dataset->samples() as $sample) { foreach ($sample as $value) { if (is_float($value)) { if (is_nan($value)) { continue 2; } } else { if ($value === $this->categoricalPlaceholder) { continue 2; } } } $donors[] = $sample; } if (empty($donors)) { throw new RuntimeException('No complete donors found in dataset.'); } $labels = array_fill(0, count($donors), ''); $this->tree->grow(Labeled::quick($donors, $labels)); $this->types = $dataset->featureTypes(); } /** * Transform the dataset in place. * * @param list> $samples * @throws RuntimeException */ public function transform(array &$samples) : void { if ($this->tree->bare() or $this->types === null) { throw new RuntimeException('Transformer has not been fitted.'); } foreach ($samples as &$sample) { $donors = []; foreach ($sample as $column => &$value) { if (is_float($value) && is_nan($value) or $value === $this->categoricalPlaceholder) { if (empty($donors)) { [$donors, $labels, $distances] = $this->tree->nearest($sample, $this->k); if ($this->weighted) { $weights = []; foreach ($distances as $distance) { $weights[] = 1.0 / (1.0 + $distance); } } } $values = array_column($donors, $column); $type = $this->types[$column]; switch ($type) { case DataType::continuous(): if (isset($weights)) { $value = Stats::weightedMean($values, $weights); } else { $value = Stats::mean($values); } break; case DataType::categorical(): default: if (isset($weights)) { $scores = array_fill_keys(array_unique($values), 0.0); foreach ($weights as $i => $weight) { $scores[$values[$i]] += $weight; } } else { $scores = array_count_values($values); } $value = argmax($scores); break; } } } } } /** * Return the string representation of the object. * * @internal * * @return string */ public function __toString() : string { return "KNN Imputer (k: {$this->k}, weighted: {$this->weighted}," . " categorical placeholder: {$this->categoricalPlaceholder}," . " tree: {$this->tree})"; } }