"""目録の集計(`tiasdata stats`.SPEC_03「CLI」).
件数と所見の値(舌色の slug 等)だけを返す.対応表(`identity.sqlite`)は開かない.
"""
import json
import sqlite3
from collections import Counter
from dataclasses import dataclass
from tiasdata import catalog, kinds
@dataclass(frozen=True)
class AnnotationStats:
"""所見の種類ごとの集計.
Attributes:
total: 有効な所見(訂正されていないもの)の数.
by_field: 値の項目 → 値 → 件数(例: `label` → `red` → 120).
by_value: 値の組み合わせ(`項目=値` を `, ` でつないだもの)→ 件数.
"""
total: int
by_field: dict[str, dict[str, int]]
by_value: dict[str, int]
@dataclass(frozen=True)
class CatalogStats:
"""目録の集計.
Attributes:
subjects: 症例の数.
captures: 撮影の数.
subjects_with_multiple_captures: 撮影を 2 つ以上持つ症例の数.
assets_by_kind: ファイルの種類 → ファイル数.
annotations: 所見の種類 → 集計(要確認の印 `review` は含めない).
superseded_annotations: 訂正された所見の数.
assets_without_capture: 撮影にひも付かないファイルの数.
review_marks_on_captures: 撮影に付いた有効な要確認の印の数.
review_marks_on_files: ファイルに付いた有効な要確認の印の数.
要確認の印は件数(`review_marks_on_captures`・`review_marks_on_files`)だけを持つ.
値は理由(reason)の全文等で,値ごとに数えても内訳にならないため.
"""
subjects: int
captures: int
subjects_with_multiple_captures: int
assets_by_kind: dict[str, int]
annotations: dict[str, AnnotationStats]
superseded_annotations: int
assets_without_capture: int
review_marks_on_captures: int
review_marks_on_files: int
def summarize_annotation_values(values: dict[str, int]) -> AnnotationStats:
"""所見の値(JSON の文字列)ごとの件数を,項目ごと・組み合わせごとの件数にまとめる.
Args:
values: 値の JSON → 件数(同じ種類の所見).
Returns:
集計.項目・値は名前の順に並べる.
"""
by_field: dict[str, Counter[str]] = {}
by_value: Counter[str] = Counter()
for value_json, count in values.items():
value = json.loads(value_json)
if not isinstance(value, dict):
by_value[value_json] += count
continue
for field, field_value in value.items():
by_field.setdefault(field, Counter())[_text(field_value)] += count
by_value[", ".join(f"{k}={_text(v)}" for k, v in sorted(value.items()))] += count
return AnnotationStats(
total=sum(values.values()),
by_field={field: dict(sorted(c.items())) for field, c in sorted(by_field.items())},
by_value=dict(sorted(by_value.items())),
)
def _text(value: object) -> str:
return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)
def collect_stats(conn: sqlite3.Connection) -> CatalogStats:
"""目録を集計する.
Args:
conn: 目録への接続(スキーマ最新).
Returns:
集計.
"""
on_captures, on_files = catalog.count_valid_annotations_by_target(conn, kinds.REVIEW_KIND)
return CatalogStats(
subjects=catalog.count_subjects(conn),
captures=catalog.count_captures(conn),
subjects_with_multiple_captures=catalog.count_subjects_with_multiple_captures(conn),
assets_by_kind=catalog.count_assets_by_kind(conn),
annotations={
kind: summarize_annotation_values(values)
for kind, values in catalog.count_annotation_values(conn).items()
if kind != kinds.REVIEW_KIND
},
superseded_annotations=catalog.count_superseded_annotations(conn),
assets_without_capture=catalog.count_assets_without_capture(conn),
review_marks_on_captures=on_captures,
review_marks_on_files=on_files,
)