Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ Variance tells you *how much* of the signal each axis carries; loadings tell you

```python
import pandas as pd
from percentify import pca_variance, pca_loadings
from percentify import pca_variance, pca_loadings, pca_transform

df = pd.DataFrame({
"height_cm": [160, 170, 180, 175, 165],
Expand All @@ -178,6 +178,7 @@ df = pd.DataFrame({

print(pca_variance(df)) # PC1 carries most of the variance
print(pca_loadings(df)) # PC1 = (height, weight) with similar signs
print(pca_transform(df)) # each row expressed as PC1, PC2, PC3 scores
```

#### Drop collinear columns before modelling
Expand Down Expand Up @@ -231,6 +232,7 @@ print(change(kpis))
| `outliers` | What percentage of each column are outliers? |
| `pca_variance` | How much variance does each principal component explain? |
| `pca_loadings` | What does each principal component consist of? |
| `pca_transform` | Where does each row sit on the principal components? |
| `imbalance` | How skewed are the classes in a target column? |
| `correlate` | Which features move together, and is it significant? |
| `skew_report` | How skewed is each column, and what transform helps? |
Expand Down
2 changes: 1 addition & 1 deletion docs/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Every function is available on the namespace. Importing functions directly works
too, and is what the examples on this page use so each call stays on one line:

```python
from percentify import change, vif, missing, cv, outliers, pca_variance, pca_loadings
from percentify import change, vif, missing, cv, outliers, pca_variance, pca_loadings, pca_transform
```

The two styles are interchangeable. Pick one and stay with it.
Expand Down
4 changes: 2 additions & 2 deletions percentify/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from .profiling import profiler, ProfileReport, Finding
from .stats import (
change, vif, missing, cv, outliers, pca_variance, pca_loadings, imbalance,
change, vif, missing, cv, outliers, pca_variance, pca_loadings, pca_transform, imbalance,
correlate, skew_report, bootstrap_ci, permutation_test, effect_size,
difference, split, display, PercentifyWarning,
)

__all__ = [
"profiler", "ProfileReport", "Finding",
"change", "vif", "missing", "cv", "outliers", "pca_variance", "pca_loadings", "imbalance",
"change", "vif", "missing", "cv", "outliers", "pca_variance", "pca_loadings", "pca_transform", "imbalance",
"correlate", "skew_report", "bootstrap_ci", "permutation_test", "effect_size",
"difference", "split", "display", "PercentifyWarning",
]
Expand Down
73 changes: 73 additions & 0 deletions percentify/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,79 @@ def pca_loadings(
return result


@_backend_aware
def pca_transform(
df: pd.DataFrame, decimals: Optional[int] = None, n_components: Optional[int] = None,
standardize: bool = True,
) -> pd.DataFrame:
"""Project a DataFrame's numeric features onto principal components.

This uses the same PCA calculation as :func:`pca_variance` and
:func:`pca_loadings`. By default, each feature is standardized before the
projection, so scores are based on correlations rather than the original
measurement scales. Set ``standardize=False`` for covariance-based PCA.

Non-numeric columns are ignored and rows containing missing numeric values
are omitted. Constant columns are dropped when standardizing because they
cannot contribute to a component.

Args:
df: DataFrame with numeric columns.
decimals: Number of decimal places to round scores to. If None, scores
are returned without rounding.
n_components: Number of component columns to return. If None, returns
every available component.
standardize: If True (default), scale each feature to unit variance
before calculating scores.

Returns:
DataFrame with one row per complete input row and columns ``PC1``,
``PC2``, and so on.
"""
if not isinstance(df, pd.DataFrame):
raise TypeError(f"pca_transform expects a pandas DataFrame, got {type(df).__name__}.")

numeric = df.select_dtypes(include=[np.number]).dropna()
if numeric.shape[1] < 2:
_warn("Numeric columns required: pca_transform needs at least 2 numeric columns.")
return pd.DataFrame()

if numeric.shape[0] < 2:
_warn("Not enough data: need at least 2 complete (non-null) rows.")
return pd.DataFrame()

X = numeric.values.astype(float)
if standardize:
scale = X.std(axis=0, ddof=1)
keep = scale > 0
if keep.sum() < 2:
_warn("Numeric columns required: after dropping constant (zero-variance) "
"columns, fewer than 2 columns remain to standardize for PCA.")
return pd.DataFrame()
X = X[:, keep]
scale = scale[keep]
X = (X - X.mean(axis=0)) / scale
else:
X = X - X.mean(axis=0)

eigenvalues, eigenvectors = np.linalg.eigh(np.cov(X, rowvar=False))
if eigenvalues.sum() == 0:
_warn("All numeric columns are constant - there are no components to transform.")
return pd.DataFrame()

eigenvectors = eigenvectors[:, np.argsort(eigenvalues)[::-1]]
if n_components is not None:
eigenvectors = eigenvectors[:, :n_components]

scores = X @ eigenvectors
result = pd.DataFrame(
scores,
index=numeric.index,
columns=[f"PC{i + 1}" for i in range(scores.shape[1])],
)
return result if decimals is None else result.round(decimals)


@_backend_aware
def imbalance(data, decimals: Optional[int] = 2) -> pd.DataFrame:
"""
Expand Down
Loading