{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Demo of the Whittaker filter\n", "Spectra are often corrupted by uncorrelated random noise. As such, smoothing is frequently a useful option to improve the quality of the spectra for visualization but also for chemometric analysis. Frequently, Savitzky-Golay smoothing is used. It however has some drawback such as:\n", "- two parameters need to be optimized\n", "- parameters may not be varied continously (integer values required)\n", "- speed\n", "- choosing ideal parameters poses a challenge\n", "\n", "Eilers et al. [1] proposed to use a Whittaker smoother instead, which improves on all four points. chemometrics implements the Whittaker smoother with additional gimics such as automatic estimation of the ideal smoothing parameter. The following example shows how the Whittaker smoother may be applied based on artificial data. First, the necessary libraries are imported." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import chemometrics as cm\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here, we want to mimique Raman spectra with a broad background contribution. `generate_spectra` and `generate_background` are helper functions exactly for this application. `generate_spectra` generates a spectra with Gaussian peaks of random intensity and peak width. `generate_background` uses a Gaussian process to generate broad background features. By adding the output of the two functions, an artificial spectrum is obtained." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "n_wl = 500\n", "n_band = 20\n", "bandwidth = 2.2\n", "n_samples = 50\n", "noise = 1\n", "\n", "np.random.seed(5)\n", "\n", "S = np.abs(cm.generate_background(n_wl)).T * 5 + cm.generate_spectra(n_wl, n_band,\n", " bandwidth)\n", "lines = plt.plot(S.T)\n", "plt.xlabel('Predictors')\n", "plt.ylabel('')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using some randomly generated concentrations and adding Gaussian noise, a number of spectra are obtained. The spectra can now be plotted with the `plot_colored_series` function. The spectra spectra are colored by concentration for easier distinction." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "C = np.random.uniform(size=n_samples)\n", "X = C[:,None] * S\n", "X = X + np.random.normal(size=X.shape, scale=noise)\n", "\n", "plt.figure(figsize=(15, 7))\n", "lines = cm.plot_colored_series(X.T, reference=C)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `Whittaker` class provides a score parameter which is based on a leave-one-out cross-validation. The score provides an estimate for the mean error after smoothing. This can be tested for the artificial spectra. The penalty is iteratively varied over 5 orders of magnitude and after each iteration the score is read out. Finally, the scores are plotted against the penalty." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "n_points = 100\n", "\n", "penalties = 10 ** np.linspace(-2, 3, n_points)\n", "scores = np.zeros(penalties.shape)\n", "\n", "whittaker = cm.Whittaker(penalty=1)\n", "\n", "for i in range(n_points):\n", " whittaker.penalty_ = penalties[i]\n", " whittaker.fit(X)\n", " scores[i] = whittaker.score(X)\n", " \n", "plt.plot(penalties, scores,)\n", "plt.xlabel('Penalty')\n", "plt.ylabel('Score')\n", "ax = plt.gca()\n", "ax.semilogx()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The penalty versus score plot shows that there is a local minimum, i.e. an optimal penalty. By not defining a penalty when initializing the Whittaker filter (or setting the penalty to `'auto'`), the `Whittaker` class runs an optimizer to find an optimal penalty. For normal applications and quick testing, this should be a reasonable starting point. This is shown below, `Whittaker` is initialized without penalty. The smoothed spectra are shown below with the original spectrum shown in red with an offset for comparison. Mostly, the smoothing works fine without distorting the shape of the peaks. In case of very sharp bands (see e.g. predictor 300), the filter may dilute them due to its smoothing characteristics. This is however also typical for other filters such as Savitzky-Golay and due to the attenuation of high frequency components.\n", "\n", "Note: due to the random generation of the spectra, it is not guaranteed that very sharp peaks will be generated in given example." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# preprocessing\n", "whittaker = cm.Whittaker()\n", "X_smoothed = whittaker.fit_transform(X)\n", "\n", "# plotting\n", "plt.figure(figsize=(15, 7))\n", "lines = cm.plot_colored_series(X_smoothed.T, reference=C)\n", "plt.plot(S.T + 5, 'r') # plot pure spectra\n", "\n", "plt.xlabel('Predictor')\n", "plt.ylabel('Signal')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The auto-fitted penalty may be recovered from the fitted transformer." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "whittaker.penalty_" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Asymmetric Whittaker\n", "Furthermore, chemometrics also implements a Whittaker filter with asymmetric penalty, i.e. weights are applied based the sign of the residuals. If the residuals are smaller zero, they are weighted in the objective function by a factor $\\lambda$, otherwise by $1-\\lambda$. The asymmetric Whittaker provides the possibility to implement a slowly varying baseline independent of physical assumptions on the baseline. The rigidness of the baseline can be adjusted by the ``penalty`` parameter. The parameter ``asym_factor`` corresponds to $\\lambda$. Similar to `Whittaker`, `AsymWhittaker` implements a scikit-learn transformer and may thus easily be used in a pipeline.\n", "\n", "The asymmetric Whittaker filter may be applied to the previously introduced artificial dataset to get rid of the baseline effect. This is shown in the first figure below. For reference, the original spectra without any preprocessing are plotted in gray in the background. With the used parameter set, reasonable background spectra are estimated and the baseline offset is largly removed.\n", "\n", "The second figure uses the `background` attribute of the asymmetric Whittaker filter to recover the previously fitted backgrounds. One can see that overall, the backgrounds follow a slow trend. The noise impacted the background estimation, which is especially visible towards the edges of the background (less information available)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "aw = cm.AsymWhittaker(penalty=10000, asym_factor=0.99)\n", "X_bg = aw.fit_transform(X_smoothed)\n", "\n", "plt.figure(figsize=(15, 7))\n", "lines = plt.plot(X.T, c=[0.8, 0.8, 0.8], alpha=0.1)\n", "lines = cm.plot_colored_series(X_bg.T, reference=C)\n", "\n", "plt.figure(figsize=(15, 7))\n", "lines = plt.plot(aw.background_.T , c=[0, 0, 0.6], alpha=0.2)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.7" } }, "nbformat": 4, "nbformat_minor": 4 }