Download scripts/train_pls_model.py from aungkomyint/PlantPerturb-Small: direct link, hf CLI and curl.
- Browser
- Download file 8.6 kB
-
https://huggingface.co/aungkomyint/PlantPerturb-Small/resolve/main/scripts/train_pls_model.py
- Command line
-
hf download hf://aungkomyint/PlantPerturb-Small/scripts/train_pls_model.py
-
curl -L -o train_pls_model.py https://huggingface.co/aungkomyint/PlantPerturb-Small/resolve/main/scripts/train_pls_model.py
8.6 kB
| """Train a supervised Partial Least Squares drought-response model.""" | |
| from pathlib import Path | |
| import json | |
| 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 | |
| from sklearn.cross_decomposition import PLSRegression | |
| from sklearn.pipeline import Pipeline | |
| from sklearn.preprocessing import StandardScaler | |
| PROJECT_ROOT = Path(__file__).resolve().parents[1] | |
| DATA_DIR = PROJECT_ROOT / "data" / "processed" | |
| RESULTS_DIR = PROJECT_ROOT / "results" / "pls" | |
| FIGURES_DIR = RESULTS_DIR / "figures" | |
| MODEL_DIR = PROJECT_ROOT / "models" | |
| COMPONENT_GRID = [1, 2, 3, 5, 10] | |
| N_INPUT_GENES = 2_000 | |
| MAX_ITERATIONS = 500 | |
| def load_data() -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: | |
| expression = pd.read_csv( | |
| DATA_DIR / "expression_log2_cpm.tsv.gz", sep="\t", index_col=0 | |
| ) | |
| groups = pd.read_csv(DATA_DIR / "sample_groups.csv").set_index("sample_group_id") | |
| groups = groups.loc[expression.columns] | |
| joined = expression.T.join(groups[["ecotype", "condition"]]) | |
| means = joined.groupby(["ecotype", "condition"]).mean() | |
| control = means.xs("control", level="condition").sort_index() | |
| drought = means.xs("drought", level="condition").sort_index() | |
| delta = drought - control | |
| splits = pd.read_csv(DATA_DIR / "ecotype_splits.csv").set_index("ecotype") | |
| return control, delta, splits | |
| def select_input_genes(control: pd.DataFrame, count: int = N_INPUT_GENES) -> pd.Index: | |
| return control.var(axis=0).nlargest(count).index | |
| def make_model(n_components: int) -> Pipeline: | |
| return Pipeline( | |
| [ | |
| ("scale_inputs", StandardScaler()), | |
| ( | |
| "pls", | |
| PLSRegression( | |
| n_components=n_components, | |
| scale=False, | |
| max_iter=MAX_ITERATIONS, | |
| tol=1e-6, | |
| ), | |
| ), | |
| ] | |
| ) | |
| 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(actual: pd.DataFrame, predicted: np.ndarray, model: 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, | |
| "mse": np.mean(error**2), | |
| "mae": np.mean(np.abs(error)), | |
| "pearson": pearson, | |
| "spearman": spearman, | |
| } | |
| ) | |
| return pd.DataFrame(rows) | |
| def summarize(metrics: pd.DataFrame) -> pd.DataFrame: | |
| summary = metrics.groupby("model")[["mse", "mae", "pearson", "spearman"]].agg( | |
| ["mean", "median", "std"] | |
| ) | |
| summary.columns = ["_".join(column) for column in summary.columns] | |
| return summary.reset_index() | |
| def main() -> None: | |
| control, delta, splits = load_data() | |
| train_ids = splits.index[splits["split"].eq("train")] | |
| validation_ids = splits.index[splits["split"].eq("validation")] | |
| test_ids = splits.index[splits["split"].eq("test")] | |
| training_genes = select_input_genes(control.loc[train_ids]) | |
| validation_rows = [] | |
| for components in COMPONENT_GRID: | |
| model = make_model(components) | |
| model.fit(control.loc[train_ids, training_genes], delta.loc[train_ids]) | |
| prediction = model.predict(control.loc[validation_ids, training_genes]) | |
| score = evaluate(delta.loc[validation_ids], prediction, "pls") | |
| validation_rows.append( | |
| { | |
| "n_components": components, | |
| "n_input_genes": len(training_genes), | |
| "validation_mse": score["mse"].mean(), | |
| "validation_mae": score["mae"].mean(), | |
| "validation_pearson": score["pearson"].mean(), | |
| "validation_spearman": score["spearman"].mean(), | |
| "maximum_iterations_used": int( | |
| np.max(model.named_steps["pls"].n_iter_) | |
| ), | |
| } | |
| ) | |
| validation = pd.DataFrame(validation_rows).sort_values( | |
| ["validation_mse", "validation_mae", "n_components"] | |
| ) | |
| selected_components = int(validation.iloc[0]["n_components"]) | |
| # Re-select variable inputs using all development ecotypes after the number | |
| # of PLS components has been fixed on validation data. | |
| development_ids = train_ids.append(validation_ids) | |
| development_genes = select_input_genes(control.loc[development_ids]) | |
| final_model = make_model(selected_components) | |
| final_model.fit( | |
| control.loc[development_ids, development_genes], | |
| delta.loc[development_ids], | |
| ) | |
| pls_prediction = final_model.predict(control.loc[test_ids, development_genes]) | |
| development_mean = delta.loc[development_ids].mean(axis=0).to_numpy() | |
| mean_prediction = np.tile(development_mean, (len(test_ids), 1)) | |
| zero_prediction = np.zeros_like(pls_prediction) | |
| pca_ridge_metrics = pd.read_csv( | |
| PROJECT_ROOT / "results" / "reduced_rank_ridge" / "test_metrics_by_ecotype.csv" | |
| ).loc[lambda frame: frame["model"].eq("pca_ridge")] | |
| test_metrics = pd.concat( | |
| [ | |
| evaluate(delta.loc[test_ids], zero_prediction, "zero_change"), | |
| evaluate(delta.loc[test_ids], mean_prediction, "development_mean"), | |
| pca_ridge_metrics, | |
| evaluate(delta.loc[test_ids], pls_prediction, "pls"), | |
| ], | |
| ignore_index=True, | |
| ) | |
| test_summary = summarize(test_metrics) | |
| RESULTS_DIR.mkdir(parents=True, exist_ok=True) | |
| FIGURES_DIR.mkdir(parents=True, exist_ok=True) | |
| MODEL_DIR.mkdir(parents=True, exist_ok=True) | |
| validation.to_csv(RESULTS_DIR / "validation_components.csv", index=False) | |
| test_metrics.to_csv(RESULTS_DIR / "test_metrics_by_ecotype.csv", index=False) | |
| test_summary.to_csv(RESULTS_DIR / "test_summary.csv", index=False) | |
| pd.Series(development_genes, name="gene_id").to_csv( | |
| RESULTS_DIR / "selected_input_genes.csv", index=False | |
| ) | |
| joblib.dump( | |
| {"input_genes": development_genes, "pipeline": final_model}, | |
| MODEL_DIR / "pls_model.joblib", | |
| ) | |
| (MODEL_DIR / "pls_model.json").write_text( | |
| json.dumps( | |
| { | |
| "model": "Partial Least Squares regression", | |
| "selected_components": selected_components, | |
| "selected_input_genes": len(development_genes), | |
| "input_gene_selection": "highest variance in development controls", | |
| "selection_metric": "mean validation MSE across ecotypes", | |
| "development_ecotypes": len(development_ids), | |
| "test_ecotypes": len(test_ids), | |
| "maximum_iterations_used_final_fit": int( | |
| np.max(final_model.named_steps["pls"].n_iter_) | |
| ), | |
| }, | |
| indent=2, | |
| ), | |
| encoding="utf-8", | |
| ) | |
| sns.set_theme(style="whitegrid", context="notebook") | |
| fig, axes = plt.subplots(1, 2, figsize=(10.5, 4.3)) | |
| ordered_validation = validation.sort_values("n_components") | |
| sns.lineplot( | |
| data=ordered_validation, | |
| x="n_components", | |
| y="validation_mse", | |
| marker="o", | |
| color="#356859", | |
| ax=axes[0], | |
| ) | |
| axes[0].set( | |
| title="PLS component selection", | |
| xlabel="PLS components", | |
| ylabel="Validation MSE", | |
| ) | |
| sns.boxplot( | |
| data=test_metrics, | |
| x="model", | |
| y="mae", | |
| hue="model", | |
| legend=False, | |
| palette=["#9AA4A6", "#3D6FB4", "#C58A2B", "#7A5195"], | |
| ax=axes[1], | |
| ) | |
| axes[1].set(title="Supervised-model test error", xlabel="Model", ylabel="MAE") | |
| axes[1].tick_params(axis="x", rotation=18) | |
| fig.tight_layout() | |
| fig.savefig(FIGURES_DIR / "pls_model_comparison.png", dpi=220) | |
| plt.close(fig) | |
| print("Selected using validation data:") | |
| print(f"Input genes: {N_INPUT_GENES}") | |
| print(f"PLS components: {selected_components}") | |
| print(f"Validation MSE: {validation.iloc[0]['validation_mse']:.6f}") | |
| print("\nFinal test means:") | |
| print( | |
| test_summary[["model", "mse_mean", "mae_mean", "pearson_mean", "spearman_mean"]] | |
| .to_string(index=False) | |
| ) | |
| if __name__ == "__main__": | |
| main() | |