Download scripts/evaluate_external_dataset.py from aungkomyint/PlantPerturb-Small: direct link, hf CLI and curl.
- Browser
- Download file 7.67 kB
-
https://huggingface.co/aungkomyint/PlantPerturb-Small/resolve/main/scripts/evaluate_external_dataset.py
- Command line
-
hf download hf://aungkomyint/PlantPerturb-Small/scripts/evaluate_external_dataset.py
-
curl -L -o evaluate_external_dataset.py https://huggingface.co/aungkomyint/PlantPerturb-Small/resolve/main/scripts/evaluate_external_dataset.py
7.67 kB
| """Evaluate the fitted PCA-ridge model on the independent E-MTAB-3279 study.""" | |
| from pathlib import Path | |
| import joblib | |
| 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] | |
| EXTERNAL_DIR = PROJECT_ROOT / "data" / "raw" / "E-MTAB-3279" | |
| PROCESSED_DIR = PROJECT_ROOT / "data" / "processed" | |
| RESULTS_DIR = PROJECT_ROOT / "results" / "external_validation" | |
| FIGURES_DIR = RESULTS_DIR / "figures" | |
| COUNTS_PATH = EXTERNAL_DIR / "Accessions.snp.htseq.txt" | |
| METADATA_PATH = EXTERNAL_DIR / "E-MTAB-3279.sdrf.txt" | |
| MODEL_PATH = PROJECT_ROOT / "models" / "pca_ridge_model.joblib" | |
| 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 metrics(actual: pd.DataFrame, predicted: np.ndarray, model_name: str) -> pd.DataFrame: | |
| rows = [] | |
| for position, ecotype in enumerate(actual.index): | |
| observed = actual.loc[ecotype].to_numpy() | |
| estimate = predicted[position] | |
| error = estimate - observed | |
| pearson, spearman = safe_correlations(observed, estimate) | |
| rows.append( | |
| { | |
| "ecotype": ecotype, | |
| "model": model_name, | |
| "mse": np.mean(error**2), | |
| "mae": np.mean(np.abs(error)), | |
| "pearson": pearson, | |
| "spearman": spearman, | |
| } | |
| ) | |
| return pd.DataFrame(rows) | |
| def load_external_expression(training_genes: pd.Index) -> tuple[pd.DataFrame, pd.DataFrame]: | |
| counts = pd.read_csv(COUNTS_PATH, sep="\t", index_col=0) | |
| counts.index.name = "gene_id" | |
| metadata = pd.read_csv(METADATA_PATH, sep="\t", keep_default_na=False) | |
| metadata = metadata.drop_duplicates("Assay Name").set_index("Assay Name") | |
| assay_names = counts.columns.str.replace( | |
| ".snp.concordant_uniq.htseq", "", regex=False | |
| ) | |
| # The processed matrix labels the fourth block as C2x/S2x, while the SDRF | |
| # metadata labels the same assays C4/S4. | |
| assay_names = assay_names.str.replace(r"([CS])2x$", r"\g<1>4", regex=True) | |
| if not assay_names.isin(metadata.index).all(): | |
| missing = assay_names[~assay_names.isin(metadata.index)].tolist() | |
| raise ValueError(f"External count columns missing from metadata: {missing}") | |
| missing_genes = training_genes.difference(counts.index) | |
| if len(missing_genes): | |
| raise ValueError(f"External counts lack {len(missing_genes)} training genes.") | |
| library_sizes = counts.sum(axis=0) | |
| log2_cpm = np.log2(counts.div(library_sizes, axis="columns") * 1_000_000 + 1) | |
| log2_cpm = log2_cpm.loc[training_genes] | |
| sample_metadata = metadata.loc[assay_names].reset_index() | |
| sample_metadata["count_column"] = counts.columns.to_numpy() | |
| sample_metadata["condition"] = sample_metadata[ | |
| "Factor Value[growth condition]" | |
| ].str.strip() | |
| sample_metadata["ecotype"] = sample_metadata["Factor Value[ecotype]"].str.strip() | |
| sample_expression = log2_cpm.T.reset_index(names="count_column").merge( | |
| sample_metadata[["count_column", "ecotype", "condition"]], | |
| on="count_column", | |
| validate="one_to_one", | |
| ) | |
| ecotype_condition = sample_expression.drop(columns="count_column").groupby( | |
| ["ecotype", "condition"] | |
| ).mean() | |
| control = ecotype_condition.xs("control", level="condition").sort_index() | |
| drought = ecotype_condition.xs("mild drought", level="condition").sort_index() | |
| if not control.index.equals(drought.index): | |
| raise ValueError("External control and drought ecotypes do not align.") | |
| return control, drought - control | |
| def internal_development_mean(training_genes: pd.Index) -> tuple[np.ndarray, pd.DataFrame]: | |
| expression = pd.read_csv( | |
| PROCESSED_DIR / "expression_log2_cpm.tsv.gz", sep="\t", index_col=0 | |
| ) | |
| groups = pd.read_csv(PROCESSED_DIR / "sample_groups.csv").set_index("sample_group_id") | |
| groups = groups.loc[expression.columns] | |
| splits = pd.read_csv(PROCESSED_DIR / "ecotype_splits.csv").set_index("ecotype") | |
| joined = expression.T.join(groups[["ecotype", "condition"]]) | |
| means = joined.groupby(["ecotype", "condition"]).mean() | |
| internal_delta = means.xs("drought", level="condition") - means.xs( | |
| "control", level="condition" | |
| ) | |
| development_ids = splits.index[splits["split"].isin(["train", "validation"])] | |
| return internal_delta.loc[development_ids, training_genes].mean(axis=0).to_numpy(), splits | |
| def main() -> None: | |
| model = joblib.load(MODEL_PATH) | |
| training_genes = pd.Index(model.feature_names_in_) | |
| control, external_delta = load_external_expression(training_genes) | |
| development_mean, internal_splits = internal_development_mean(training_genes) | |
| model_prediction = model.predict(control) | |
| mean_prediction = np.tile(development_mean, (len(control), 1)) | |
| zero_prediction = np.zeros_like(model_prediction) | |
| result = pd.concat( | |
| [ | |
| metrics(external_delta, zero_prediction, "zero_change"), | |
| metrics(external_delta, mean_prediction, "development_mean"), | |
| metrics(external_delta, model_prediction, "pca_ridge"), | |
| ], | |
| ignore_index=True, | |
| ) | |
| internal_name = result["ecotype"].replace({"Shahdara": "Sha"}) | |
| result["relationship_to_model_development"] = internal_name.map( | |
| internal_splits["split"] | |
| ).fillna("external_only") | |
| summary = result.groupby("model")[["mse", "mae", "pearson", "spearman"]].agg( | |
| ["mean", "median", "std"] | |
| ) | |
| summary.columns = ["_".join(column) for column in summary.columns] | |
| summary = summary.reset_index() | |
| RESULTS_DIR.mkdir(parents=True, exist_ok=True) | |
| FIGURES_DIR.mkdir(parents=True, exist_ok=True) | |
| result.to_csv(RESULTS_DIR / "external_metrics_by_ecotype.csv", index=False) | |
| summary.to_csv(RESULTS_DIR / "external_summary.csv", index=False) | |
| sns.set_theme(style="whitegrid", context="notebook") | |
| plot_data = result.loc[result["model"].ne("zero_change")] | |
| fig, axes = plt.subplots(1, 2, figsize=(10, 4.2)) | |
| sns.boxplot(data=result, x="model", y="mae", hue="model", legend=False, | |
| palette=["#9AA4A6", "#3D6FB4", "#C58A2B"], ax=axes[0]) | |
| sns.stripplot(data=result, x="model", y="mae", color="#222222", size=4, | |
| alpha=0.7, ax=axes[0]) | |
| axes[0].set(title="External-study error", xlabel="Model", ylabel="MAE") | |
| axes[0].tick_params(axis="x", rotation=15) | |
| sns.barplot(data=plot_data, x="ecotype", y="pearson", hue="model", | |
| palette={"development_mean": "#3D6FB4", "pca_ridge": "#C58A2B"}, | |
| ax=axes[1]) | |
| axes[1].axhline(0, color="#333333", linewidth=0.8) | |
| axes[1].set(title="External correlation by ecotype", xlabel="Ecotype", | |
| ylabel="Pearson correlation") | |
| axes[1].tick_params(axis="x", rotation=30) | |
| axes[1].legend(title="Model", frameon=False) | |
| fig.tight_layout() | |
| fig.savefig(FIGURES_DIR / "external_validation_comparison.png", dpi=220) | |
| plt.close(fig) | |
| print(f"External ecotypes: {', '.join(control.index)}") | |
| print("Relationship to original split:") | |
| print(result.drop_duplicates("ecotype")[["ecotype", "relationship_to_model_development"]].to_string(index=False)) | |
| print("\nExternal summary (means across six ecotypes):") | |
| print(summary[["model", "mse_mean", "mae_mean", "pearson_mean", "spearman_mean"]].to_string(index=False)) | |
| if __name__ == "__main__": | |
| main() | |