diff --git a/frontend/src/components/CulturalStats.tsx b/frontend/src/components/CulturalStats.tsx index e62b956..430937a 100644 --- a/frontend/src/components/CulturalStats.tsx +++ b/frontend/src/components/CulturalStats.tsx @@ -39,6 +39,21 @@ const CulturalStats = ({ data }: CulturalStatsProps) => { return `${dominantLabel} (${(dominant[1] * 100).toFixed(1)}%)`; }; + const stanceSublabel = ( + per1kTokens: number | undefined, + emotionAvg: Record | undefined, + ) => { + const rateLabel = + typeof per1kTokens === "number" + ? `${per1kTokens.toFixed(1)} per 1k words` + : "Word frequency"; + const emotionLabel = topEmotion(emotionAvg); + + return emotionLabel === "—" + ? rateLabel + : `${rateLabel} • Avg mood: ${emotionLabel}`; + }; + return (
@@ -107,41 +122,37 @@ const CulturalStats = ({ data }: CulturalStatsProps) => { diff --git a/frontend/src/types/ApiTypes.ts b/frontend/src/types/ApiTypes.ts index 3143c32..dcd5969 100644 --- a/frontend/src/types/ApiTypes.ts +++ b/frontend/src/types/ApiTypes.ts @@ -168,6 +168,10 @@ type StanceMarkers = { certainty_per_1k_tokens: number; deontic_per_1k_tokens: number; permission_per_1k_tokens: number; + hedge_emotion_avg?: Record; + certainty_emotion_avg?: Record; + deontic_emotion_avg?: Record; + permission_emotion_avg?: Record; }; type EntityEmotionAggregate = { diff --git a/server/analysis/cultural.py b/server/analysis/cultural.py index af64091..47e8e4f 100644 --- a/server/analysis/cultural.py +++ b/server/analysis/cultural.py @@ -67,6 +67,12 @@ class CulturalAnalysis: def get_stance_markers(self, df: pd.DataFrame) -> dict[str, Any]: s = df[self.content_col].fillna("").astype(str) + emotion_exclusions = {"emotion_neutral", "emotion_surprise"} + emotion_cols = [ + c + for c in df.columns + if c.startswith("emotion_") and c not in emotion_exclusions + ] hedge_pattern = re.compile( r"\b(maybe|perhaps|possibly|probably|likely|seems|seem|i think|i feel|i guess|kind of|sort of|somewhat)\b" @@ -88,7 +94,7 @@ class CulturalAnalysis: 0, 1 ) - return { + result = { "hedge_total": int(hedge_counts.sum()), "certainty_total": int(certainty_counts.sum()), "deontic_total": int(deontic_counts.sum()), @@ -107,6 +113,32 @@ class CulturalAnalysis: ), } + if emotion_cols: + emo = df[emotion_cols].apply(pd.to_numeric, errors="coerce").fillna(0.0) + + result["hedge_emotion_avg"] = ( + emo.loc[hedge_counts > 0].mean() + if (hedge_counts > 0).any() + else pd.Series(0.0, index=emotion_cols) + ).to_dict() + result["certainty_emotion_avg"] = ( + emo.loc[certainty_counts > 0].mean() + if (certainty_counts > 0).any() + else pd.Series(0.0, index=emotion_cols) + ).to_dict() + result["deontic_emotion_avg"] = ( + emo.loc[deontic_counts > 0].mean() + if (deontic_counts > 0).any() + else pd.Series(0.0, index=emotion_cols) + ).to_dict() + result["permission_emotion_avg"] = ( + emo.loc[perm_counts > 0].mean() + if (perm_counts > 0).any() + else pd.Series(0.0, index=emotion_cols) + ).to_dict() + + return result + def get_avg_emotions_per_entity( self, df: pd.DataFrame, top_n: int = 25, min_posts: int = 10 ) -> dict[str, Any]: