data: evidence base builder + literature methods doc (refs #237, refs #244)

- Collection hierarchy: 20 sub-collections under "Skin Substitutes"
  (Clinical Evidence, CMS Policy, Enforcement, etc.)
- COI/funding enrichment: 2,862 PubMed articles tagged with
  coi:single-product (2,003), stance:skeptical (598),
  stance:favorable (510), coi:industry-linked (11)
- All 22 expected tag types verified present
- Methods documentation: search queries, PRISMA flow, tag schema,
  collection hierarchy, limitations
This commit is contained in:
kert
2026-03-25 11:33:32 -04:00
parent b822aa9443
commit c1f60e16ef
2 changed files with 553 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
# Skin Substitutes Literature Review: Methods
## Search Strategy
### Databases
- **PubMed** (MEDLINE) via NCBI E-utilities (esearch + efetch XML)
- **Grey literature**: OIG, CMS, GAO, MedPAC, DOJ, court filings, MAC LCDs, industry
### Date Range
- PubMed: inception through 2026-03-25
- Grey literature: 2015-current (enforcement), 2012-current (CMS rules)
### Search Date
2026-03-25
## PubMed Search Queries
### Domain 1: Clinical Efficacy
```
("skin substitutes"[MeSH] OR "biological dressings"[MeSH]
OR "tissue scaffolds"[MeSH] OR "bioengineered skin"[tiab]
OR "cellular tissue product"[tiab] OR "wound matrix"[tiab]
OR "skin substitute"[tiab] OR "skin substitutes"[tiab]
OR "Apligraf"[tiab] OR "EpiFix"[tiab] OR "Grafix"[tiab]
OR "DermACELL"[tiab] OR "Dermagraft"[tiab])
AND ("wound healing"[MeSH] OR "treatment outcome"[MeSH]
OR "clinical trial"[pt] OR "randomized controlled trial"[pt]
OR "systematic review"[pt] OR "meta-analysis"[pt]
OR "efficacy"[tiab] OR "effectiveness"[tiab])
```
**Result: 7,448 records**
### Domain 2: Cost-Effectiveness
```
("skin substitutes"[MeSH] OR "biological dressings"[MeSH]
OR "skin substitute"[tiab] OR "skin substitutes"[tiab]
OR "cellular tissue product"[tiab])
AND ("costs and cost analysis"[MeSH]
OR "cost-benefit analysis"[MeSH]
OR "health care costs"[MeSH] OR "medicare"[MeSH]
OR "reimbursement"[tiab] OR "cost-effectiveness"[tiab]
OR "economic"[tiab] OR "spending"[tiab]
OR "average sales price"[tiab] OR "ASP"[tiab])
```
**Result: 123 records**
### Domain 3: Fraud, Waste, and Abuse
```
("skin substitutes"[MeSH] OR "biological dressings"[MeSH]
OR "skin substitute"[tiab] OR "skin substitutes"[tiab]
OR "cellular tissue product"[tiab] OR "wound care"[tiab])
AND ("fraud"[MeSH] OR "waste"[tiab] OR "abuse"[tiab]
OR "inappropriate"[tiab] OR "overutilization"[tiab]
OR "billing"[tiab] OR "enforcement"[tiab]
OR "compliance"[tiab] OR "medically unnecessary"[tiab]
OR "upcoding"[tiab] OR "kickback"[tiab])
```
**Result: 358 records**
## PRISMA Flow
```
Records identified through PubMed:
Clinical efficacy: 7,448
Cost-effectiveness: 123
Fraud/waste/abuse: 358
─────────────────────────────
TOTAL identified: 7,929
Duplicates removed: 112
Unique articles: 7,817
By publication type:
Meta-analyses: 85
Systematic reviews: 1,395
RCTs: 455
Other (observational,
case reports, etc.): 5,882
```
## Grey Literature Sources (20 documents)
| Source | Count | Description |
|-----------|------:|------------------------------------------------------|
| OIG | 3 | Sept 2025 payment trends, SAB, audit reports |
| CMS | 4 | OPPS/PFS final rules (2024-2026), Benefit Policy |
| DOJ | 1 | National healthcare fraud enforcement action |
| Court | 3 | Jenson (S.D. Tex.), Gehrke/King (D. Ariz.), Vohra |
| GAO | 1 | Part B biologicals spending (GAO-23-105537) |
| MedPAC | 2 | June 2024, March 2025 Reports to Congress |
| MAC LCD | 4 | Noridian, CGS, First Coast, Palmetto |
| Industry | 2 | Alliance position statement, WHS guidelines |
## Conflict of Interest / Stance Enrichment
PubMed articles were enriched with automated COI and stance detection:
| Tag | Count | Method |
|-----------------------|------:|-------------------------------------------------|
| coi:single-product | 2,003 | Only one brand name mentioned in title/abstract |
| stance:skeptical | 598 | Negative outcome language in abstract |
| stance:favorable | 510 | Positive outcome language in title |
| coi:industry-linked | 11 | Funding/employment patterns in extra metadata |
**Enrichment coverage:** 2,862 / 7,817 PubMed articles (36.6%)
## Tag Schema
All items tagged with `module:skin-subs` plus:
- **Source tags:** `source:pubmed`, `source:oig`, `source:cms`, `source:court`,
`source:doj`, `source:gao`, `source:medpac`, `source:mac-lcd`, `source:industry`
- **Type tags:** `type:clinical`, `type:economic`, `type:fraud`, `type:rct`,
`type:review`, `type:meta-analysis`, `type:report`, `type:rule`, `type:filing`,
`type:lcd`, `type:position`, `type:press-release`, `type:manual`
- **Year tags:** `year:YYYY`
- **COI tags:** `coi:industry-linked`, `coi:single-product`
- **Stance tags:** `stance:skeptical`, `stance:favorable`
- **Entity tags:** `entity:oig`, `entity:doj`, `entity:gao`, `entity:medpac`,
`entity:noridian`, `entity:cgs`, `entity:first-coast`, `entity:palmetto`
- **Case tags:** `case:jenson`, `case:gehrke-king`, `case:vohra`
- **Rule tags:** `rule:cy2026-opps`, `rule:cy2025-opps`, `rule:cy2024-pfs`
## Collection Hierarchy
```
Skin Substitutes/
Clinical Evidence/
RCTs (455 items)
Systematic Reviews (1,395 items)
Meta-Analyses (85 items)
Observational Studies (5,547 items)
CMS Policy/
Final Rules (OPPS/PFS) (3 items)
Benefit Policy Manuals (1 item)
ASP Pricing Files
OIG Reports (3 items)
GAO & MedPAC (3 items)
Enforcement/
DOJ Press Releases (1 item)
Court Filings (3 items)
MAC LCDs (4 items)
Market Data
Industry & Societies (2 items)
Cost-Effectiveness (70 items)
Fraud & Abuse Literature (265 items)
```
## Scripts
| Script | Purpose |
|--------|---------|
| `dev/scripts/search_pubmed_skin_subs.py` | PubMed E-utilities search, XML parsing, bib.sqlite storage |
| `dev/scripts/collect_grey_lit_skin_subs.py` | Curated grey literature catalogue, bib.sqlite storage |
| `dev/scripts/build_skin_subs_evidence_base.py` | Collection hierarchy, COI enrichment, verification |
## Limitations
1. **No full-text screening**: Articles included based on PubMed metadata only; no manual
title/abstract screening for relevance (full PRISMA would require human review)
2. **COI detection is heuristic**: Based on keyword patterns in abstracts and metadata,
not full-text disclosure sections
3. **Grey literature is curated, not systematic**: Known key documents captured; no
systematic search of OIG/GAO/PACER databases
4. **No Cochrane Library**: Only PubMed searched for journal literature
5. **No citation network analysis**: Forward/backward snowball not yet performed (#237)
6. **No quantitative meta-analysis**: Study data extraction and pooling not yet done (#237)

View File

@@ -0,0 +1,386 @@
"""Build the Skin Substitutes evidence base in bib.sqlite.
Creates collection hierarchy, assigns items to sub-collections,
enriches PubMed articles with conflict-of-interest and funding
analysis, and verifies tag schema coverage.
Addresses issues #244 (Zotero evidence base) and #237 (partial —
COI/funding enrichment of PubMed results).
Usage:
uv run python dev/scripts/build_skin_subs_evidence_base.py
uv run python dev/scripts/build_skin_subs_evidence_base.py --dry-run
"""
from __future__ import annotations
import argparse
import re
from datetime import datetime
from bib.store import Store
# ---------------------------------------------------------------------------
# Collection hierarchy for skin-subs research
# ---------------------------------------------------------------------------
SKIN_SUBS_COLLECTIONS = {
"Skin Substitutes": {
"Clinical Evidence": {
"RCTs": {},
"Systematic Reviews": {},
"Meta-Analyses": {},
"Observational Studies": {},
},
"CMS Policy": {
"Final Rules (OPPS/PFS)": {},
"Benefit Policy Manuals": {},
"ASP Pricing Files": {},
},
"OIG Reports": {},
"GAO & MedPAC": {},
"Enforcement": {
"DOJ Press Releases": {},
"Court Filings": {},
},
"MAC LCDs": {},
"Market Data": {},
"Industry & Societies": {},
"Cost-Effectiveness": {},
"Fraud & Abuse Literature": {},
},
}
# ---------------------------------------------------------------------------
# Known manufacturer names for COI detection
# ---------------------------------------------------------------------------
MANUFACTURERS = [
"organogenesis", "mimedx", "smith nephew", "smith & nephew",
"integra", "solsys", "amnioexcel", "derma sciences",
"healthpoint", "shire", "acelity", "kci", "3m",
"molnlycke", "medline", "hollister", "coloplast",
"stryker", "zimmer biomet", "wright medical",
"solventum", "apria", "anika", "musculoskeletal transplant",
"surmodics", "tissue regenix", "nuo therapeutics",
"sanara medtech", "kerecis", "human bioprocessing",
"alphatec", "biosig technologies",
]
# Brand names that indicate manufacturer-linked studies
BRAND_NAMES = [
"apligraf", "dermagraft", "epifix", "grafix", "amnioexcel",
"dermacell", "oasis", "primatrix", "integra", "graftjacket",
"dermapure", "affinity", "biovance", "cytal", "endoform",
"kerecis omega3", "novafix", "puraply", "restorigin",
"surgicraft", "theraskin", "amnioburn", "clarix",
"epicord", "genesis", "grafix core", "grafix prime",
"innovamatrix", "nushield", "stravix", "woundex",
]
# Patterns suggesting industry funding
FUNDING_PATTERNS = [
r"funded by .{0,50}(organogenesis|mimedx|smith|integra|solsys|amnio)",
r"grant from .{0,50}(organogenesis|mimedx|smith|integra|solsys)",
r"financial support.{0,50}(organogenesis|mimedx|smith|integra)",
r"supported by .{0,50}(organogenesis|mimedx|smith|integra|solsys)",
r"sponsored by .{0,50}(organogenesis|mimedx|smith|integra|solsys)",
r"employee of .{0,50}(organogenesis|mimedx|smith|integra|solsys)",
r"consultant.{0,30}(organogenesis|mimedx|smith|integra|solsys)",
r"speaker.{0,30}(organogenesis|mimedx|smith|integra|solsys)",
r"advisory board.{0,30}(organogenesis|mimedx|smith|integra|solsys)",
r"honorari.{0,30}(organogenesis|mimedx|smith|integra|solsys)",
r"conflict.{0,50}interest",
r"disclosur.{0,80}(stock|equity|consult|employ|honorar|speaker|grant)",
]
# Patterns suggesting independent/skeptical perspective
SKEPTICAL_PATTERNS = [
r"no (significant )?difference",
r"lack.{0,20}evidence",
r"insufficient evidence",
r"low.{0,15}quality evidence",
r"limited evidence",
r"no.{0,15}superiority",
r"similar (outcome|result|efficacy|effectiveness)",
r"(waste|fraud|abuse|overutiliz|unnecessary|inappropriate)",
r"(overspend|excessive.{0,15}cost|cost concern)",
r"(marketing|promotional|commercial bias)",
]
# ---------------------------------------------------------------------------
# Collection assignment rules
# ---------------------------------------------------------------------------
def assign_collection(tags: list[str], title: str, abstract: str) -> str:
"""Return the most specific sub-collection name for an item."""
tag_set = set(tags)
# Grey literature — by source tag
if "source:oig" in tag_set:
return "OIG Reports"
if "source:gao" in tag_set or "source:medpac" in tag_set:
return "GAO & MedPAC"
if "source:doj" in tag_set:
return "DOJ Press Releases"
if "source:court" in tag_set:
return "Court Filings"
if "source:mac-lcd" in tag_set:
return "MAC LCDs"
if "source:industry" in tag_set:
return "Industry & Societies"
if "source:cms" in tag_set:
for t in tag_set:
if t.startswith("type:rule"):
return "Final Rules (OPPS/PFS)"
if t.startswith("type:manual"):
return "Benefit Policy Manuals"
return "CMS Policy"
# PubMed — by type tag
if "type:meta-analysis" in tag_set:
return "Meta-Analyses"
if "type:review" in tag_set:
return "Systematic Reviews"
if "type:rct" in tag_set:
return "RCTs"
if "type:economic" in tag_set:
return "Cost-Effectiveness"
if "type:fraud" in tag_set:
return "Fraud & Abuse Literature"
if "type:clinical" in tag_set:
return "Observational Studies"
return "Skin Substitutes" # fallback to root
def detect_coi(extra: str, abstract: str, title: str) -> list[str]:
"""Detect conflict-of-interest indicators, return enrichment tags."""
tags: list[str] = []
text = f"{extra}\n{abstract}\n{title}".lower()
# Check for manufacturer mentions in author affiliations
authors_section = ""
for line in extra.split("\n"):
if line.startswith("Authors:"):
authors_section = line.lower()
break
# Industry funding patterns
for pattern in FUNDING_PATTERNS:
if re.search(pattern, text, re.IGNORECASE):
tags.append("coi:industry-linked")
break
# Single-product studies (often manufacturer-funded)
brand_count = sum(1 for b in BRAND_NAMES if b in text)
if brand_count == 1:
tags.append("coi:single-product")
# Skeptical / critical perspective
for pattern in SKEPTICAL_PATTERNS:
if re.search(pattern, text, re.IGNORECASE):
tags.append("stance:skeptical")
break
# Pro-product sentiment (positive claims in title)
title_lower = title.lower()
pro_patterns = [
r"(effective|superior|promising|excellent|favorable|beneficial)",
r"(accelerat|improv|enhanc|advanc).{0,20}(heal|wound|outcome)",
r"(novel|innovative|breakthrough).{0,20}(treatment|therapy|approach)",
]
for pattern in pro_patterns:
if re.search(pattern, title_lower):
tags.append("stance:favorable")
break
return tags
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Build skin-subs evidence base"
)
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
print("=" * 70)
print("Skin Substitutes Evidence Base Builder")
print(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 70)
store = Store()
con = store._con()
# --- Step 1: Create collection hierarchy ---
print("\n--- Step 1: Creating collection hierarchy ---")
col_map = store.ensure_collections(SKIN_SUBS_COLLECTIONS)
for name, key in sorted(col_map.items()):
print(f" {key} {name}")
print(f" Total collections created/verified: {len(col_map)}")
# --- Step 2: Load all skin-subs items ---
print("\n--- Step 2: Loading skin-subs items ---")
rows = con.execute(
"""SELECT DISTINCT i.id, i.key, i.title, i.abstract, i.extra
FROM items i
JOIN item_tags it ON i.id = it.item_id
JOIN tags t ON it.tag_id = t.id
WHERE t.name = 'module:skin-subs'"""
).fetchall()
print(f" Total items: {len(rows)}")
# Pre-load tags for each item
item_tags: dict[int, list[str]] = {}
tag_rows = con.execute(
"""SELECT it.item_id, t.name
FROM item_tags it
JOIN tags t ON it.tag_id = t.id
WHERE it.item_id IN (
SELECT DISTINCT i.id FROM items i
JOIN item_tags it2 ON i.id = it2.item_id
JOIN tags t2 ON it2.tag_id = t2.id
WHERE t2.name = 'module:skin-subs'
)"""
).fetchall()
for tr in tag_rows:
item_tags.setdefault(tr["item_id"], []).append(tr["name"])
# --- Step 3: Assign to collections ---
print("\n--- Step 3: Assigning items to collections ---")
collection_counts: dict[str, int] = {}
assignments: list[tuple[int, str]] = [] # (item_id, collection_key)
for row in rows:
tags = item_tags.get(row["id"], [])
col_name = assign_collection(
tags, row["title"] or "", row["abstract"] or ""
)
if col_name in col_map:
assignments.append((row["id"], col_map[col_name]))
collection_counts[col_name] = (
collection_counts.get(col_name, 0) + 1
)
print(" Assignment distribution:")
for name, count in sorted(
collection_counts.items(), key=lambda x: -x[1]
):
print(f" {name:30s}: {count:>5}")
# --- Step 4: COI / funding enrichment ---
print("\n--- Step 4: COI / funding / stance enrichment ---")
coi_tags_to_add: dict[int, list[str]] = {}
coi_counts: dict[str, int] = {}
for row in rows:
tags = item_tags.get(row["id"], [])
# Only enrich PubMed items
if "source:pubmed" not in tags:
continue
new_tags = detect_coi(
row["extra"] or "", row["abstract"] or "", row["title"] or ""
)
if new_tags:
coi_tags_to_add[row["id"]] = new_tags
for t in new_tags:
coi_counts[t] = coi_counts.get(t, 0) + 1
print(" Enrichment tag counts:")
for tag, count in sorted(coi_counts.items(), key=lambda x: -x[1]):
print(f" {tag:30s}: {count:>5}")
print(f" Items enriched: {len(coi_tags_to_add)} / {len(rows)}")
if args.dry_run:
print("\n[DRY RUN] Skipping writes")
store.close()
return
# --- Step 5: Write collection assignments ---
print("\n--- Step 5: Writing collection assignments ---")
for item_id, col_key in assignments:
col_row = con.execute(
"SELECT id FROM collections WHERE key = ?", (col_key,)
).fetchone()
if col_row:
con.execute(
"INSERT OR IGNORE INTO collection_items "
"(collection_id, item_id) VALUES (?, ?)",
(col_row["id"], item_id),
)
con.commit()
print(f" Assigned {len(assignments)} items to collections")
# --- Step 6: Write COI enrichment tags ---
print("\n--- Step 6: Writing COI enrichment tags ---")
total_tags_added = 0
for item_id, new_tags in coi_tags_to_add.items():
for tag in new_tags:
tag_id = store._ensure_tag(tag)
con.execute(
"INSERT OR IGNORE INTO item_tags (item_id, tag_id) "
"VALUES (?, ?)",
(item_id, tag_id),
)
total_tags_added += 1
con.commit()
print(f" Added {total_tags_added} enrichment tags")
# --- Step 7: Verify ---
print("\n--- Step 7: Verification ---")
# Tag schema coverage
expected_tags = [
"module:skin-subs",
"source:pubmed", "source:oig", "source:cms", "source:court",
"source:doj", "source:gao", "source:medpac",
"source:mac-lcd", "source:industry",
"type:clinical", "type:economic", "type:fraud",
"type:rct", "type:review", "type:meta-analysis",
"type:report", "type:rule", "type:filing",
"type:lcd", "type:position", "type:press-release",
]
for tag in expected_tags:
count = con.execute(
"SELECT count(*) FROM item_tags it "
"JOIN tags t ON it.tag_id = t.id WHERE t.name = ?",
(tag,),
).fetchone()[0]
status = "OK" if count > 0 else "MISSING"
print(f" {status:7s} {tag:30s}: {count:>5}")
# Collection item counts
print("\n Collection item counts:")
for name, key in sorted(col_map.items()):
count = con.execute(
"SELECT count(*) FROM collection_items ci "
"JOIN collections c ON ci.collection_id = c.id "
"WHERE c.key = ?",
(key,),
).fetchone()[0]
if count > 0:
print(f" {name:30s}: {count:>5}")
# Total
total = con.execute(
"""SELECT count(DISTINCT i.id) FROM items i
JOIN item_tags it ON i.id = it.item_id
JOIN tags t ON it.tag_id = t.id
WHERE t.name = 'module:skin-subs'"""
).fetchone()[0]
print(f"\n Total module:skin-subs items: {total}")
store.close()
print("\nDone.")
if __name__ == "__main__":
main()