NIH Audit is an interactive analytics platform for exploring the NIH research portfolio using semantic search, LLM-based grant classification, and funding visualization.
The NIH funds over $50 billion in biomedical research annually across hundreds of disease areas and research programs. While NIH RePORTER provides access to individual grants, it offers limited insight into the type of research being funded (e.g., basic science versus therapeutic development) across agencies, disease areas, or funding mechanisms.
This project builds an end-to-end analytics pipeline that ingests NIH grant data, classifies grants into eight research categories using large language models, and provides interactive tools for exploring funding patterns across the NIH portfolio.
Users can analyze funding at multiple levels, including:
The entire NIH portfolio Individual NIH institutes and centers (e.g., National Cancer Institute) Activity codes (e.g., R01, R21, SBIR/STTR) Disease or research topics using hybrid semantic search
Each portfolio includes interactive visualizations that allow users to drill down from aggregate funding statistics to the individual grants contributing to those results.
Visit https://nihaudit.com
The application allows users to:
Automatically categorizes grants into eight research stages:
| Layer | Technologies |
|---|---|
|
Frontend Backend Database AI/ML Infrastructure Tooling |
HTML, CSS, JavaScript, Plotly FastAPI, Python PostgreSQL, pgvector OpenAI GPT-5.4-mini, PubMedBERT, MS MARCO (cross-encoder) Modal, Hetzner VM Git, GitHub, pytest, logging |
For our search pipeline, we use a vector similarity search (with a similarity threshold) to ensure high recall, then re-rank with a fine-tuned cross-encoder essential for achieving high precision. Without the cross-encoder, a "multiple sclerosis" query would return confounding grants containing "systemic sclerosis", for example.
The semantic search pipeline was benchmarked against 15 NIH Research, Condition, and Disease Categorization (RCDC) portfolios spanning both narrowly defined diseases (e.g., Multiple Sclerosis, Endometriosis) and broader clinical concepts (e.g., Breast Cancer, Heart Disease). The RCDC categorization is what is officially used by NIH for congressional reporting and therefore makes for the most suitable comparison. Retrieval quality was measured using precision and recall before and after cross-encoder reranking.
As the graph above demonstrates, high-recall embedding retrieval first generates a broad candidate set. Then, cross-encoder reranking substantially improves precision while preserving most relevant grants. Varying the score threshold of our reranker can toggle the precision/recall tradeoff, but we found that threshold = -2.0 provides the right balance.
The key findings from evaluation were that narrowly defined disease entities (e.g., multiple Sclerosis, endometriosis) showed strong agreement with RCDC, while broader umbrella categories (e.g., heart disease) were more challenging. Broad RCDC categories encompass diverse disease subtypes and clinical concepts, suggesting that additional query expansion, ontology-aware retrieval, or task-specific training examples may further improve recall.
Because no public benchmark exists for categorizing NIH grants into the proposed translational research ontology, I used expert-curated challenge sets rather than randomly sampled grants for evaluation.
I constructed challenge sets by collecting grants that were difficult to classify during ontology development, including:
Below are examples of ambiguities in research categories represented in the challenge sets:
| Category | Difficult Distinction |
|---|---|
|
Diagnostic Therapeutic Research Tool |
Diagnostic tool vs. biomarker discovery Therapeutic development vs. mechanistic biology Method development vs. use of an existing method |
Model performance was evaluated using precision, recall, and F1 score. Though these metrics are far less than what they would be with a randomly sampled grant dataset, they provide a useful benchmark for future models.
Finally, our final production prompts achieved approximately 97% coverage across the NIH grant corpus for FY2025 (65K grants). The remaining ~3% of grants represented ambiguous or out-of-scope cases, requiring additional prompt optimization.
PubMedBert was trained on PubMed abstracts, therefore embedding representations are especially suited for the biomedical text found in NIH grant abstracts.
This combination provides a unified database for grant metadata and embeddings, enabling straightforward joins and filtering. In addition, this approach is free, enables simple construction of indices (HNSW and B-tree), and allows simple analysis of latency / performance.
Pure semantic search misses a subset of grants as in the case of the query "multiple sclerosis" failing to capture grants only mentioning the abbreviation ("MS"). Hybrid retrieval with keyword search ensures high recall of grants with synonymous terms.
The goal of this application is to retrieve an exhaustive list of all NIH grants given a particular query to understand accurate funding allocation. This is unlike traditional search systems where users are only looking for a few, highly relevant results.
While range hybrid search achieves high recall of all potentially relevant grants, precision suffers as a number of different biomedical terms carry similar lexical and semantic features (i.e. both "multiple sclerosis" and "systemic sclerosis" share "sclerosis").
While the initial PubMedBERT model is training on biomedical text, the MS MARCO cross-encoder I used is a generalist model that lacks the ability to discern biomedical text associations (i.e. multiple sclerosis and myelin). By manually generating labeled query-grant abstract pairs, I am able to significantly boost the precision of our cross-encoder.
Binary classifier prompts allow independent characterization of categories per grant (with sufficient inclusion/exclusion criteria) while also allowing grants to receive multiple labels when appropriate. Multi-label classification runs the risk of forcing mutually exclusive predictions.
βββ .github/
β βββ workflows/
β βββ tests.yaml
βββ .gitignore
βββ LICENSE
βββ README.md
βββ app/
β βββ __init__.py
β βββ main.py
β βββ startup.py
β βββ static/
β β βββ css/
β β β βββ styles.css
β β β βββ styles_v12.css
β β βββ fonts/
β β β βββ primer.ttf
β β βββ images/
β β β βββ curved-thin-arrow-icon.svg
β β βββ js/
β β βββ script.js
β βββ templates/
β βββ categories.html
β βββ contact_us.html
β βββ index.html
β βββ partials/
β β βββ search_bar.html
β βββ portfolio.html
β βββ results.html
βββ core/
β βββ __init__.py
β βββ config.py
β βββ constants.py
β βββ db/
β β βββ __init__.py
β β βββ connection.py
β βββ embedding/
β β βββ __init__.py
β β βββ config.py
β β βββ embedding_job.py
β β βββ model.py
β β βββ persistence.py
β β βββ selection.py
β βββ ingest/
β β βββ __init__.py
β β βββ config.py
β β βββ hashing.py
β β βββ ingest.py
β β βββ normalize.py
β β βββ org_resolution.py
β β βββ persistence.py
β β βββ process.py
β β βββ reporter_client.py
β βββ llm/
β β βββ __init__.py
β β βββ batch.py
β β βββ constants.py
β β βββ parser.py
β β βββ prompt_loader.py
β β βββ prompts/
β β βββ README.md
β βββ logging_config.py
β βββ search/
β β βββ __init__.py
β β βββ cache.py
β β βββ candidate_retrieval.py
β β βββ combine.py
β β βββ constants.py
β β βββ fill_abstract.py
β β βββ hybrid.py
β β βββ load_docs.py
β β βββ modal_reranker.py
β β βββ postprocess.py
β β βββ query_embedding.py
β β βββ reranker.py
β βββ services/
β β βββ __init__.py
β β βββ activity_service.py
β β βββ agency_service.py
β β βββ formatting.py
β β βββ grant_service.py
β β βββ portfolio_service.py
β β βββ search_service.py
β βββ utils/
β βββ __init__.py
β βββ abstract_tokens.py
β βββ compile_parquet.py
β βββ query_expansion.py
βββ data/
β βββ activity_code_list.csv
β βββ agencies_list.csv
β βββ rcdc_synonyms.json
βββ evaluation/
β βββ __init__.py
β βββ categorization/
β β βββ __init__.py
β β βββ challenge_sets/
β β β βββ research_tool.csv
β β βββ classify.py
β β βββ grant_loader.py
β β βββ metrics.py
β β βββ run_eval.py
β βββ search/
β βββ __init__.py
β βββ benchmark.csv
β βββ ground_truth/
β β βββ Alzheimer's_Disease.csv
β β βββ Amyotrophic_Lateral_Sclerosis_(ALS).csv
β β βββ Asthma.csv
β β βββ Autism.csv
β β βββ Breast_Cancer.csv
β β βββ Crohn's_Disease.csv
β β βββ Cystic_Fibrosis.csv
β β βββ Depression.csv
β β βββ Diabetes.csv
β β βββ Endometriosis.csv
β β βββ HIV_AIDS.csv
β β βββ Heart_Disease.csv
β β βββ Lupus.csv
β β βββ Multiple_Sclerosis.csv
β β βββ Parkinson's_Disease.csv
β βββ metrics.py
β βββ rcdc.py
β βββ run_eval.py
βββ modal_deploy/
β βββ __init__.py
β βββ reranker_app.py
βββ pipelines/
β βββ __init__.py
β βββ ingest_nih.py
β βββ llm/
β β βββ __init__.py
β β βββ combine_results.py
β β βββ generate_classification_batch.py
β β βββ generate_summary_batch.py
β β βββ import_classification_results.py
β β βββ import_summary_results.py
β β βββ retrieve_batch.py
β β βββ submit_batch.py
β βββ migration/
β β βββ __init__.py
β β βββ hash_historical.py
β βββ run_embedding.py
β βββ train_reranker.py
βββ requirements.txt
βββ scripts/
β βββ compare_pairs_reranker.py
β βββ update_agency_code.py
βββ tests/
βββ embedding/
β βββ test_embedding_job.py
β βββ test_model.py
β βββ test_persistence.py
β βββ test_selection.py
βββ ingest/
β βββ test_config.py
β βββ test_hashing.py
β βββ test_ingest.py
β βββ test_normalize.py
β βββ test_org_resolution.py
β βββ test_persistence.py
β βββ test_process.py
β βββ test_reporter_client.py
βββ llm/
β βββ test_batch.py
β βββ test_parser.py
β βββ test_prompt_loader.py
βββ search/
β βββ test_cache.py
β βββ test_candidate_retrieval.py
β βββ test_combine.py
β βββ test_fill_abstracts.py
β βββ test_hybrid.py
β βββ test_load_docs.py
β βββ test_postprocess.py
β βββ test_query_embedding.py
β βββ test_reranker.py
βββ services/
β βββ test_formatting.py
β βββ test_grant_service.py
β βββ test_portfolio_service.py
β βββ test_search_service.py
βββ test_main.py
βββ utils/
βββ test_query_expansion.py
git clone https://github.com/pradeepbugga/NIH_Portfolio-Analytics.git
cd NIH_Portfolio_Analytics
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Create a .env file:
OPENAI_API_KEY=...
DATABASE_URL=...
createdb nih_portfolio
Enable pgvector:
CREATE EXTENSION vector;
Run the schema:
psql -d nih_portfolio -f database/schema.sql
Import NIH data:
python -m pipelines.ingest_nih
Generate embeddings:
python -m pipelines.run_embeddings
Fine-tuning cross-encoder:
The training pipeline is included; proprietary training labels and final model weights are not distributed.
LLM Categorization
Production prompt templates are proprietary and therefore not included in the public repository.
Start the API:
uvicorn app.main:app --reload
Visit
http://localhost:8000
pytest
This repository is licensed under the MIT License. Certain proprietary assetsβincluding production LLM prompts, manually curated training datasets, and fine-tuned model weightsβare intentionally excluded from the public release.
NIH Audit is an interactive analytics platform for exploring the NIH research portfolio using semantic search, LLM-based grant classification, and funding visualization.
The NIH funds over $50 billion in biomedical research annually across hundreds of disease areas and research programs. While NIH RePORTER provides access to individual grants, it offers limited insight into the type of research being funded (e.g., basic science versus therapeutic development) across agencies, disease areas, or funding mechanisms.
This project builds an end-to-end analytics pipeline that ingests NIH grant data, classifies grants into eight research categories using large language models, and provides interactive tools for exploring funding patterns across the NIH portfolio.
Users can analyze funding at multiple levels, including:
The entire NIH portfolio Individual NIH institutes and centers (e.g., National Cancer Institute) Activity codes (e.g., R01, R21, SBIR/STTR) Disease or research topics using hybrid semantic search
Each portfolio includes interactive visualizations that allow users to drill down from aggregate funding statistics to the individual grants contributing to those results.
Visit https://nihaudit.com
The application allows users to:
Automatically categorizes grants into eight research stages:
| Layer | Technologies |
|---|---|
|
Frontend Backend Database AI/ML Infrastructure Tooling |
HTML, CSS, JavaScript, Plotly FastAPI, Python PostgreSQL, pgvector OpenAI GPT-5.4-mini, PubMedBERT, MS MARCO (cross-encoder) Modal, Hetzner VM Git, GitHub, pytest, logging |
For our search pipeline, we use a vector similarity search (with a similarity threshold) to ensure high recall, then re-rank with a fine-tuned cross-encoder essential for achieving high precision. Without the cross-encoder, a "multiple sclerosis" query would return confounding grants containing "systemic sclerosis", for example.
The semantic search pipeline was benchmarked against 15 NIH Research, Condition, and Disease Categorization (RCDC) portfolios spanning both narrowly defined diseases (e.g., Multiple Sclerosis, Endometriosis) and broader clinical concepts (e.g., Breast Cancer, Heart Disease). The RCDC categorization is what is officially used by NIH for congressional reporting and therefore makes for the most suitable comparison. Retrieval quality was measured using precision and recall before and after cross-encoder reranking.
As the graph above demonstrates, high-recall embedding retrieval first generates a broad candidate set. Then, cross-encoder reranking substantially improves precision while preserving most relevant grants. Varying the score threshold of our reranker can toggle the precision/recall tradeoff, but we found that threshold = -2.0 provides the right balance.
The key findings from evaluation were that narrowly defined disease entities (e.g., multiple Sclerosis, endometriosis) showed strong agreement with RCDC, while broader umbrella categories (e.g., heart disease) were more challenging. Broad RCDC categories encompass diverse disease subtypes and clinical concepts, suggesting that additional query expansion, ontology-aware retrieval, or task-specific training examples may further improve recall.
Because no public benchmark exists for categorizing NIH grants into the proposed translational research ontology, I used expert-curated challenge sets rather than randomly sampled grants for evaluation.
I constructed challenge sets by collecting grants that were difficult to classify during ontology development, including:
Below are examples of ambiguities in research categories represented in the challenge sets:
| Category | Difficult Distinction |
|---|---|
|
Diagnostic Therapeutic Research Tool |
Diagnostic tool vs. biomarker discovery Therapeutic development vs. mechanistic biology Method development vs. use of an existing method |
Model performance was evaluated using precision, recall, and F1 score. Though these metrics are far less than what they would be with a randomly sampled grant dataset, they provide a useful benchmark for future models.
Finally, our final production prompts achieved approximately 97% coverage across the NIH grant corpus for FY2025 (65K grants). The remaining ~3% of grants represented ambiguous or out-of-scope cases, requiring additional prompt optimization.
PubMedBert was trained on PubMed abstracts, therefore embedding representations are especially suited for the biomedical text found in NIH grant abstracts.
This combination provides a unified database for grant metadata and embeddings, enabling straightforward joins and filtering. In addition, this approach is free, enables simple construction of indices (HNSW and B-tree), and allows simple analysis of latency / performance.
Pure semantic search misses a subset of grants as in the case of the query "multiple sclerosis" failing to capture grants only mentioning the abbreviation ("MS"). Hybrid retrieval with keyword search ensures high recall of grants with synonymous terms.
The goal of this application is to retrieve an exhaustive list of all NIH grants given a particular query to understand accurate funding allocation. This is unlike traditional search systems where users are only looking for a few, highly relevant results.
While range hybrid search achieves high recall of all potentially relevant grants, precision suffers as a number of different biomedical terms carry similar lexical and semantic features (i.e. both "multiple sclerosis" and "systemic sclerosis" share "sclerosis").
While the initial PubMedBERT model is training on biomedical text, the MS MARCO cross-encoder I used is a generalist model that lacks the ability to discern biomedical text associations (i.e. multiple sclerosis and myelin). By manually generating labeled query-grant abstract pairs, I am able to significantly boost the precision of our cross-encoder.
Binary classifier prompts allow independent characterization of categories per grant (with sufficient inclusion/exclusion criteria) while also allowing grants to receive multiple labels when appropriate. Multi-label classification runs the risk of forcing mutually exclusive predictions.
βββ .github/
β βββ workflows/
β βββ tests.yaml
βββ .gitignore
βββ LICENSE
βββ README.md
βββ app/
β βββ __init__.py
β βββ main.py
β βββ startup.py
β βββ static/
β β βββ css/
β β β βββ styles.css
β β β βββ styles_v12.css
β β βββ fonts/
β β β βββ primer.ttf
β β βββ images/
β β β βββ curved-thin-arrow-icon.svg
β β βββ js/
β β βββ script.js
β βββ templates/
β βββ categories.html
β βββ contact_us.html
β βββ index.html
β βββ partials/
β β βββ search_bar.html
β βββ portfolio.html
β βββ results.html
βββ core/
β βββ __init__.py
β βββ config.py
β βββ constants.py
β βββ db/
β β βββ __init__.py
β β βββ connection.py
β βββ embedding/
β β βββ __init__.py
β β βββ config.py
β β βββ embedding_job.py
β β βββ model.py
β β βββ persistence.py
β β βββ selection.py
β βββ ingest/
β β βββ __init__.py
β β βββ config.py
β β βββ hashing.py
β β βββ ingest.py
β β βββ normalize.py
β β βββ org_resolution.py
β β βββ persistence.py
β β βββ process.py
β β βββ reporter_client.py
β βββ llm/
β β βββ __init__.py
β β βββ batch.py
β β βββ constants.py
β β βββ parser.py
β β βββ prompt_loader.py
β β βββ prompts/
β β βββ README.md
β βββ logging_config.py
β βββ search/
β β βββ __init__.py
β β βββ cache.py
β β βββ candidate_retrieval.py
β β βββ combine.py
β β βββ constants.py
β β βββ fill_abstract.py
β β βββ hybrid.py
β β βββ load_docs.py
β β βββ modal_reranker.py
β β βββ postprocess.py
β β βββ query_embedding.py
β β βββ reranker.py
β βββ services/
β β βββ __init__.py
β β βββ activity_service.py
β β βββ agency_service.py
β β βββ formatting.py
β β βββ grant_service.py
β β βββ portfolio_service.py
β β βββ search_service.py
β βββ utils/
β βββ __init__.py
β βββ abstract_tokens.py
β βββ compile_parquet.py
β βββ query_expansion.py
βββ data/
β βββ activity_code_list.csv
β βββ agencies_list.csv
β βββ rcdc_synonyms.json
βββ evaluation/
β βββ __init__.py
β βββ categorization/
β β βββ __init__.py
β β βββ challenge_sets/
β β β βββ research_tool.csv
β β βββ classify.py
β β βββ grant_loader.py
β β βββ metrics.py
β β βββ run_eval.py
β βββ search/
β βββ __init__.py
β βββ benchmark.csv
β βββ ground_truth/
β β βββ Alzheimer's_Disease.csv
β β βββ Amyotrophic_Lateral_Sclerosis_(ALS).csv
β β βββ Asthma.csv
β β βββ Autism.csv
β β βββ Breast_Cancer.csv
β β βββ Crohn's_Disease.csv
β β βββ Cystic_Fibrosis.csv
β β βββ Depression.csv
β β βββ Diabetes.csv
β β βββ Endometriosis.csv
β β βββ HIV_AIDS.csv
β β βββ Heart_Disease.csv
β β βββ Lupus.csv
β β βββ Multiple_Sclerosis.csv
β β βββ Parkinson's_Disease.csv
β βββ metrics.py
β βββ rcdc.py
β βββ run_eval.py
βββ modal_deploy/
β βββ __init__.py
β βββ reranker_app.py
βββ pipelines/
β βββ __init__.py
β βββ ingest_nih.py
β βββ llm/
β β βββ __init__.py
β β βββ combine_results.py
β β βββ generate_classification_batch.py
β β βββ generate_summary_batch.py
β β βββ import_classification_results.py
β β βββ import_summary_results.py
β β βββ retrieve_batch.py
β β βββ submit_batch.py
β βββ migration/
β β βββ __init__.py
β β βββ hash_historical.py
β βββ run_embedding.py
β βββ train_reranker.py
βββ requirements.txt
βββ scripts/
β βββ compare_pairs_reranker.py
β βββ update_agency_code.py
βββ tests/
βββ embedding/
β βββ test_embedding_job.py
β βββ test_model.py
β βββ test_persistence.py
β βββ test_selection.py
βββ ingest/
β βββ test_config.py
β βββ test_hashing.py
β βββ test_ingest.py
β βββ test_normalize.py
β βββ test_org_resolution.py
β βββ test_persistence.py
β βββ test_process.py
β βββ test_reporter_client.py
βββ llm/
β βββ test_batch.py
β βββ test_parser.py
β βββ test_prompt_loader.py
βββ search/
β βββ test_cache.py
β βββ test_candidate_retrieval.py
β βββ test_combine.py
β βββ test_fill_abstracts.py
β βββ test_hybrid.py
β βββ test_load_docs.py
β βββ test_postprocess.py
β βββ test_query_embedding.py
β βββ test_reranker.py
βββ services/
β βββ test_formatting.py
β βββ test_grant_service.py
β βββ test_portfolio_service.py
β βββ test_search_service.py
βββ test_main.py
βββ utils/
βββ test_query_expansion.py
git clone https://github.com/pradeepbugga/NIH_Portfolio-Analytics.git
cd NIH_Portfolio_Analytics
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Create a .env file:
OPENAI_API_KEY=...
DATABASE_URL=...
createdb nih_portfolio
Enable pgvector:
CREATE EXTENSION vector;
Run the schema:
psql -d nih_portfolio -f database/schema.sql
Import NIH data:
python -m pipelines.ingest_nih
Generate embeddings:
python -m pipelines.run_embeddings
Fine-tuning cross-encoder:
The training pipeline is included; proprietary training labels and final model weights are not distributed.
LLM Categorization
Production prompt templates are proprietary and therefore not included in the public repository.
Start the API:
uvicorn app.main:app --reload
Visit
http://localhost:8000
pytest
This repository is licensed under the MIT License. Certain proprietary assetsβincluding production LLM prompts, manually curated training datasets, and fine-tuned model weightsβare intentionally excluded from the public release.