""" Flight Delay Prediction - Streamlit App Dataset: Kaggle 2015 Flight Delays (flights.csv) o datos sinteticos de demo. Arquitectura: entrenamiento on-demand guardado en st.session_state (sin depender de archivos .joblib persistidos en disco -> evita problemas de Spaces que reinician o no conservan outputs/ entre despliegues). """ import io import warnings import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import streamlit as st from sklearn.compose import ColumnTransformer from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier from sklearn.impute import SimpleImputer from sklearn.linear_model import LogisticRegression from sklearn.metrics import ( accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, confusion_matrix, RocCurveDisplay, ) from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline from sklearn.preprocessing import OneHotEncoder, StandardScaler warnings.filterwarnings("ignore") st.set_page_config( page_title="Prediccion de Retrasos de Vuelos", page_icon="\u2708\ufe0f", layout="wide", initial_sidebar_state="expanded", ) st.markdown(""" """, unsafe_allow_html=True) AIRLINES = ["AA", "DL", "UA", "WN", "B6", "AS", "NK", "F9"] AIRPORTS = ["ATL", "LAX", "ORD", "DFW", "JFK", "SFO", "MIA", "DEN", "SEA", "BOS"] DOW_LABELS = {1: "Lunes", 2: "Martes", 3: "Miercoles", 4: "Jueves", 5: "Viernes", 6: "Sabado", 7: "Domingo"} FEATURES = [ "AIRLINE", "ORIGIN_AIRPORT", "DESTINATION_AIRPORT", "ROUTE", "MONTH", "DAY_OF_WEEK", "DEP_HOUR", "DISTANCE", "IS_WEEKEND", "ROUTE_FREQUENCY", "AIRLINE_AVG_DELAY", "ORIGIN_AVG_DELAY", "DEST_AVG_DELAY", ] @st.cache_data def generate_synthetic_data(n_samples: int = 8000, seed: int = 42) -> pd.DataFrame: rng = np.random.default_rng(seed) month = rng.integers(1, 13, n_samples) day = rng.integers(1, 29, n_samples) day_of_week = rng.integers(1, 8, n_samples) airline = rng.choice(AIRLINES, n_samples) origin = rng.choice(AIRPORTS, n_samples) dest = rng.choice(AIRPORTS, n_samples) sched_hour = rng.integers(5, 24, n_samples) distance = rng.integers(150, 2700, n_samples) p = ( 0.12 + 0.012 * (sched_hour >= 17) + 0.035 * np.isin(month, [6, 7, 12]) + 0.03 * np.isin(origin, ["ORD", "JFK", "LAX"]) + 0.02 * np.isin(airline, ["WN", "B6", "F9"]) + 0.01 * (day_of_week >= 5) ) delayed = rng.binomial(1, np.clip(p, 0.02, 0.6)) dep_delay = np.where(delayed == 1, rng.gamma(2.2, 18, n_samples) + 16, rng.normal(0, 7, n_samples)) dep_delay = np.round(dep_delay).astype(int) scheduled_departure = sched_hour * 100 + rng.choice([0, 5, 10, 15, 20, 30, 45, 50], n_samples) return pd.DataFrame({ "YEAR": 2015, "MONTH": month, "DAY": day, "DAY_OF_WEEK": day_of_week, "AIRLINE": airline, "ORIGIN_AIRPORT": origin, "DESTINATION_AIRPORT": dest, "SCHEDULED_DEPARTURE": scheduled_departure, "DEPARTURE_DELAY": dep_delay, "DISTANCE": distance, "CANCELLED": 0, "DIVERTED": 0, }) @st.cache_data def load_uploaded(file_bytes) -> pd.DataFrame: return pd.read_csv(io.BytesIO(file_bytes), low_memory=False) def clean_data(df: pd.DataFrame) -> pd.DataFrame: df = df.copy() if "CANCELLED" in df.columns: df = df[df["CANCELLED"] == 0] if "DIVERTED" in df.columns: df = df[df["DIVERTED"] == 0] df = df.drop_duplicates() df["SCHEDULED_DEPARTURE"] = pd.to_numeric(df["SCHEDULED_DEPARTURE"], errors="coerce") df["DEPARTURE_DELAY"] = pd.to_numeric(df["DEPARTURE_DELAY"], errors="coerce") df = df[df["DEPARTURE_DELAY"].between(-60, 600)] for col in ["AIRLINE", "ORIGIN_AIRPORT", "DESTINATION_AIRPORT"]: df[col] = df[col].astype(str).str.upper().str.strip() df = df.dropna(subset=["AIRLINE", "ORIGIN_AIRPORT", "DESTINATION_AIRPORT", "DEPARTURE_DELAY", "SCHEDULED_DEPARTURE"]) return df def feature_engineer(df: pd.DataFrame) -> pd.DataFrame: df = df.copy() df["DELAYED"] = (df["DEPARTURE_DELAY"] > 15).astype(int) df["DEP_HOUR"] = (df["SCHEDULED_DEPARTURE"] // 100).clip(0, 23).astype(int) df["IS_WEEKEND"] = df["DAY_OF_WEEK"].isin([6, 7]).astype(int) df["ROUTE"] = df["ORIGIN_AIRPORT"] + "_" + df["DESTINATION_AIRPORT"] return df def compute_stats(df: pd.DataFrame) -> dict: return { "route_freq": df.groupby("ROUTE").size(), "airline_avg_delay": df.groupby("AIRLINE")["DEPARTURE_DELAY"].mean(), "origin_avg_delay": df.groupby("ORIGIN_AIRPORT")["DEPARTURE_DELAY"].mean(), "dest_avg_delay": df.groupby("DESTINATION_AIRPORT")["DEPARTURE_DELAY"].mean(), } def apply_stats(df: pd.DataFrame, stats: dict) -> pd.DataFrame: df = df.copy() df["ROUTE_FREQUENCY"] = df["ROUTE"].map(stats["route_freq"]).fillna(1) df["AIRLINE_AVG_DELAY"] = df["AIRLINE"].map(stats["airline_avg_delay"]).fillna(0) df["ORIGIN_AVG_DELAY"] = df["ORIGIN_AIRPORT"].map(stats["origin_avg_delay"]).fillna(0) df["DEST_AVG_DELAY"] = df["DESTINATION_AIRPORT"].map(stats["dest_avg_delay"]).fillna(0) return df def build_preprocessor(X: pd.DataFrame) -> ColumnTransformer: cat_cols = [c for c in ["AIRLINE", "ORIGIN_AIRPORT", "DESTINATION_AIRPORT", "ROUTE"] if c in X.columns] num_cols = [c for c in FEATURES if c not in cat_cols] try: encoder = OneHotEncoder(handle_unknown="ignore", sparse_output=False) except TypeError: encoder = OneHotEncoder(handle_unknown="ignore", sparse=False) return ColumnTransformer([ ("num", Pipeline([("imputer", SimpleImputer(strategy="median")), ("scaler", StandardScaler())]), num_cols), ("cat", Pipeline([("imputer", SimpleImputer(strategy="most_frequent")), ("onehot", encoder)]), cat_cols), ]) def train_models(raw_df, model_names, n_estimators, test_size): clean = clean_data(raw_df) feat = feature_engineer(clean) train_idx, test_idx = train_test_split( feat.index, test_size=test_size, random_state=42, stratify=feat["DELAYED"] ) train_df, test_df = feat.loc[train_idx], feat.loc[test_idx] stats = compute_stats(train_df) train_df = apply_stats(train_df, stats) test_df = apply_stats(test_df, stats) X_train, X_test = train_df[FEATURES], test_df[FEATURES] y_train, y_test = train_df["DELAYED"], test_df["DELAYED"] preprocessor = build_preprocessor(X_train) model_map = {} if "Bosque Aleatorio" in model_names: model_map["Bosque Aleatorio"] = RandomForestClassifier( n_estimators=n_estimators, max_depth=10, class_weight="balanced", random_state=42, n_jobs=-1 ) if "Gradient Boosting" in model_names: model_map["Gradient Boosting"] = GradientBoostingClassifier( n_estimators=min(n_estimators, 100), random_state=42 ) if "Regresion Logistica" in model_names: model_map["Regresion Logistica"] = LogisticRegression(max_iter=1000, class_weight="balanced") results = {} for name, clf in model_map.items(): pipe = Pipeline([("preprocess", preprocessor), ("model", clf)]) pipe.fit(X_train, y_train) y_pred = pipe.predict(X_test) y_prob = pipe.predict_proba(X_test)[:, 1] results[name] = { "pipe": pipe, "acc": accuracy_score(y_test, y_pred), "precision": precision_score(y_test, y_pred, zero_division=0), "recall": recall_score(y_test, y_pred, zero_division=0), "f1": f1_score(y_test, y_pred, zero_division=0), "roc_auc": roc_auc_score(y_test, y_prob), "cm": confusion_matrix(y_test, y_pred), "y_test": y_test, "y_prob": y_prob, } fi = None if "Bosque Aleatorio" in results: rf_pipe = results["Bosque Aleatorio"]["pipe"] cat_names = rf_pipe.named_steps["preprocess"].named_transformers_["cat"].named_steps["onehot"].get_feature_names_out() num_cols = [c for c in FEATURES if c not in ["AIRLINE", "ORIGIN_AIRPORT", "DESTINATION_AIRPORT", "ROUTE"]] all_names = list(num_cols) + list(cat_names) importances = rf_pipe.named_steps["model"].feature_importances_ fi = pd.Series(importances, index=all_names).sort_values(ascending=False).head(15) return results, stats, fi def predict_flight(pipe, stats, airline, origin, dest, month, day_of_week, dep_hour, distance): route = f"{origin}_{dest}" row = pd.DataFrame([{ "AIRLINE": airline, "ORIGIN_AIRPORT": origin, "DESTINATION_AIRPORT": dest, "ROUTE": route, "MONTH": month, "DAY_OF_WEEK": day_of_week, "DEP_HOUR": dep_hour, "DISTANCE": distance, "IS_WEEKEND": int(day_of_week in (6, 7)), "ROUTE_FREQUENCY": stats["route_freq"].get(route, 1), "AIRLINE_AVG_DELAY": stats["airline_avg_delay"].get(airline, 0.0), "ORIGIN_AVG_DELAY": stats["origin_avg_delay"].get(origin, 0.0), "DEST_AVG_DELAY": stats["dest_avg_delay"].get(dest, 0.0), }])[FEATURES] return float(pipe.predict_proba(row)[0, 1]) with st.sidebar: st.markdown("## \u2699\ufe0f Configuracion") st.markdown("### \U0001F4C2 Fuente de datos") uploaded = st.file_uploader( "Sube flights.csv (opcional)", type=["csv"], help="Sube el CSV real de Kaggle (2015 Flight Delays). Si no, se usa data sintetica de demo." ) n_synthetic = st.slider("Muestras sinteticas (si no subes archivo)", 2000, 30000, 8000, step=1000) st.markdown("---") st.markdown("### \U0001F916 Modelos a entrenar") model_choices = st.multiselect( "Selecciona clasificadores", ["Bosque Aleatorio", "Gradient Boosting", "Regresion Logistica"], default=["Bosque Aleatorio", "Regresion Logistica"], ) st.markdown("### \U0001F522 Parametros de entrenamiento") test_frac = st.slider("Fraccion de test", 0.1, 0.4, 0.25, step=0.05) n_trees = st.slider("Arboles (RF / GB)", 20, 200, 80, step=10) st.markdown("---") train_btn = st.button("\U0001F680 Entrenar modelos", width='stretch', type="primary") st.markdown("---") st.markdown("### \U0001F52E Predictor de retraso") pred_airline = st.selectbox("Aerolinea", AIRLINES) pred_origin = st.selectbox("Aeropuerto origen", AIRPORTS, index=0) pred_dest = st.selectbox("Aeropuerto destino", AIRPORTS, index=1) pred_month = st.slider("Mes", 1, 12, 6) pred_dow_label = st.selectbox("Dia de la semana", list(DOW_LABELS.values()), index=4) pred_hour = st.slider("Hora de salida (24h)", 0, 23, 17) pred_distance = st.slider("Distancia (millas)", 100, 3000, 900, step=50) predict_btn = st.button("\U0001F50D Predecir retraso", width='stretch') if uploaded: raw_df = load_uploaded(uploaded.read()) data_source = "CSV subido" else: raw_df = generate_synthetic_data(n_synthetic) data_source = f"Datos sinteticos de demo ({n_synthetic:,} muestras)" clean_df = clean_data(raw_df) feat_df = feature_engineer(clean_df) st.markdown('

\u2708\ufe0f Prediccion de Retrasos de Vuelos

', unsafe_allow_html=True) st.markdown( f'

Machine Learning sobre datos de vuelos  |  ' f'Datos: {data_source}  |  {len(raw_df):,} registros

', unsafe_allow_html=True, ) for key in ["results", "stats", "fi"]: if key not in st.session_state: st.session_state[key] = None if train_btn: if not model_choices: st.warning("Selecciona al menos un modelo.") else: with st.spinner("Entrenando modelos... puede tardar un momento"): res, stats, fi = train_models(raw_df, model_choices, n_trees, test_frac) st.session_state.results = res st.session_state.stats = stats st.session_state.fi = fi st.success("Modelos entrenados correctamente.") tab1, tab2, tab3, tab4 = st.tabs([ "\U0001F4CA Analisis exploratorio", "\U0001F9E0 Resultados del modelo", "\U0001F4C8 Importancia de variables", "\U0001F52E Predictor", ]) with tab1: st.markdown('

Analisis Exploratorio

', unsafe_allow_html=True) c1, c2, c3, c4 = st.columns(4) c1.metric("Vuelos totales", f"{len(raw_df):,}") c2.metric("Tasa de retraso", f"{feat_df['DELAYED'].mean()*100:.1f}%") c3.metric("Aerolineas", feat_df["AIRLINE"].nunique()) c4.metric("Aeropuertos", feat_df["ORIGIN_AIRPORT"].nunique()) st.markdown("---") col_a, col_b = st.columns(2) with col_a: st.markdown("**Tasa de retraso por aerolinea**") by_airline = feat_df.groupby("AIRLINE")["DELAYED"].mean().sort_values(ascending=False) fig, ax = plt.subplots(figsize=(6, 4)) colors = plt.cm.Blues_r(np.linspace(0.2, 0.8, len(by_airline))) by_airline.plot(kind="bar", ax=ax, color=colors) ax.set_ylabel("Tasa de retraso"); ax.tick_params(axis="x", rotation=0) fig.tight_layout(); st.pyplot(fig); plt.close(fig) with col_b: st.markdown("**Tasa de retraso por aeropuerto de origen**") by_airport = feat_df.groupby("ORIGIN_AIRPORT")["DELAYED"].mean().sort_values(ascending=False).head(10) fig, ax = plt.subplots(figsize=(6, 4)) by_airport.plot(kind="bar", ax=ax, color="#F0A500", alpha=0.85) ax.set_ylabel("Tasa de retraso"); ax.tick_params(axis="x", rotation=45) fig.tight_layout(); st.pyplot(fig); plt.close(fig) col_c, col_d = st.columns(2) with col_c: st.markdown("**Tasa de retraso por hora de salida**") by_hour = feat_df.groupby("DEP_HOUR")["DELAYED"].mean() fig, ax = plt.subplots(figsize=(6, 3.5)) ax.plot(by_hour.index, by_hour.values, color="#1F3864", linewidth=2.5, marker="o", markersize=4) ax.fill_between(by_hour.index, by_hour.values, alpha=0.15, color="#1F3864") ax.set_xlabel("Hora (24h)"); ax.set_ylabel("Tasa de retraso") fig.tight_layout(); st.pyplot(fig); plt.close(fig) with col_d: st.markdown("**Tasa de retraso por mes**") by_month = feat_df.groupby("MONTH")["DELAYED"].mean() fig, ax = plt.subplots(figsize=(6, 3.5)) ax.bar(by_month.index, by_month.values, color="#2E75B6", alpha=0.85) ax.set_xlabel("Mes"); ax.set_ylabel("Tasa de retraso") fig.tight_layout(); st.pyplot(fig); plt.close(fig) st.markdown("**Distribucion de minutos de retraso**") fig, ax = plt.subplots(figsize=(12, 3)) ax.hist(clean_df["DEPARTURE_DELAY"].dropna(), bins=60, color="#1F3864", alpha=0.8) ax.set_xlabel("Minutos de retraso"); ax.set_ylabel("Cantidad de vuelos") fig.tight_layout(); st.pyplot(fig); plt.close(fig) st.markdown("**Mapa de calor: Hora x Dia de la semana**") pivot_data = feat_df.groupby(["DAY_OF_WEEK", "DEP_HOUR"])["DELAYED"].mean().unstack(fill_value=0) fig, ax = plt.subplots(figsize=(14, 4)) sns.heatmap(pivot_data, ax=ax, cmap="YlOrRd", linewidths=0.3, cbar_kws={"label": "Tasa de retraso"}) ax.set_xlabel("Hora del dia"); ax.set_ylabel("Dia de la semana (1=Lun)") fig.tight_layout(); st.pyplot(fig); plt.close(fig) with st.expander("Vista previa de los datos"): st.dataframe(raw_df.head(200), width='stretch') with tab2: st.markdown('

Evaluacion de Modelos

', unsafe_allow_html=True) if st.session_state.results is None: st.markdown( '
Configura tus modelos en la barra lateral y presiona ' 'Entrenar modelos para ver resultados aqui.
', unsafe_allow_html=True, ) else: results = st.session_state.results st.markdown("**Comparacion de metricas**") metrics_df = pd.DataFrame({ name: {"Accuracy": r["acc"], "Precision": r["precision"], "Recall": r["recall"], "F1": r["f1"], "ROC AUC": r["roc_auc"]} for name, r in results.items() }).T.round(4) st.dataframe(metrics_df, width='stretch') fig, ax = plt.subplots(figsize=(max(4, len(results) * 2.5), 4)) bars = ax.bar(metrics_df.index, metrics_df["ROC AUC"], color=["#1F3864", "#2E75B6", "#F0A500"][:len(results)], alpha=0.85) ax.axhline(0.5, color="gray", linestyle="--", linewidth=1, label="Azar (0.5)") ax.set_ylim(0, 1); ax.set_ylabel("ROC AUC"); ax.legend() for bar, val in zip(bars, metrics_df["ROC AUC"]): ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.01, f"{val:.3f}", ha="center", fontweight="bold") fig.tight_layout(); st.pyplot(fig); plt.close(fig) fig, ax = plt.subplots(figsize=(7, 5)) for name, r in results.items(): RocCurveDisplay.from_predictions(r["y_test"], r["y_prob"], name=name, ax=ax) ax.set_title("Curvas ROC"); fig.tight_layout(); st.pyplot(fig); plt.close(fig) for name, r in results.items(): with st.expander(f"{name} - ROC AUC: {r['roc_auc']:.4f}"): fig, ax = plt.subplots(figsize=(5, 4)) sns.heatmap(r["cm"], annot=True, fmt="d", cmap="Blues", xticklabels=["No retraso", "Retraso"], yticklabels=["No retraso", "Retraso"], ax=ax) ax.set_xlabel("Prediccion"); ax.set_ylabel("Real") fig.tight_layout(); st.pyplot(fig); plt.close(fig) with tab3: st.markdown('

Importancia de Variables

', unsafe_allow_html=True) if st.session_state.fi is None: st.markdown( '
Entrena un modelo Bosque Aleatorio para ver la importancia de variables.
', unsafe_allow_html=True, ) else: fi = st.session_state.fi col_fi1, col_fi2 = st.columns([2, 1]) with col_fi1: fig, ax = plt.subplots(figsize=(8, 5)) colors = plt.cm.RdYlGn(np.linspace(0.2, 0.8, len(fi)))[::-1] fi.plot(kind="barh", ax=ax, color=colors) ax.set_xlabel("Importancia"); ax.set_title("Importancia de variables - Bosque Aleatorio") ax.invert_yaxis() fig.tight_layout(); st.pyplot(fig); plt.close(fig) with col_fi2: st.dataframe(fi.reset_index().rename(columns={"index": "Variable", 0: "Importancia"}).round(4), width='stretch') with tab4: st.markdown('

Predictor de Retraso de Vuelo

', unsafe_allow_html=True) st.markdown( '
Configura los datos del vuelo en la barra lateral, entrena un modelo, ' 'luego presiona Predecir retraso.
', unsafe_allow_html=True, ) if predict_btn: if st.session_state.results is None: st.warning("Primero entrena al menos un modelo.") elif pred_origin == pred_dest: st.warning("El aeropuerto de origen y destino no pueden ser el mismo.") else: results = st.session_state.results stats = st.session_state.stats day_of_week = [k for k, v in DOW_LABELS.items() if v == pred_dow_label][0] st.markdown("### Predicciones") for name, r in results.items(): prob = predict_flight( r["pipe"], stats, pred_airline, pred_origin, pred_dest, pred_month, day_of_week, pred_hour, pred_distance, ) pct = prob * 100 if pct < 20: nivel, color = "Bajo riesgo", "#27ae60" elif pct < 45: nivel, color = "Riesgo moderado", "#F0A500" else: nivel, color = "Alto riesgo", "#c0392b" st.markdown(f"**{name}** -> `{pct:.1f}%` de probabilidad de retraso - " f"{nivel}", unsafe_allow_html=True) st.progress(min(int(pct), 100)) st.markdown("---") st.markdown( "Prediccion de retrasos de vuelos. Dataset: Kaggle 2015 Flight Delays " "(o datos sinteticos de demo si no se sube CSV)", unsafe_allow_html=True, )