CNS cell-entity extraction evaluation#132
Conversation
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.
There was a problem hiding this comment.
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.
| 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, | ||
| ) | ||
| ) |
There was a problem hiding this comment.
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,
)
)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>
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
Results (current output)
The evaluation now covers only the 90 papers that have structsense output.
Breakdowns by entity type (cell_phenotype, cell_vague, cell_hetero) and per-PMID are included in the summary.
Files
The ground-truth XML, source PDFs, and StructSense JSON outputs are intentionally not committed (data artifacts).
Notes / caveats for reviewers
How to run
python3 evaluation/cns_cell/evaluate_extraction.py --detail-csv evaluation/cns_cell/evaluation_detail.csv