Skip to content

Node.js Examples

Complete Node.js examples demonstrating fastlowess with native N-API bindings.

Batch Smoothing

Process complete datasets with confidence intervals and diagnostics.

const fastlowess = require('../../bindings/nodejs');

/**
 * fastlowess Batch Smoothing Example
 * 
 * This example demonstrates batch LOWESS smoothing features:
 * - Basic smoothing with different parameters
 * - Robustness iterations for outlier handling
 * - Confidence and prediction intervals
 * - Diagnostics and cross-validation
 *
 * The Lowess class is the primary interface for
 * processing complete datasets that fit in memory.
 */

function generateSampleData(nPoints = 1000) {
    // Generate complex sample data with a trend, seasonality, and outliers.
    const x = new Float64Array(nPoints);
    const y = new Float64Array(nPoints);
    const yTrue = new Float64Array(nPoints);

    for (let i = 0; i < nPoints; i++) {
        x[i] = (i / (nPoints - 1)) * 50; // range 0 to 50
        // Trend + Seasonality
        yTrue[i] = 0.5 * x[i] + 5 * Math.sin(x[i] * 0.5);
        // Gaussian noise (approx)
        let u1 = Math.random();
        let u2 = Math.random();
        let z = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
        y[i] = yTrue[i] + z * 1.5;
    }

    // Add significant outliers (10% of data)
    const nOutliers = Math.round(nPoints * 0.1);
    for (let k = 0; k < nOutliers; k++) {
        const idx = Math.floor(Math.random() * nPoints);
        const sign = Math.random() < 0.5 ? -1 : 1;
        y[idx] += (10 + Math.random() * 10) * sign;
    }

    return { x, y, yTrue };
}

function main() {
    console.log("=== fastlowess Batch Smoothing Example ===");

    // 1. Generate Data
    const { x, y } = generateSampleData(1000);
    console.log(`Generated ${x.length} data points with outliers.`);

    // 2. Basic Smoothing (Default parameters)
    console.log("Running basic smoothing...");
    // Use a smaller fraction (0.05) to capture the sine wave seasonality
    const _resBasic = new fastlowess.Lowess({ iterations: 0, fraction: 0.05 }).fit(x, y);

    // 3. Robust Smoothing (IRLS)
    console.log("Running robust smoothing (3 iterations)...");
    const _resRobust = new fastlowess.Lowess({
        fraction: 0.05,
        iterations: 3,
        robustnessMethod: "bisquare",
        returnRobustnessWeights: true
    }).fit(x, y);

    // 4. Uncertainty Quantification
    console.log("Computing confidence and prediction intervals...");
    const resIntervals = new fastlowess.Lowess({
        fraction: 0.05,
        confidenceIntervals: 0.95,
        predictionIntervals: 0.95,
        returnDiagnostics: true
    }).fit(x, y);

    // 5. Cross-Validation for optimal fraction
    console.log("Running cross-validation to find optimal fraction...");
    const cvFractions = [0.05, 0.1, 0.2, 0.4];

    // We pass the fractions and CV method to the smooth function.
    // The main result returned will be the fit using the BEST fraction.
    // We can also retrieve the score for each fraction.
    const resCV = new fastlowess.Lowess({
        cvFractions,
        cvMethod: "kfold",
        cvK: 5,
        // We can request robustness weights or diagnostics for the final best model too
        returnDiagnostics: true
    }).fit(x, y);

    console.log(`Optimal fraction selected: ${resCV.fractionUsed}`);

    // Check scores if available
    const scores = resCV.cvScores;
    // Note: cvScores array corresponds to the input fractions order.
    if (scores) {
        console.log("CV Scores (RMSE):");
        for (let i = 0; i < cvFractions.length; i++) {
            console.log(` - Fraction ${cvFractions[i]}: ${scores[i].toFixed(4)}`);
        }
    }
    // Diagnostics Printout
    if (resIntervals.diagnostics) {
        const diag = resIntervals.diagnostics;
        console.log("\nFit Statistics (Intervals Model):");
        console.log(` - R²:   ${diag.rSquared.toFixed(4)}`);
        console.log(` - RMSE: ${diag.rmse.toFixed(4)}`);
        console.log(` - MAE:  ${diag.mae.toFixed(4)}`);
    }

    // 6. Boundary Policy Comparison
    console.log("\nDemonstrating boundary policy effects on linear data...");
    const xl = new Float64Array(50);
    const yl = new Float64Array(50);
    for (let i = 0; i < 50; i++) {
        xl[i] = (i / 49) * 10;
        yl[i] = 2 * xl[i] + 1;
    }

    const rExt = new fastlowess.Lowess({ fraction: 0.6, boundaryPolicy: "extend" }).fit(xl, yl);
    const rRef = new fastlowess.Lowess({ fraction: 0.6, boundaryPolicy: "reflect" }).fit(xl, yl);
    const rZr = new fastlowess.Lowess({ fraction: 0.6, boundaryPolicy: "zero" }).fit(xl, yl);

    console.log("Boundary policy comparison:");
    console.log(` - Extend (Default): first=${rExt.y[0].toFixed(2)}, last=${rExt.y[49].toFixed(2)}`);
    console.log(` - Reflect:          first=${rRef.y[0].toFixed(2)}, last=${rRef.y[49].toFixed(2)}`);
    console.log(` - Zero:             first=${rZr.y[0].toFixed(2)}, last=${rZr.y[49].toFixed(2)}`);

    console.log("\n=== Batch Smoothing Example Complete ===");
}

main();

Download batch_smoothing.js


Streaming Smoothing

Process large datasets in memory-efficient chunks.

const fastlowess = require('../../bindings/nodejs');

/**
 * fastlowess Streaming Smoothing Example
 * 
 * This example demonstrates streaming LOWESS smoothing for large datasets:
 * - Basic chunked processing
 * - Handling datasets that don't fit in memory
 * - Parallel execution for extreme speed
 */

function main() {
    console.log("=== fastlowess Streaming Mode Example ===");

    // 1. Generate Very Large Dataset
    // 100,000 points
    const nPoints = 100000;
    console.log(`Generating large dataset: ${nPoints} points...`);

    // Pre-allocate arrays
    const x = new Float64Array(nPoints);
    const y = new Float64Array(nPoints);

    for (let i = 0; i < nPoints; i++) {
        x[i] = (i / (nPoints - 1)) * 100; // range 0 to 100
        // Gaussian noise
        let u1 = Math.random();
        let u2 = Math.random();
        let z = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
        y[i] = Math.cos(x[i] * 0.1) + z * 0.5;
    }

    // 2. Regular Batch Smoothing (for comparison)
    console.log("Running Batch LOWESS (Parallel)...");
    const batchStart = process.hrtime.bigint();
    const resBatch = new fastlowess.Lowess({ fraction: 0.01 }).fit(x, y);
    const batchEnd = process.hrtime.bigint();
    const batchTime = Number(batchEnd - batchStart) / 1e9;
    console.log(`Batch took: ${batchTime.toFixed(4)} seconds`);

    // 3. Streaming Mode
    // Divide the data into chunks of 2,000 for low memory usage
    console.log("Running Streaming LOWESS (Chunked)...");
    const streamStart = process.hrtime.bigint();

    const streamer = new fastlowess.StreamingLowess(
        { fraction: 0.01 },
        { chunkSize: 2000, overlap: 200 }
    );

    // Simulate reading chunks from a stream/file
    // In a real app we wouldn't load all x/y into memory first
    const chunkSize = 2000;
    const resChunks = [];

    for (let i = 0; i < nPoints; i += chunkSize) {
        // Slice creates a view or copy depending on usage;
        // Float64Array.subarray creates a view (zero copy), but the binding expects TypedArray.
        // If the binding makes a copy anyway, subarray is fine.
        const chunkX = x.subarray(i, Math.min(i + chunkSize, nPoints));
        const chunkY = y.subarray(i, Math.min(i + chunkSize, nPoints));

        const chunkRes = streamer.processChunk(chunkX, chunkY);
        if (chunkRes) resChunks.push(chunkRes.y);
    }
    const finalChunk = streamer.finalize();
    if (finalChunk) resChunks.push(finalChunk.y);

    // Combine chunks into one array for comparison
    const totalLen = resChunks.reduce((acc, c) => acc + c.length, 0);
    const streamY = new Float64Array(totalLen);
    let offset = 0;
    for (const c of resChunks) {
        streamY.set(c, offset);
        offset += c.length;
    }

    const streamEnd = process.hrtime.bigint();
    const streamTime = Number(streamEnd - streamStart) / 1e9;
    console.log(`Streaming took: ${streamTime.toFixed(4)} seconds`);

    // 4. Verify Accuracy
    let mse = 0;
    // Lengths might differ slightly if streaming dropped points or handled boundaries differently,
    // but usually they should match if configured right.
    // Let's assume matches for this example or truncate to min.
    const cmpLen = Math.min(resBatch.y.length, streamY.length);
    const batchY = resBatch.y;

    for (let i = 0; i < cmpLen; i++) {
        mse += Math.pow(batchY[i] - streamY[i], 2);
    }
    mse /= cmpLen;

    console.log(`Mean Squared Difference (Batch vs Stream): ${mse.toExponential(2)}`);

    // Show sample of results
    console.log("\nSample comparison (indices 1000-1005):");
    console.log("Index\tBatch\t\tStreaming\tDiff");
    for (let i = 1000; i <= 1005; i++) {
        if (i < cmpLen) {
            const diff = Math.abs(batchY[i] - streamY[i]);
            console.log(`${i}\t${batchY[i].toFixed(6)}\t${streamY[i].toFixed(6)}\t${diff.toFixed(6)}`);
        }
    }

    console.log("\n=== Streaming Smoothing Example Complete ===");
}

main();

Download streaming_smoothing.js


Online Smoothing

Real-time smoothing with sliding window for streaming data.

const fastlowess = require('../../bindings/nodejs');

/**
 * fastlowess Online Smoothing Example
 * 
 * This example demonstrates online LOWESS smoothing for real-time data:
 * - Basic incremental processing with streaming data
 * - Real-time sensor data smoothing
 * - Different update modes (Full vs Incremental)
 * - Memory-bounded processing with sliding window
 */

function main() {
    console.log("=== fastlowess Online Smoothing Example ===");

    // 1. Simulate a real-time signal
    // A sine wave with changing frequency and random noise
    const nPoints = 1000;
    const x = new Float64Array(nPoints);
    const yTrue = new Float64Array(nPoints);
    const y = new Float64Array(nPoints);

    for (let i = 0; i < nPoints; i++) {
        x[i] = i;
        yTrue[i] = 20.0 + 5.0 * Math.sin(x[i] * 0.1) + 2.0 * Math.sin(x[i] * 0.02);
        // Gaussian noise
        let u1 = Math.random();
        let u2 = Math.random();
        let z = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
        y[i] = yTrue[i] + z * 1.2;
    }

    // Add some sudden spikes (sensor glitches)
    for (let i = 200; i < 205; i++) y[i] += 15.0;
    for (let i = 600; i < 610; i++) y[i] -= 10.0;

    console.log(`Simulating ${nPoints} real-time data points...`);

    // 2. Sequential Online Processing

    // Full Update Mode (higher accuracy)
    console.log("Processing with 'full' update mode...");
    const onlineFull = new fastlowess.OnlineLowess(
        { fraction: 0.3, iterations: 3 },
        { windowCapacity: 50, updateMode: "full" }
    );
    const resFull = new Float64Array(nPoints);
    for (let i = 0; i < nPoints; i++) {
        const chunkX = new Float64Array([x[i]]);
        const chunkY = new Float64Array([y[i]]);
        const res = onlineFull.addPoints(chunkX, chunkY);
        resFull[i] = res.y[0];
    }

    // Incremental Update Mode (faster for large windows)
    console.log("Processing with 'incremental' update mode...");
    const onlineInc = new fastlowess.OnlineLowess(
        { fraction: 0.3, iterations: 3 },
        { windowCapacity: 50, updateMode: "incremental" }
    );
    const resInc = new Float64Array(nPoints);
    for (let i = 0; i < nPoints; i++) {
        const chunkX = new Float64Array([x[i]]);
        const chunkY = new Float64Array([y[i]]);
        const res = onlineInc.addPoints(chunkX, chunkY);
        resInc[i] = res.y[0];
    }

    // Compare results
    console.log("\nResults Comparison:");

    // Show sample around spike area
    console.log("\nSample around spike (indices 198-208):");
    console.log("Index\tRaw\t\tTrue\t\tFull\t\tIncremental");
    for (let i = 198; i <= 208; i++) {
        // Handle initial points where output might be null (though by 198 it should be fine)
        const fullVal = isNaN(resFull[i]) ? "NaN" : resFull[i].toFixed(2);
        const incVal = isNaN(resInc[i]) ? "NaN" : resInc[i].toFixed(2);
        console.log(`${i}\t${y[i].toFixed(2)}\t\t${yTrue[i].toFixed(2)}\t\t${fullVal}\t\t${incVal}`);
    }

    // Calculate overall statistics
    // Filter out initial NaNs
    let mseFull = 0;
    let mseInc = 0;
    let count = 0;

    for (let i = 0; i < nPoints; i++) {
        if (!isNaN(resFull[i]) && !isNaN(resInc[i])) {
            mseFull += Math.pow(resFull[i] - yTrue[i], 2);
            mseInc += Math.pow(resInc[i] - yTrue[i], 2);
            count++;
        }
    }
    mseFull /= count;
    mseInc /= count;

    console.log("\nMean Squared Error vs True Signal:");
    console.log(` - Full Update:        ${mseFull.toFixed(4)}`);
    console.log(` - Incremental Update: ${mseInc.toFixed(4)}`);

    console.log("\n=== Online Smoothing Example Complete ===");
}

main();

Download online_smoothing.js


Running the Examples

# Install the package
cd bindings/nodejs
npm install
npm run build

# Run examples
node examples/nodejs/batch_smoothing.js
node examples/nodejs/streaming_smoothing.js
node examples/nodejs/online_smoothing.js

Quick Start

const { smooth } = require('fastlowess');

// Generate sample data
const x = Array.from({ length: 100 }, (_, i) => i * 0.1);
const y = x.map(xi => Math.sin(xi) + Math.random() * 0.2);

// Basic smoothing
const result = smooth(x, y, { fraction: 0.3 });
console.log('Smoothed values:', result.y);

// With options
const resultWithOptions = smooth(x, y, {
    fraction: 0.3,
    iterations: 3,
    confidenceIntervals: 0.95,
    returnDiagnostics: true
});

console.log('R²:', resultWithOptions.diagnostics.rSquared);

TypeScript Support

The package includes TypeScript definitions:

import { smooth, SmoothOptions, LowessResult } from 'fastlowess';

const options: SmoothOptions = {
    fraction: 0.3,
    iterations: 3,
    confidenceIntervals: 0.95
};

const result: LowessResult = smooth(x, y, options);