Claude Cursor opencode Skill

data-scientist

Expert data processing specialist with intelligent DuckDB/Polars selection for maximum performance. Always includes numpy, never uses pandas, runs everything through uv. Triggers: 'analyze the data', 'analyze this file', 'what is in this CSV/parquet/json', 'summarize this', 'grou

LLM Mart · 0 points · 13 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download code-yeongyu-oh-my-openagent-packages_shared-skills_skills_data-scientist-05dcba6.zip · 16 KB
Part of code-yeongyu/oh-my-openagent — 51 skills

Install

skills CLI npx skills add https://github.com/code-yeongyu/oh-my-openagent/tree/dev/packages/shared-skills/skills/data-scientist
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install code-yeongyu-oh-my-openagent@llmmart
Git git clone https://github.com/code-yeongyu/oh-my-openagent.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole code-yeongyu/oh-my-openagent collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Data Scientist: Hybrid-Engine Data Processing

Answer data questions through the cheapest engine and surface that can prove the answer, and decide where the computation should live before touching the data.

Execution surfaces: resident kernel first

A persistent REPL/eval kernel (many harnesses expose one for JavaScript and Python) is the default surface. Reason: each one-shot process pays roughly a second of spawn-plus-import overhead and re-scans the input file, while a resident connection amortizes both — after a one-time load, repeat queries return in milliseconds. Exploration is repeat queries, so this difference dominates the session.

  1. JavaScript kernel (Bun): run scripts/ensure-js-deps.sh once; it prints the absolute import path for @duckdb/node-api. Dynamic-import it, connect once, query across cells.
  2. Python kernel: the default surface for Python work. duckdb/numpy/matplotlib are typically resident; Polars and pyarrow come from scripts/ensure-py-deps.sh, which installs them once into a user cache keyed to the kernel's interpreter — sys.path.insert the printed directory and import. The interpreter itself is never mutated.
  3. uv lane (uv run --with ...): isolation for a heavy or crash-prone one-shot that should not take the kernel down.
  4. No kernel (plain-shell harness): the same engines as one-shots — bun -e for DuckDB-js, uv run python -c for the Python stack — batching several questions per process.

Per-surface patterns and pitfalls: read references/execution-surfaces.md before first use.

Engine selection

  • DuckDB for SQL-shaped work: direct file queries, joins, aggregation, subqueries, window functions. It queries CSV/Parquet/JSON in place without loading, spills to disk past its memory limit, and reads remote files with the same syntax.
  • Polars when the pipeline is DataFrame-shaped: expression-chain transforms, reshapes, streaming datasets past RAM — resident in the Python kernel via ensure-py-deps.sh. Read references/polars-lane.md — the current 1.x API differs from widely-memorized older spellings.
  • numpy when numeric work goes beyond SQL/DataFrame aggregation: statistical tests, linear algebra, FFT, random sampling.
  • matplotlib for every chart — read references/visualization.md first; it carries the quality bar and a mandatory visual check.

Performance folklore ("X is Nx faster at filtering") varies with data shape, cardinality, and hardware. When the engine choice materially matters, measure on the actual data instead of trusting remembered multipliers.

Placement: decide where the computation lives

Probe before you compute — one cell: file size, free RAM, and (when unclear) a row count via a direct scan. Then place the work:

  • Load into memory when the working set stays within roughly a quarter of free RAM AND the session will run repeated queries: CREATE TABLE t AS SELECT ... (or a collected DataFrame) once, then iterate. One scan up front converts every later query from a file re-scan into milliseconds.
  • Query in place / stream when the question is single-pass, or the data exceeds RAM: DuckDB reads files directly (FROM 'data.csv'); past RAM, cap DuckDB's memory and let it spill, or use Polars' streaming engine in the Python kernel. NEVER load a larger-than-RAM dataset fully into memory — swapping stalls the whole machine, while streaming merely takes longer.
  • Query remotely, in place when the data lives elsewhere: DuckDB reads http(s)/S3 Parquet and CSV with projection and predicate pushdown, so fetch the columns and rows the question needs, never the whole file. When data sits on another machine you can execute on, ship the query to the data and return the small result. Rule: result much smaller than data — move the query; repeated local iteration planned — move a pruned copy of the data once.

Sizing heuristics and recipes: references/placement.md.

Hard rules

  • NEVER use pandas. DuckDB and Polars beat it decisively on every workload this skill covers, and the environments this skill assumes do not ship it — .df() on a DuckDB result raises unless pandas is installed; convert with .pl() via Arrow instead.
  • Excel files are not read directly: export to CSV or Parquet first.

Output contract

Answer the question; report row counts and timing for anything heavy; then stop — no bonus charts, no extra exploration passes beyond what the question needed. Chart when asked, or when the answer is a shape (trend, distribution, comparison) that prose cannot carry — then follow references/visualization.md including its visual QA step.

References

