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