Skip to content

CNS cell-entity extraction evaluation#132

Open
puja-trivedi wants to merge 7 commits into
mainfrom
cns_evalution
Open

CNS cell-entity extraction evaluation#132
puja-trivedi wants to merge 7 commits into
mainfrom
cns_evalution

Conversation

@puja-trivedi

@puja-trivedi puja-trivedi commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Add CNS cell-entity extraction evaluation

Adds an evaluation harness that measures how many ground-truth cell entities StructSense actually extracts from the CNS/MeSH corpus.

What it does

evaluation/cns_cell/evaluate_extraction.py compares StructSense's per-paper JSON output against the ground-truth BioC XML (NLM-CellLink_CNS_MeSH_train_val.xml). For every annotation row in the ground truth, it checks whether StructSense extracted that entity — comparing entity text only (annotation labels are ignored, since StructSense uses a large open label vocabulary that doesn't map cleanly to the 3 ground-truth cell types).

Matching rules

  • Entity text: normalized whole-string equality (case-insensitive, collapsed whitespace, stripped surrounding punctuation).
  • Duplicates: every annotation row is scored independently.
  • Two recall metrics reported side by side:
    • SAME-SENTENCE — entity extracted in the same sentence the ground truth annotated it in. Because the ground truth is clean curated text while StructSense parses PDFs (which inject reference numbers mid-sentence, merge captions, drop apostrophes, etc.), sentence correspondence uses token overlap (default ≥0.6) rather than exact substring.
    • ANYWHERE — entity extracted anywhere in the paper (location ignored).

Results (current output)
The evaluation now covers only the 90 papers that have structsense output.

Metric Extracted Recall
Same-sentence 224/869 25.8%
Anywhere 478/869 55.0%

Breakdowns by entity type (cell_phenotype, cell_vague, cell_hetero) and per-PMID are included in the summary.

Files

  • evaluate_extraction.py — the script (CLI-configurable: --sentence-mode, --threshold, --min-token-len, --detail-csv, --summary-out).
  • evaluation_summary.txt — shareable summary report.
  • evaluation_detail.csv — per-annotation results with matched_anywhere / matched_same_sentence flags.

The ground-truth XML, source PDFs, and StructSense JSON outputs are intentionally not committed (data artifacts).

Notes / caveats for reviewers

  • Only 103 of 208 ground-truth PMIDs have StructSense output, so the "covered PMIDs" denominator is the fairer read of extraction quality.
  • The same-sentence vs anywhere gap reflects that StructSense often extracts an entity but tags a different mention than the annotated one, plus PDF-vs-curated text differences.

How to run

python3 evaluation/cns_cell/evaluate_extraction.py --detail-csv evaluation/cns_cell/evaluation_detail.csv

Compare structsense extraction output against the CNS/MeSH ground-truth
XML on entity text (labels ignored). Reports two recall metrics side by
side per corpus, entity type, and PMID: same-sentence (entity extracted
in the annotated sentence) and anywhere-in-paper. Includes summary and
per-annotation CSV outputs.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an evaluation script (evaluate_extraction.py) and its corresponding summary report (evaluation_summary.txt) to assess structsense entity extraction against CNS/MeSH ground-truth XML. The code review feedback highlights a critical bug where only the first passage of each document is parsed instead of all passages, and recommends using context managers with explicit UTF-8 encoding when opening files to prevent resource leaks and encoding issues.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +111 to +141
for doc in root.findall("document"):
passage = doc.find("passage")
if passage is None:
continue
pmid = passage_id = None
for infon in passage.findall("infon"):
key = infon.get("key")
if key == "article-id_pmid":
pmid = infon.text
elif key == "passage_id":
passage_id = infon.text
passage_text = passage.findtext("text") or ""
for ann in passage.findall("annotation"):
loc = ann.find("location")
if loc is None:
continue
atype = None
for infon in ann.findall("infon"):
if infon.get("key") == "type":
atype = infon.text
rows.append(
GTAnnotation(
pmid=pmid or "",
passage_id=passage_id or (doc.findtext("id") or ""),
entity_text=ann.findtext("text") or "",
entity_type=atype or "",
offset=int(loc.get("offset", 0)),
length=int(loc.get("length", 0)),
passage_text=passage_text,
)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation only processes the first <passage> element of each <document> because it uses doc.find("passage"). In BioC XML format, a document typically contains multiple passages (e.g., title, abstract, paragraphs). Using doc.findall("passage") in a nested loop ensures all passages and their annotations are evaluated.

    for doc in root.findall("document"):
        for passage in doc.findall("passage"):
            pmid = passage_id = None
            for infon in passage.findall("infon"):
                key = infon.get("key")
                if key == "article-id_pmid":
                    pmid = infon.text
                elif key == "passage_id":
                    passage_id = infon.text
            passage_text = passage.findtext("text") or ""
            for ann in passage.findall("annotation"):
                loc = ann.find("location")
                if loc is None:
                    continue
                atype = None
                for infon in ann.findall("infon"):
                    if infon.get("key") == "type":
                        atype = infon.text
                rows.append(
                    GTAnnotation(
                        pmid=pmid or "",
                        passage_id=passage_id or (doc.findtext("id") or ""),
                        entity_text=ann.findtext("text") or "",
                        entity_type=atype or "",
                        offset=int(loc.get("offset", 0)),
                        length=int(loc.get("length", 0)),
                        passage_text=passage_text,
                    )
                )

Comment thread evaluation/cns_cell/evaluate_extraction.py
Comment thread evaluation/cns_cell/evaluate_extraction.py Outdated
Comment thread evaluation/cns_cell/evaluate_extraction.py Outdated
puja-trivedi and others added 6 commits July 6, 2026 10:39
Compare structsense extraction output against the CNS/MeSH ground-truth
XML on entity text (labels ignored), over the 90 papers that have output.
Reports two recall metrics side by side per corpus, entity type, and PMID:
same-sentence (entity extracted in the annotated sentence) and
anywhere-in-paper. Writes a Markdown summary and a per-annotation CSV.
…into cns_evalution

# Conflicts:
#	evaluation/cns_cell/evaluate_extraction.py
#	evaluation/cns_cell/evaluation_detail.csv
Gemini suggestion:
The file is opened with open(path) but never explicitly closed, which can lead to resource leaks when processing many files. Additionally, opening files without specifying an encoding can cause decoding errors on platforms where the default encoding is not UTF-8 (e.g., Windows). Using a context manager (with) and specifying encoding="utf-8" is safer and more robust.

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Gemini suggestion: Opening files for writing without specifying an encoding can lead to platform-dependent encoding issues (e.g., writing CP1252 on Windows instead of UTF-8). It is recommended to always specify encoding="utf-8" when writing text files.

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Gemini suggestion: Opening files for writing without specifying an encoding can lead to platform-dependent encoding issues. It is recommended to always specify encoding="utf-8" when writing text files.

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant