--- license: mit --- # Exoplanet Classification using NASA Datasets (Kepler, K2, TESS) — README This README explains **how we trained** a **mission-agnostic** classifier to predict whether a detected object is **CONFIRMED**, **CANDIDATE**, or **FALSE POSITIVE** using public catalogs from **Kepler**, **K2**, and **TESS**. It documents the data ingestion, harmonization, feature engineering (including SNR logic), preprocessing, model selection, evaluation, class-imbalance handling, explainability, and inference artifacts. --- ## Project Structure (key files) ``` . ├─ Exoplanet_Classification_NASA_Kepler_K2_TESS_withSNR_ROBUST.ipynb # main notebook └─ artifacts/ ├─ exoplanet_best_model.joblib # full sklearn Pipeline (preprocessing + estimator) ├─ exoplanet_feature_columns.json # exact feature order used during training ├─ exoplanet_class_labels.json # label names in prediction index order └─ exoplanet_metadata.json # summary (best model name, n_features, timestamp) ``` --- ## Datasets - **Kepler Cumulative:** `cumulative_YYYY.MM.DD_HH.MM.SS.csv` - **K2 Planets & Candidates:** `k2pandc_YYYY.MM.DD_HH.MM.SS.csv` - **TESS Objects of Interest (TOI):** `TOI_YYYY.MM.DD_HH.MM.SS.csv` Each table contains per-candidate features and a mission-specific disposition field. These missions are **complementary** (different cadences/systems), so merging increases coverage and diversity. --- ## Environment - Python ≥ 3.9 - Libraries: `pandas`, `numpy`, `matplotlib`, `scikit-learn`, `imblearn`, optional `xgboost`, optional `shap`, `joblib`. > If `xgboost` or `shap` aren’t installed, the notebook automatically skips those steps. --- ## Reproducibility - Random seeds set to **42** in model splits and estimators. - Stratified splits used for consistent class composition. - The exact **feature order** and **class label order** are saved to JSON and re-used at inference. --- ## Data Loading (robust, no `low_memory`) CSV parsing is **robust**: - Try multiple separators and engines (prefers **Python engine** with `on_bad_lines="skip"`). - Normalize column names to `snake_case`. - Re-detect header if the first read looks suspicious (e.g., numeric column names). - Drop columns that are entirely empty. > We deliberately **do not** pass `low_memory` (it’s unsupported by the Python engine and can degrade type inference). --- ## Label Harmonization Different catalogs use different labels. We map all to a **unified set**: - **CONFIRMED**: `CONFIRMED`, `CP`, `KP` - **CANDIDATE**: `CANDIDATE`, `PC`, `CAND`, `APC` - **FALSE POSITIVE**: `FALSE POSITIVE`, `FP`, `FA`, `REFUTED` Rows without a recognized disposition are dropped before modeling. --- ## Feature Harmonization (mission-agnostic) We build a **canonical numeric feature set** by scanning a list of **alias groups** and picking the **first numeric-coercible** column present per mission, e.g.: - Period: `['koi_period','orbital_period','period','pl_orbper','per']` - Duration: `['koi_duration','duration','tran_dur']` - Depth: `['koi_depth','depth','tran_depth']` - Stellar context: `['koi_steff','st_teff','teff']`, `['koi_slogg','st_logg','logg']`, `['koi_smet','st_metfe','feh']`, `['koi_srad','st_rad','star_radius']` - Transit geometry: `['koi_impact','impact','b']`, `['koi_sma','sma','a_rs','semi_major_axis']` - **SNR/MES (broad):** `['koi_snr','koi_model_snr','koi_mes','mes','max_mes','snr','detection_snr','transit_snr','signal_to_noise','koi_max_snr']` We **coerce to numeric** (`errors='coerce'`) and accept if any non-null values remain. --- ## Feature Engineering Physics-motivated features improve separability: - **Duty cycle**: `duty_cycle = koi_duration / (koi_period * 24.0)` (transit duration relative to orbital period) - **Log transforms**: `log_koi_period = log10(koi_period)` `log_koi_depth = log10(koi_depth)` - **Equilibrium-temperature proxy**: - Simple: `teq_proxy = koi_steff` - Refined (if `a/R*` available or derivable): `teq_proxy = koi_steff / sqrt(2 * a_rs)` - **SNR logic (guaranteed SNR-like feature)**: - If any mission exposes a usable SNR/MES, we use **`koi_snr`** and also compute **`log_koi_snr`**. - Otherwise we compute a **mission-agnostic proxy**: ``` snr_proxy = koi_depth * sqrt( koi_duration / (koi_period * 24.0) ) log_snr_proxy = log10(snr_proxy) ``` > The exact features used in training are whatever appear in `artifacts/exoplanet_feature_columns.json` (this list is created dynamically from the actual files you load). --- ## Mission-Agnostic Policy - We keep a temporary `mission` column only for auditing, then **drop it** before training. - Features are derived from **physical quantities**, not the mission identity. --- ## Preprocessing & Split - Keep **numeric** columns only. - **Imputation**: median (`SimpleImputer(strategy="median")`). - **Scaling**: `RobustScaler()` (robust to outliers). - **Label encoding**: `LabelEncoder` → integer target (needed by XGBoost). - **Split**: `train_test_split(test_size=0.2, stratify=y, random_state=42)`. --- ## Model Selection & Training We wrap preprocessing and the classifier in a **single Pipeline**, and optimize **macro-F1** with `RandomizedSearchCV` using `StratifiedKFold`: - **Random Forest** (class_weight="balanced") - `n_estimators`: 150–600 (fast mode: 150/300) - `max_depth`: None or small integers - `min_samples_split`, `min_samples_leaf` - **SVM (RBF)** (class_weight="balanced") - `C`: logspace grid - `gamma`: `["scale","auto"]` - **XGBoost** (optional; only if installed) - `objective="multi:softprob"`, `eval_metric="mlogloss"`, `tree_method="hist"` - Grid over `learning_rate`, `max_depth`, `subsample`, `colsample_bytree`, `n_estimators` > We use a `FAST` flag (defaults to **True**) to keep grids small for iteration speed. Disabling FAST expands the search. --- ## Evaluation - **Primary metric**: **Macro-F1** (treats classes evenly). - Also report **accuracy** and per-class **precision/recall/F1**. - **Confusion matrices** (shared color scale across models). - **ROC AUC** (one-vs-rest) when probability estimates are available. --- ## Handling Class Imbalance We provide an **optional** SMOTE section using `imblearn.Pipeline`: ``` ImbPipeline([ ("impute", SimpleImputer(median)), ("scale", RobustScaler()), ("smote", SMOTE()), ("clf", RandomForestClassifier or SVC(probability=True)) ]) ``` - We **avoid nested sklearn Pipelines** inside the imblearn pipeline to prevent: > `TypeError: All intermediate steps of the chain should not be Pipelines`. The SMOTE models are tuned on macro-F1 and compared against the non-SMOTE runs. --- ## Explainability - **Permutation importance** computed on the **full pipeline** (so it reflects preprocessing). - **SHAP** (if installed) for tree-based models: - Global bar chart of mean |SHAP| across classes - Per-class bar chart (e.g., CONFIRMED) - SHAP summary bar (optional) If the best model isn’t tree-based, SHAP is skipped automatically. --- ## Saved Artifacts After selecting the **best model** (highest macro-F1 on the held-out test set), we serialize: - `artifacts/exoplanet_best_model.joblib` – the full sklearn **Pipeline** - `artifacts/exoplanet_feature_columns.json` – **exact feature order** expected at inference - `artifacts/exoplanet_class_labels.json` – class names in output index order - `artifacts/exoplanet_metadata.json` – name, n_features, labels, timestamp These provide **stable inference** even if the notebook or environment changes later. --- ## Inference Usage We include a helper that builds an input row with the **exact training feature order**, warns about **unknown/missing** keys, and prints predictions and class probabilities: ```python from pathlib import Path import json, joblib, numpy as np, pandas as pd ARTIFACTS_DIR = Path("artifacts") model = joblib.load(ARTIFACTS_DIR / "exoplanet_best_model.joblib") feature_columns = json.loads((ARTIFACTS_DIR / "exoplanet_feature_columns.json").read_text()) class_labels = json.loads((ARTIFACTS_DIR / "exoplanet_class_labels.json").read_text()) def predict_with_debug(model, feature_columns, class_labels, params: dict): X = pd.DataFrame([params], dtype=float).reindex(columns=feature_columns) y_idx = int(model.predict(X)[0]) label = class_labels[y_idx] print("Prediction:", label) try: proba = model.predict_proba(X)[0] for lbl, p in sorted(zip(class_labels, proba), key=lambda t: t[1], reverse=True): print(f" {lbl:>15s}: {p:.3f}") except Exception: pass return label ``` ### Engineered features to compute in your inputs - `duty_cycle = koi_duration / (koi_period * 24.0)` - `log_koi_period = log10(koi_period)` - `log_koi_depth = log10(koi_depth)` - `teq_proxy = koi_steff` (or `koi_steff / sqrt(2 * a_rs)` if using refined variant) - If the trained feature list includes **`koi_snr`**: also send `log_koi_snr = log10(koi_snr)` - Else, if it includes **`snr_proxy`**: send `snr_proxy = koi_depth * sqrt(koi_duration / (koi_period*24.0))` and `log_snr_proxy` > You can check which SNR flavor was used by inspecting `feature_columns.json`. --- ## Design Choices & Rationale - **Mission-agnostic**: We drop mission identity; rely on physical features to generalize. - **Macro-F1**: More balanced assessment when class counts differ (especially for FALSE POSITIVE). - **RobustScaler**: Less sensitive to outliers than StandardScaler. - **LabelEncoder**: Ensures XGBoost gets integers (fixes “Invalid classes inferred” errors). - **SNR strategy**: If SNR/MES exists we use it; otherwise we inject a **physics-motivated proxy** to preserve detection strength information across missions. --- ## Troubleshooting - **CSV ParserError / “python engine” warnings** We avoid `low_memory` and use Python engine with `on_bad_lines="skip"`. The loader will try alternate separators and re-try with a detected header line. - **SMOTE “intermediate steps should not be Pipelines”** We use a single `imblearn.Pipeline` with inline steps (impute → scale → SMOTE → clf). - **`PicklingError: QuantileCapper`** We removed custom transformers and rely on standard sklearn components to ensure picklability. - **`AttributeError: 'numpy.ndarray' has no attribute 'columns'`** We compute permutation importance on the **pipeline** (not the bare estimator), and we take feature names from `X_train.columns`. - **XGBoost “Invalid classes inferred”** We label-encode `y` to integers (`LabelEncoder`) before fitting. - **Different inputs return the “same” class** Often caused by: (a) too few features provided (most imputed), (b) features outside training ranges (scaled to similar z-scores), or (c) providing keys not used by the model. Use `predict_with_debug` to see **recognized/unknown/missing** features and adjust your dict. --- ## How to Re-train 1. Open the notebook `Exoplanet_Classification_NASA_Kepler_K2_TESS_withSNR_ROBUST.ipynb`. 2. Ensure the three CSV files are available at the paths set in the **Data Loading** cell. 3. (Optional) Set `FAST = False` in the training cell to run a broader hyper-parameter search. 4. Run all cells. Artifacts are written to `./artifacts/`. --- ## Extending the Work - Add more SNR/MES name variants if future catalogs introduce new headers. - Experiment with **calibrated probabilities** (e.g., `CalibratedClassifierCV`) for better ROC behavior. - Try additional models (LightGBM/CatBoost) if available. - Incorporate vetting-pipeline shape features (e.g., V-shape metrics) when harmonizable across missions. - Add **temporal cross-validation** per mission or per release to stress test generalization.