Tutorial: From Hello World to Stacking

This tutorial builds progressively from a minimal pipeline to advanced model stacking. Each section introduces one new concept on top of the previous one. All examples use synthetic data generated on the fly, so you can run them without any external files.

Estimated time: 20 minutes.


1. Hello World

The simplest possible nirs4all pipeline needs three things: data, a model, and nirs4all.run().

import nirs4all
from sklearn.cross_decomposition import PLSRegression

dataset = nirs4all.generate(n_samples=200, random_state=42)
result = nirs4all.run(
    pipeline=[PLSRegression(n_components=5)],
    dataset=dataset,
    verbose=1,
)
print(f"RMSE: {result.best_rmse:.4f}")

nirs4all.generate() creates a synthetic NIRS dataset with realistic spectral characteristics. The pipeline is a plain Python list containing a single model – nirs4all auto-detects the step type. After training, the result object gives you best_rmse, best_r2, and other score accessors.


2. Add Preprocessing

Raw spectra benefit from preprocessing before the model sees them. Add standard scaling and Standard Normal Variate (SNV) scatter correction.

import nirs4all
from sklearn.preprocessing import MinMaxScaler
from sklearn.cross_decomposition import PLSRegression
from nirs4all.operators.transforms import SNV

dataset = nirs4all.generate(n_samples=200, random_state=42)

result = nirs4all.run(pipeline=[
    MinMaxScaler(),
    SNV(),
    PLSRegression(n_components=10),
], dataset=dataset, verbose=1)
print(f"RMSE: {result.best_rmse:.4f}")

Steps execute in order: MinMaxScaler scales features to [0, 1], SNV removes scatter effects, and then PLSRegression trains on the cleaned spectra. You can chain as many preprocessing steps as needed.


3. Add Cross-Validation

Without a splitter, the model trains once on the training set. Adding a cross-validation splitter gives you reliable performance estimates across multiple train/validation splits.

import nirs4all
from sklearn.preprocessing import MinMaxScaler
from sklearn.cross_decomposition import PLSRegression
from sklearn.model_selection import ShuffleSplit
from nirs4all.operators.transforms import SNV

dataset = nirs4all.generate(n_samples=200, random_state=42)

result = nirs4all.run(pipeline=[
    MinMaxScaler(),
    SNV(),
    ShuffleSplit(n_splits=5, test_size=0.25, random_state=42),
    {"model": PLSRegression(n_components=10)},
], dataset=dataset, verbose=1)
print(f"RMSE: {result.best_rmse:.4f}")
print(f"R2:   {result.best_r2:.4f}")

ShuffleSplit creates 5 random train/validation splits. Each fold trains a separate model and predicts on held-out samples. The reported scores are aggregated across all folds, giving a more trustworthy estimate than a single split.

Note

Once a splitter is present, wrapping the model with {"model": ...} makes the pipeline structure explicit. Auto-detection also works, but the dict form is recommended for clarity.


4. Compare Preprocessings with _or_

Instead of writing three separate pipelines to compare SNV, MSC, and Detrend, use the _or_ generator keyword. It expands into one pipeline variant per alternative.

import nirs4all
from sklearn.cross_decomposition import PLSRegression
from sklearn.model_selection import ShuffleSplit
from nirs4all.operators.transforms import SNV, MSC, Detrend

dataset = nirs4all.generate(n_samples=200, random_state=42)

pipeline = [
    {"_or_": [SNV, MSC, Detrend]},
    ShuffleSplit(n_splits=5, test_size=0.25, random_state=42),
    {"model": PLSRegression(n_components=10)},
]

result = nirs4all.run(pipeline=pipeline, dataset=dataset, verbose=1)

print("Top 3 configurations:")
for pred in result.top(n=3, display_metrics=["rmse", "r2"]):
    print(f"  {pred.get('preprocessings', '')}: "
          f"RMSE={pred.get('rmse', 0):.4f}, "
          f"R2={pred.get('r2', 0):.4f}")

_or_ takes a list of classes (not instances). nirs4all instantiates each one, creating 3 pipeline variants that are all evaluated. result.top(3) returns the best 3, ranked by validation score by default.

Tip

