Report every target venue and expose missing historical coverage
Metadata integrity checks / metadata (push) Successful in 29s

This commit is contained in:
2026-11-06 17:01:40 +08:00
parent 827ba17a99
commit 16f428cae9
3 changed files with 86 additions and 1 deletions
+10 -1
View File
@@ -6,7 +6,7 @@ Paper Atlas 是一个**长期维护、按需或按批次运行的研究数据工
目标包括:完整历史元数据导入、逐年会议目录、跨平台论文去重、公开拒稿/撤稿信息、平台标签与自定义标签隔离、可恢复更新和覆盖率报告。
**目前仍在开发,不能视为完整数据集。** 已实现 SQLite 数据结构、arXiv 元数据 ZIP 流式导入、断点恢复、跨平台标识合并及审计、PMLR、COLM、NeurIPS、ACL Anthology、ICLR 官方公开程序、CVF/ECVA 视觉会议元数据采集、带清单核验的会议导入和只读查询。PeerRead 提供公开 ICLR 2017 明确决定的历史补充;近期 ICLR 投稿与决定快照解析器正在验证。arXiv 元数据 ZIP 已完整下载(1,878,831,586 字节),主批次已读至 EOF 并完成 3,233,620 条源记录的导入;这不是去重后的论文数,最终全库审计尚未完成。其他会议适配器、历史缺失届次、完整投稿历史、跨平台疑似重复复核和覆盖率核验仍待完成。回归检查在远端执行,已配置 ces-control:host 对应的 CI 工作流,实际 CI 结果以 Gitea Actions 为准。`venues.json` 的 41 个会议是候选覆盖范围,不代表已完成采集,也不是与 COLM 比较的客观排名。
**目前仍在开发,不能视为完整数据集。** 已实现 SQLite 数据结构、arXiv 元数据 ZIP 流式导入、断点恢复、跨平台标识合并及审计、PMLR、COLM、NeurIPS、ACL Anthology、ICLR 官方公开程序、CVF/ECVA 视觉会议元数据采集、带清单核验的会议导入和只读查询。PeerRead 提供公开 ICLR 2017 明确决定的历史补充;近期 ICLR 公开决定快照已解析,实际主库进度以运行日志与统计为准。arXiv 元数据 ZIP 已完整下载(1,878,831,586 字节),主批次读取 3,233,620 行至 EOF;主库有 3,233,527 条 arXiv 来源记录,93 行差额仍需按原始 ID 独立核验。这些不是去重后的全库论文数,最终全库审计尚未完成。其他会议适配器、历史缺失届次、完整投稿历史、跨平台疑似重复复核和覆盖率核验仍待完成。回归检查在远端执行,已配置 ces-control:host 对应的 CI 工作流,实际 CI 结果以 Gitea Actions 为准。`venues.json` 的 41 个会议是候选覆盖范围,不代表已完成采集,也不是与 COLM 比较的客观排名。
## 数据范围与来源
@@ -100,6 +100,15 @@ python3 -B query.py --db /data/paper-index/papers.sqlite --venue COLM --year 202
查询连接使用 SQLite 只读模式,结果输出 JSON。`--stats` 中 `papers` 是按当前规则去重后的论文实体数,`records_by_source` 是各平台已入库记录数,`decision_events_by_status` 是决定事件数。采集文件 manifest 的 `count` 是已采集数量,不能等同已入库数量,也不能把各平台记录数相加当成独立论文数。
全部目标会议的覆盖报告会保留尚无入库记录的项目,避免只展示已经做完的会议:
```sh
python3 -B coverage_report.py --db /data/paper-index/papers.sqlite \
--out /data/paper-index/collected/venue-coverage-current.json
```
报告按会议、年份和 track 列出投稿数、独立论文数及来源清单状态,包含联合会议别名。没有数据的目标明确标为 no_imported_records;已有数据也保持“历史待独立核验”,不会因单个文件导入通过就宣称历年覆盖完成。同一论文可以出现于多个会议或年份,各会议论文数也不能直接相加作为全库数量。
用 `--paper 531782` 可查看单篇论文的所有已知来源、平台标签、投稿届次及带证据的决定历史。会议筛选不区分大小写,状态筛选使用该次投稿的最新已知决定。当前没有公共 Web 页面或网络查询服务。
## 官方会议元数据采集
+41
View File
@@ -0,0 +1,41 @@
"""Show every target venue, including those with no imported records."""
import argparse
import json
from pathlib import Path
import sqlite3
from submission_evidence import timestamp
def report(db,registry):
registry=json.loads(Path(registry).read_text());targets=registry['venues']
names=[row['acronym'].casefold() for row in targets]
if len(names)!=len(set(names)):raise ValueError('Duplicate target venue')
scope='(v.acronym=? COLLATE NOCASE OR EXISTS(SELECT 1 FROM venue_aliases a WHERE a.venue_id=v.id AND a.alias=? COLLATE NOCASE))'
c=sqlite3.connect(Path(db).resolve().as_uri()+'?mode=ro',uri=True);c.row_factory=sqlite3.Row
try:
c.execute('BEGIN');venues=[]
for target in targets:
acronym=target['acronym'];params=(acronym,acronym)
editions=[]
for row in c.execute('SELECT e.id,v.acronym AS canonical_venue,e.year,e.track,count(s.id) AS submissions,count(DISTINCT s.paper_id) AS papers FROM editions e JOIN venues v ON v.id=e.venue_id LEFT JOIN submissions s ON s.edition_id=e.id WHERE '+scope+' GROUP BY e.id ORDER BY e.year,e.track',params):
edition=dict(row)
edition['sources']=[dict(source) for source in c.execute('SELECT so.name,es.status,es.expected_count,es.imported_count,es.list_url,es.last_success_at,es.last_error FROM edition_sources es JOIN sources so ON so.id=es.source_id WHERE es.edition_id=? ORDER BY so.name',(row['id'],))]
editions.append(edition)
totals=c.execute('SELECT count(s.id),count(DISTINCT s.paper_id) FROM submissions s JOIN editions e ON e.id=s.edition_id JOIN venues v ON v.id=e.venue_id WHERE '+scope,params).fetchone()
venues.append({'acronym':acronym,'field':target['field'],'imported_submissions':totals[0],'distinct_papers':totals[1],
'history_status':'partial_history_pending_independent_audit' if totals[0] else 'no_imported_records',
'discovery_sources':target.get('discovery_sources',[]),'editions':editions})
return {'checked_at':timestamp(),'target_venue_count':len(targets),'venues_with_imported_records':sum(bool(v['imported_submissions']) for v in venues),
'scope_note':registry['scope_note'],'interpretation':'All registry targets are listed. Edition source complete means that source file passed import checks; it does not prove full historical or abstract coverage. Papers can appear in multiple venues/years, so do not sum venue paper counts as a global total.',
'venues':venues}
finally:c.close()
if __name__=='__main__':
parser=argparse.ArgumentParser();parser.add_argument('--db',required=True)
parser.add_argument('--registry',default=str(Path(__file__).with_name('venues.json')));parser.add_argument('--out')
args=parser.parse_args();result=report(args.db,args.registry);text=json.dumps(result,ensure_ascii=False,indent=2)
if args.out:
path=Path(args.out);stage=path.with_suffix('.part');stage.write_text(text+'\n');stage.replace(path)
else:print(text)
+35
View File
@@ -0,0 +1,35 @@
import json
from pathlib import Path
import tempfile
import unittest
from coverage_report import report
from index_core import Store
class CoverageTests(unittest.TestCase):
def setUp(self):
self.tmp=tempfile.TemporaryDirectory();self.addCleanup(self.tmp.cleanup)
self.root=Path(self.tmp.name);self.db=self.root/'papers.sqlite';self.store=Store(self.db);self.addCleanup(self.store.conn.close)
self.registry=self.root/'venues.json';self.registry.write_text(json.dumps({'scope_note':'Candidate scope','venues':[{'acronym':'COLING','field':'language'},{'acronym':'AAAI','field':'general_ai'}]}))
def test_zero_coverage_targets_are_listed_and_joint_alias_is_counted_once(self):
paper,record=self.store.record('acl_anthology','joint-paper','https://example.org','Joint paper','Abstract',['Jane Doe'])
edition=self.store.decision(paper,record,'COLING+LREC',2024,'main','joint-paper','https://example.org','accepted','Accepted',aliases=['COLING','LREC'])
self.store.decision(paper,record,'COLING+LREC',2024,'main','joint-paper','https://example.org/another-observation','accepted','Accepted',aliases=['COLING'])
self.store.conn.commit();result=report(self.db,self.registry)
self.assertEqual(result['target_venue_count'],2);self.assertEqual(result['venues_with_imported_records'],1)
coling,aaai=result['venues'];self.assertEqual(coling['imported_submissions'],1)
self.assertEqual(coling['editions'][0]['canonical_venue'],'COLING+LREC')
self.assertEqual(aaai['history_status'],'no_imported_records');self.assertEqual(aaai['editions'],[])
self.assertNotEqual(coling['history_status'],'complete')
def test_same_paper_across_years_is_not_two_distinct_papers(self):
paper,record=self.store.record('source','one','https://example.org','A paper','Abstract',[])
for year in (2024,2025):
self.store.decision(paper,record,'COLING',year,'main',str(year),'https://example.org','accepted','Accepted')
self.store.conn.commit();coling=report(self.db,self.registry)['venues'][0]
self.assertEqual(coling['imported_submissions'],2);self.assertEqual(coling['distinct_papers'],1)
if __name__=='__main__':unittest.main()