import numpy as np


def pca_fit(X, n_components):
    mean = X.mean(axis=0)
    X_centered = X - mean
    # SVD-based PCA: more numerically stable than eigendecomposing
    # the covariance matrix directly.
    U, S, Vt = np.linalg.svd(X_centered, full_matrices=False)
    components = Vt[:n_components]
    eigenvalues = (S ** 2) / (X.shape[0] - 1)
    explained_variance_ratio = eigenvalues[:n_components] / eigenvalues.sum()
    return {
        "mean": mean,
        "components": components,
        "explained_variance_ratio": explained_variance_ratio,
    }


def pca_transform(X, model):
    return (X - model["mean"]) @ model["components"].T


def pca_inverse_transform(Z, model):
    return Z @ model["components"] + model["mean"]
