Skip to main content

pubmed-research-classifier

Classify PubMed articles as research or non-research using a trained MLP on top of EMBO/ModernBERT-neg-sampling-PubMed embeddings.

v0.3.0 ships Workflow v2 production weights (amplified research definition, default decision threshold τ = 0.75) plus an optional PMID label cache backed by the private Hub dataset EMBO/pubmed-research-classifier (~30M precomputed labels, DuckDB lookup).

Model weights, StandardScaler, and publication-type vocabulary are bundled — no external model downloads are needed for the embedding-mode API.

Installation

# Embedding mode only (pass precomputed ModernBERT vectors)
pip install pubmed-research-classifier

# Text mode (package embeds internally)
pip install "pubmed-research-classifier[embed]"

# Precomputed PMID label cache (DuckDB + Hugging Face Hub)
pip install "pubmed-research-classifier[cache]"

# Everything
pip install "pubmed-research-classifier[all]"
pip install -U "pubmed-research-classifier>=0.3.0"

Hugging Face token (label cache / Hub publish)

The lookup table is a private dataset. You need a Hub token:

# Read access — enough for load_label_cache / lookup_pmid
export HF_TOKEN=hf_xxxxxxxx
# (alias also accepted: HUGGING_FACE_HUB_TOKEN)

# Or interactive login
huggingface-cli login
  • Read token → download / look up labels.
  • Write token → monthly pubmed-rc-publish-labels --upload (maintainer only).

Ask an EMBO data owner for access if you get 401/403. Do not commit tokens to git; use .env (untracked), CI secrets, or a password manager.

Override the on-disk cache directory with:

export PUBMED_RC_CACHE_DIR=/path/to/cache

Research definition (v2)

Labels follow the amplified Workflow v2 definition used in the Scientometrics pipeline:

  • Research — methodology-backed work with data/analysis; systematic reviews / meta-analyses; resources (datasets, software, code); methods / theoretical models; clinical and observational designs that report such findings.
  • Non-research — narrative reviews without new analysis; perspectives, primers, letters/editorials (opinion-only), errata / retractions / news-like items.

p_nr / probability is P(non-research). Default threshold 0.75: label is non-research when p_nr >= 0.75, else research.

Training provenance: MLP_with_pt (ModernBERT title + abstract + scalars + MeSH publication-type multi-hot), seed 42, frozen split from the paper repo (models/v2/).

Quick start

Text mode

from pubmed_research_classifier import classify

result = classify({
    "title": "Structural basis of CRISPR-Cas9 activity",
    "abstract": "We report crystal structures of Cas9 ...",
    "pub_types": ["Journal Article"],
    "n_authors": 8,
    "n_refs": 42,
})
# {"label": "research", "p_nr": 0.018}

Embedding mode

Pre-compute embeddings with EMBO/ModernBERT-neg-sampling-PubMed using normalize_embeddings=True, then pass them directly:

from pubmed_research_classifier import classify
import numpy as np

result = classify({
    "title_emb":     title_embedding,    # np.ndarray, shape (768,)
    "abstract_emb":  abstract_embedding, # np.ndarray, shape (768,); zeros if absent
    "has_abstract":  True,
    "length_title":  52,
    "length_abstract": 1240,
    "pub_types":     ["Journal Article"],
    "n_authors":     8,
    "n_refs":        42,
})

Batch

results = classify(records, batch_size=128)
# list in the same order as input

Custom threshold

classify(record)                 # τ = 0.75 (default)
classify(record, threshold=0.95) # higher NR precision

PMID label cache (lookup table)

Precomputed labels for ~30.4M OpenAlex–PubMed PMIDs (Hub v1.0.0, classifier 0.2.0 weights) for fast lookup before running the MLP.

pip install "pubmed-research-classifier[cache]"
export HF_TOKEN=hf_...   # read access
from pubmed_research_classifier import (
    load_label_cache,
    lookup_pmid,
    classify_pmid,
)

# First call downloads the Hub CSV (~1 GB) and builds a local DuckDB index
# under ~/.cache/pubmed_research_classifier/ (or PUBMED_RC_CACHE_DIR).
cache = load_label_cache(revision="v1.0.0")
print(len(cache))  # ~30_426_295

lookup_pmid("10006576")
# {"PMID": "10006576", "class": "research",
#  "probability": 0.009..., "source": "cache"}

lookup_pmid("99999999")
# None

# Prefer cache; run the bundled MLP only on a miss
classify_pmid("10006576")
# source == "cache"

classify_pmid(
    "99999999",
    record={
        "title": "An unseen article title",
        "abstract": "Abstract text ...",
        "pub_types": ["Journal Article"],
        "n_authors": 3,
        "n_refs": 12,
    },
)
# {"PMID": "99999999", "class": "...", "probability": ..., "source": "model"}

Batch lookup (order-preserving):

rows = cache.lookup_many(["10006576", "10047518", "99999999"])
# [dict, dict, None]

probability is P(non-research), same as p_nr from classify.

Client calls never write back to Hugging Face. New classifications with source="model" stay local until a maintainer publishes a new Hub revision (see below).

For bulk joins of millions of PMIDs, query the DuckDB file or Hub CSV with DuckDB/Polars directly rather than calling lookup_pmid in a tight Python loop.

Monthly Hub refresh (maintainers)

Typical monthly job: classify new PubMed/OpenAlex PMIDs → CSV → merge into the previous DuckDB table → upload data/<new_revision>/labels.csv.

1. Produce a CSV of new labels

Columns (aliases accepted):

