Simple Data Analysis Core - v2.1.14
    Preparing search index...

    Simple Data Analysis Core - v2.1.14

    Simple data analysis core (SDA-core)

    SDA-core is a lightweight DuckDB-powered TypeScript library for tabular, SQL, CSV, Parquet, and geospatial data analysis on Node.js. It has one runtime dependency: DuckDB. It requires Node.js 24.21.0 or newer and uses ECMAScript modules (ESM).

    Choose this package for core data loading, cleaning, joining, statistics, and geospatial operations. For AI, vector search, Google Sheets, and data visualization features, use the full simple-data-analysis library.

    The library is available on npm, with its API documentation.

    AI coding assistants and agents can start with the concise llms.txt index. The complete generated API reference is available in llm.md.

    The library is maintained by Nael Shiab, computational journalist and senior data producer for CBC News.

    Tip

    To learn how to use SDA, check out Code Like a Journalist, a free and open-source data analysis and data visualization course in TypeScript.

    npm i @nshiab/simple-data-analysis-core
    

    Use "type": "module" in your project’s package.json and import the public classes from @nshiab/simple-data-analysis-core. Extension utilities are available from @nshiab/simple-data-analysis-core/helpers.

    To quickly set up a data project with essential folders, configurations, and documentation for AI agents, you can use @nshiab/setup-data-project.

    npx @nshiab/setup-data-project
    

    Transformation methods are synchronous and chainable. SDA-core queues their work and combines compatible operations into a single DuckDB statement. Async methods such as getData(), log(), and writeData() execute pending work before returning results, so only the final method in a chain needs to be awaited.

    await table.filter("salary > 50000").selectColumns(["name", "salary"]).log();
    

    Use await table.run() or await sdb.run() to execute pending work without reading or exporting a result. Both execute queued operations across all tables in the database, in the order the methods were called.

    These are end-to-end workflow comparisons.

    These benchmarks compare SDA-core with raw DuckDB and popular Python and R libraries, measuring duration and peak memory.

    They were run on a MacBook Pro with an Apple M4 Max and 64 GB of memory. JavaScript implementations run on Node.js. Run npm run benchmark to regenerate these workflow tables on macOS with Node.js, Python, and R installed and the input files available locally. The AHCCD file may also be placed in test/data/files/.

    Each implementation runs once to warm up, then three times in fresh processes. Duration includes process startup, loading, computation, and file output; memory is whole-process peak RSS. Warm-up outputs are checked against raw DuckDB, including every cleaned tabular row, and every measured result is checked again. Tables show mean duration with population standard deviation and mean peak memory. Raw measurements are retained in benchmarks/.work/observations.csv.

    Tabular versions: @duckdb/node-api 1.5.6-r.1; DuckDB v1.5.6 (Node.js 24.21.0); SDA-core 2.1.13 (Node.js 24.21.0); pandas 3.0.5 (Python 3.14.7); tidyverse 2.0.0 (R 4.6.1).

    Spatial versions: @duckdb/node-api 1.5.6-r.1; DuckDB v1.5.6 (Node.js 24.21.0); SDA-core 2.1.13 (Node.js 24.21.0); GeoPandas 1.1.4 (Python 3.14.7); sf 1.1.2 (R 4.6.1).

    Using 22,051,025 temperature records (ahccd.csv, 1.77 GB, in benchmarks/data/), we remove missing temperatures, convert dates and numbers, save the cleaned data, then calculate average temperatures by station and decade and export the sorted results.

    Library Mean duration Duration difference Mean peak memory Memory difference
    DuckDB 1.16 ± 0.02 s -12.8% 2,430 MiB -1.1%
    SDA-core 1.33 ± 0.01 s baseline 2,457 MiB baseline
    pandas 27.98 ± 0.04 s +2002.1% 4,699 MiB +91.2%
    tidyverse 78.60 ± 0.46 s +5804.3% 8,178 MiB +232.8%

    Using 335,024 Montreal public trees (arbres-publics.csv, 135.5 MB) and 91 neighbourhood boundaries (quartierreferencehabitation.geojson, 1.14 MB), both in benchmarks/data/, we remove missing coordinates, create points, join trees to neighbourhoods, then count trees per neighbourhood and export the sorted results.

    Library Mean duration Duration difference Mean peak memory Memory difference
    DuckDB 0.72 ± 0.01 s -11.8% 285 MiB -9.9%
    SDA-core 0.81 ± 0.00 s baseline 316 MiB baseline
    GeoPandas 1.11 ± 0.00 s +36.3% 292 MiB -7.7%
    sf 1.58 ± 0.01 s +94.4% 489 MiB +54.8%

    These additional benchmarks compare Core and DuckDB on data transfer, joins, aggregations, and JavaScript geometry updates to help track and improve performance over time. Run npm run benchmark-core to regenerate these tables. See the benchmark methodology for fixture sizes, validation, and measurement details. Peak process memory includes setup, warm-up, the measured operation, and validation.

    Operation Rows Batch size Implementation Mean duration ± SD Mean peak process memory
    Join → aggregate 1,000,000 — Core 11.54 ± 0.03 ms 174.4 MiB
    Join → aggregate 1,000,000 — DuckDB 10.59 ± 0.06 ms 168.0 MiB

    loadArray() measures JavaScript → DuckDB; getData() measures DuckDB → JavaScript. updateWithJS() measures the round trip with a simple numeric increment, including Core's staging and table replacement.

    Operation Rows Batch size Implementation Mean duration ± SD Mean peak process memory
    loadArray() 100,000 — Core 8.63 ± 0.04 ms 355.8 MiB
    loadArray() 100,000 — DuckDB 16.28 ± 0.16 ms 282.4 MiB
    getData() 100,000 — Core 10.14 ± 0.04 ms 302.2 MiB
    getData() 100,000 — DuckDB 12.05 ± 0.16 ms 289.2 MiB
    updateWithJS() 100,000 1000 Core 60.85 ± 0.19 ms 306.5 MiB
    updateWithJS() 100,000 1000 DuckDB 52.77 ± 0.37 ms 314.1 MiB
    updateWithJS() 100,000 10000 Core 35.22 ± 1.31 ms 323.8 MiB
    updateWithJS() 100,000 10000 DuckDB 34.60 ± 1.68 ms 334.7 MiB

    Points have one position per geometry; polygons have one ring with 1,001 positions. Both implementations transfer GeoJSON through JavaScript and stage writes. These updates add a label and shift every longitude by 0.01 degrees. A batch size of — means all input rows at once. Attribute-only updates are also measured and retained in the raw results; their similar timings are omitted here to keep the table concise.

    Operation Rows Batch size Implementation Mean duration ± SD Mean peak process memory
    Points: geometry update 100,000 — Core 174.87 ± 0.50 ms 469.9 MiB
    Points: geometry update 100,000 — DuckDB 172.13 ± 1.32 ms 537.3 MiB
    Points: geometry update 100,000 1000 Core 245.27 ± 4.96 ms 298.3 MiB
    Points: geometry update 100,000 1000 DuckDB 206.80 ± 1.60 ms 272.4 MiB
    Polygons: geometry update 1,000 — Core 549.70 ± 0.67 ms 1067.7 MiB
    Polygons: geometry update 1,000 — DuckDB 555.25 ± 4.17 ms 1103.9 MiB
    Polygons: geometry update 1,000 100 Core 549.62 ± 2.49 ms 786.0 MiB
    Polygons: geometry update 1,000 100 DuckDB 552.29 ± 0.43 ms 881.3 MiB

    The full simple-data-analysis library is itself an extension of SDA-core. It subclasses SimpleTable to add AI, Google Sheets and charting methods, then subclasses SimpleDB so every table created by the database uses that extended table class.

    Follow the same pattern when building an extension. To make new table methods chainable, define them on a SimpleTable subclass, give them a return type of this and return this after queuing their work. Then extend SimpleDB<YourTable> and set its tableClass to your subclass. Methods that create tables should always use this.sdb.newTable() so they also return your extended table type.

    Builders return this synchronously; their queued work runs when an observer such as getData(), log(), or run() is called. Observers execute pending work across the database in program order.

    import { SimpleDB as CoreDB, SimpleTable as CoreTable } from "@nshiab/simple-data-analysis-core";

    class MyTable extends CoreTable {
    selectForPublication(columns: string[]): this {
    this.selectColumns(columns);
    return this;
    }
    }

    class MyDB extends CoreDB<MyTable> {
    constructor() {
    super();
    this.tableClass = MyTable;
    }
    }

    const sdb = new MyDB();
    await sdb
    .newTable("articles")
    .loadData("articles.csv")
    .selectForPublication(["title", "author"])
    .log();

    If an extension must perform asynchronous work before queuing table builders, use queueAsyncBarrier(). The callback runs at the barrier's position in database-wide program order, and builders it queues run before later chained operations:

    import { queueAsyncBarrier } from "@nshiab/simple-data-analysis-core/helpers";

    class RemoteTable extends CoreTable {
    loadRemote(url: string): this {
    queueAsyncBarrier(this, {
    method: "loadRemote()",
    parameters: { url },
    execute: async () => {
    const rows = (await fetch(url).then((response) => response.json())) as {
    [key: string]: unknown;
    }[];
    this.loadArray(rows);
    },
    });
    return this;
    }
    }

    The callback must await all asynchronous work that can queue builders. If it rejects, captured builders that have not already run are discarded. Builders already drained by an observer inside the callback remain applied; queueAsyncBarrier() does not provide database rollback.

    This differs from a failure while executing queued builders: during replay, the failing table's remaining chain is aborted, but unexecuted work for other tables is requeued for their next observation.

    Other useful helpers, imported from @nshiab/simple-data-analysis-core/helpers:

    • updateColumnsWithJS(): enrich selected columns asynchronously in bounded batches inside queueAsyncBarrier(), preserving untouched columns, including geometry. Return one result per input row, in the same order.
    • queryDB() with mergeOptions(): execute SQL with the library's logging and diagnostic settings. Queries flush queued work before running.
    • quoteIdentifier(): quote table and column names safely when constructing SQL.

    See the exported helpers for the complete list and links to their documentation and examples. Helpers outside that list are internal.