Spaces:
Sleeping
Sleeping
| """Plot generation for reports.""" | |
| from __future__ import annotations | |
| from pathlib import Path | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| import pandas as pd | |
| from meowcontext_lab.data import EXPECTED_LABELS | |
| from meowcontext_lab.evaluate import approximate_confusion_matrix | |
| from meowcontext_lab.models import predict_from_features | |
| def _finish(path: Path) -> Path: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| plt.tight_layout() | |
| plt.savefig(path, dpi=160) | |
| plt.close() | |
| return path | |
| def plot_context_counts(df: pd.DataFrame, path: Path) -> Path: | |
| counts = df["context"].value_counts().reindex(EXPECTED_LABELS) | |
| plt.figure(figsize=(7, 4)) | |
| colors = ["#4c78a8", "#f58518", "#54a24b"] | |
| plt.bar(counts.index, counts.values, color=colors) | |
| plt.ylabel("Clips") | |
| plt.xticks(rotation=18, ha="right") | |
| plt.title("OpenFARM CatMeows context counts") | |
| return _finish(path) | |
| def plot_cat_context_heatmap(df: pd.DataFrame, path: Path) -> Path: | |
| table = pd.crosstab(df["cat_id"], df["context"]).reindex(columns=EXPECTED_LABELS, fill_value=0) | |
| plt.figure(figsize=(7, 6)) | |
| plt.imshow(table.to_numpy(), aspect="auto", cmap="viridis") | |
| plt.colorbar(label="Clips") | |
| plt.yticks(range(len(table.index)), table.index, fontsize=7) | |
| plt.xticks(range(len(EXPECTED_LABELS)), EXPECTED_LABELS, rotation=20, ha="right") | |
| plt.title("Context distribution by cat") | |
| return _finish(path) | |
| def plot_random_vs_heldout(results: pd.DataFrame, path: Path) -> Path: | |
| x = np.arange(len(results)) | |
| width = 0.38 | |
| plt.figure(figsize=(9, 4.8)) | |
| plt.bar(x - width / 2, results["random_split"], width, label="Random split", color="#4c78a8") | |
| plt.bar(x + width / 2, results["cat_heldout"], width, label="Cat-heldout", color="#e45756") | |
| plt.axhline(0.333, color="#333333", linestyle=":", linewidth=1, label="Chance") | |
| plt.ylabel("Balanced accuracy") | |
| plt.ylim(0, 0.72) | |
| plt.xticks(x, results["model"], rotation=22, ha="right") | |
| plt.legend(frameon=False) | |
| plt.title("Random split vs cat-heldout evaluation") | |
| return _finish(path) | |
| def plot_confusion_matrix(model: str, split: str, score: float, path: Path) -> Path: | |
| matrix = approximate_confusion_matrix(score) | |
| plt.figure(figsize=(5, 4.5)) | |
| plt.imshow(matrix, cmap="Blues") | |
| plt.colorbar(label="Illustrative clips") | |
| plt.xticks(range(len(EXPECTED_LABELS)), EXPECTED_LABELS, rotation=25, ha="right") | |
| plt.yticks(range(len(EXPECTED_LABELS)), EXPECTED_LABELS) | |
| for row_idx in range(matrix.shape[0]): | |
| for col_idx in range(matrix.shape[1]): | |
| plt.text(col_idx, row_idx, str(matrix[row_idx, col_idx]), ha="center", va="center") | |
| plt.xlabel("Predicted") | |
| plt.ylabel("Actual") | |
| plt.title(f"{model} ({split})") | |
| return _finish(path) | |
| def plot_per_class_recall(results: pd.DataFrame, path: Path) -> Path: | |
| rows = [] | |
| offsets = np.array([-0.035, 0.0, 0.035]) | |
| for _, row in results.iterrows(): | |
| for split in ("random_split", "cat_heldout"): | |
| base = float(row[split]) | |
| for label, offset in zip(EXPECTED_LABELS, offsets, strict=True): | |
| rows.append( | |
| { | |
| "model": row["model"], | |
| "split": split, | |
| "context": label, | |
| "recall": min(1.0, max(0.0, base + float(offset))), | |
| } | |
| ) | |
| recall_df = pd.DataFrame(rows) | |
| pivot = recall_df.groupby(["model", "split"])["recall"].mean().unstack() | |
| plt.figure(figsize=(9, 4.8)) | |
| x = np.arange(len(pivot.index)) | |
| width = 0.38 | |
| plt.bar(x - width / 2, pivot["random_split"], width, color="#4c78a8", label="Random split") | |
| plt.bar(x + width / 2, pivot["cat_heldout"], width, color="#e45756", label="Cat-heldout") | |
| plt.ylabel("Mean per-class recall") | |
| plt.ylim(0, 0.72) | |
| plt.xticks(x, pivot.index, rotation=22, ha="right") | |
| plt.legend(frameon=False) | |
| plt.title("Per-class recall summary") | |
| return _finish(path) | |
| def plot_demo_examples( | |
| bundle: dict[str, object], feature_examples: pd.DataFrame, path: Path | |
| ) -> Path: | |
| rows = [] | |
| for _, row in feature_examples.iterrows(): | |
| features = { | |
| "duration_sec": row["duration_sec"], | |
| "rms_energy": row["rms_energy"], | |
| "peak_abs_amplitude": row["peak_abs_amplitude"], | |
| "zero_crossing_rate": row["zero_crossing_rate"], | |
| "spectral_centroid_hz": row["spectral_centroid_hz"], | |
| } | |
| prediction = predict_from_features(bundle, features) | |
| for label, probability in prediction.probabilities.items(): | |
| rows.append( | |
| { | |
| "example": row["context"], | |
| "context": label, | |
| "probability": probability, | |
| } | |
| ) | |
| probs = pd.DataFrame(rows) | |
| pivot = probs.pivot(index="example", columns="context", values="probability").reindex( | |
| columns=EXPECTED_LABELS | |
| ) | |
| x = np.arange(len(pivot.index)) | |
| width = 0.25 | |
| plt.figure(figsize=(8, 4.5)) | |
| colors = ["#4c78a8", "#f58518", "#54a24b"] | |
| for idx, label in enumerate(EXPECTED_LABELS): | |
| plt.bar(x + (idx - 1) * width, pivot[label], width, label=label, color=colors[idx]) | |
| plt.ylabel("Predicted probability") | |
| plt.ylim(0, 1) | |
| plt.xticks(x, pivot.index, rotation=18, ha="right") | |
| plt.legend(frameon=False) | |
| plt.title("Demo model example probabilities") | |
| return _finish(path) | |