parent
c5bcb95d74
commit
15a473ce3d
27 changed files with 380 additions and 9 deletions
@ -0,0 +1 @@ |
|||||||
|
|
||||||
@ -0,0 +1 @@ |
|||||||
|
|
||||||
@ -0,0 +1,13 @@ |
|||||||
|
# 最終結果テンプレート |
||||||
|
|
||||||
|
## 実験概要 |
||||||
|
## 実装システム |
||||||
|
## 実験条件 |
||||||
|
## 取得データと有効解析人数 |
||||||
|
## 反応率・反応時間・Yaw変化 |
||||||
|
## 統計検定と効果量 |
||||||
|
## 除外理由・ログ品質 |
||||||
|
## 制約 |
||||||
|
## 今後の課題 |
||||||
|
|
||||||
|
統計的有意差や高い反応率だけから因果関係を断定しない。 |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
# 本実験プロトコル |
||||||
|
|
||||||
|
1. Pilot Diagnosticsの重大Issueを解消する。 |
||||||
|
2. Prompt/Controlを同じ場所・時間帯で交互に実施する。 |
||||||
|
3. カメラ位置・高さ・角度、音声内容・音量、trigger、response window、turn閾値を固定する。 |
||||||
|
4. 各セッションでconditionとcamera_position_noteを記録する。 |
||||||
|
5. 実験中に条件を変更しない。変更時はCalibration Historyへ記録する。 |
||||||
|
6. 実験後にanalyze、quality-check、pilot-diagnostics、main-progressを確認する。 |
||||||
|
7. 評価不能者を反応なしに含めない。 |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
# Phase 12 本実験・最終結果整理仕様 |
||||||
|
|
||||||
|
Phase 12は、パイロット診断後の固定設定で本実験データを収集し、進捗、設定スナップショット、解析、品質検査、診断、最終レポート、発表用成果物を一貫して管理する。 |
||||||
|
|
||||||
|
## 進捗 |
||||||
|
|
||||||
|
Prompt/Controlの総数、有効解析人数、セッション数、目標達成率を集計する。両条件が最低有効人数以上で、camera_position_noteが記録されている場合に最終解析可能とする。目標人数30、最低人数20は初期運用値であり、正式なサンプルサイズ設計ではない。 |
||||||
|
|
||||||
|
## 設定スナップショット |
||||||
|
|
||||||
|
アプリバージョン、設定ファイル、可能ならgit commit、カメラ位置、音声内容・音量、trigger、response window、turn閾値、顔処理頻度、Yaw方向をYAML/Markdownで保存する。 |
||||||
|
|
||||||
|
## 最終ワークフロー |
||||||
|
|
||||||
|
Phase 8解析、Phase 10品質確認、Phase 11診断、最終レポート、発表用成果物を順番に実行する。一部失敗は`final_workflow_result.json`へwarning/errorとして残す。カメラやYOLOは起動しない。 |
||||||
|
|
||||||
|
## 結果の扱い |
||||||
|
|
||||||
|
有効解析者だけを反応率の分母に使い、評価不能を反応なしへ混ぜない。Prompt/Controlは音声以外のカメラ、照明、通行方向、trigger、閾値を揃える。p値、効果量、反応率差を記載しても因果関係を断定しない。 |
||||||
|
|
||||||
|
## 出力 |
||||||
|
|
||||||
|
`main_experiment_progress.csv/.md`、設定snapshot YAML/Markdown、`final_experiment_report.md`、`final_experiment_report_summary.csv`、`presentation_assets.md`、`presentation_tables.csv`、`final_workflow_result.json`を生成する。 |
||||||
|
|
||||||
|
## GUI/CLI |
||||||
|
|
||||||
|
Final Resultsタブと`main-progress`、`snapshot`、`final-analysis`、`final-report`、`presentation-assets`を提供する。すべてオフライン処理である。 |
||||||
|
|
||||||
|
## 制約とPhase 13 |
||||||
|
|
||||||
|
本機能は研究デザインの妥当性、個人識別、ID switch解消、因果推論を保証しない。Phase 13では論文章立て、図表キャプション、最終スライド、最終成果物チェックを支援する。 |
||||||
@ -1,3 +1,3 @@ |
|||||||
"""Phase 1-5 pedestrian detection, tracking, pose, turn, and voice system.""" |
"""Phase 1-5 pedestrian detection, tracking, pose, turn, and voice system.""" |
||||||
|
|
||||||
__version__ = "0.11.0" |
__version__ = "0.12.0" |
||||||
|
|||||||
@ -0,0 +1,19 @@ |
|||||||
|
"""Reproducible main-experiment setting snapshot.""" |
||||||
|
from __future__ import annotations |
||||||
|
import subprocess |
||||||
|
from dataclasses import asdict |
||||||
|
from datetime import datetime |
||||||
|
from pathlib import Path |
||||||
|
import yaml |
||||||
|
from .config_loader import AppConfig |
||||||
|
from .models import ExperimentSnapshot |
||||||
|
class ExperimentSnapshotBuilder: |
||||||
|
def __init__(self,config:AppConfig)->None:self.config=config |
||||||
|
def build_snapshot(self,config_path:Path|None=None)->ExperimentSnapshot: |
||||||
|
commit=None |
||||||
|
if self.config.experiment_snapshot.include_git_commit: |
||||||
|
try:commit=subprocess.run(["git","rev-parse","HEAD"],capture_output=True,text=True,timeout=2,check=True).stdout.strip() |
||||||
|
except Exception:pass |
||||||
|
return ExperimentSnapshot(f"snapshot_{datetime.now():%Y%m%d_%H%M%S}",datetime.now().astimezone().isoformat(timespec="seconds"),str(config_path or "config/experiment.yaml"),None,self.config.application.version,commit,self.config.unified_logging.camera_position_note,self.config.experiment_protocol.voice_prompt_text,self.config.audio.volume,self.config.voice_prompt.trigger_strategy,self.config.voice_prompt.response_window_sec,self.config.turn_detection.weak_threshold_deg,self.config.face.process_every_n_frames,self.config.turn_detection.signage_yaw_direction,"本実験用固定設定") |
||||||
|
def export_yaml(self,s,output_path):output_path.parent.mkdir(parents=True,exist_ok=True);output_path.write_text(yaml.safe_dump(asdict(s),allow_unicode=True,sort_keys=False),encoding="utf-8");return output_path |
||||||
|
def export_markdown(self,s,output_path):output_path.write_text("# 実験設定スナップショット\n\n"+"\n".join(f"- {k}: {v}" for k,v in asdict(s).items())+"\n",encoding="utf-8");return output_path |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
"""Best-effort final offline workflow with explicit per-stage status.""" |
||||||
|
from __future__ import annotations |
||||||
|
import json |
||||||
|
from dataclasses import asdict |
||||||
|
from datetime import datetime |
||||||
|
from pathlib import Path |
||||||
|
from .analysis_cli import run_analysis_cli |
||||||
|
from .config_loader import AppConfig |
||||||
|
from .final_report_builder import FinalReportBuilder |
||||||
|
from .models import FinalAnalysisWorkflowResult |
||||||
|
from .phase10_cli import run_quality |
||||||
|
from .phase11_cli import run_diagnostics |
||||||
|
from .presentation_assets_builder import PresentationAssetsBuilder |
||||||
|
class FinalAnalysisWorkflow: |
||||||
|
def __init__(self,config:AppConfig)->None:self.config=config |
||||||
|
def run(self,logs:Path,analysis:Path,output:Path)->FinalAnalysisWorkflowResult: |
||||||
|
outputs=[];warnings=[];errors=[];flags={"analysis":False,"quality":False,"diagnostics":False,"report":False,"assets":False};output.mkdir(parents=True,exist_ok=True) |
||||||
|
try:outputs += [str(x) for x in run_analysis_cli(self.config.analysis,logs,analysis)];flags["analysis"]=True |
||||||
|
except Exception as e:errors.append(f"analysis: {e}") |
||||||
|
try:outputs += [str(x) for x in run_quality(self.config,logs,analysis,output)];flags["quality"]=True |
||||||
|
except Exception as e:warnings.append(f"quality: {e}") |
||||||
|
try:outputs += [str(x) for x in run_diagnostics(self.config,analysis,logs,output)];flags["diagnostics"]=True |
||||||
|
except Exception as e:warnings.append(f"diagnostics: {e}") |
||||||
|
report=None |
||||||
|
try:b=FinalReportBuilder();report=b.build(analysis);outputs += [str(x) for x in b.export(report,output/"final_experiment_report.md",output/"final_experiment_report_summary.csv")];flags["report"]=True |
||||||
|
except Exception as e:errors.append(f"report: {e}") |
||||||
|
try: |
||||||
|
if report is None:report=FinalReportBuilder().build(analysis) |
||||||
|
b=PresentationAssetsBuilder();outputs += [str(x) for x in b.export(b.build(report,analysis),output/"presentation_assets.md",output/"presentation_tables.csv")];flags["assets"]=True |
||||||
|
except Exception as e:warnings.append(f"assets: {e}") |
||||||
|
result=FinalAnalysisWorkflowResult(f"workflow_{datetime.now():%Y%m%d_%H%M%S}",datetime.now().astimezone().isoformat(timespec="seconds"),str(logs),str(analysis),str(output),flags["analysis"],flags["quality"],flags["diagnostics"],flags["report"],flags["assets"],outputs,warnings,errors);(output/"final_workflow_result.json").write_text(json.dumps(asdict(result),ensure_ascii=False,indent=2),encoding="utf-8");return result |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
"""Final descriptive report assembled from Phase 8 fixed-schema CSVs.""" |
||||||
|
from __future__ import annotations |
||||||
|
import csv |
||||||
|
from dataclasses import asdict |
||||||
|
from datetime import datetime |
||||||
|
from pathlib import Path |
||||||
|
from .models import FinalExperimentReport |
||||||
|
def _rows(path): |
||||||
|
if not path.exists():return [] |
||||||
|
with path.open(encoding="utf-8-sig",newline="") as f:return list(csv.DictReader(f)) |
||||||
|
def _n(d,k): |
||||||
|
try:return float(d[k]) if d.get(k) not in {None,""} else None |
||||||
|
except:return None |
||||||
|
class FinalReportBuilder: |
||||||
|
def build(self,analysis:Path)->FinalExperimentReport: |
||||||
|
cond={x.get("condition"):x for x in _rows(analysis/"condition_summary.csv")};p=cond.get("prompt",{});c=cond.get("control",{});test=(_rows(analysis/"response_rate_test.csv") or [{}])[0];nums={x.get("metric_name"):x for x in _rows(analysis/"numeric_comparisons.csv")};react=nums.get("reaction_time_sec",{});yaw=nums.get("max_yaw_delta_toward_signage",{});ex=sorted(_rows(analysis/"exclusion_summary.csv"),key=lambda x:float(x.get("count") or 0),reverse=True)[:5] |
||||||
|
return FinalExperimentReport(f"final_{datetime.now():%Y%m%d_%H%M%S}",datetime.now().astimezone().isoformat(timespec="seconds"),"デジタルサイネージ声掛け実験",0,0,0,int(float(p.get("valid_voice_analysis_count") or 0)),int(float(c.get("valid_voice_analysis_count") or 0)),_n(p,"response_rate"),_n(c,"response_rate"),_n(test,"rate_difference"),_n(test,"p_value"),test.get("test_name"),_n(test,"effect_size"),_n(test,"odds_ratio"),_n(p,"mean_reaction_time_sec"),_n(c,"mean_reaction_time_sec"),_n(react,"p_value"),_n(p,"mean_max_yaw_delta"),_n(c,"mean_max_yaw_delta"),_n(yaw,"p_value"),[f"{x.get('condition')}:{x.get('exclusion_reason')} ({x.get('count')})" for x in ex],"評価不能者を反応なしに含めず、有効解析者を分母とした。","観察研究であり因果関係を断定できない。カメラ位置、照明、通行方向、ID switchの影響がある。","Prompt/Controlの記述的比較結果を示す。統計結果のみから音声効果を断定しない。") |
||||||
|
def export(self,r:FinalExperimentReport,md:Path,csv_path:Path)->list[Path]: |
||||||
|
md.parent.mkdir(parents=True,exist_ok=True);d=asdict(r);md.write_text("# 最終実験結果\n\n"+"\n".join(f"- **{k}**: {v}" for k,v in d.items())+"\n",encoding="utf-8") |
||||||
|
row=dict(d);row["top_exclusion_reasons"]=" | ".join(r.top_exclusion_reasons) |
||||||
|
with csv_path.open("w",encoding="utf-8-sig",newline="") as f:w=csv.DictWriter(f,fieldnames=row.keys());w.writeheader();w.writerow(row) |
||||||
|
return [md,csv_path] |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
"""Phase 12 final experiment controls.""" |
||||||
|
from pathlib import Path |
||||||
|
from PySide6.QtCore import Signal |
||||||
|
from PySide6.QtWidgets import QFormLayout,QLineEdit,QListWidget,QPushButton,QVBoxLayout,QWidget,QHBoxLayout |
||||||
|
class FinalResultsPanel(QWidget): |
||||||
|
operation_requested=Signal(str,str,str,str);open_requested=Signal(str) |
||||||
|
def __init__(self)->None: |
||||||
|
super().__init__();layout=QVBoxLayout(self);form=QFormLayout();self.logs=QLineEdit("data/logs/unified");self.analysis=QLineEdit("data/analysis");self.output=QLineEdit("data/final_results");form.addRow("Logs",self.logs);form.addRow("Analysis",self.analysis);form.addRow("Output",self.output);layout.addLayout(form);row=QHBoxLayout() |
||||||
|
for label,op in (("Progress","main-progress"),("Snapshot","snapshot"),("Run final analysis","final-analysis"),("Final report","final-report"),("Presentation assets","presentation-assets")): |
||||||
|
b=QPushButton(label);b.clicked.connect(lambda checked=False,x=op:self.operation_requested.emit(x,self.logs.text(),self.analysis.text(),self.output.text()));row.addWidget(b) |
||||||
|
layout.addLayout(row);self.files=QListWidget();self.files.itemDoubleClicked.connect(lambda i:self.open_requested.emit(i.text()));layout.addWidget(self.files) |
||||||
|
def set_outputs(self,paths:list[Path])->None:self.files.clear();self.files.addItems([str(p) for p in paths]) |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
"""Main experiment plan and collection progress management.""" |
||||||
|
from __future__ import annotations |
||||||
|
import csv |
||||||
|
from dataclasses import asdict |
||||||
|
from datetime import datetime |
||||||
|
from pathlib import Path |
||||||
|
from .config_loader import AppConfig |
||||||
|
from .models import MainExperimentPlan,MainExperimentProgress |
||||||
|
def _b(v):return str(v).lower() in {"true","1","yes"} |
||||||
|
class MainExperimentManager: |
||||||
|
def __init__(self,config:AppConfig)->None:self.config=config |
||||||
|
def build_plan(self)->MainExperimentPlan: |
||||||
|
c=self.config.main_experiment;return MainExperimentPlan(f"plan_{datetime.now():%Y%m%d}","デジタルサイネージ本実験",datetime.now().astimezone().isoformat(timespec="seconds"),c.target_prompt_valid_records,c.target_control_valid_records,c.minimum_prompt_valid_records,c.minimum_control_valid_records,c.planned_prompt_sessions,c.planned_control_sessions,c.condition_order_rule,c.camera_position_policy,self.config.experiment_protocol.voice_prompt_text,self.config.audio.volume,self.config.voice_prompt.response_window_sec,self.config.turn_detection.weak_threshold_deg,"目標人数は初期運用値であり正式な検定設計ではない") |
||||||
|
def evaluate_progress(self,logs_dir:Path,analysis_dir:Path|None=None)->MainExperimentProgress: |
||||||
|
rows=[] |
||||||
|
if logs_dir.exists(): |
||||||
|
for p in logs_dir.rglob("*person_analysis_table.csv"): |
||||||
|
with p.open(encoding="utf-8-sig",newline="") as f:rows.extend(csv.DictReader(f)) |
||||||
|
p=[r for r in rows if r.get("prompt_condition")=="prompt"];c=[r for r in rows if r.get("prompt_condition")=="control"];pv=sum(_b(r.get("valid_for_voice_analysis")) for r in p);cv=sum(_b(r.get("valid_for_voice_analysis")) for r in c);ps=len({r.get("session_id") for r in p});cs=len({r.get("session_id") for r in c});cfg=self.config.main_experiment;target=pv>=cfg.target_prompt_valid_records and cv>=cfg.target_control_valid_records;camera=all(r.get("camera_position_note","").strip() for r in rows);ready=pv>=cfg.minimum_prompt_valid_records and cv>=cfg.minimum_control_valid_records and camera;issue="none" if ready else "有効人数不足" if min(pv,cv)<min(cfg.minimum_prompt_valid_records,cfg.minimum_control_valid_records) else "camera_position_note欠損";action="最終解析へ進む" if ready else "不足条件を同じ環境で追加収集する" if "人数" in issue else "カメラ位置記録を確認する" |
||||||
|
return MainExperimentProgress(self.build_plan().plan_id,datetime.now().astimezone().isoformat(timespec="seconds"),len(p),len(c),pv,cv,ps,cs,pv/cfg.target_prompt_valid_records,cv/cfg.target_control_valid_records,pv+cv,target,ready,issue,action) |
||||||
|
def export_progress_csv(self,p:MainExperimentProgress,path:Path)->Path: |
||||||
|
path.parent.mkdir(parents=True,exist_ok=True) |
||||||
|
with path.open("w",encoding="utf-8-sig",newline="") as f:w=csv.DictWriter(f,fieldnames=asdict(p).keys());w.writeheader();w.writerow(asdict(p)) |
||||||
|
return path |
||||||
|
def export_progress_markdown(self,p:MainExperimentProgress,path:Path)->Path:path.write_text("# 本実験収集進捗\n\n"+"\n".join(f"- {k}: {v}" for k,v in asdict(p).items())+"\n",encoding="utf-8");return path |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
from pathlib import Path |
||||||
|
from .config_loader import AppConfig |
||||||
|
from .experiment_snapshot import ExperimentSnapshotBuilder |
||||||
|
from .final_analysis_workflow import FinalAnalysisWorkflow |
||||||
|
from .final_report_builder import FinalReportBuilder |
||||||
|
from .main_experiment_manager import MainExperimentManager |
||||||
|
from .presentation_assets_builder import PresentationAssetsBuilder |
||||||
|
def progress(c:AppConfig,logs:Path,analysis:Path,output:Path):m=MainExperimentManager(c);p=m.evaluate_progress(logs,analysis);return [m.export_progress_csv(p,output/"main_experiment_progress.csv"),m.export_progress_markdown(p,output/"main_experiment_progress.md")] |
||||||
|
def snapshot(c:AppConfig,config_path:Path,output:Path):b=ExperimentSnapshotBuilder(c);s=b.build_snapshot(config_path);stamp=s.snapshot_id;return [b.export_yaml(s,output/f"{stamp}.yaml"),b.export_markdown(s,output/f"{stamp}.md")] |
||||||
|
def final_report(c:AppConfig,analysis:Path,output:Path):b=FinalReportBuilder();return b.export(b.build(analysis),output/"final_experiment_report.md",output/"final_experiment_report_summary.csv") |
||||||
|
def assets(c:AppConfig,analysis:Path,output:Path):r=FinalReportBuilder().build(analysis);b=PresentationAssetsBuilder();return b.export(b.build(r,analysis),output/"presentation_assets.md",output/"presentation_tables.csv") |
||||||
|
def workflow(c:AppConfig,logs:Path,analysis:Path,output:Path):return FinalAnalysisWorkflow(c).run(logs,analysis,output) |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
"""Inventory final tables, plots, and slide-ready notes.""" |
||||||
|
from __future__ import annotations |
||||||
|
import csv |
||||||
|
from dataclasses import asdict |
||||||
|
from pathlib import Path |
||||||
|
from .models import FinalExperimentReport,PresentationAsset |
||||||
|
class PresentationAssetsBuilder: |
||||||
|
def build(self,report:FinalExperimentReport,analysis:Path)->list[PresentationAsset]: |
||||||
|
assets=[PresentationAsset("table_summary","table","条件別主要結果","final_experiment_report_summary.csv","有効人数、反応率、反応時間、Yaw","結果")] |
||||||
|
for i,p in enumerate(sorted((analysis/"plots").glob("*.png")),1):assets.append(PresentationAsset(f"figure_{i}","figure",p.stem,str(p),"Phase 8生成図","結果")) |
||||||
|
return assets |
||||||
|
def export(self,assets:list[PresentationAsset],md:Path,csv_path:Path)->list[Path]: |
||||||
|
md.parent.mkdir(parents=True,exist_ok=True);md.write_text("# 発表用成果物\n\n## スライド構成\n研究目的 → システム → 条件 → 結果 → 除外・品質 → 制約 → 今後\n\n"+"\n".join(f"- {a.asset_type}: [{a.title}]({a.file_path}) — {a.description}" for a in assets)+"\n\n統計的有意差は因果関係を直接証明しない。\n",encoding="utf-8") |
||||||
|
fields=["asset_id","asset_type","title","file_path","description","suggested_slide"] |
||||||
|
with csv_path.open("w",encoding="utf-8-sig",newline="") as f:w=csv.DictWriter(f,fieldnames=fields);w.writeheader();w.writerows(asdict(a) for a in assets) |
||||||
|
return [md,csv_path] |
||||||
@ -0,0 +1,5 @@ |
|||||||
|
from pathlib import Path |
||||||
|
from src.config_loader import load_config |
||||||
|
from src.experiment_snapshot import ExperimentSnapshotBuilder |
||||||
|
def test_snapshot_exports(tmp_path:Path)->None: |
||||||
|
b=ExperimentSnapshotBuilder(load_config("config/default.yaml",None));s=b.build_snapshot();assert s.voice_volume==.8;assert b.export_yaml(s,tmp_path/"s.yaml").exists();assert "camera_position_note" in b.export_markdown(s,tmp_path/"s.md").read_text(encoding="utf-8") |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
from pathlib import Path |
||||||
|
from src.final_report_builder import FinalReportBuilder |
||||||
|
def test_empty_final_report_exports(tmp_path:Path)->None: |
||||||
|
b=FinalReportBuilder();r=b.build(tmp_path);assert "因果関係" in r.limitations;paths=b.export(r,tmp_path/"r.md",tmp_path/"r.csv");assert paths[1].read_bytes().startswith(b"\xef\xbb\xbf") |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
import csv |
||||||
|
from pathlib import Path |
||||||
|
from src.config_loader import load_config |
||||||
|
from src.main_experiment_manager import MainExperimentManager |
||||||
|
def test_plan_empty_progress_exports(tmp_path:Path)->None: |
||||||
|
m=MainExperimentManager(load_config("config/default.yaml",None));assert m.build_plan().target_prompt_valid_records==30;p=m.evaluate_progress(tmp_path);assert not p.ready_for_final_analysis;assert m.export_progress_csv(p,tmp_path/"p.csv").read_bytes().startswith(b"\xef\xbb\xbf");assert m.export_progress_markdown(p,tmp_path/"p.md").exists() |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
from main import build_phase12_parser |
||||||
|
def test_phase12_parsers()->None: |
||||||
|
for command in ("main-progress","snapshot","final-analysis","final-report","presentation-assets"):assert build_phase12_parser(command).parse_args([]).output |
||||||
@ -0,0 +1,5 @@ |
|||||||
|
from pathlib import Path |
||||||
|
from src.final_report_builder import FinalReportBuilder |
||||||
|
from src.presentation_assets_builder import PresentationAssetsBuilder |
||||||
|
def test_empty_assets_export(tmp_path:Path)->None: |
||||||
|
b=PresentationAssetsBuilder();assets=b.build(FinalReportBuilder().build(tmp_path),tmp_path);paths=b.export(assets,tmp_path/"a.md",tmp_path/"a.csv");assert "因果関係" in paths[0].read_text(encoding="utf-8") |
||||||
Loading…
Reference in new issue