Read When
references/execution-surfaces.md before the first query on any surface: kernel patterns, one-shot recipes, escalation rules
references/polars-lane.md DataFrame-shaped pipeline or data past RAM: current API, Arrow handoff, package sets
references/placement.md before heavy or remote work: sizing probe, memory limits, remote reads
references/visualization.md before any chart: type selection, quality bar, CJK fonts, visual QA
references/uv-setup.md uv missing or broken on this machine

CLI fallback

When no kernel or REPL surface exists, uv run scripts/quick-query.py <file> [SQL] (--filter <polars-sql-expr>, --describe) answers ad-hoc questions with zero code. Supports CSV, Parquet, JSON, NDJSON.

Files (oh-my-openagent)
  • references
    • execution-surfaces.md 4 KB
      # Execution surfaces
      
      How to run the engines on each surface, and when to escalate between them.
      
      ## Persistent kernel, JavaScript (Bun)
      
      One-time setup per machine — the bundled script installs `@duckdb/node-api` into a user-level
      cache outside any repo and prints the absolute import path (its only stdout line):
      
      ```bash
      bash scripts/ensure-js-deps.sh          # run from the skill directory
      ```
      
      In the kernel — top-level `require` may not exist, dynamic import always works:
      
      ```js
      const { DuckDBInstance } = await import("<printed path>");
      const db = await DuckDBInstance.create(":memory:");
      const conn = await db.connect();
      const reader = await conn.runAndReadAll("SELECT category, SUM(v) AS total FROM 'data.csv' GROUP BY 1");
      reader.getRowObjects();                  // array of plain row objects
      ```
      
      - The connection and any tables created live across cells — connect once per session, reuse.
      - COUNT/SUM over integer columns return BigInt; convert (`Number(x)` or `String(x)`) before
        `JSON.stringify`, which throws on BigInt.
      - Bun builtins cover ingest gaps with zero installs: `Bun.JSONL.parse`, `Bun.JSON5.parse`,
        `Bun.XML.parse`, `Bun.TOML.parse`, `Bun.Archive` for tarballs.
      - nodejs-polars is NOT part of this skill's toolkit: its API lags the Python release by
        major versions (option objects that work in Python throw napi type errors). Polars work
        belongs to the Python kernel (below).
      
      ## Persistent kernel, Python (the default Python surface)
      
      duckdb, numpy, and matplotlib are typically resident — import and use them directly.
      Polars and pyarrow rarely ship with a kernel, so inject them once per session (run from the
      skill directory; the script installs on first use, then just prints the path):
      
      ```python
      import subprocess, sys
      site = subprocess.run(["bash", "scripts/ensure-py-deps.sh", sys.executable],
                            capture_output=True, text=True, check=True).stdout.strip()
      sys.path.insert(0, site)
      import polars as pl
      import pyarrow
      ```
      
      - The install goes to a user cache keyed to the kernel's interpreter version; the
        interpreter itself is never mutated (it is frequently an externally-managed system
        Python, and mutating it breaks other tools).
      - After injection the whole Python stack is resident: `duckdb.sql(...).pl()` hands off via
        Arrow, `duckdb.register(name, df)` goes the other way, and Polars lazy pipelines run
        in-kernel across cells.
      - `duckdb.sql("SELECT ... FROM 'data.csv'")` queries files in place; without the injection,
        keep results in DuckDB or fetch plain Python values (`.fetchall()`).
      - matplotlib figures render natively in kernels that display rich output; also save a PNG so
        the artifact survives the session.
      
      ## uv lane (fallback and isolation)
      
      ```bash
      uv run --with duckdb --with polars --with pyarrow --with numpy python -c "<code>"
      ```
      
      - Reach for it when there is no kernel, or when a heavy, crash-prone one-shot should not
        run inside (and possibly take down) the kernel.
      - Include exactly the packages the code imports, plus pyarrow whenever `.pl()` is used.
      - Each invocation pays process spawn plus imports (roughly 0.3s warm) and re-reads its inputs —
        fine for one-shots, wasteful for exploration loops.
      - Past a few lines, a temp file beats `-c` quoting: write the script, `uv run script.py`.
      
      ## No kernel at all
      
      Same engines, one process per batch of questions:
      
      ```bash
      bun -e '<the JavaScript kernel pattern above>'         # DuckDB via @duckdb/node-api
      uv run --with duckdb python -c "<sql via duckdb.sql>"  # DuckDB via Python
      uv run scripts/quick-query.py data.csv "SELECT ..."    # zero-code CLI fallback
      ```
      
      ## Escalation rules
      
      Start on the resident kernel. Move a step down when a concrete need appears:
      
      - polars/pyarrow missing from the kernel — inject via `ensure-py-deps.sh` (above), not a
        uv one-shot.
      - Crash-prone or memory-hungry one-shot that should not take the kernel down — uv lane.
      - No kernel on this harness — one-shot recipes above.
      - Data lives remotely or exceeds local RAM — read `placement.md` and move the query, not
        the data.
      
    • placement.md 3.3 KB
      # Placement: where should this computation live?
      
      Decide before touching the data. Wrong placement wastes minutes (re-scanning a file queried
      ten times) or kills the machine (loading a dataset larger than RAM and swapping).
      
      ## The probe (run first, once)
      
      Three facts, one cell or script:
      
      ```python
      import os, shutil, subprocess, sys
      size = os.path.getsize("data.csv")            # bytes on disk
      disk_free = shutil.disk_usage(".").free       # spill headroom
      if sys.platform == "darwin":
          ram = int(subprocess.run(["sysctl", "-n", "hw.memsize"], capture_output=True, text=True).stdout)
      else:
          ram = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
      # row estimate without loading (DuckDB streams the scan):
      # duckdb.sql("SELECT count(*) FROM 'data.csv'")
      ```
      
      CSV typically expands 2-5x in memory (string columns dominate); Parquet expands less
      predictably — compressed columns can inflate 10x. Estimate the working set from the
      decompressed size of the columns the question actually touches, not the file size.
      
      ## In memory — load once, iterate
      
      When the working set stays within roughly 25% of free RAM AND the session will run repeated
      queries: load once (`CREATE TABLE t AS SELECT ...` in DuckDB, or a collected DataFrame),
      then iterate. One scan up front converts every later query from a file re-scan into
      milliseconds. Prune at load time — select only the needed columns, filter obvious dross —
      so the resident table is the working set, not the raw file.
      
      ## In place / streaming — single pass, or bigger than RAM
      
      - Single-pass questions: query the file directly (`FROM 'data.csv'`). Loading first is pure
        waste.
      - Bigger than RAM, SQL-shaped: cap DuckDB and let it spill —
      
        ```sql
        SET memory_limit = '4GB';
        SET temp_directory = '/tmp/duckdb_spill';
        ```
      
        Aggregations, sorts, and window functions run out-of-core: slower, but bounded.
      - Bigger than RAM, DataFrame-shaped: Polars streaming (`collect(engine="streaming")` on a
        lazy plan) in the resident kernel — or a uv one-shot on kernel-less harnesses.
      - Manual chunked loops (read N rows, process, repeat) are the last resort — the engines'
        own out-of-core paths are faster and simpler than hand-rolled chunking.
      
      ## Remote, in place — move the query to the data
      
      - Files behind http(s)/S3: DuckDB's httpfs extension reads Parquet and CSV remotely with
        projection and predicate pushdown —
      
        ```sql
        INSTALL httpfs; LOAD httpfs;   -- one-time per environment
        SELECT region, SUM(amount) FROM 'https://example.com/sales.parquet'
        WHERE sale_date >= '2026-01-01' GROUP BY region;
        ```
      
        Only matching row groups and referenced columns cross the network, not the file.
      - Data on another machine you can execute on (a remote worker with more RAM, a box closer
        to the data): run the query there and return the aggregate. A group-by result is
        kilobytes; the source is gigabytes.
      - Decision rule: result much smaller than data — move the query. Repeated local iteration
        on one slice — move a pruned copy of that slice once, then work locally in memory.
      
      ## Hardware notes
      
      - Both engines parallelize across all cores by default; leave that alone except on shared
        machines (`SET threads = N` in DuckDB, `POLARS_MAX_THREADS` for Polars).
      - Sustained swapping is the failure mode to avoid on memory-tight machines: when the probe
        says the working set is close to free RAM, choose streaming, not hope.
      
    • polars-lane.md 3.5 KB
      # Polars lane (resident Python kernel)
      
      When the work is DataFrame-shaped, Polars is the right engine — and it runs in the resident
      Python kernel by default. Kernels rarely ship polars/pyarrow preinstalled, so inject them
      once per session; the install lands in a user cache keyed to the kernel's interpreter, and
      the interpreter itself is never mutated (run with the skill directory as cwd, or spell out
      the script's absolute path):
      
      ```python
      import subprocess, sys
      site = subprocess.run(["bash", "scripts/ensure-py-deps.sh", sys.executable],
                            capture_output=True, text=True, check=True).stdout.strip()
      sys.path.insert(0, site)
      import polars as pl
      ```
      
      After this, Polars lives across cells like every other resident engine: lazy frames,
      intermediate results, and the DuckDB handoff all persist with no per-call process cost.
      
      ## When Polars wins over DuckDB SQL
      
      - Expression-chain transforms: many derived columns, per-column conditional logic, string
        pipelines — `with_columns` chains read and optimize better than nested SQL SELECTs.
      - Reshapes: `unpivot`/`pivot` beat SQL gymnastics.
      - Larger-than-RAM pipelines: the streaming engine executes lazy plans in chunks.
      - Window-heavy feature engineering with `over()`.
      
      SQL-shaped work (joins, aggregation, ad-hoc questions) stays in DuckDB; mixed pipelines hand
      off zero-copy (below) instead of forcing one engine to do everything.
      
      ## Current API (1.x) — older spellings fail or warn
      
      Training data is full of the pre-1.0 API. Current names:
      
      | Use | Not |
      | --- | --- |
      | `pl.scan_csv` / `pl.scan_parquet` + `.collect()` | eager `read_*` on big files |
      | `.group_by(...)` | `.groupby(...)` |
      | `pl.len()` | `pl.count()` |
      | `.collect(engine="streaming")` | `.collect(streaming=True)` |
      | `.unpivot(...)` | `.melt(...)` |
      
      Lazy first: `scan_*` builds a plan, pushes filters and projections down to the file read, and
      executes once at `.collect()`. Eager `read_*` is for small files mutated interactively.
      
      ```python
      out = (pl.scan_csv("data.csv")
             .filter(pl.col("value") > 100)
             .group_by("category")
             .agg(pl.col("value").sum().alias("total"), pl.len().alias("n"))
             .sort("total", descending=True)
             .collect())
      ```
      
      ## Zero-copy handoff with DuckDB
      
      Both engines speak Arrow, so mixed pipelines pay no serialization cost — all in-kernel:
      
      ```python
      import duckdb
      df = duckdb.sql("SELECT * FROM 'orders.csv' o JOIN 'items.csv' i USING (id)").pl()
      shaped = df.with_columns((pl.col("qty") * pl.col("price")).alias("rev"))
      duckdb.register("shaped", shaped)
      out = duckdb.sql("SELECT category, SUM(rev) AS total FROM shaped GROUP BY 1").pl()
      ```
      
      - `.pl()` requires pyarrow — the injection above provides it; without it, it raises
        `ModuleNotFoundError`.
      - Never `.df()`: it requires pandas (raising without it), and pandas is banned and absent.
      
      ## Streaming past RAM
      
      ```python
      out = (pl.scan_parquet("huge.parquet")
             .filter(pl.col("status") == "active")
             .group_by("region").agg(pl.len())
             .collect(engine="streaming"))
      ```
      
      Streaming executes lazy plans only — keep the plan lazy end-to-end, with no intermediate
      `.collect()` breaking it into eager pieces.
      
      ## Kernel-less fallback (uv one-shot)
      
      On a harness with no persistent kernel, the same code runs as one-shots — batch several
      questions per process, since each invocation pays spawn plus imports:
      
      ```bash
      uv run --with duckdb --with polars --with pyarrow python -c "
      import duckdb
      import polars as pl
      df = duckdb.sql(\"SELECT * FROM 'data.csv'\").pl()
      print(df.group_by('category').agg(pl.len()).sort('category'))
      "
      ```
      
    • uv-setup.md 2.7 KB
      # uv Setup — Per-Platform
      
      This skill's uv lane (kernel-less harnesses, isolated one-shots) runs through `uv run --with ...`, and `scripts/ensure-py-deps.sh` uses uv as its installer. If `uv --version` fails, set uv up with the automated scripts or the manual commands below, then verify.
      
      ## Automated (recommended)
      
      | Platform | Command |
      |---|---|
      | macOS / Linux / WSL / Git Bash | `bash scripts/setup-uv.sh` |
      | Windows (PowerShell) | `powershell -ExecutionPolicy Bypass -File scripts/setup-uv.ps1` |
      
      Both scripts: detect OS + architecture → install uv to the latest release when missing → upgrade it when present (`uv self update`) → make it resolvable for the current shell → verify with `uv --version`. They are idempotent — safe to re-run any time.
      
      ## Manual install per platform
      
      ### macOS
      
      ```bash
      curl -LsSf https://astral.sh/uv/install.sh | sh        # official installer → ~/.local/bin/uv
      # or, with Homebrew:
      brew install uv
      ```
      
      ### Linux (x86_64 / aarch64)
      
      ```bash
      curl -LsSf https://astral.sh/uv/install.sh | sh        # official installer → ~/.local/bin/uv
      ```
      
      The installer detects glibc vs musl and downloads the right static binary. On minimal containers, ensure `curl` (or `wget`) exists; `wget -qO- https://astral.sh/uv/install.sh | sh` is the fallback.
      
      ### Windows (native)
      
      ```powershell
      powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
      # or, with winget:
      winget install --id=astral-sh.uv -e
      ```
      
      ### Windows Subsystem for Linux / Git Bash
      
      Use the Linux/macOS installer inside the Unix shell, not the PowerShell installer:
      
      ```bash
      curl -LsSf https://astral.sh/uv/install.sh | sh
      ```
      
      ### CI
      
      ```yaml
      # GitHub Actions
      - uses: astral-sh/setup-uv@v5
      # or plain shell anywhere:
      - run: curl -LsSf https://astral.sh/uv/install.sh | sh
      ```
      
      ## PATH notes
      
      - The official installers put the binary in `~/.local/bin` (Unix) or `%USERPROFILE%\.local\bin` (Windows). New shells get it automatically on most setups; an already-open shell needs `export PATH="$HOME/.local/bin:$PATH"` (Unix) or `$env:Path = "$env:USERPROFILE\.local\bin;$env:Path"` (PowerShell) once.
      - Homebrew and winget install into their own prefixes that are already on PATH.
      
      ## Upgrade to latest
      
      ```bash
      uv self update
      ```
      
      `uv self update` only works for official-installer binaries; Homebrew/winget installs upgrade through their package manager (`brew upgrade uv` / `winget upgrade astral-sh.uv`). The setup scripts handle this automatically.
      
      ## Verify
      
      ```bash
      uv --version
      ```
      
      ## Offline / air-gapped
      
      Download the matching archive from the uv GitHub releases page, extract it, and put the `uv` binary anywhere on PATH. `uv run --with <pkg>` still needs network for first-time package resolution unless a mirror is configured via `UV_INDEX_URL`.
      
    • visualization.md 2.9 KB
      # Visualization
      
      A chart exists to answer a question at a glance. Render it with matplotlib (resident in most
      Python kernels; `uv run --with matplotlib` otherwise), then look at it before delivering —
      a chart nobody inspected is not evidence.
      
      ## When to chart
      
      Chart when the user asked for one, and default to charting when the answer is a shape prose
      cannot carry: a trend over time, a distribution, a comparison across many categories, a
      relationship between variables. Skip the chart when a number or a five-row table answers the
      question — decoration dilutes the answer.
      
      ## Chart type follows the question
      
      | Question shape | Chart |
      | --- | --- |
      | How did X change over time? | line, datetime x-axis |
      | Which categories are biggest? | horizontal bar, sorted by value |
      | How is X distributed? | histogram (tune bin count) or box plot per group |
      | Is X related to Y? | scatter; add a trend line only when it aids the eye |
      | Composition of a whole? | stacked or 100% bar — pie only for four or fewer slices |
      | Many series over time? | small multiples over one spaghetti chart |
      
      ## Quality bar — every chart
      
      - Title states the finding ("Seoul overtook Busan in March"), not the dataset name.
      - Axis labels carry units. Tick density stays readable: `fig.autofmt_xdate()` for dates,
        rotate or abbreviate long category names.
      - Size for the medium: inline chat reads well around `figsize=(10, 6)` at default dpi;
        documents want `dpi=150` or more at export.
      - `tight_layout()` (or `constrained_layout=True`) before saving — clipped labels are the
        most common chart defect.
      - Few series: label lines directly, or keep the legend inside empty plot space. Many
        series: gray the context, color only the series that answers the question.
      - The default color cycle is fine; avoid rainbow palettes and 3D. Sort categorical bars by
        value, never alphabetically.
      
      ## CJK and other non-Latin text
      
      Matplotlib's default font renders CJK as empty boxes (tofu). Set a fallback before plotting
      whenever any label or title contains CJK:
      
      ```python
      import platform
      import matplotlib
      cjk = {"Darwin": "AppleGothic", "Windows": "Malgun Gothic"}.get(platform.system(), "Noto Sans CJK KR")
      matplotlib.rcParams["font.family"] = [cjk, "DejaVu Sans"]
      matplotlib.rcParams["axes.unicode_minus"] = False   # keeps the minus sign rendering
      ```
      
      ## Output contract
      
      1. Save a PNG next to the work: `plt.savefig(path, dpi=150, bbox_inches="tight")`.
      2. Also render inline when the surface displays rich output (kernels usually do).
      3. Report the file path together with the answer.
      
      ## Visual QA — mandatory
      
      Open the produced image — kernel display, or the harness's image-reading surface — and
      check four things: labels readable and unclipped, no tofu or mojibake, nothing overlapping,
      and the chart actually shows the finding the title claims. A failed check means fix and
      re-render, not ship with a caveat. This one pass catches nearly every chart defect;
      skipping it is how tofu titles reach users.
      
  • scripts
    • ensure-js-deps.sh 983 B
      #!/usr/bin/env bash
      # Install @duckdb/node-api into a user-level cache (outside any repo) and print the
      # absolute import path as the ONLY stdout line. Idempotent: re-runs reuse the install.
      set -euo pipefail
      
      log() { printf '[ensure-js-deps] %s\n' "$*" >&2; }
      
      CACHE_DIR="${OMO_DATA_SCIENTIST_CACHE:-$HOME/.cache/omo-data-scientist}"
      IMPORT_PATH="$CACHE_DIR/node_modules/@duckdb/node-api/lib/index.js"
      
      if ! command -v bun >/dev/null 2>&1; then
        log "bun is required (https://bun.sh); install it, or use the uv lane instead."
        exit 1
      fi
      
      if [ ! -f "$IMPORT_PATH" ]; then
        log "installing @duckdb/node-api into $CACHE_DIR"
        mkdir -p "$CACHE_DIR"
        [ -f "$CACHE_DIR/package.json" ] || printf '{"name":"omo-data-scientist-cache","private":true}\n' > "$CACHE_DIR/package.json"
        (cd "$CACHE_DIR" && bun add @duckdb/node-api 1>&2)
      fi
      
      if [ ! -f "$IMPORT_PATH" ]; then
        log "install finished but $IMPORT_PATH is missing; inspect $CACHE_DIR"
        exit 1
      fi
      
      printf '%s\n' "$IMPORT_PATH"
      
    • ensure-py-deps.sh 1.3 KB
      #!/usr/bin/env bash
      # Install polars + pyarrow for a given Python interpreter into a user-level cache
      # (never mutating the interpreter itself) and print the site directory as the ONLY
      # stdout line. Idempotent: re-runs reuse the install. arg1 = python executable
      # (default: python3); pass the kernel's sys.executable for kernel use.
      set -euo pipefail
      
      log() { printf '[ensure-py-deps] %s\n' "$*" >&2; }
      
      PYTHON_BIN="${1:-python3}"
      
      if ! command -v uv >/dev/null 2>&1; then
        log "uv is required (see references/uv-setup.md); install it first."
        exit 1
      fi
      if ! command -v "$PYTHON_BIN" >/dev/null 2>&1 && [ ! -x "$PYTHON_BIN" ]; then
        log "python executable not found: $PYTHON_BIN"
        exit 1
      fi
      
      TAG="$("$PYTHON_BIN" -c 'import sys; print(f"cp{sys.version_info[0]}{sys.version_info[1]}")')" \
        || { log "not a working python interpreter: $PYTHON_BIN"; exit 1; }
      CACHE_DIR="${OMO_DATA_SCIENTIST_CACHE:-$HOME/.cache/omo-data-scientist}"
      SITE_DIR="$CACHE_DIR/py-$TAG"
      
      if [ ! -d "$SITE_DIR/polars" ] || [ ! -d "$SITE_DIR/pyarrow" ]; then
        log "installing polars + pyarrow for $TAG into $SITE_DIR"
        mkdir -p "$SITE_DIR"
        uv pip install --python "$PYTHON_BIN" --target "$SITE_DIR" polars pyarrow 1>&2
      fi
      
      if [ ! -d "$SITE_DIR/polars" ]; then
        log "install finished but $SITE_DIR/polars is missing; inspect $SITE_DIR"
        exit 1
      fi
      
      printf '%s\n' "$SITE_DIR"
      
    • quick-query.py 3.3 KB
      #!/usr/bin/env -S uv run --script
      # /// script
      # requires-python = ">=3.11"
      # dependencies = [
      #     "duckdb",
      #     "polars",
      #     "numpy",
      #     "pyarrow",
      #     "typer",
      #     "rich",
      # ]
      # ///
      
      """Quick data query runner. SQL arg -> DuckDB; --filter (Polars SQL) -> Polars; --describe -> schema + stats."""
      
      from __future__ import annotations
      
      from pathlib import Path
      
      import typer
      from rich import print as rprint
      from rich.table import Table
      
      
      def _run_duckdb(file_path: Path, sql: str) -> None:
          import duckdb
      
          table_ref = f"'{file_path}'"
          stem = file_path.stem
          query = sql
          for alias in ("data", "df", stem):
              query = query.replace(f"FROM {alias} ", f"FROM {table_ref} ")
              query = query.replace(f"FROM {alias}\n", f"FROM {table_ref}\n")
              query = query.replace(f"from {alias} ", f"from {table_ref} ")
              query = query.replace(f"from {alias}\n", f"from {table_ref}\n")
              if query.endswith(f"FROM {alias}") or query.endswith(f"from {alias}"):
                  query = query[: -len(alias)] + table_ref
      
          result = duckdb.sql(query)
          df = result.pl()
          _print_polars(df)
      
      
      def _run_polars_filter(file_path: Path, expr: str) -> None:
          import polars as pl
      
          df = _read_file(file_path)
          filtered = df.filter(pl.sql_expr(expr))
          _print_polars(filtered)
      
      
      def _run_describe(file_path: Path) -> None:
          df = _read_file(file_path)
      
          rprint(f"\n[bold]Schema:[/bold] {file_path.name} ({len(df)} rows × {len(df.columns)} cols)")
          for name, dtype in zip(df.columns, df.dtypes):
              rprint(f"  {name}: [cyan]{dtype}[/cyan]")
      
          rprint("\n[bold]Statistics:[/bold]")
          _print_polars(df.describe())
      
      
      def _read_file(file_path: Path):  # noqa: ANN202
          import polars as pl
      
          suffix = file_path.suffix.lower()
          if suffix == ".csv":
              return pl.read_csv(file_path)
          if suffix == ".parquet":
              return pl.read_parquet(file_path)
          if suffix == ".json":
              return pl.read_json(file_path)
          if suffix in (".jsonl", ".ndjson"):
              return pl.read_ndjson(file_path)
          rprint(f"[red]Unsupported format:[/red] {suffix} (Excel: export to CSV or Parquet first)")
          raise SystemExit(1)
      
      
      def _print_polars(df) -> None:  # noqa: ANN001
          table = Table(show_lines=False)
          for col_name in df.columns:
              table.add_column(col_name)
          for row in df.iter_rows():
              table.add_row(*(str(v) for v in row))
          rprint(table)
          rprint(f"[dim]{df.shape[0]} rows × {df.shape[1]} cols[/dim]")
      
      
      def main(
          file: Path = typer.Argument(help="Data file (csv, parquet, json, ndjson)"),
          sql: str = typer.Argument(None, help="SQL query (uses DuckDB). Use 'data' as table name."),
          filter_expr: str = typer.Option(None, "--filter", "-f", help="Polars SQL filter, e.g. 'amount > 100'"),
          describe: bool = typer.Option(False, "--describe", "-d", help="Print schema + stats"),
      ) -> None:
          """Query data files with DuckDB (SQL) or Polars (SQL filter expressions)."""
          if not file.exists():
              rprint(f"[red]File not found:[/red] {file}")
              raise SystemExit(1)
      
          if describe:
              _run_describe(file)
          elif filter_expr:
              _run_polars_filter(file, filter_expr)
          elif sql:
              _run_duckdb(file, sql)
          else:
              _run_describe(file)
      
      
      if __name__ == "__main__":
          typer.run(main)
      
    • setup-uv.ps1 2.3 KB · in bundle
    • setup-uv.sh 1.8 KB
      #!/usr/bin/env bash
      set -euo pipefail
      
      log() { printf '[setup-uv] %s\n' "$*"; }
      
      os="$(uname -s 2>/dev/null || echo unknown)"
      arch="$(uname -m 2>/dev/null || echo unknown)"
      log "detected os=${os} arch=${arch}"
      
      case "$os" in
        Darwin|Linux) ;;
        MINGW*|MSYS*|CYGWIN*)
          log "Git Bash / MSYS environment detected - installing the Windows build is not supported here."
          log "Use the Linux installer inside WSL, or run scripts/setup-uv.ps1 in PowerShell instead."
          exit 1
          ;;
        *)
          log "unsupported OS: ${os} - see references/uv-setup.md for manual steps."
          exit 1
          ;;
      esac
      
      if command -v uv >/dev/null 2>&1; then
        current="$(uv --version 2>/dev/null | head -n1)"
        log "uv already installed (${current}) - upgrading"
        if uv self update >/dev/null 2>&1; then
          log "uv self update succeeded"
        else
          log "uv self update unavailable (package-manager install) - trying Homebrew"
          if command -v brew >/dev/null 2>&1; then
            brew upgrade uv >/dev/null 2>&1 || brew install uv
          else
            log "no Homebrew; keeping current uv (already functional)"
          fi
        fi
      else
        log "uv not found - installing latest via the official installer"
        if command -v curl >/dev/null 2>&1; then
          curl -LsSf https://astral.sh/uv/install.sh | sh
        elif command -v wget >/dev/null 2>&1; then
          wget -qO- https://astral.sh/uv/install.sh | sh
        elif command -v brew >/dev/null 2>&1; then
          log "no curl/wget - installing via Homebrew"
          brew install uv
        else
          log "neither curl, wget, nor brew available - install one of them and re-run."
          exit 1
        fi
      fi
      
      if ! command -v uv >/dev/null 2>&1; then
        export PATH="$HOME/.local/bin:$PATH"
      fi
      
      if ! command -v uv >/dev/null 2>&1; then
        log "FAIL: uv still not on PATH after install - add \$HOME/.local/bin to PATH and retry."
        exit 1
      fi
      
      log "OK: $(uv --version)"
      
  • SKILL.md 5.7 KB
    ---
    name: data-scientist
    description: "Processes and analyzes data with resident-kernel engines (DuckDB, Polars) and one-shot tools. Use for CSV/parquet/JSON analysis, group-by/join/aggregation, time series, distributions, cleaning, or plotting a dataset."
    ---
    
    # Data Scientist: Hybrid-Engine Data Processing
    
    Answer data questions through the cheapest engine and surface that can prove the answer, and
    decide where the computation should live before touching the data.
    
    ## Execution surfaces: resident kernel first
    
    A persistent REPL/eval kernel (many harnesses expose one for JavaScript and Python) is the
    default surface. Reason: each one-shot process pays roughly a second of spawn-plus-import
    overhead and re-scans the input file, while a resident connection amortizes both — after a
    one-time load, repeat queries return in milliseconds. Exploration is repeat queries, so this
    difference dominates the session.
    
    1. **JavaScript kernel (Bun)**: run `scripts/ensure-js-deps.sh` once; it prints the absolute
       import path for `@duckdb/node-api`. Dynamic-import it, connect once, query across cells.
    2. **Python kernel**: the default surface for Python work. duckdb/numpy/matplotlib are
       typically resident; Polars and pyarrow come from `scripts/ensure-py-deps.sh`, which
       installs them once into a user cache keyed to the kernel's interpreter —
       `sys.path.insert` the printed directory and import. The interpreter itself is never
       mutated.
    3. **uv lane** (`uv run --with ...`): isolation for a heavy or crash-prone one-shot that
       should not take the kernel down.
    4. **No kernel** (plain-shell harness): the same engines as one-shots — `bun -e` for
       DuckDB-js, `uv run python -c` for the Python stack — batching several questions per
       process.
    
    Per-surface patterns and pitfalls: read `references/execution-surfaces.md` before first use.
    
    ## Engine selection
    
    - **DuckDB** for SQL-shaped work: direct file queries, joins, aggregation, subqueries,
      window functions. It queries CSV/Parquet/JSON in place without loading, spills to disk
      past its memory limit, and reads remote files with the same syntax.
    - **Polars** when the pipeline is DataFrame-shaped: expression-chain transforms, reshapes,
      streaming datasets past RAM — resident in the Python kernel via `ensure-py-deps.sh`.
      Read `references/polars-lane.md` — the current 1.x API differs from widely-memorized
      older spellings.
    - **numpy** when numeric work goes beyond SQL/DataFrame aggregation: statistical tests,
      linear algebra, FFT, random sampling.
    - **matplotlib** for every chart — read `references/visualization.md` first; it carries the
      quality bar and a mandatory visual check.
    
    Performance folklore ("X is Nx faster at filtering") varies with data shape, cardinality,
    and hardware. When the engine choice materially matters, measure on the actual data instead
    of trusting remembered multipliers.
    
    ## Placement: decide where the computation lives
    
    Probe before you compute — one cell: file size, free RAM, and (when unclear) a row count via
    a direct scan. Then place the work:
    
    - **Load into memory** when the working set stays within roughly a quarter of free RAM AND
      the session will run repeated queries: `CREATE TABLE t AS SELECT ...` (or a collected
      DataFrame) once, then iterate. One scan up front converts every later query from a file
      re-scan into milliseconds.
    - **Query in place / stream** when the question is single-pass, or the data exceeds RAM:
      DuckDB reads files directly (`FROM 'data.csv'`); past RAM, cap DuckDB's memory and let it
      spill, or use Polars' streaming engine in the Python kernel. NEVER load a larger-than-RAM
      dataset fully into memory — swapping stalls the whole machine, while streaming merely
      takes longer.
    - **Query remotely, in place** when the data lives elsewhere: DuckDB reads http(s)/S3
      Parquet and CSV with projection and predicate pushdown, so fetch the columns and rows the
      question needs, never the whole file. When data sits on another machine you can execute
      on, ship the query to the data and return the small result. Rule: result much smaller
      than data — move the query; repeated local iteration planned — move a pruned copy of the
      data once.
    
    Sizing heuristics and recipes: `references/placement.md`.
    
    ## Hard rules
    
    - **NEVER use pandas.** DuckDB and Polars beat it decisively on every workload this skill
      covers, and the environments this skill assumes do not ship it — `.df()` on a DuckDB
      result raises unless pandas is installed; convert with `.pl()` via Arrow instead.
    - Excel files are not read directly: export to CSV or Parquet first.
    
    ## Output contract
    
    Answer the question; report row counts and timing for anything heavy; then stop — no bonus
    charts, no extra exploration passes beyond what the question needed. Chart when asked, or
    when the answer is a shape (trend, distribution, comparison) that prose cannot carry — then
    follow `references/visualization.md` including its visual QA step.
    
    ## References
    
    | Read | When |
    | --- | --- |
    | `references/execution-surfaces.md` | before the first query on any surface: kernel patterns, one-shot recipes, escalation rules |
    | `references/polars-lane.md` | DataFrame-shaped pipeline or data past RAM: current API, Arrow handoff, package sets |
    | `references/placement.md` | before heavy or remote work: sizing probe, memory limits, remote reads |
    | `references/visualization.md` | before any chart: type selection, quality bar, CJK fonts, visual QA |
    | `references/uv-setup.md` | uv missing or broken on this machine |
    
    ## CLI fallback
    
    When no kernel or REPL surface exists, `uv run scripts/quick-query.py <file> [SQL]`
    (`--filter <polars-sql-expr>`, `--describe`) answers ad-hoc questions with zero code.
    Supports CSV, Parquet, JSON, NDJSON.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related