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.2.0 ships the Workflow v2 production weights (amplified research definition, default decision threshold τ = 0.75). Model weights, StandardScaler, and publication-type vocabulary are bundled — no external model downloads are needed for the embedding-mode API.
Installation
# Embedding mode only (no sentence-transformers required)
pip install pubmed-research-classifier
# Text mode (package embeds internally)
pip install "pubmed-research-classifier[embed]"
Upgrade from 0.1.x (narrow v1 definition / older weights):
pip install -U "pubmed-research-classifier>=0.2.0"
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 is P(non-research). Default threshold 0.75: label is non-research
when p_nr >= 0.75, else research. Raise τ (e.g. 0.95) only when you need
higher precision on non-research at the cost of overall agreement.
Training provenance: MLP_with_pt (ModernBERT title + abstract + 6 continuous
scalars + 194 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 — millions of records
results = classify(records, batch_size=128)
# Returns a list in the same order as the input.
Custom threshold
# Default τ=0.75 (recommended)
classify(record)
# Higher precision on non-research (fewer NR calls)
classify(record, threshold=0.95)
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
{"label": "research", "p_nr": 0.018}
{"label": "non-research", "p_nr": 0.921}
p_nr is P(non-research) from the model.
Default threshold: 0.75 (configurable via classify(..., threshold=0.75)).
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).
If you fetch articles via the NCBI E-utilities API (efetch, XML format), you can extract it like this:
import xml.etree.ElementTree as ET
def has_funding_from_xml(article_xml: str) -> bool:
"""Return True if the PubMed XML contains at least one <Grant> entry."""
root = ET.fromstring(article_xml)
return len(root.findall(".//Grant")) > 0
Or, if you are working with a parsed xml.etree.ElementTree.Element object
(e.g. the <PubmedArticle> node returned by your ETL pipeline):
has_funding = len(article_element.findall(".//Grant")) > 0
If you do not have access to the raw XML and only have the metadata fields,
omit has_funding entirely — the package will fall back to checking whether
any of the pub_types start with "Research Support", which is a reasonable
proxy and is already captured separately in the model's publication-type
features.
Changelog
0.2.0
- Bundle Workflow v2
MLP_with_ptweights (models/v2/from the paper repo). - Labels follow the amplified research / non-research definition (see above).
- Default decision threshold remains τ = 0.75.
0.1.0
- Initial release with v1 (narrow) research definition weights.
Publishing a new version to PyPI
The built artifacts live in pubmed-research-classifier/dist/.
Workflow for every new release
-
Update the model weights — copy new
mlp_best.pt,scaler.joblib, and/ormlp_config.jsonintosrc/pubmed_research_classifier/_data/and overwrite the old files. -
Bump the version in two places:
# pyproject.toml version = "0.2.0"
# src/pubmed_research_classifier/__init__.py __version__ = "0.2.0"
-
Update regression expectations in
tests/test_classify.pyfor the new weights, then runpytest. -
Rebuild the wheel:
cd pubmed-research-classifier pip install build # first time only python -m build # produces dist/pubmed_research_classifier-0.2.0-py3-none-any.whl # and dist/pubmed_research_classifier-0.2.0.tar.gz
-
Upload to PyPI:
pip install twine # first time only twine upload dist/pubmed_research_classifier-0.2.0* # Username: __token__ # Password: <your PyPI API token>
PyPI API tokens are managed at https://pypi.org/manage/account/token/. Use a project-scoped token (not account-wide) for safety.
-
Verify the release:
pip install "pubmed-research-classifier==0.2.0" --force-reinstall python -c "from pubmed_research_classifier import classify; print('ok')"
First-time PyPI setup
If the package does not yet exist on PyPI, the first upload creates it automatically. You will need a PyPI account and a project-scoped (or account-scoped) API token. Test releases can go to https://test.pypi.org first:
twine upload --repository testpypi dist/pubmed_research_classifier-0.2.0*
pip install --index-url https://test.pypi.org/simple/ pubmed-research-classifier
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pubmed_research_classifier-0.2.0.tar.gz.
File metadata
- Download URL: pubmed_research_classifier-0.2.0.tar.gz
- Upload date:
- Size: 562.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.9.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
af1f3775b62ba65c6cf4042f6fb53616a972b4efff735baa7ada1bbda78cf6ae
|
|
| MD5 |
24e7829926923a2940138f0fb30b34be
|
|
| BLAKE2b-256 |
2cf139cfc35d6b3466674719d5fb8d091df7216215365be313b18e5a5104ad17
|
File details
Details for the file pubmed_research_classifier-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pubmed_research_classifier-0.2.0-py3-none-any.whl
- Upload date:
- Size: 561.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.9.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a4e21dd9c57c206c00f2c0d7808348ba91ec69218d476a0620278d1c35b9b714
|
|
| MD5 |
06293dae4cc14f1eced4114dff3c49dd
|
|
| BLAKE2b-256 |
15ce795283fcedf29ff77e6d492ec9ac4106e9b3e02396f471714a4d164cc8af
|