Skip to content

Instantly share code, notes, and snippets.

@tjburch
Created February 21, 2024 01:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tjburch/062547b3600f81db73b40feb044bab2a to your computer and use it in GitHub Desktop.
Save tjburch/062547b3600f81db73b40feb044bab2a to your computer and use it in GitHub Desktop.
Sklearn-style transformer to create orthogonal polynomials
from typing import Optional
from sklearn.base import BaseEstimator, TransformerMixin
class OrthogonalPolynomialTransformer(BaseEstimator, TransformerMixin):
"""Transforms input data using orthogonal polynomials."""
def __init__(self, degree: int = 1) -> None:
self.degree = degree + 1 # Account for constant term
self.norm2 = None
self.alpha = None
def fit(self, X: np.ndarray, y: Optional[np.ndarray] = None) -> 'OrthogonalPolynomialTransformer':
"""Calculate transformation matrix, extract norm2 and alpha."""
# Reset state-related attributes at the beginning of each fit call
self.norm2 = None
self.alpha = None
X = np.asarray(X).flatten()
if self.degree >= len(np.unique(X)):
raise ValueError("'degree' must be less than the number of unique data points.")
# Center data around its mean
mean = np.mean(X)
X_centered = X - mean
# Create Vandermonde matrix for centered data and perform QR decomposition
vandermonde = np.vander(X_centered, N=self.degree + 1, increasing=True)
Q, R = np.linalg.qr(vandermonde)
# Compute transformation matrix and norms
diagonal = np.diag(np.diag(R)) # extract diagonal, then create diagonal matrix
transformation_matrix = np.dot(Q, diagonal)
self.norm2 = np.sum(transformation_matrix**2, axis=0)
# Get alpha
# Normalized weighted sum sqared of transformation matrix
weighted_sums = np.sum(
(transformation_matrix**2) * np.reshape(X_centered, (-1, 1)),
axis=0
)
normalized_sums = weighted_sums / self.norm2
adjusted_sums = normalized_sums + mean
self.alpha = adjusted_sums[:self.degree]
return self
def transform(self, X: np.ndarray) -> np.ndarray:
"""Iteratively apply up to 'degree'."""
X = np.asarray(X).flatten()
transformed_X = np.empty((len(X), self.degree + 1)) # Adjusted to include all polynomial degrees
transformed_X[:, 0] = 1 # x^0
if self.degree > 0:
transformed_X[:, 1] = X - self.alpha[0]
if self.degree > 1:
for i in range(1, self.degree):
transformed_X[:, i + 1] = (
(X - self.alpha[i]) * transformed_X[:, i] -
(self.norm2[i] / self.norm2[i - 1]) * transformed_X[:, i - 1]
)
transformed_X /= np.sqrt(self.norm2)
# return without constant term
return transformed_X[:, 1:self.degree]
def fit_transform(self, X: np.ndarray, y: Optional[np.ndarray] = None) -> np.ndarray:
self.fit(X, y)
return self.transform(X)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment