Source code for spectoprep.preprocessing.scatter
"""
Scatter correction methods for spectroscopic data preprocessing.
"""
import numpy as np
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.utils.validation import check_is_fitted
[docs]
class StandardNormalVariate(BaseEstimator, TransformerMixin):
"""
Standard Normal Variate (SNV) transformation.
SNV is a row-wise transformation that centers and scales each spectrum
individually. It's commonly used to remove scatter effects in
spectroscopic data.
Attributes
----------
mean_ : ndarray of shape (n_samples, 1)
Mean of each sample (row) computed during fit.
std_ : ndarray of shape (n_samples, 1)
Standard deviation of each sample computed during fit.
is_fitted_ : bool
Flag indicating if the transformer has been fitted.
"""
[docs]
def fit(self, X, y=None):
"""
Compute mean and standard deviation of each sample.
Parameters
----------
X : array-like of shape (n_samples, n_features)
The input spectra.
y : None
Ignored.
Returns
-------
self : object
Returns self.
"""
self.mean_ = np.mean(X, axis=1, keepdims=True)
self.std_ = np.std(X, axis=1, keepdims=True)
self.is_fitted_ = True
return self
[docs]
def transform(self, X):
"""
Apply the StandardNormalVariate transformation.
Parameters
----------
X : array-like of shape (n_samples, n_features)
The input spectra.
Returns
-------
X_transformed : ndarray of shape (n_samples, n_features)
Transformed spectra.
"""
check_is_fitted(self, "is_fitted_")
mean = np.mean(X, axis=1, keepdims=True)
std = np.std(X, axis=1, keepdims=True)
# Guard against constant spectra (std == 0) which would yield inf/nan.
std = np.where(std == 0, 1.0, std)
return (X - mean) / std
[docs]
class MultiplicativeScatterCorrection(BaseEstimator, TransformerMixin):
"""
Multiplicative Scatter Correction (MSC) for spectroscopic data.
MSC performs a linear regression of each spectrum on a reference spectrum
(usually the mean spectrum) and corrects using the estimated coefficients.
Attributes
----------
mean_reference : ndarray of shape (n_features,)
Reference spectrum (mean of all spectra by default).
"""
def __init__(self):
self.mean_reference = None
[docs]
def fit(self, X, y=None):
"""
Compute the reference spectrum.
Parameters
----------
X : array-like of shape (n_samples, n_features)
The input spectra.
y : None
Ignored.
Returns
-------
self : object
Returns self.
"""
self.mean_reference = np.mean(X, axis=0)
return self
[docs]
def transform(self, X):
"""
Apply the MSC transformation.
Parameters
----------
X : array-like of shape (n_samples, n_features)
The input spectra.
Returns
-------
X_transformed : ndarray of shape (n_samples, n_features)
Transformed spectra.
"""
if self.mean_reference is None:
raise ValueError("MSC instance is not fitted yet.")
corrected_spectra = []
for spectrum in X:
coef = np.polyfit(self.mean_reference, spectrum, 1)
# Guard against a flat reference / zero slope (coef[0] == 0).
slope = coef[0] if coef[0] != 0 else 1.0
corrected = (spectrum - coef[1]) / slope
corrected_spectra.append(corrected)
return np.array(corrected_spectra)
[docs]
class ExtendedMultiplicativeScatterCorrection(BaseEstimator, TransformerMixin):
"""
Extended Multiplicative Scatter Correction (EMSC) for spectroscopic data.
EMSC extends the MSC method by incorporating polynomial terms to account
for more complex spectral variations.
Parameters
----------
order : int, default=2
Order of the polynomial used in the correction.
Attributes
----------
reference_spectrum : ndarray of shape (n_features,)
Reference spectrum (mean of training spectra).
"""
def __init__(self, order=2):
self.order = order
self.reference_spectrum = None
[docs]
def fit(self, X, y=None):
"""
Compute the reference spectrum.
Parameters
----------
X : array-like of shape (n_samples, n_features)
The input spectra.
y : None
Ignored.
Returns
-------
self : object
Returns self.
"""
self.reference_spectrum = np.mean(X, axis=0)
return self
[docs]
def transform(self, X):
"""
Apply the EMSC transformation.
Parameters
----------
X : array-like of shape (n_samples, n_features)
The input spectra.
Returns
-------
X_transformed : ndarray of shape (n_samples, n_features)
Transformed spectra.
"""
if self.reference_spectrum is None:
raise ValueError("EMSC instance is not fitted yet.")
corrected_spectra = []
for spectrum in X:
Xt = np.vstack([self.reference_spectrum**i for i in range(self.order + 1)]).T
coef = np.linalg.lstsq(Xt, spectrum, rcond=None)[0]
corrected = spectrum - Xt[:, 1:] @ coef[1:]
corrected_spectra.append(corrected)
return np.array(corrected_spectra)
[docs]
class LocalizedSNV(BaseEstimator, TransformerMixin):
"""
Localized Standard Normal Variate (LSNV) using a sliding window.
LSNV applies the SNV transformation using a local window around each
wavelength point rather than the entire spectrum.
Parameters
----------
window_size : int, default=11
Size of the sliding window. Must be odd.
"""
def __init__(self, window_size=11):
self.window_size = window_size
[docs]
def fit(self, X, y=None):
"""
No-op.
Parameters
----------
X : array-like of shape (n_samples, n_features)
The input spectra.
y : None
Ignored.
Returns
-------
self : object
Returns self.
"""
if self.window_size % 2 == 0:
raise ValueError("window_size must be odd")
return self
[docs]
def transform(self, X):
"""
Apply LSNV transformation.
Parameters
----------
X : array-like of shape (n_samples, n_features)
The input spectra.
Returns
-------
X_transformed : ndarray of shape (n_samples, n_features)
Transformed spectra.
"""
def localized_snv(spectrum):
half_win = self.window_size // 2
corrected = np.zeros_like(spectrum)
for i in range(len(spectrum)):
start = max(0, i - half_win)
end = min(len(spectrum), i + half_win + 1)
window = spectrum[start:end]
std = np.std(window)
corrected[i] = (spectrum[i] - np.mean(window)) / (std if std != 0 else 1.0)
return corrected
return np.apply_along_axis(localized_snv, axis=1, arr=X)
[docs]
class RobustNormalVariate(BaseEstimator, TransformerMixin):
"""
Robust Normal Variate (RNV) Preprocessing.
RNV is a robust version of SNV that uses percentiles instead of mean and
standard deviation to reduce the influence of outliers.
Parameters
----------
lower_percentile : float, default=25
The lower percentile for robust scaling.
upper_percentile : float, default=75
The upper percentile for robust scaling.
"""
def __init__(self, lower_percentile=25, upper_percentile=75):
self.lower_percentile = lower_percentile
self.upper_percentile = upper_percentile
[docs]
def fit(self, X, y=None):
"""
No-op.
Parameters
----------
X : array-like of shape (n_samples, n_features)
The input spectra.
y : None
Ignored.
Returns
-------
self : object
Returns self.
"""
return self
[docs]
def transform(self, X):
"""
Apply RNV transformation.
Parameters
----------
X : array-like of shape (n_samples, n_features)
The input spectra.
Returns
-------
X_transformed : ndarray of shape (n_samples, n_features)
Transformed spectra.
"""
lower_bound = np.percentile(X, self.lower_percentile, axis=1, keepdims=True)
upper_bound = np.percentile(X, self.upper_percentile, axis=1, keepdims=True)
scale = upper_bound - lower_bound
# Guard against a zero inter-percentile range (e.g. constant spectra).
scale = np.where(scale == 0, 1.0, scale)
median = np.median(X, axis=1, keepdims=True)
return (X - median) / scale