Paper2Lab / src /paper2lab /evaluation /reproducibility.py
unknown
Deploy Paper2Lab HF Space
71276a4
Raw
History Blame Contribute Delete
11.6 kB
"""
reproducibility.py — Missing-information detection and reproducibility scoring.
Scores are heuristic and evidence-based. The goal is to produce a useful local
candidate before Nemotron refinement, while avoiding overconfident scores when
PDF extraction evidence is noisy.
"""
from __future__ import annotations
import re
from typing import Any, Dict, List, Tuple
# ---------------------------------------------------------------------------
# Text helpers
# ---------------------------------------------------------------------------
def _clean(text: str) -> str:
text = text or ""
text = text.replace("\x00", " ").replace("\u00a0", " ")
text = re.sub(r"\s+", " ", text)
text = re.sub(r"\b10\.\d{4,9}/[-._;()/:A-Za-z0-9]+", "", text)
return text.strip(" .;:\n\t")
def _joined_text(extracted: Dict[str, Any]) -> str:
parts: List[str] = []
for sec in extracted.get("sections", []) or []:
if sec.get("role") in {"references", "appendix", "boilerplate"}:
continue
parts.append(str(sec.get("title", "")))
parts.append(str(sec.get("text", "")))
return _clean("\n".join(parts)).lower()
def _has_any(text: str, terms: List[str]) -> bool:
return any(t.lower() in text for t in terms)
def _matched_terms(text: str, terms: List[str], limit: int = 5) -> List[str]:
return [t for t in terms if t.lower() in text][:limit]
# ---------------------------------------------------------------------------
# Paper-type-specific reproducibility checks
# ---------------------------------------------------------------------------
def _check_items(paper_type: str) -> Dict[str, List[str]]:
if paper_type == "systematic_review":
return {
"search databases specified": [
"pubmed", "scopus", "web of knowledge", "eric", "cochrane",
"database", "databases",
],
"search date range specified": [
"january", "february", "march", "april", "may", "june",
"july", "august", "september", "october", "november", "december",
"between", "from", "until", "to january", "published between",
],
"inclusion criteria specified": [
"inclusion criteria", "eligibility criteria", "eligible studies",
],
"exclusion criteria specified": [
"exclusion criteria", "excluded", "not being", "were excluded",
],
"screening process specified": [
"screened", "screening", "titles and abstracts", "two independent",
"reviewers", "duplicates", "endnote",
],
"quality assessment specified": [
"quality assessment", "risk of bias", "best evidence medical education",
"valid tool", "critical appraisal", "assessment tool",
],
"number of included studies specified": [
"included", "enrolled", "final review", "studies were included",
"articles were included", "10 articles", "ten studies",
],
}
if paper_type == "machine_learning":
return {
"dataset details specified": [
"dataset", "training set", "test set", "validation set", "benchmark",
"corpus", "samples", "instances",
],
"train/validation/test split specified": [
"train", "validation", "test", "split", "dev set", "development set",
],
"model architecture specified": [
"architecture", "layers", "encoder", "decoder", "transformer", "cnn",
"resnet", "bert", "attention", "feed-forward",
],
"hyperparameters specified": [
"learning rate", "batch size", "epochs", "optimizer", "dropout",
"weight decay", "warmup", "scheduler",
],
"hardware specified": [
"gpu", "tpu", "cuda", "p100", "v100", "a100", "nvidia",
],
"evaluation metrics specified": [
"accuracy", "f1", "auc", "bleu", "rouge", "perplexity", "rmse", "mae",
"precision", "recall",
],
"code availability specified": [
"github", "code", "repository", "available at", "source code",
],
"random seed specified": ["random seed", "seed"],
}
if paper_type == "clinical_study":
return {
"cohort or participants specified": [
"patients", "participants", "cohort", "subjects", "population",
],
"inclusion criteria specified": ["inclusion criteria", "eligible"],
"exclusion criteria specified": ["exclusion criteria", "excluded"],
"outcomes specified": ["outcome", "endpoint", "mortality", "diagnosis"],
"statistical analysis specified": [
"statistical analysis", "p-value", "confidence interval", "regression",
],
"ethics approval specified": [
"ethics", "institutional review", "informed consent", "irb",
],
}
return {
"data/source details specified": [
"data", "dataset", "source", "samples", "studies", "articles",
],
"method/procedure specified": [
"method", "procedure", "approach", "experiment", "analysis",
],
"evaluation or analysis specified": [
"evaluation", "result", "metric", "analysis", "measured", "assessed",
],
"limitations discussed": ["limitation", "limitations", "future work"],
}
# ---------------------------------------------------------------------------
# Evidence quality / noise handling
# ---------------------------------------------------------------------------
_NOISY_EVIDENCE_MARKERS = [
"the there",
"being accordingly",
"endnote teachers",
"resultsare",
"analysis of the resultsare",
"table 2:",
"department of",
"university of",
"medical sciences",
"corresponding author",
"access this article online",
"how to cite",
"need this systematic review",
"the that",
]
def _roadmap_blob(paper_card: Dict[str, Any]) -> str:
roadmap = paper_card.get("reproduction_roadmap") or {}
parts: List[str] = []
for key in [
"datasets",
"software_requirements",
"experimental_steps",
"evaluation_procedure",
"expected_outputs",
"missing_for_reproduction",
]:
value = roadmap.get(key, [])
if isinstance(value, list):
for item in value:
if isinstance(item, dict):
parts.extend(str(v) for v in item.values())
else:
parts.append(str(item))
elif value:
parts.append(str(value))
return _clean(" ".join(parts)).lower()
def _noise_report(extracted: Dict[str, Any], paper_card: Dict[str, Any]) -> Tuple[int, List[str]]:
"""Return count and examples of noisy evidence markers."""
blob = _roadmap_blob(paper_card)
if not blob:
# Fallback to body text only if roadmap is not yet attached.
blob = _joined_text(extracted)
found = [m for m in _NOISY_EVIDENCE_MARKERS if m in blob]
# Extra generic noise signals.
if len(re.findall(r"\[\d+", blob)) >= 12:
found.append("many citation fragments")
if re.search(r"\b(the|and|of)\s+\1\b", blob):
found.append("repeated function-word artifact")
return len(found), found[:8]
def _apply_score_caps(
paper_type: str,
score: float,
missing: List[str],
extracted: Dict[str, Any],
paper_card: Dict[str, Any],
) -> Tuple[float, List[str], Dict[str, Any]]:
"""Prevent misleadingly high scores when evidence is noisy or incomplete."""
diagnostics: Dict[str, Any] = {}
noise_count, noise_examples = _noise_report(extracted, paper_card)
diagnostics["noise_count"] = noise_count
diagnostics["noise_examples"] = noise_examples
if noise_count > 0:
msg = "some extracted evidence appears noisy due to PDF layout"
if msg not in missing:
missing.append(msg)
# Systematic reviews should not get 1.0 if roadmap/evidence is visibly noisy.
if paper_type == "systematic_review":
if noise_count >= 3:
score = min(score, 0.65)
elif noise_count >= 1:
score = min(score, 0.75)
roadmap = paper_card.get("reproduction_roadmap") or {}
if not roadmap.get("experimental_steps"):
score = min(score, 0.70)
if not roadmap.get("evaluation_procedure"):
score = min(score, 0.70)
# ML papers need either hyperparameters or code/hardware to be strong.
if paper_type == "machine_learning":
text = _joined_text(extracted)
has_hparams = _has_any(text, ["learning rate", "batch size", "optimizer", "dropout", "epoch"])
has_code = _has_any(text, ["github", "repository", "code available", "source code"])
has_hardware = _has_any(text, ["gpu", "tpu", "cuda", "p100", "v100", "a100"])
if not has_hparams:
score = min(score, 0.80)
if not has_code and not has_hardware:
score = min(score, 0.85)
return round(score, 3), missing, diagnostics
def _score_level(score: float) -> str:
if score >= 0.80:
return "strong"
if score >= 0.50:
return "partial"
return "weak"
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def reproducibility_report(extracted: Dict[str, Any], paper_card: Dict[str, Any]) -> Dict[str, Any]:
paper_type = paper_card.get("paper_type", "general_research")
text = _joined_text(extracted)
checks = _check_items(paper_type)
detected: List[str] = []
missing: List[str] = []
evidence: Dict[str, List[str]] = {}
for label, terms in checks.items():
if _has_any(text, terms):
detected.append(label)
evidence[label] = _matched_terms(text, terms)
else:
missing.append(label)
# Candidate-card overrides for generic papers.
if paper_card.get("datasets_or_data_sources") and "data/source details specified" in missing:
missing.remove("data/source details specified")
detected.append("data/source details specified")
evidence["data/source details specified"] = ["paper_card.datasets_or_data_sources"]
if paper_card.get("metrics_or_measurements") and "evaluation or analysis specified" in missing:
missing.remove("evaluation or analysis specified")
detected.append("evaluation or analysis specified")
evidence["evaluation or analysis specified"] = ["paper_card.metrics_or_measurements"]
total = max(1, len(checks))
score = len(detected) / total
score, missing, diagnostics = _apply_score_caps(
paper_type=paper_type,
score=score,
missing=missing,
extracted=extracted,
paper_card=paper_card,
)
# Deduplicate while preserving order.
detected = list(dict.fromkeys(detected))
missing = list(dict.fromkeys(missing))
return {
"paper_type": paper_type,
"score": score,
"level": _score_level(score),
"detected_items": detected,
"missing_items": missing,
"evidence_terms": evidence,
"diagnostics": diagnostics,
}