Pass classes (e.g. SNV) to _or_, not instances (e.g. SNV()). nirs4all handles instantiation internally so it can apply generator combinations correctly.


5. Tune Hyperparameters with _range_

The _range_ generator creates a linear sweep of numeric values. Use it to search over PLS component counts without writing a loop.

import nirs4all
from sklearn.cross_decomposition import PLSRegression
from sklearn.model_selection import ShuffleSplit
from nirs4all.operators.transforms import SNV

dataset = nirs4all.generate(n_samples=200, random_state=42)

result = nirs4all.run(pipeline=[
    SNV(),
    ShuffleSplit(n_splits=5, test_size=0.25, random_state=42),
    {"n_components": {"_range_": [2, 20, 3]}, "model": PLSRegression},
], dataset=dataset, verbose=1)

print("Top 3 component counts:")
for pred in result.top(n=3, display_metrics=["rmse"]):
    print(f"  n_components={pred.get('model_name', '')}: "
          f"RMSE={pred.get('rmse', 0):.4f}")

{"_range_": [2, 20, 3]} generates the values [2, 5, 8, 11, 14, 17, 20] (inclusive end, step of 3). Each value is used as n_components for a separate PLSRegression variant. Note that the model is passed as a class (PLSRegression) rather than an instance, because nirs4all constructs each variant with the swept parameter.

You can combine _or_ on preprocessing with _range_ on model parameters to search a larger space. nirs4all evaluates the full Cartesian product of all generator expansions.


6. Branch and Stack

The most powerful pattern in nirs4all is model stacking: run multiple sub-pipelines in parallel, merge their out-of-fold predictions, and train a meta-model on top.

import nirs4all
from sklearn.cross_decomposition import PLSRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.linear_model import Ridge
from sklearn.model_selection import ShuffleSplit
from nirs4all.operators.transforms import SNV, MSC

dataset = nirs4all.generate(n_samples=200, random_state=42)

pipeline = [
    ShuffleSplit(n_splits=5, test_size=0.25, random_state=42),
    {"branch": [
        [SNV(), PLSRegression(n_components=10)],
        [MSC(), RandomForestRegressor(n_estimators=50, random_state=42)],
    ]},
    {"merge": "predictions"},
    {"model": Ridge()},
]

result = nirs4all.run(pipeline=pipeline, dataset=dataset, verbose=1)
print(f"Stacked RMSE: {result.best_rmse:.4f}")
print(f"Stacked R2:   {result.best_r2:.4f}")

Here is what happens step by step:

  1. Branch – the data is duplicated and sent to two independent sub-pipelines. The first applies SNV then PLS; the second applies MSC then Random Forest.

  2. Merge – "predictions" collects out-of-fold predictions from each branch and stacks them as new features. During cross-validation, only held-out predictions are used, avoiding data leakage.

  3. Meta-model – Ridge() learns how to combine the two sets of predictions into a final output.

This is classic model stacking and often outperforms any single model.


7. Export and Predict

Once you have a trained pipeline you are happy with, export it to a .n4a bundle and use it later on new data.

import nirs4all
from sklearn.cross_decomposition import PLSRegression
from sklearn.model_selection import ShuffleSplit
from sklearn.preprocessing import MinMaxScaler
from nirs4all.operators.transforms import SNV

# Train
dataset = nirs4all.generate(n_samples=200, random_state=42)
result = nirs4all.run(pipeline=[
    MinMaxScaler(),
    SNV(),
    ShuffleSplit(n_splits=5, test_size=0.25, random_state=42),
    {"model": PLSRegression(n_components=10)},
], dataset=dataset, verbose=1)

# Export the best model
result.export("my_model.n4a")

# Later: load and predict on new samples
new_data = nirs4all.generate(n_samples=50, random_state=99)
predictions = nirs4all.predict(model="my_model.n4a", data=new_data)
print(f"Predicted {len(predictions)} samples")
print(f"First 5 predictions: {predictions.y_pred[:5]}")

The .n4a bundle contains the full preprocessing chain and the trained model. nirs4all.predict() replays every step in the correct order, so new data receives the exact same transformations that were applied during training.


8. Next Steps

You now know the core workflow: generate or load data, build a pipeline, run it, inspect results, and export. Here are some directions to explore next.