Column Aliases Notes
PMID pmid digits; pmid: prefix OK
class label research or non-research
probability p_nr P(non-research) float
import csv
from pubmed_research_classifier import classify_pmid, load_label_cache

load_label_cache(revision="v1.0.0")

new_rows = []
for pmid, record in monthly_records:  # your ETL
    out = classify_pmid(pmid, record=record)
    if out["source"] == "model":      # only cache misses
        new_rows.append({
            "PMID": out["PMID"],
            "class": out["class"],
            "probability": out["probability"],
        })

with open("new_pmids.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.DictWriter(f, fieldnames=["PMID", "class", "probability"])
    w.writeheader()
    w.writerows(new_rows)

2. Merge + export + upload

Needs a Hub token with write access to EMBO/pubmed-research-classifier.

CLI:

export HF_TOKEN=hf_...   # write-capable

# Dry run: merge + write local DuckDB/CSV only
pubmed-rc-publish-labels \
  --new-csv new_pmids.csv \
  --base-revision v1.0.0 \
  --new-revision v1.1.0

# Publish
pubmed-rc-publish-labels \
  --new-csv new_pmids.csv \
  --base-revision v1.0.0 \
  --new-revision v1.1.0 \
  --upload

Python:

from pubmed_research_classifier import publish_label_revision

result = publish_label_revision(
    new_csv="new_pmids.csv",
    base_revision="v1.0.0",
    new_revision="v1.1.0",
    upload=True,   # False = local DuckDB + CSV only
)
print(result["stats"])
# {'n_before': ..., 'n_after': ..., 'n_inserted': ..., 'n_updated': ..., ...}
print(result["csv_path"], result["db_path"])

Incoming PMIDs overwrite existing rows (re-score). New PMIDs are appended. Users pin load_label_cache(revision="v1.1.0") after you publish.

Input fields

Field Type Mode Notes
title str text
abstract str or None text empty/None → treated as absent
title_emb array (768,) embed L2-normalised
abstract_emb array (768,) embed L2-normalised; zeros if absent
has_abstract bool embed
length_title int embed auto-derived from title in text mode
length_abstract int embed auto-derived from abstract in text mode
pub_types list[str] or str both PubMed PT tags; comma-sep string accepted
n_authors int both
n_refs int both
has_funding bool both optional; inferred from "Research Support" PTs if omitted

Output

# classify(...)
{"label": "research",     "p_nr": 0.018}
{"label": "non-research", "p_nr": 0.921}

# lookup_pmid / classify_pmid
{"PMID": "10006576", "class": "research", "probability": 0.009, "source": "cache"}

Obtaining has_funding from PubMed XML

has_funding is True when the article's PubMed XML record contains at least one <Grant> element inside a <GrantList>. It is not the same as the "Research Support, …" publication type tags (those are a separate, coarser signal also used by the model via pub_types).

import xml.etree.ElementTree as ET

def has_funding_from_xml(article_xml: str) -> bool:
    root = ET.fromstring(article_xml)
    return len(root.findall(".//Grant")) > 0

If you omit has_funding, the package falls back to checking whether any pub_types start with "Research Support".

Changelog

0.3.0

  • Optional Hub label cache ([cache]): DuckDB-backed lookup for EMBO/pubmed-research-classifier (~30M PMIDs).
  • APIs: load_label_cache, lookup_pmid, classify_pmid.
  • Maintainer publish path: publish_label_revision + CLI pubmed-rc-publish-labels (merge CSV → DuckDB → Hub upload).
  • Requires HF_TOKEN for Hub access (read for lookup; write for --upload).

0.2.0

  • Bundle Workflow v2 MLP_with_pt weights.
  • Amplified research / non-research definition; default τ = 0.75.

0.1.0

  • Initial release with v1 (narrow) research definition weights.

Publishing a new version to PyPI

Artifacts land in pubmed-research-classifier/dist/.

  1. Update bundled weights under src/pubmed_research_classifier/_data/ if needed.

  2. Bump version in pyproject.toml and __version__ in __init__.py.

  3. Update regression expectations in tests/, then:

    pip install -e ".[cache]"
    pytest -m "not integration"
    
  4. Build and upload:

    pip install build twine
    python -m build
    twine upload dist/pubmed_research_classifier-0.3.0*
    
  5. Verify:

    pip install "pubmed-research-classifier==0.3.0" --force-reinstall
    python -c "from pubmed_research_classifier import classify, load_label_cache; print('ok')"
    

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pubmed_research_classifier-0.3.0.tar.gz (573.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pubmed_research_classifier-0.3.0-py3-none-any.whl (572.1 kB view details)

Uploaded Python 3

File details

Details for the file pubmed_research_classifier-0.3.0.tar.gz.

File metadata

File hashes

Hashes for pubmed_research_classifier-0.3.0.tar.gz
Algorithm Hash digest
SHA256 fb40de3a8998f68621a6cd8d55ff93651e56241275a13053711493a43352f0ab
MD5 936a0ef35c47c047df1c9b3d6686313f
BLAKE2b-256 8f4bebcd3821cc1f0fd79cd43afbf69e6f6040e12352b23273ce08dfb1e80b01

See more details on using hashes here.

File details

Details for the file pubmed_research_classifier-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pubmed_research_classifier-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f3f3656620dcd78985a0d680aba3cbf0626ba69c30a511563603025fa368f589
MD5 01ead84dc574f359fbee649be5bc85ed
BLAKE2b-256 f0045506ee3ff8a6d98206919bad5918d54b6133609fd0a0b6a05f642562e8a7

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page