🎉 75% of content is free forever — Unlock Premium from $10/mo →
CW
Search courses…
💼 Servicesℹ️ About✉️ ContactView Pricing Plansfrom $10

Functional Data Analysis (FDA)

Advanced Statistical MethodsFunctional Methods🟢 Free Lesson

Advertisement

Functional Data Analysis (FDA)

Advanced Statistical Methods

Treating Curves and Signals as Data Points

Functional data analysis treats entire curves, surfaces, or functions as individual observations. Basis expansions, functional PCA, and registration methods enable statistical analysis of inherently functional data.

  • Biomechanics — Analyze gait patterns by treating each walking cycle as a functional observation
  • Meteorology — Compare daily temperature profiles across weather stations as functional data
  • Finance — Treat intraday trading curves as functional observations for pattern recognition

FDA lets you do statistics on curves, not just numbers.


Functional data analysis treats observations as functions defined on a continuous domain (typically an interval), rather than as vectors of discrete measurements. The paradigm shift from multivariate to functional statistics arises naturally in longitudinal studies, spectrometry, kinematics, and any setting where the sampling frequency is dense relative to the underlying signal.


Functional Data Representation

In practice, we observe discretized or noisy versions:

where is the true curve and are measurement errors. FDA begins by converting these discrete observations into smooth functional representations.


Basis Expansions

The most common approach represents each curve as a linear combination of known basis functions:

where are basis functions and are coefficient vectors.

B-Spline Basis

B-splines offer local support: changing one coefficient affects only nearby curve segments. The roughness penalty on the second derivative enforces smoothness:

where . The smoothing parameter controls the bias–variance tradeoff.

Fourier Basis

The Fourier basis is optimal for capturing oscillatory behavior and provides natural frequency-domain interpretation. The roughness penalty is proportional to .


Functional PCA

The eigenfunctions satisfy the Fredholm integral equation:

where is the sample covariance function.


Functional Linear Models

Scalar-on-Function Regression

The coefficient function is estimated by penalized splines:

where is the design matrix.

Function-on-Function Regression

where is a bivariate coefficient surface. Truncated basis expansion converts this to a multivariate regression problem.


Depth for Functional Data

Functional depth generalizes the concept of median and quantiles to function spaces, enabling robust descriptive statistics, boxplots for functional data, and outlier detection.


Registration / Warping

The registration problem decomposes the observed variation into amplitude variation (vertical differences after alignment) and phase variation (horizontal shifts in features):


Python Implementation

import numpy as np
import pandas as pd
from scipy.interpolate import BSpline
from scipy.optimize import minimize_scalar
import matplotlib.pyplot as plt

np.random.seed(42)

# --- Generate functional data ---
t = np.linspace(0, 1, 200)
n = 30

# True signal + phase variation
def true_signal(t):
    return np.sin(2 * np.pi * t) + 0.5 * np.cos(4 * np.pi * t)

# Generate curves with random warping and amplitude perturbation
x = np.zeros((n, len(t)))
for i in range(n):
    phase_shift = np.random.normal(0, 0.05)
    amp = 1 + np.random.normal(0, 0.1)
    freq = 1 + np.random.normal(0, 0.05)
    x[i, :] = amp * np.sin(2 * np.pi * freq * (t + phase_shift)) + \
              0.5 * np.cos(4 * np.pi * freq * (t + phase_shift)) + \
              np.random.normal(0, 0.05, len(t))

# --- B-spline basis expansion ---
n_basis = 25
knots = np.linspace(0, 1, n_basis - 4 + 2)
knots = np.concatenate([[0, 0, 0], knots[1:-1], [1, 1, 1]])

from scipy.interpolate import BSpline
B = np.zeros((len(t), n_basis))
for k in range(n_basis):
    coeffs = np.zeros(n_basis)
    coeffs[k] = 1.0
    spl = BSpline(knots, coeffs, 3)
    B[:, k] = spl(t)

