Skip to content

Plotting

neureptrace.plot_time_decode

plot_time_decode_results(results_csv, out_path, *, metrics=METRIC_COLUMNS, chance=None, title=None)

Plot time-resolved decoding metrics from a raw or aggregated CSV file.

Source code in src/neureptrace/plot_time_decode.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def plot_time_decode_results(
    results_csv: Path,
    out_path: Path,
    *,
    metrics: tuple[str, ...] = METRIC_COLUMNS,
    chance: float | None = None,
    title: str | None = None,
) -> Path:
    """Plot time-resolved decoding metrics from a raw or aggregated CSV file."""
    metrics = _validate_metrics(metrics)
    summary = _summary_from_csv(results_csv, metrics=metrics)
    group_columns = _group_columns(summary)
    plot_groups = list(summary.groupby(group_columns, sort=True, dropna=False)) if group_columns else [(None, summary)]
    n_metrics = len(metrics)
    n_cols = 2 if n_metrics > 1 else 1
    n_rows = (n_metrics + n_cols - 1) // n_cols
    fig, axes = plt.subplots(n_rows, n_cols, figsize=(6 * n_cols, 3.6 * n_rows), squeeze=False)
    axes_flat = axes.ravel()

    for index, metric in enumerate(metrics):
        ax = axes_flat[index]
        for group_name, group in plot_groups:
            mean = group[f"{metric}_mean"]
            sem = group[f"{metric}_sem"].fillna(0.0)
            label = _group_label(group_name) if group_name is not None else metric
            ax.plot(group["time"], mean, label=label)
            ax.fill_between(group["time"], mean - sem, mean + sem, alpha=0.2)
        if metric == "accuracy" and chance is not None:
            ax.axhline(chance, color="0.4", linestyle="--", linewidth=1.0, label="chance")
        ax.axvline(0.0, color="0.6", linestyle=":", linewidth=1.0)
        ax.set_xlabel("Time (s)")
        ax.set_ylabel(metric)
        ax.set_title(metric.replace("_", " ").title())
        ax.legend(loc="best")

    for index in range(n_metrics, len(axes_flat)):
        axes_flat[index].axis("off")

    if title:
        fig.suptitle(title)
    fig.tight_layout()
    out_path.parent.mkdir(parents=True, exist_ok=True)
    fig.savefig(out_path, dpi=150)
    plt.close(fig)
    return out_path

neureptrace.plot_calibration

plot_reliability_diagram(reliability_bins_csv, out_path, *, time_window=None, title=None)

Plot a reliability diagram from aggregated reliability-bin CSV output.

Source code in src/neureptrace/plot_calibration.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
def plot_reliability_diagram(
    reliability_bins_csv: Path,
    out_path: Path,
    *,
    time_window: tuple[float, float] | None = None,
    title: str | None = None,
) -> Path:
    """Plot a reliability diagram from aggregated reliability-bin CSV output."""
    curve = summarize_reliability_curve(pd.read_csv(reliability_bins_csv), time_window=time_window)
    if curve.empty:
        raise ValueError("No reliability-bin rows available to plot.")
    groups = list(curve[["decoder", "emission_mode"]].drop_duplicates().itertuples(index=False, name=None))

    n_groups = len(groups)
    n_cols = min(3, n_groups)
    n_rows = (n_groups + n_cols - 1) // n_cols
    fig, axes = plt.subplots(n_rows, n_cols, figsize=(4.2 * n_cols, 4.0 * n_rows), squeeze=False)
    axes_flat = axes.ravel()

    for index, (decoder, emission_mode) in enumerate(groups):
        ax = axes_flat[index]
        group = curve[(curve["decoder"] == decoder) & (curve["emission_mode"] == emission_mode) & (curve["n_samples"] > 0)].sort_values("bin")
        ax.plot([0.0, 1.0], [0.0, 1.0], color="0.5", linestyle="--", linewidth=1.0, label="perfect")
        ax.plot(group["confidence"], group["accuracy"], color="tab:blue", linewidth=1.5)
        sizes = 25 + 125 * group["n_samples"] / max(group["n_samples"].max(), 1)
        ax.scatter(group["confidence"], group["accuracy"], s=sizes, color="tab:blue", edgecolor="white", linewidth=0.6)
        ax.set_xlim(0.0, 1.0)
        ax.set_ylim(0.0, 1.0)
        ax.set_xlabel("Mean confidence")
        ax.set_ylabel("Empirical accuracy")
        ax.set_title(_display_label({"decoder": decoder, "emission_mode": emission_mode}))
        ax.grid(True, color="0.9", linewidth=0.8)

    for index in range(n_groups, len(axes_flat)):
        axes_flat[index].axis("off")

    if title:
        fig.suptitle(title)
    fig.tight_layout()
    out_path.parent.mkdir(parents=True, exist_ok=True)
    fig.savefig(out_path, dpi=160)
    plt.close(fig)
    return out_path

summarize_reliability_curve(reliability_bins, *, time_window=None)

Aggregate reliability bins over an optional time window for plotting.

Source code in src/neureptrace/plot_calibration.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def summarize_reliability_curve(
    reliability_bins: pd.DataFrame,
    *,
    time_window: tuple[float, float] | None = None,
) -> pd.DataFrame:
    """Aggregate reliability bins over an optional time window for plotting."""
    required = {"time", "bin", "bin_left", "bin_right", "n_samples", "accuracy", "confidence"}
    missing = sorted(required.difference(reliability_bins.columns))
    if missing:
        raise ValueError(f"Reliability bins are missing required columns: {missing}")

    bins = _window(reliability_bins.copy(), time_window)
    if "decoder" not in bins.columns:
        bins["decoder"] = "overall"
    else:
        bins["decoder"] = bins["decoder"].astype("object").where(bins["decoder"].notna(), "overall")
    if "emission_mode" not in bins.columns:
        bins["emission_mode"] = "calibrated"
    else:
        bins["emission_mode"] = bins["emission_mode"].astype("object").where(bins["emission_mode"].notna(), "calibrated")

    mass_column = _SAMPLE_WEIGHT_COLUMN if _SAMPLE_WEIGHT_COLUMN in bins.columns else "n_samples"
    rows = []
    group_columns = ["decoder", "emission_mode", "bin", "bin_left", "bin_right"]
    for keys, group in bins.groupby(group_columns, sort=True):
        sample_counts = _validated_sample_counts(group["n_samples"])
        n_samples = int(sample_counts.sum())
        aggregation_mass = group[mass_column]
        if mass_column == _SAMPLE_WEIGHT_COLUMN:
            aggregation_mass = aggregation_mass.where(aggregation_mass.notna(), sample_counts)
        aggregation_mass = _validated_aggregation_mass(aggregation_mass, column=mass_column)
        accuracy_values = _validated_probability_values(group["accuracy"], aggregation_mass, column="accuracy")
        confidence_values = _validated_probability_values(group["confidence"], aggregation_mass, column="confidence")
        max_mass = float(aggregation_mass.max())
        if max_mass > 0.0:
            scaled_mass = aggregation_mass / max_mass
            weights = scaled_mass / float(scaled_mass.sum())
            accuracy = float((accuracy_values.fillna(0.0) * weights).sum())
            confidence = float((confidence_values.fillna(0.0) * weights).sum())
        else:
            accuracy = float("nan")
            confidence = float("nan")
        rows.append(
            {
                **dict(zip(group_columns, keys)),
                "n_samples": n_samples,
                "accuracy": accuracy,
                "confidence": confidence,
                "gap": accuracy - confidence if max_mass > 0.0 else float("nan"),
            }
        )
    return pd.DataFrame(rows)