Download scripts/evaluate_baselines.py from aungkomyint/PlantPerturb-Small: direct link, hf CLI and curl.
- Browser
- Download file 8.61 kB
-
https://huggingface.co/aungkomyint/PlantPerturb-Small/resolve/main/scripts/evaluate_baselines.py
- Command line
-
hf download hf://aungkomyint/PlantPerturb-Small/scripts/evaluate_baselines.py
-
curl -L -o evaluate_baselines.py https://huggingface.co/aungkomyint/PlantPerturb-Small/resolve/main/scripts/evaluate_baselines.py
8.61 kB
| """Create ecotype-level splits and evaluate simple drought-response baselines.""" | |
| from pathlib import Path | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| import pandas as pd | |
| import seaborn as sns | |
| from scipy.stats import pearsonr, spearmanr | |
| PROJECT_ROOT = Path(__file__).resolve().parents[1] | |
| DATA_DIR = PROJECT_ROOT / "data" / "processed" | |
| RESULTS_DIR = PROJECT_ROOT / "results" / "baselines" | |
| FIGURES_DIR = RESULTS_DIR / "figures" | |
| EXPRESSION_PATH = DATA_DIR / "expression_log2_cpm.tsv.gz" | |
| GROUPS_PATH = DATA_DIR / "sample_groups.csv" | |
| SPLITS_PATH = DATA_DIR / "ecotype_splits.csv" | |
| RANDOM_SEED = 42 | |
| TRAIN_FRACTION = 0.70 | |
| VALIDATION_FRACTION = 0.15 | |
| CONTROL = "#356859" | |
| DROUGHT = "#C58A2B" | |
| BLUE = "#3D6FB4" | |
| def build_ecotype_matrices() -> tuple[pd.DataFrame, pd.DataFrame]: | |
| expression = pd.read_csv(EXPRESSION_PATH, sep="\t", index_col=0) | |
| groups = pd.read_csv(GROUPS_PATH).set_index("sample_group_id") | |
| groups = groups.loc[expression.columns] | |
| sample_expression = expression.T.join(groups[["ecotype", "condition"]]) | |
| # Col-0 and Oy-0 have repeated experimental blocks. Averaging creates one | |
| # descriptive control vector and one drought vector for each ecotype. | |
| ecotype_condition = sample_expression.groupby(["ecotype", "condition"]).mean() | |
| control = ecotype_condition.xs("control", level="condition").sort_index() | |
| drought = ecotype_condition.xs("drought", level="condition").sort_index() | |
| if not control.index.equals(drought.index): | |
| raise ValueError("Control and drought ecotype sets are different.") | |
| if not control.columns.equals(drought.columns): | |
| raise ValueError("Control and drought gene sets are different.") | |
| delta = drought - control | |
| return control, delta | |
| def make_splits(ecotypes: pd.Index) -> pd.DataFrame: | |
| rng = np.random.default_rng(RANDOM_SEED) | |
| shuffled = rng.permutation(np.asarray(sorted(ecotypes))) | |
| n_total = len(shuffled) | |
| n_train = round(TRAIN_FRACTION * n_total) | |
| n_validation = round(VALIDATION_FRACTION * n_total) | |
| split = np.empty(n_total, dtype=object) | |
| split[:n_train] = "train" | |
| split[n_train : n_train + n_validation] = "validation" | |
| split[n_train + n_validation :] = "test" | |
| splits = pd.DataFrame({"ecotype": shuffled, "split": split}) | |
| splits["random_seed"] = RANDOM_SEED | |
| return splits.sort_values("ecotype").reset_index(drop=True) | |
| def safe_correlations(actual: np.ndarray, predicted: np.ndarray) -> tuple[float, float]: | |
| if np.allclose(predicted, predicted[0]) or np.allclose(actual, actual[0]): | |
| return np.nan, np.nan | |
| return pearsonr(actual, predicted).statistic, spearmanr(actual, predicted).statistic | |
| def evaluate_prediction( | |
| actual_delta: pd.DataFrame, | |
| predicted_delta: np.ndarray, | |
| baseline: str, | |
| split: str, | |
| ) -> pd.DataFrame: | |
| records = [] | |
| for ecotype, row in actual_delta.iterrows(): | |
| actual = row.to_numpy() | |
| error = predicted_delta - actual | |
| pearson, spearman = safe_correlations(actual, predicted_delta) | |
| records.append( | |
| { | |
| "ecotype": ecotype, | |
| "split": split, | |
| "baseline": baseline, | |
| "mse": np.mean(error**2), | |
| "mae": np.mean(np.abs(error)), | |
| "pearson": pearson, | |
| "spearman": spearman, | |
| } | |
| ) | |
| return pd.DataFrame(records) | |
| def summarize_metrics(metrics: pd.DataFrame) -> pd.DataFrame: | |
| value_columns = ["mse", "mae", "pearson", "spearman"] | |
| summary = ( | |
| metrics.groupby(["split", "baseline"])[value_columns] | |
| .agg(["mean", "median", "std"]) | |
| .reset_index() | |
| ) | |
| summary.columns = [ | |
| "_".join(part for part in column if part) | |
| if isinstance(column, tuple) | |
| else column | |
| for column in summary.columns | |
| ] | |
| return summary | |
| def plot_metric_distributions(metrics: pd.DataFrame) -> None: | |
| evaluation = metrics.loc[metrics["split"].isin(["validation", "test"])].copy() | |
| sns.set_theme(style="whitegrid", context="notebook") | |
| fig, axes = plt.subplots(1, 2, figsize=(10, 4.2)) | |
| sns.boxplot( | |
| data=evaluation, | |
| x="split", | |
| y="mae", | |
| hue="baseline", | |
| palette={"zero_change": "#9AA4A6", "training_mean": BLUE}, | |
| ax=axes[0], | |
| ) | |
| axes[0].set(title="Absolute error by held-out ecotype", xlabel="Split", ylabel="MAE") | |
| axes[0].legend(title="Baseline", frameon=False) | |
| correlation = evaluation.loc[evaluation["baseline"].eq("training_mean")] | |
| correlation_long = correlation.melt( | |
| id_vars=["ecotype", "split"], | |
| value_vars=["pearson", "spearman"], | |
| var_name="metric", | |
| value_name="correlation", | |
| ) | |
| sns.boxplot( | |
| data=correlation_long, | |
| x="split", | |
| y="correlation", | |
| hue="metric", | |
| palette={"pearson": CONTROL, "spearman": DROUGHT}, | |
| ax=axes[1], | |
| ) | |
| axes[1].axhline(0, color="#333333", linewidth=0.8) | |
| axes[1].set( | |
| title="Training-mean response correlation", | |
| xlabel="Split", | |
| ylabel="Correlation across genes", | |
| ) | |
| axes[1].legend(title="Metric", frameon=False) | |
| fig.tight_layout() | |
| fig.savefig(FIGURES_DIR / "baseline_metric_distributions.png", dpi=220) | |
| plt.close(fig) | |
| def plot_representative_test_example( | |
| delta: pd.DataFrame, metrics: pd.DataFrame, mean_response: pd.Series | |
| ) -> None: | |
| test_metrics = metrics.loc[ | |
| metrics["split"].eq("test") & metrics["baseline"].eq("training_mean") | |
| ].sort_values("pearson") | |
| representative = test_metrics.iloc[len(test_metrics) // 2]["ecotype"] | |
| actual = delta.loc[representative] | |
| predicted = mean_response | |
| sns.set_theme(style="whitegrid", context="notebook") | |
| fig, ax = plt.subplots(figsize=(6.3, 5.3)) | |
| ax.hexbin( | |
| actual, | |
| predicted, | |
| gridsize=55, | |
| mincnt=1, | |
| cmap="YlGnBu", | |
| linewidths=0, | |
| ) | |
| limit = max(abs(actual).max(), abs(predicted).max()) | |
| ax.plot([-limit, limit], [-limit, limit], linestyle="--", color="#555555") | |
| pearson_value = test_metrics.loc[ | |
| test_metrics["ecotype"].eq(representative), "pearson" | |
| ].iloc[0] | |
| ax.set( | |
| title=f"Representative test ecotype: {representative}", | |
| xlabel="Actual drought - control expression", | |
| ylabel="Training-mean predicted response", | |
| ) | |
| ax.text( | |
| 0.03, | |
| 0.97, | |
| f"Pearson r = {pearson_value:.3f}", | |
| transform=ax.transAxes, | |
| va="top", | |
| bbox={"facecolor": "white", "alpha": 0.85, "edgecolor": "none"}, | |
| ) | |
| fig.tight_layout() | |
| fig.savefig(FIGURES_DIR / "representative_test_prediction.png", dpi=220) | |
| plt.close(fig) | |
| def main() -> None: | |
| _, delta = build_ecotype_matrices() | |
| splits = make_splits(delta.index) | |
| split_lookup = splits.set_index("ecotype")["split"] | |
| train_ecotypes = split_lookup.index[split_lookup.eq("train")] | |
| mean_response = delta.loc[train_ecotypes].mean(axis=0) | |
| zero_response = np.zeros(delta.shape[1], dtype=float) | |
| metric_frames = [] | |
| for split_name in ["train", "validation", "test"]: | |
| ecotypes = split_lookup.index[split_lookup.eq(split_name)] | |
| actual = delta.loc[ecotypes] | |
| metric_frames.append( | |
| evaluate_prediction(actual, zero_response, "zero_change", split_name) | |
| ) | |
| metric_frames.append( | |
| evaluate_prediction( | |
| actual, mean_response.to_numpy(), "training_mean", split_name | |
| ) | |
| ) | |
| metrics = pd.concat(metric_frames, ignore_index=True) | |
| summary = summarize_metrics(metrics) | |
| RESULTS_DIR.mkdir(parents=True, exist_ok=True) | |
| FIGURES_DIR.mkdir(parents=True, exist_ok=True) | |
| splits.to_csv(SPLITS_PATH, index=False) | |
| metrics.to_csv(RESULTS_DIR / "baseline_metrics_by_ecotype.csv", index=False) | |
| summary.to_csv(RESULTS_DIR / "baseline_summary.csv", index=False) | |
| mean_response.rename("mean_delta").to_csv( | |
| RESULTS_DIR / "training_mean_response.tsv.gz", | |
| sep="\t", | |
| compression="gzip", | |
| ) | |
| plot_metric_distributions(metrics) | |
| plot_representative_test_example(delta, metrics, mean_response) | |
| print("Split sizes:") | |
| print(splits["split"].value_counts().reindex(["train", "validation", "test"])) | |
| print("\nValidation and test summary (means across ecotypes):") | |
| columns = ["split", "baseline", "mse_mean", "mae_mean", "pearson_mean", "spearman_mean"] | |
| print(summary.loc[summary["split"].ne("train"), columns].to_string(index=False)) | |
| print(f"\nWrote outputs to {RESULTS_DIR}") | |
| if __name__ == "__main__": | |
| main() | |