# -*- coding: utf-8 -*-
"""
ラーメン豚山 スープ評価データ 再現可能な分析スクリプト
============================================================
入力 CSV (../data/raw_csv/base_export.csv) は「回答結果」フル版エクスポート(11列):
  テキスト, 提出者, 時間帯, スープ濃度, 添付ファイル,
  スープのみの点数, 濃度込みの点数, 評価備考, 通知するブロック長, 結果(JSON), 親レコード
- 「結果」列の JSON には既存AIモデルの pred_score と動画パスが入っている。
- スープのみ/濃度込み の点数が空の行は「人間未採点(AI推論のみ)」の行。

実行: python analysis.py   出力: 標準出力 + analysis_output.txt (UTF-8)
"""
import pandas as pd, numpy as np, re, os, json

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
CSV  = os.path.join(ROOT, "data", "raw_csv", "base_export.csv")
LOG  = []
def p(*a):
    s = " ".join(str(x) for x in a); LOG.append(s); print(s)

# ---------- 読み込み ----------
raw = pd.read_csv(CSV)
p("== 0. ファイル構造 ==")
p("列:", list(raw.columns))
p("総行数(ヘッダ除く):", len(raw))

df = raw.rename(columns={
    'テキスト':'dt','提出者':'store','時間帯':'slot','スープ濃度':'brix_raw',
    '添付ファイル':'video','スープのみの点数':'soup','濃度込みの点数':'total',
    '評価備考':'comment','通知するブロック長':'evaluator','結果':'result'})
df = df.dropna(subset=['dt']).copy()

# 既存AIモデルの pred_score を JSON から抽出
def pred_score(s):
    try:
        j = json.loads(s); return j['results'][0].get('pred_score')
    except Exception:
        return np.nan
df['ai_pred'] = df['result'].apply(pred_score)

for c in ['soup','total']:
    df[c] = pd.to_numeric(df[c], errors='coerce')

def parse_brix(x):
    x = str(x)
    if '10' in x and ('～' in x or '〜' in x): return 9.5
    if '14' in x and ('～' in x or '〜' in x): return 14.5
    m = re.findall(r'\d+', x); return float(m[0]) if m else np.nan
df['brix'] = df['brix_raw'].apply(parse_brix)

# ---------- 人間採点の有無 ----------
p("\n== 1. 件数の内訳 ==")
p("全レコード:", len(df))
p("人間採点あり(soup,total 両方):", int((df['soup'].notna() & df['total'].notna()).sum()))
p("AI推論のみ(人間採点なし):", int((df['soup'].isna() | df['total'].isna()).sum()))

human = df[df['soup'].notna() & df['total'].notna()].copy()
bad = human[human['total'] > 100]
p("\n== 2. 異常値(total>100; 入力ミス) ==")
p(bad[['dt','store','brix_raw','soup','total']].to_string(index=False) if len(bad) else "なし")
clean = human[human['total'] <= 100].copy()
p("人間採点・有効件数(分析対象):", len(clean))
p("対象期間:", clean['dt'].min(), "〜", clean['dt'].max())

# ---------- 評価者別 ----------
p("\n== 3. 評価者別集計(有効件数ベース) ==")
g = clean.groupby('evaluator').agg(
        n=('soup','size'), soup平均=('soup','mean'),
        total平均=('total','mean')).round(1).sort_values('n', ascending=False)
p(g.to_string())

# ---------- soup と total の関係 ----------
clean['diff'] = clean['total'] - clean['soup']
p("\n== 4. スープのみ点 と 濃度込み点 の関係 ==")
p("total == soup :", int((clean['diff']==0).sum()), "件")
p("total >  soup (加点):", int((clean['diff']>0).sum()), "件")
p("total <  soup (減点):", int((clean['diff']<0).sum()), "件")
p("diff 平均:", round(clean['diff'].mean(),2), " 標準偏差:", round(clean['diff'].std(),2))
p("--- 加点ケース(total>soup) 一覧 ---")
up = clean[clean['diff']>0].sort_values('dt')
p(up[['dt','slot','brix_raw','soup','total','diff']].to_string(index=False))

