agentsclimarketplace

Python bio sql for bioinformatics

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

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.From its SKILL.md

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.
  • 4 stars4 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.

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 609 of the 712 authors here whose files we hold, read 2026-09-06

  • Index all foreign key columnsin 26 of 609
  • Use cursor pagination instead of offsetin 25 of 609, across 20 files
  • Use timestamptz for timestampsin 21 of 609
  • Specify columns instead of using select starin 20 of 609, across 10 files
  • Use parameterized queries for all database interactionsin 20 of 609, across 19 files
  • Use Enum for categorical datain 17 of 609, across 7 files
  • Order by frequently filtered columnsin 17 of 609, across 7 files
  • Batch data insertsin 17 of 609, across 7 files
  • Use expand-contract pattern for schema changesin 17 of 609
  • Use materialized views for real-time aggregationsin 16 of 609, across 6 files
  • Partition tables by timein 16 of 609, across 6 files
  • Use smallest appropriate data typesin 16 of 609, across 6 files

Said here and by no other author read

  • use sqlite3 for relational schema storage
  • use pandas read_sql_query for data retrieval
  • use executescript to create database tables
  • use executemany for bulk data insertion
  • use inner join for matched rows only
  • use left join to preserve unmatched rows

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 325,949. 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.