agentsclimarketplace

Python bio sql for bioinformatics

Skill Pavel-Kravchenko/Bioinformatics/Skills/python-bio-sql-for-bioinformatics

208 bioinformatics skills for Claude Code — NGS, single-cell, metagenomics, structural biology, algorithms, AI for science

Install
npx -y skills add Pavel-Kravchenko/Bioinformatics --skill python-bio-sql-for-bioinformatics

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Query genomic data with SQLite (sqlite3, pandas.read_sql_query): SELECT/WHERE/GROUP BY/HAVING, INNER/LEFT JOIN, subqueries, parameterized inserts. Use when writing SQL over gene/variant tables or building a SQLite database.

SKILL.md

9.7 KB, ~2.4k tokens by cl100k_base, as published. Nobody here has run it

SQL for Bioinformatics

When to Use

  • Storing gene annotations, variant calls, or expression results in a relational schema instead of flat files.
  • Writing SQL joins/aggregations to answer questions like "genes with pathogenic variants AND high tumor expression."
  • Building a local queryable cache of data pulled from Ensembl, UCSC, NCBI, or dbSNP (all backed by relational DBs).
  • Loading a pandas DataFrame from a SQL query (pd.read_sql_query) for downstream plotting/stats.
  • Avoiding SQL-injection bugs when a query needs to include a user- or pipeline-supplied gene name or position.

Version Compatibility

  • Python ≥3.10, sqlite3 (stdlib, no install needed), pandas ≥2.0.
  • Same SQL patterns apply to PostgreSQL/MySQL via sqlalchemy + pd.read_sql_query, with minor dialect differences (e.g. SERIAL vs INTEGER PRIMARY KEY AUTOINCREMENT).

Prerequisites

  • pip install pandas numpy (sqlite3 ships with Python).
  • Basic familiarity with relational schema design (primary/foreign keys) and pandas DataFrames.

Key Concepts

JOIN type determines which rows survive: INNER JOIN — only matched rows; LEFT JOIN — all left rows, NULL for unmatched right side.

HAVING vs WHERE: WHERE filters rows before grouping; HAVING filters groups after aggregation. WHERE COUNT(*) > 3 is invalid SQL — use HAVING.

Subqueries vs JOINs: A JOIN is usually more readable and faster. A subquery with IN (SELECT ...) is clearer when you only need membership in a small inner set, not its columns.

Always use parameterized queries: Never build SQL with f-strings/.format() containing external input — that's SQL injection. Use cursor.execute("... WHERE gene = ?", (gene_name,)).

Goal: build a small relational schema (genes, variants, expression, pathways) and populate it so it can be queried like a mini Ensembl/dbSNP mirror.

Approach: create tables with executescript, then bulk-load rows with executemany and parameterized ? placeholders.

import sqlite3
import pandas as pd
import numpy as np


