check(); if ($dimensions < 1) { throw new InvalidArgumentException('Dimensions must be' . " greater than 0, $dimensions given."); } $this->dimensions = $dimensions; } /** * Return the data types that this transformer is compatible with. * * @internal * * @return list */ public function compatibility() : array { return [ DataType::continuous(), ]; } /** * Is the transformer fitted? * * @return bool */ public function fitted() : bool { return $this->mean and $this->eigenvectors; } /** * Return the percentage of information lost due to the transformation. * * @return float|null */ public function lossiness() : ?float { return $this->lossiness; } /** * Fit the transformer to a dataset. * * @param Dataset $dataset * @throws InvalidArgumentException */ public function fit(Dataset $dataset) : void { SamplesAreCompatibleWithTransformer::with($dataset, $this)->check(); $xT = Matrix::quick($dataset->samples())->transpose(); $eig = $xT->covariance()->eig(true); $eigenvalues = $eig->eigenvalues(); $eigenvectors = $eig->eigenvectors()->asArray(); $totalVariance = array_sum($eigenvalues); array_multisort($eigenvalues, SORT_DESC, $eigenvectors); $eigenvalues = array_slice($eigenvalues, 0, $this->dimensions); $eigenvectors = array_slice($eigenvectors, 0, $this->dimensions); $eigenvectors = Matrix::quick($eigenvectors)->transpose(); $noiseVariance = $totalVariance - array_sum($eigenvalues); $lossiness = $noiseVariance / ($totalVariance ?: EPSILON); $this->mean = $xT->mean()->transpose(); $this->eigenvectors = $eigenvectors; $this->lossiness = $lossiness; } /** * Transform the dataset in place. * * @param list> $samples * @throws RuntimeException */ public function transform(array &$samples) : void { if (!$this->mean or !$this->eigenvectors) { throw new RuntimeException('Transformer has not been fitted.'); } $samples = Matrix::build($samples) ->subtract($this->mean) ->matmul($this->eigenvectors) ->asArray(); } /** * Return the string representation of the object. * * @internal * * @return string */ public function __toString() : string { return "Principal Component Analysis (dimensions: {$this->dimensions})"; } }