Files
SignageSystem/src/statistical_analyzer.py
T
2026-08-09 21:41:44 +09:00

63 lines
5.1 KiB
Python

"""Phase 8 condition summaries and statistical comparisons."""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
import math
from .config_loader import AnalysisSettings
from .models import (AnalysisConditionSummary,AnalysisResult,AnalysisRunSummary,ExclusionSummary,
TurnLevelDistributionResult)
from .statistical_tests import compare_numeric,compare_response_counts
class StatisticalAnalyzer:
"""Analyze prompt/control records while preserving evaluability distinctions."""
def __init__(self,config:AnalysisSettings)->None:self.config=config
def _conditions(self,df):
allowed=[]
if self.config.include_prompt_condition:allowed.append("prompt")
if self.config.include_control_condition:allowed.append("control")
return df[df["prompt_condition"].isin(allowed)]
def summarize_conditions(self,df)->list[AnalysisConditionSummary]:
out=[]
for condition in ("prompt","control"):
group=df[df.prompt_condition==condition];valid=group[group.valid_for_voice_analysis==True];responses=valid[valid.response_detected==True]
rt=responses.reaction_time_sec.dropna();yaw=group.max_yaw_delta_toward_signage.dropna();levels=group.turn_level.fillna("not_evaluable")
out.append(AnalysisConditionSummary(condition,len(group),len(valid),len(responses),len(valid)-len(responses),len(responses)/len(valid) if len(valid) else None,int(group.excluded.fillna(False).sum()),int((group.valid_for_turn_analysis==False).sum()),_mean(rt),_median(rt),_std(rt),_mean(yaw),_median(yaw),_std(yaw),int((levels=="subtle").sum()),int((levels=="weak").sum()),int((levels=="medium").sum()),int((levels=="strong").sum()),int(levels.isin(["weak","medium","strong"]).sum()),int(levels.isin(["medium","strong"]).sum())))
return out
def compare_response_rates(self,df):
d=self._conditions(df);p=d[(d.prompt_condition=="prompt")&(d.valid_for_voice_analysis==True)];c=d[(d.prompt_condition=="control")&(d.valid_for_voice_analysis==True)]
return compare_response_counts(int((p.response_detected==True).sum()),len(p),int((c.response_detected==True).sum()),len(c),self.config.response_rate_test)
def compare_numeric_metric(self,df,metric_name:str):
d=self._conditions(df)
if metric_name=="reaction_time_sec":d=d[d.response_detected==True]
elif metric_name=="max_yaw_delta_toward_signage":
if self.config.yaw_metric_denominator=="valid_turn_analysis":d=d[d.valid_for_turn_analysis==True]
elif self.config.yaw_metric_denominator=="valid_voice_analysis":d=d[d.valid_for_voice_analysis==True]
p=d[d.prompt_condition=="prompt"][metric_name].dropna().astype(float).tolist();c=d[d.prompt_condition=="control"][metric_name].dropna().astype(float).tolist()
return compare_numeric(p,c,metric_name,self.config.numeric_test)
def summarize_turn_levels(self,df)->list[TurnLevelDistributionResult]:
out=[]
for condition in ("prompt","control"):
s=df[df.prompt_condition==condition].turn_level.fillna("not_evaluable")
counts={x:int((s==x).sum()) for x in ("none","face_appeared","subtle","weak","medium","strong","not_evaluable")}
out.append(TurnLevelDistributionResult(condition,counts["none"],counts["face_appeared"],counts["subtle"],counts["weak"],counts["medium"],counts["strong"],counts["not_evaluable"],len(s)))
return out
def summarize_exclusions(self,df)->list[ExclusionSummary]:
out=[]
for condition in ("prompt","control"):
group=df[df.prompt_condition==condition];excluded=group[group.excluded==True];counts=excluded.exclusion_reason.fillna("unknown").value_counts()
out.extend(ExclusionSummary(condition,str(reason),int(count),count/len(group) if len(group) else None) for reason,count in counts.items())
return out
def run_analysis(self,df,input_files_count:int=0)->AnalysisResult:
filtered=self._conditions(df);now=datetime.now().astimezone();run=AnalysisRunSummary(now.strftime("analysis_%Y%m%d_%H%M%S"),input_files_count,len(filtered),int((filtered.prompt_condition=="prompt").sum()),int((filtered.prompt_condition=="control").sum()),int(((filtered.prompt_condition=="prompt")&(filtered.valid_for_voice_analysis==True)).sum()),int(((filtered.prompt_condition=="control")&(filtered.valid_for_voice_analysis==True)).sum()),now.isoformat(timespec="seconds"),self.config.output_directory,"Observational analysis; causality is not established")
return AnalysisResult(self.summarize_conditions(filtered),self.compare_response_rates(filtered),[self.compare_numeric_metric(filtered,"reaction_time_sec"),self.compare_numeric_metric(filtered,"max_yaw_delta_toward_signage")],self.summarize_turn_levels(filtered),self.summarize_exclusions(filtered),run)
def _values(s):return [float(x) for x in s if not (isinstance(x,float) and math.isnan(x))]
def _mean(s):
v=_values(s);return sum(v)/len(v) if v else None
def _median(s):
import statistics
v=_values(s);return statistics.median(v) if v else None
def _std(s):
import statistics
v=_values(s);return statistics.stdev(v) if len(v)>1 else None