# Smooth each curve
lambdas = np.logspace(-4, 0, 20)
x_smooth = np.zeros_like(x)
for i in range(n):
    best_gcv = np.inf
    best_lam = lambdas[0]
    for lam in lambdas:
        R = np.zeros((n_basis, n_basis))
        for k in range(n_basis - 2):
            R[k:k+3, k:k+3] += lam * np.array([[1, -2, 1], [-2, 4, -2], [1, -2, 1]])
        c = np.linalg.solve(B.T @ B + R, B.T @ x[i, :])
        residual = x[i, :] - B @ c
        gcv = np.mean(residual**2) / (1 - n_basis / len(t))**2
        if gcv < best_gcv:
            best_gcv = gcv
            best_lam = lam
    R = np.zeros((n_basis, n_basis))
    for k in range(n_basis - 2):
        R[k:k+3, k:k+3] += best_lam * np.array([[1, -2, 1], [-2, 4, -2], [1, -2, 1]])
    c = np.linalg.solve(B.T @ B + R, B.T @ x[i, :])
    x_smooth[i, :] = B @ c

print(f"Smoothed {n} curves with {n_basis} B-splines")

# --- Functional PCA ---
mean_func = x_smooth.mean(axis=0)
centered = x_smooth - mean_func

# Covariance matrix in basis coordinates
c_basis = np.linalg.lstsq(B, centered.T, rcond=None)[0].T
cov_matrix = np.cov(c_basis.T)
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)

# Reverse for descending order
idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]

# Eigenfunctions
psi = np.zeros((len(t), min(5, n_basis)))
for k in range(min(5, n_basis)):
    psi[:, k] = B @ eigenvectors[:, k]
    psi[:, k] /= np.sqrt(np.trapz(psi[:, k] ** 2, t))

var_explained = eigenvalues / eigenvalues.sum()
print(f"\nFPCA variance explained (first 5 PCs): {var_explained[:5].round(4)}")
print(f"Cumulative: {np.cumsum(var_explained)[:5].round(4)}")

# --- Functional Depth (simplified MBD) ---
def modified_band_depth(X, t_grid):
    n, m = X.shape
    mbd = np.zeros(n)
    for i in range(n):
        count = 0
        total = 0
        for j in range(n):
            if j == i:
                continue
            for k in range(j + 1, n):
                if k == i:
                    continue
                lower = np.minimum(X[j], X[k])
                upper = np.maximum(X[j], X[k])
                count += np.sum((X[i] >= lower) & (X[i] <= upper))
                total += m
                # Approximate proportion for speed
        mbd[i] = count / total if total > 0 else 0
    return mbd

# Use subset for speed
mbd = modified_band_depth(x_smooth[:10, ::5], t[::5])
print(f"\nFunctional depths (first 10): {mbd.round(4)}")

# --- Visualization ---
fig, axes = plt.subplots(2, 2, figsize=(12, 8))

# Raw curves
for i in range(min(10, n)):
    axes[0, 0].plot(t, x[i, :], alpha=0.3, lw=0.5)
axes[0, 0].set_title("Raw Curves")
axes[0, 0].set_xlabel("t")
axes[0, 0].set_ylabel("x(t)")

# Smoothed curves
for i in range(min(10, n)):
    axes[0, 1].plot(t, x_smooth[i, :], alpha=0.5, lw=0.8)
axes[0, 1].set_title("Smoothed (B-splines)")
axes[0, 1].set_xlabel("t")

# Eigenfunctions
colors = ['red', 'blue', 'green', 'orange', 'purple']
for k in range(min(3, psi.shape[1])):
    axes[1, 0].plot(t, psi[:, k], color=colors[k], lw=1.5,
                    label=f"PC{k+1} ({var_explained[k]*100:.1f}%)")
axes[1, 0].legend(fontsize=9)
axes[1, 0].set_title("FPCA Eigenfunctions")
axes[1, 0].axhline(y=0, color='gray', ls='--', lw=0.5)

# Scree plot
axes[1, 1].bar(range(1, 8), var_explained[:7] * 100, color='steelblue')
axes[1, 1].set_xlabel("Component")
axes[1, 1].set_ylabel("% Variance")
axes[1, 1].set_title("Scree Plot")

plt.tight_layout()
plt.savefig("fda_analysis.png", dpi=150, bbox_inches="tight")
plt.show()

Summary

Need Expert Statistics Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement