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,
        robustness_method: "bisquare",
        return_robustness_weights: true
    }).fit(x, y);

    // 4. Uncertainty Quantification
    console.log("Computing confidence and prediction intervals...");
    const resIntervals = new fastlowess.Lowess({
        fraction: 0.05,
        confidence_intervals: 0.95,
        prediction_intervals: 0.95,
        return_diagnostics: 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({
        cv_fractions: cvFractions,
        cv_method: "kfold",
        cv_k: 5,
        // We can request robustness weights or diagnostics for the final best model too
        return_diagnostics: true
    }).fit(x, y);

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

    // Check scores if available
    const scores = resCV.cv_scores;
    // 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.r_squared.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, boundary_policy: "extend" }).fit(xl, yl);
    const rRef = new fastlowess.Lowess({ fraction: 0.6, boundary_policy: "reflect" }).fit(xl, yl);
    const rZr = new fastlowess.Lowess({ fraction: 0.6, boundary_policy: "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)}`);

    // Custom Weights
    console.log("\nCustom Weights Examples:");

    const cwX = new Float64Array(10).map((_, i) => i);
    const cwY = new Float64Array(10).map((_, i) => i * 2);
    cwY[5] = 100.0;
    const cwNoW = new fastlowess.Lowess({ fraction: 0.5, iterations: 0 }).fit(cwX, cwY);
    const cwWeights = new Float64Array(10).fill(1.0); cwWeights[5] = 0.0;
    const cwZeroW = new fastlowess.Lowess({ fraction: 0.5, iterations: 0 }).fit(cwX, cwY, cwWeights);
    const errNoW = Math.abs(cwNoW.y[5] - 10.0);
    const errZeroW = Math.abs(cwZeroW.y[5] - 10.0);
    console.log(` - Zero weight at outlier: error ${errNoW.toFixed(2)} -> ${errZeroW.toFixed(2)}`);

    const spX = new Float64Array(15).map((_, i) => i);
    const spY = new Float64Array(15).fill(0.0); spY[7] = 10.0;
    const spEq = new fastlowess.Lowess({ fraction: 0.6, iterations: 0 }).fit(spX, spY);
    const spW = new Float64Array(15).fill(1.0); spW[7] = 100.0;
    const spHi = new fastlowess.Lowess({ fraction: 0.6, iterations: 0 }).fit(spX, spY, spW);
    console.log(` - High weight at spike: fit ${spEq.y[7].toFixed(4)} -> ${spHi.y[7].toFixed(4)}`);

    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 },
        { chunk_size: 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.process_chunk(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 },
        { window_capacity: 50, update_mode: "full" }
    );
    const resFull = new Float64Array(nPoints);
    for (let i = 0; i < nPoints; i++) {
        const res = onlineFull.add_point(x[i], y[i]);
        resFull[i] = res !== null ? res.smoothed : y[i];
    }

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

    // 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 { Lowess } = require('fastlowess');

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

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

// With options
const resultWithOptions = new Lowess({
    fraction: 0.3,
    iterations: 3,
    confidence_intervals: 0.95,
    return_diagnostics: true
}).fit(x, y);

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

TypeScript Support

The package includes TypeScript definitions:

const { Lowess, LowessResult } = require('fastlowess');

const n = 100;
const x = Float64Array.from({ length: n }, (_, i) => i * 2 * Math.PI / (n - 1));
const y = Float64Array.from(x, (xi, i) => Math.sin(xi) + (((i * 7 + 3) % 17) / 17 - 0.5) * 0.6);

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

const model = new Lowess(options);
const result: LowessResult = model.fit(x, y);