def build_demo_db() -> sqlite3.Connection:
    """Create an in-memory SQLite DB with genes/variants/expression/pathway tables."""
    conn = sqlite3.connect(":memory:")
    conn.executescript("""
        CREATE TABLE genes (
            gene_id    INTEGER PRIMARY KEY,
            symbol     TEXT NOT NULL,
            chromosome TEXT,
            start_pos  INTEGER,
            end_pos    INTEGER,
            biotype    TEXT
        );
        CREATE TABLE variants (
            variant_id  INTEGER PRIMARY KEY,
            gene_id     INTEGER REFERENCES genes(gene_id),
            position    INTEGER,
            ref_allele  TEXT,
            alt_allele  TEXT,
            clinical_significance TEXT
        );
        CREATE TABLE expression (
            expr_id   INTEGER PRIMARY KEY,
            gene_id   INTEGER REFERENCES genes(gene_id),
            tissue    TEXT,
            condition TEXT,
            tpm       REAL
        );
        CREATE TABLE pathways (
            pathway_id   INTEGER PRIMARY KEY,
            pathway_name TEXT
        );
        CREATE TABLE gene_pathway (
            gene_id    INTEGER REFERENCES genes(gene_id),
            pathway_id INTEGER REFERENCES pathways(pathway_id)
        );
    """)

    genes = [
        (1, 'BRCA1', 'chr17', 43044295, 43125483, 'protein_coding'),
        (2, 'TP53',  'chr17',  7661779,  7687538, 'protein_coding'),
        (3, 'EGFR',  'chr7',  55019017, 55207337, 'protein_coding'),
        (4, 'MYC',   'chr8', 127735434, 127742951, 'protein_coding'),
        (5, 'KRAS',  'chr12', 25204789, 25250936, 'protein_coding'),
        (6, 'PTEN',  'chr10', 89692905, 89728532, 'protein_coding'),
        (7, 'RB1',   'chr13', 47775885, 47954065, 'protein_coding'),
    ]
    conn.executemany("INSERT INTO genes VALUES (?,?,?,?,?,?)", genes)

    variants = [
        (1, 1, 43045629, 'A', 'T', 'pathogenic'),
        (2, 2, 7674220,  'C', 'T', 'pathogenic'),
        (3, 3, 55181320, 'G', 'A', 'likely_pathogenic'),
        (4, 5, 25245347, 'G', 'T', 'pathogenic'),
        (5, 6, 89711933, 'T', 'A', 'benign'),
    ]
    conn.executemany("INSERT INTO variants VALUES (?,?,?,?,?,?)", variants)

    rng = np.random.default_rng(42)
    expr_rows, eid = [], 1
    for gid in range(1, 8):
        for tissue in ['liver', 'kidney', 'brain']:
            for condition in ['normal', 'tumor']:
                base = rng.uniform(10, 200)
                tpm = round(base * (1.8 if condition == 'tumor' else 1.0) + rng.normal(0, 5), 2)
                expr_rows.append((eid, gid, tissue, condition, max(tpm, 0.1)))
                eid += 1
    conn.executemany("INSERT INTO expression VALUES (?,?,?,?,?)", expr_rows)

    conn.executemany("INSERT INTO pathways VALUES (?,?)",
                      [(1, 'DNA repair'), (2, 'Cell cycle'), (3, 'Apoptosis')])
    conn.executemany("INSERT INTO gene_pathway VALUES (?,?)",
                      [(1, 1), (2, 1), (2, 2), (4, 2), (3, 3), (2, 3)])
    conn.commit()
    return conn


conn = build_demo_db()

Goal: filter, aggregate, and join across the schema — the operations that make SQL worth using over flat-file grep.

Approach: push filtering/grouping into the database with pd.read_sql_query, and let HAVING filter on the aggregate rather than the raw column.

def query_examples(conn: sqlite3.Connection) -> dict[str, pd.DataFrame]:
    """Run representative SELECT/JOIN/subquery patterns and return each result as a DataFrame."""
    results = {}

    # WHERE + ORDER BY on a computed column
    results["long_genes_chr17"] = pd.read_sql_query("""
        SELECT symbol, chromosome, (end_pos - start_pos) AS length
        FROM genes
        WHERE chromosome = 'chr17' AND (end_pos - start_pos) > 20000
        ORDER BY length DESC
    """, conn)

    # GROUP BY + HAVING: filters on the aggregate, not the raw rows
    results["high_tumor_expr"] = pd.read_sql_query("""
        SELECT g.symbol, ROUND(AVG(e.tpm), 2) AS avg_tumor_tpm
        FROM genes g
        JOIN expression e ON g.gene_id = e.gene_id
        WHERE e.condition = 'tumor'
        GROUP BY g.symbol
        HAVING AVG(e.tpm) > 50
        ORDER BY avg_tumor_tpm DESC
    """, conn)

    # LEFT JOIN: keep genes with zero variants (INNER JOIN would drop them)
    results["variant_counts"] = pd.read_sql_query("""
        SELECT g.symbol, COUNT(v.variant_id) AS n_variants
        FROM genes g
        LEFT JOIN variants v ON g.gene_id = v.gene_id
        GROUP BY g.symbol
        ORDER BY n_variants DESC
    """, conn)

    # Subquery: genes that satisfy two independent conditions
    results["tumor_pathogenic"] = pd.read_sql_query("""
        SELECT symbol FROM genes
        WHERE gene_id IN (
            SELECT gene_id FROM expression
            WHERE condition = 'tumor'
            GROUP BY gene_id
            HAVING AVG(tpm) > 50
        )
        AND gene_id IN (
            SELECT gene_id FROM variants WHERE clinical_significance = 'pathogenic'
        )
    """, conn)

    return results


