# src/analyze_results.py from __future__ import annotations from pathlib import Path import argparse import sys import numpy as np import pandas as pd import matplotlib.pyplot as plt def _read_csv_safely(path: Path) -> pd.DataFrame: """ Устойчивое чтение CSV: пробуем ; , и авто. Выбираем тот, где получилось >= 5 колонок. """ tries = [ ("utf-8-sig", ","), # СНАЧАЛА КОММА — чаще для наших predicted.csv ("utf-8-sig", ";"), ("utf-8", ","), ("utf-8", ";"), ("utf-8-sig", None), ("utf-8", None), ] last_err = None best_df = None best_info = None for enc, sep in tries: try: if sep is None: df = pd.read_csv(path, encoding=enc, sep=None, engine="python") got = f"auto" else: df = pd.read_csv(path, encoding=enc, sep=sep) got = sep # эвристика: нормальные файлы имеют >= 5 колонок if df.shape[1] >= 5: print(f"[i] CSV прочитан с encoding='{enc}', sep='{got}'") return df # запомним самый «лучший» (по числу колонок), если ни один не пройдёт порог if best_df is None or df.shape[1] > best_df.shape[1]: best_df, best_info = df, (enc, got) except Exception as e: last_err = e if best_df is not None: enc, got = best_info print(f"[!] Не удалось надёжно определить разделитель, взят лучший вариант encoding='{enc}', sep='{got}' (cols={best_df.shape[1]})") return best_df raise last_err if last_err else RuntimeError("Не удалось прочитать CSV") def _resolve_col(df: pd.DataFrame, candidates) -> str: for c in candidates: if c in df.columns: return c raise KeyError(f"Не удалось найти колонку из вариантов: {candidates}\nИмеющиеся колонки: {list(df.columns)}") def mae(y_true: np.ndarray, y_pred: np.ndarray) -> float: return float(np.mean(np.abs(y_true - y_pred))) def main(): ap = argparse.ArgumentParser() ap.add_argument("--input", type=str, default="data/processed/predicted.csv", help="Путь к CSV с колонкой pred_score") args = ap.parse_args() in_path = Path(args.input) if not in_path.exists(): print(f"❌ Не найден файл с предсказаниями: {in_path}") sys.exit(1) df = _read_csv_safely(in_path) # Колонки y_col = _resolve_col(df, ["Оценка экзаменатора", "оценка экзаменатора", "score", "y", "target"]) p_col = _resolve_col(df, ["pred_score", "pred", "prediction"]) # Колонка номера вопроса: поддержим оба варианта if "№ вопроса" in df.columns: q_col = "№ вопроса" else: q_col = _resolve_col(df, ["question_number", "q", "номер вопроса", "№ вопроса"]) # Числовые массивы y = pd.to_numeric(df[y_col], errors="coerce").to_numpy() p = pd.to_numeric(df[p_col], errors="coerce").to_numpy() # Общая MAE m_all = mae(y, p) print(f"MAE (вся выборка): {m_all:.3f}\n") # MAE по вопросам g = (df[[q_col]].copy()) g["y"] = y g["p"] = p mae_by_q = ( g.groupby(q_col, as_index=True) .apply(lambda s: mae(s["y"].to_numpy(), s["p"].to_numpy())) .to_frame("MAE") ) print("MAE по вопросам:") print(mae_by_q) out_dir = Path("reports"); out_dir.mkdir(parents=True, exist_ok=True) mae_by_q.to_csv(out_dir / "metrics_summary.csv", encoding="utf-8-sig") print(f"\n✅ Сохранено: {out_dir / 'metrics_summary.csv'}") # Графики # 1) гистограмма ошибок err = np.abs(y - p) plt.figure() plt.hist(err[~np.isnan(err)], bins=30) plt.title("Absolute Error Histogram") plt.xlabel("|y - pred|"); plt.ylabel("count") plt.tight_layout(); plt.savefig(out_dir / "error_hist.png"); plt.close() print(f"📊 Гистограмма: {out_dir / 'error_hist.png'}") # 2) mae_by_q барплот plt.figure() mae_by_q["MAE"].plot(kind="bar") plt.ylabel("MAE"); plt.title("MAE by question") plt.tight_layout(); plt.savefig(out_dir / "mae_by_q.png"); plt.close() print(f"📊 MAE по вопросам: {out_dir / 'mae_by_q.png'}") # 3) scatter plt.figure() plt.scatter(y, p, alpha=0.3) plt.xlabel("true"); plt.ylabel("pred"); plt.title("Pred vs True") plt.tight_layout(); plt.savefig(out_dir / "pred_vs_true.png"); plt.close() print(f"📊 Scatter: {out_dir / 'pred_vs_true.png'}") if __name__ == "__main__": main()