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.""" |
||||
|
||||
__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