tables = query_examples(conn)
print(tables["tumor_pathogenic"])

Goal: write results back to the database and read them out with a safe, parameterized filter.

Approach: CREATE TABLE IF NOT EXISTS, bulk executemany, then execute with a ? placeholder — never an f-string — for the caller-supplied value.

def save_de_results(conn: sqlite3.Connection, de_data: list[tuple]) -> None:
    """Persist differential-expression results (gene_id, log2fc, pvalue, padj, significant)."""
    conn.execute("""
        CREATE TABLE IF NOT EXISTS de_results (
            gene_id INTEGER REFERENCES genes(gene_id),
            log2fc REAL, pvalue REAL, padj REAL, significant INTEGER
        )
    """)
    conn.executemany("INSERT INTO de_results VALUES (?,?,?,?,?)", de_data)
    conn.commit()


def fetch_gene_variants(conn: sqlite3.Connection, gene_symbol: str) -> pd.DataFrame:
    """Look up variants for one gene safely (parameterized -- never f-string the symbol in)."""
    return pd.read_sql_query(
        "SELECT v.* FROM variants v JOIN genes g ON v.gene_id = g.gene_id WHERE g.symbol = ?",
        conn, params=(gene_symbol,)
    )


save_de_results(conn, [(1, 2.3, 0.001, 0.01, 1), (2, 1.8, 0.005, 0.03, 1)])
assert not fetch_gene_variants(conn, "BRCA1").empty
conn.close()

Pitfalls

  • JOIN type: INNER loses unmatched rows; LEFT preserves them — choose deliberately, especially when counting "genes with 0 variants."
  • HAVING vs WHERE: putting an aggregate in WHERE raises an error; use HAVING after GROUP BY.
  • SQL injection: f-string/.format() SQL with external input is a real vulnerability — always use ? placeholders and params=.
  • Off-by-one coordinates: Python ranges are half-open [start, stop); genomic coordinates (GFF/VCF) are often 1-based, BED is 0-based — check before comparing across formats.
  • In-memory DB scope: sqlite3.connect(":memory:") is per-connection; a second connect(":memory:") call gets an empty, unrelated database.

See Also

  • bio-expression-matrix-counts-ingest — loading count/TPM matrices before pushing them into SQL tables.
  • bio-variant-calling-vcf-basics — parsing VCF into the tabular form used by the variants table here.
  • bio-database-access-entrez-fetch — pulling gene/variant records from NCBI to populate a local DB.
  • polars — a faster DataFrame-native alternative to SQL joins for larger local datasets.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Gives 0 of the 12 instructions most databases sql skills give in ~2.4k tokens

Counted across 589 of the 662 authors here whose files we hold, read 2026-08-07

  • Use parameterized queriesin 37 of 589, across 34 files
  • Use timestamptz for timestampsin 30 of 589, across 14 files
  • Index foreign keysin 29 of 589, across 18 files
  • Create indexes concurrentlyin 29 of 589, across 24 files
  • Use numeric type for moneyin 25 of 589, across 8 files
  • Use cursor pagination instead of offsetin 24 of 589, across 17 files
  • Select only required columnsin 24 of 589, across 20 files
  • Add indexes manually on foreign key columnsin 22 of 589, across 12 files
  • Normalize to third normal formin 19 of 589, across 10 files
  • Configure connection poolingin 19 of 589, across 17 files
  • Put equality columns before range columns in indexesin 18 of 589, across 10 files
  • Read individual rule files for detailed explanationsin 18 of 589, across 4 files

Said here and by no other author read

  • create tables with executescript
  • bulk-load rows with executemany
  • use parameterized question mark placeholders for external input
  • push filtering and grouping into the database
  • filter groups with having after group by
  • choose inner or left joins deliberately

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.