import numpy as np

class PCA:
    
    def __init__(self, n_components):
        self.n_components = n_components
        self._center = None
        self._cov_mat = None
        self._eigen_values = None
        self._eigen_vectors = None
        self.explained_variance_ratio = None
    
    def fit(self, X):
        '''
        Build the subspace on which to project the data. It is generated by the eigen vectors corresponding to the k highest 
        eigen values:
            1- center X
            2- compute covariance matrix
            3- compute k eigen vectors V={v_i}_k corresponding to the k larger eigenvalues \lamb={\lamb_i}_k
            4- project onto span(V)
            
        As the covariance matrix of X is symmetric, its eigen vectors are orthogonal, and as such the covariance of the projection of X in the subspace
        generated by its eigen vectors is a diagonal matrix where entries are its eigen values. The explained variance ratio per principal direction is
        simply the corresponding eigen value divided by the sum of all the eigen values (which is the total variance).
        '''
        self._center = X.mean(axis=0)
        self._cov_mat = np.cov(X - self._center, rowvar=False)
        
        lambs, vs = np.linalg.eigh(self._cov_mat)
        lambs, vs = lambs[::-1], np.flip(vs, axis=1)
        
        self._eigen_values, self._eigen_vectors = lambs[:self.n_components], vs[:, :self.n_components]
        self.explained_variance_ratio = self._eigen_values / lambs.sum()
        
    def transform(self, X):
        return (X - self._center) @ self._eigen_vectors
    
    def fit_transform(self, X):
        self.fit(X)
        return self.transform(X)