# ---------- 目標Brix ----------
clean['dinner'] = clean['slot'].astype(str).str.contains('開店前') == False
p("\n== 5. 時間帯別 目標Brix(濃度込み=100の最頻Brix) ==")
for label, sub in [('開店前', clean[~clean['dinner']]), ('ディナー', clean[clean['dinner']])]:
    perfect = sub[sub['total']>=100]
    p(f"{label}: 満点n={len(perfect)} brix最頻={perfect['brix'].mode().tolist()} 平均={perfect['brix'].mean():.2f}")

# ---------- Brixズレと diff の関係（方向の検証）----------
clean['target']     = np.where(clean['dinner'], 12.0, 13.0)
clean['signed_gap'] = clean['brix'] - clean['target']
p("\n== 6. (brix-目標) と diff(total-soup) の関係 ==")
tbl = clean.groupby(clean['signed_gap'].round(1)).agg(
        n=('diff','size'), diff平均=('diff','mean')).round(2)
p(tbl.to_string())
p("corr(signed_gap, diff) =", round(clean['signed_gap'].corr(clean['diff']),3))
p("corr(|gap|,      diff) =", round(clean['signed_gap'].abs().corr(clean['diff']),3))

# ---------- 回帰 ----------
p("\n== 7. 重回帰: 濃度込み点 ~ 状態語 + 符号付きBrixズレ + 評価者 ==")
clean = clean.reset_index(drop=True)
c = clean['comment'].fillna('')
feat = {
 'good':r'良い|◎|◯|完璧|状態良|good|Good|グッド|素晴','oil_hi':r'アブラ感[がは]?強|脂感|油.*強|重い|アブラスープ|アブラ先行',
 'light':r'軽|アブラ感[がは]?(?:少|足|弱)','fire':r'火[がは]?強|火力',
 'sep':r'分離|乳化不|油層|白い|しろい|シャバ|白優先|乳化が進|乳化しすぎ','thin':r'濃度.*(?:低|薄|欲|足り)|薄い|軽そう',
 'boro':r'ボロ|崩れ|くずれ','unseen':r'見え|見ず|見に|雲|くもって|湯気|煙|沸いて|投稿|撮影|タイミング',
 'water':r'水位','gara':r'ガラ感.*(?:増|足|頑張|出す)'}
X = pd.DataFrame({k: c.str.contains(v, regex=True).astype(float) for k,v in feat.items()})
# 符号付き: 不足側ペナルティ と 超過側ペナルティ を分離（加点ケースも表現できる形）
X['gap_under'] = np.maximum(0.0, -clean['signed_gap'].values)  # 目標未満の量
X['gap_over']  = np.maximum(0.0,  clean['signed_gap'].values)  # 目標超過の量
Xd = pd.get_dummies(pd.concat([X, clean['evaluator'].reset_index(drop=True)], axis=1),
                    columns=['evaluator'], drop_first=True)
Xm = np.column_stack([np.ones(len(Xd))] + [Xd[col].values.astype(float) for col in Xd.columns])
y  = clean['total'].values.astype(float)
beta, *_ = np.linalg.lstsq(Xm, y, rcond=None)
for n,b in zip(['(切片)']+list(Xd.columns), beta): p(f"  {n:28s} {b:+7.2f}")
pred = Xm @ beta
r2 = 1 - ((y-pred)**2).sum()/((y-y.mean())**2).sum()
p(f"  R^2 = {r2:.3f}   MAE = {np.abs(y-pred).mean():.2f}   N = {len(y)}")

# ---------- 既存AIモデル(pred_score) vs 人間 ----------
p("\n== 8. 既存AIモデル pred_score vs 人間スコア ==")
m = clean[clean['ai_pred'].notna()]
if len(m):
    p("対応件数:", len(m))
    p("corr(ai_pred, soup) =", round(m['ai_pred'].corr(m['soup']),3))
    p("corr(ai_pred, total)=", round(m['ai_pred'].corr(m['total']),3))
    p("MAE(ai_pred vs total)=", round((m['ai_pred']-m['total']).abs().mean(),2))
    p("ai_pred 値分布:", m['ai_pred'].value_counts().sort_index().to_dict())

with open(os.path.join(HERE, "analysis_output.txt"), "w", encoding="utf-8") as fo:
    fo.write("\n".join(LOG))
p("\n[analysis_output.txt に保存]")
