{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":101849,"databundleVersionId":13093295,"sourceType":"competition"},{"sourceId":9629432,"sourceType":"datasetVersion","datasetId":5846888},{"sourceId":564250,"sourceType":"modelInstanceVersion","modelInstanceId":426187,"modelId":443655}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"Code taken and edited from: seowoohyeon\nlink to orignal code made https://www.kaggle.com/code/seowoohyeon/resnet-for-airs-finetuned-lb0-369\n\nImprovements made:\n+0.001 lb score \nμ / airs predictions improved by adding log based validation (log model uses symmetry more has better symmetry checking and a couple other things)\n(symmetry for dips is characteristic of transits and transits also have strict symmetry, and symmetry is not common for noise or other non-transit events). \n\nFuture improvements you could try to add:\nRight now log model only works for cases were no transits were detected you can try increasing how much log model could validate.\nLog model is right now very strict to make sure no false postives are added. Maybe try making it less strict that could help.\nLog model also predicts for confidence however I am unsure if the way I calculate confidence would really help or hurt by fixing the way I measure confidence you could probably increase score by a lot.\n","metadata":{}},{"cell_type":"code","source":"# =========================================================\n# Ariel Data Challenge 2025 — 0.360 → 0.361狙いの軽パッチ\n# 変更点：\n#  - (#5) predictions1 を波長方向に極軽スムージングして 0.7:0.3 でブレンド\n#  - (#4) sigma_fgs / sigma_air のクリップ幅と最終係数を僅かに拡張\n# 他はオリジナルに準拠\n# =========================================================\n\n# install pqdm for parallel processing (Kaggleオフライン用ホイール)\n!pip install --no-index --find-links=/kaggle/input/ariel-2024-pqdm pqdm\n\nimport os\nimport time\nimport itertools\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\nfrom tqdm import tqdm\nfrom pqdm.threads import pqdm\nfrom astropy.stats import sigma_clip\nfrom scipy.optimize import minimize\nfrom torch.utils.data import DataLoader, TensorDataset, random_split\nfrom sklearn.preprocessing import StandardScaler\nfrom scipy.signal import savgol_filter\nfrom sklearn.metrics import mean_squared_error\nimport matplotlib.pyplot as plt\n\n# ======== パスとモード ========\nROOT_PATH = \"/kaggle/input/ariel-data-challenge-2025\"\nMODE = \"test\"\n__t0 = time.perf_counter()\n\n\n# ======== Config ========\nclass Config:\n    DATA_PATH = '/kaggle/input/ariel-data-challenge-2025'\n    DATASET = \"test\"\n\n    SCALE = 0.946\n    SIGMA = 0.00056\n\n    \n    CUT_INF = 39\n    CUT_SUP = 321\n    \n    SENSOR_CONFIG = {\n        \"AIRS-CH0\": {\n            \"raw_shape\": [11250, 32, 356],\n            \"calibrated_shape\": [1, 32, CUT_SUP - CUT_INF],\n            \"linear_corr_shape\": (6, 32, 356),\n            \"dt_pattern\": (0.1, 4.5), \n            \"binning\": 30\n        },\n        \"FGS1\": {\n            \"raw_shape\": [135000, 32, 32],\n            \"calibrated_shape\": [1, 32, 32],\n            \"linear_corr_shape\": (6, 32, 32),\n            \"dt_pattern\": (0.1, 0.1),\n            \"binning\": 30 * 12\n        }\n    }\n    \n    MODEL_PHASE_DETECTION_SLICE = slice(30, 140)\n    MODEL_OPTIMIZATION_DELTA = 11\n    MODEL_POLYNOMIAL_DEGREE = 3\n    \n    N_JOBS = 3\n\n# ======== ログモデル関連関数 ========\ndef log_preprocess(signal_2d, binning=3):\n    \"\"\"\n    Preprocess for log model with adaptive smoothing.\n    \"\"\"\n    signal_1d = signal_2d[:, 1:].mean(axis=1)\n    if binning > 1:\n        n_bins = len(signal_1d) // binning\n        signal_1d = np.array([signal_1d[i*binning:(i+1)*binning].mean() for i in range(n_bins)])\n    noise_level = np.std(signal_1d)\n    window = 23 if noise_level < 0.005 else 31\n    signal_1d = savgol_filter(signal_1d, window, 2)\n    return signal_1d\n\n\ndef detect(points, window_size=4, eps=1e-12, strength_threshold=0.75, symmetry_tolerance=1.1):\n    \"\"\"\n    Detect local minima using mean-of-neighbors and relaxed symmetry check.\n    Returns (center_idx, center_flux, symmetry_score) if found, else None.\n    \"\"\"\n    arr = np.array(points)\n    flux = np.clip(arr[:, 1], eps, None)\n    if len(flux) < (2 * window_size + 1):\n        return None\n\n    s = np.mean(flux)\n    mad = np.median(np.abs(flux - s)) + eps\n    global_log_ref = np.median(np.log(flux))\n    cutoff = np.quantile(flux, (1-strength_threshold))\n    \n    flux_length = len(flux)\n    window_length = 9\n    \n    smoothed = savgol_filter(flux, window_length=window_length, polyorder=2)\n    residuals = flux - smoothed\n    noise_level = np.std(residuals)\n    noise_level_and_mad=(1-(noise_level/mad+eps))*mad\n    if noise_level_and_mad<0:\n        noise_level_and_mad=noise_level\n    max_offset = window_size\n    sym_gaps = []\n    for center in range(window_size, len(flux) - window_size):\n        sym_gaps = []\n        if flux[center] > cutoff:\n            continue\n        left = flux[center - window_size:center]\n        right = flux[center + 1:center + 1 + window_size]\n\n        if flux[center] >= left.mean() or flux[center] >= right.mean():\n            continue\n\n        for offset in range(1, max_offset + 1):\n            lhs_val = flux[center - offset]\n            rhs_val = flux[center + offset]\n            sym_gaps.append(abs(np.log(lhs_val/flux[center]) - np.log(rhs_val/flux[center])))\n        \n        symmetry_gap = np.mean(sym_gaps)\n        if symmetry_gap > symmetry_tolerance * mad:\n            continue\n\n        symmetry_score = 1.0 - np.clip(symmetry_gap / (mad * symmetry_tolerance), 0, 1)\n        local_log_strength = np.mean(np.log(np.clip([flux[center], *left, *right], eps, None)))\n        if local_log_strength < global_log_ref * strength_threshold:\n            continue\n\n        return center, flux[center], symmetry_score\n\n    return None\n\n\n\ndef log_model_ensemble_adjust(preprocessed_data, transit_predictions, sigma,\n                              log_threshold=0.75, window_size=4,\n                              min_relative_dip=5e-4, max_injection=0.015):\n    \"\"\"\n    Adjust transit_predictions using log-model, conservatively.\n    \"\"\"\n    adjusted = transit_predictions.copy()\n    adjusted_sigma = sigma.copy()\n    \n    for i, signal in enumerate(preprocessed_data):\n        flux = signal[:, 1:].mean(axis=1)\n        points = np.stack([np.arange(len(flux)), flux], axis=1)\n        \n        result = detect(points, window_size=window_size, strength_threshold=log_threshold)\n        if result is None:\n            continue\n        \n        center_idx, center_flux, symmetry_score = result\n        w = window_size\n        \n        left = flux[max(0, center_idx - w):center_idx]\n        right = flux[center_idx + 1:center_idx + 1 + w]\n        surrounding = np.concatenate([left, right])\n        \n        baseline = np.median(surrounding) if surrounding.size else np.median(flux)\n        dip = max(0.0, baseline - center_flux)\n        \n        # --- conservative scaling ---\n        relative_dip = dip / max(baseline, 1e-12)\n        if relative_dip < min_relative_dip:\n            continue  # skip tiny dips\n        \n        # scale injection by symmetry_score and relative_dip\n        inj = np.clip(dip * symmetry_score * 0.7, 1e-5, max_injection)\n        \n        # blend gently with original prediction\n        blend_factor = 0.5  # can tune 0.5~0.7\n        adjusted[i] = (1 - blend_factor) * adjusted[i] + blend_factor * inj\n        \n        # adjust sigma conservatively\n        adjusted_sigma[i] = np.clip(adjusted_sigma[i] * (1.0 - 0.2 * symmetry_score),\n                                    1e-6, 0.1)\n        # clip final prediction\n        adjusted[i] = np.clip(adjusted[i], 1e-5, max_injection)\n    \n    return adjusted, adjusted_sigma\n# ======== 位相補助 ========\ndef _phase_detector_signal(signal, cfg):\n    sl = cfg.MODEL_PHASE_DETECTION_SLICE\n    min_idx = int(np.argmin(signal[sl])) + sl.start\n    s1 = signal[:min_idx]; s2 = signal[min_idx:]\n    if s1.size < 3 or s2.size < 3:\n        return 0, len(signal) - 1\n    g1 = np.gradient(s1); g1_max = np.max(g1) if np.size(g1) else 0.0\n    g2 = np.gradient(s2); g2_max = np.max(g2) if np.size(g2) else 0.0\n    if g1_max != 0: g1 /= g1_max\n    if g2_max != 0: g2 /= g2_max\n    phase1 = int(np.argmin(g1)); phase2 = int(np.argmax(g2)) + min_idx\n    return phase1, phase2\n\n\n# ======== σ推定（#4: クリップ幅＆仕上げ係数を拡張） ========\ndef estimate_sigma_fgs(preprocessed_data, cfg):\n    \"\"\"FGS1用 σ のソフト推定（保守拡張版）\"\"\"\n    sig_rel = []\n    delta = cfg.MODEL_OPTIMIZATION_DELTA\n    eps = 1e-12\n    for single in preprocessed_data:\n        air_white = savgol_filter(single[:, 1:].mean(axis=1), 20, 2)\n        p1, p2 = _phase_detector_signal(air_white, cfg)\n        p1 = max(delta, p1)\n        p2 = min(len(air_white) - delta - 1, p2)\n\n        fgs = single[:, 0]\n        oot = (fgs[: p1 - delta] if p1 - delta > 0 else np.empty(0, fgs.dtype))\n        if p2 + delta < fgs.size:\n            oot = np.concatenate([oot, fgs[p2 + delta :]])\n        inn = fgs[p1 + delta : max(p1 + delta, p2 - delta)]\n\n        if oot.size == 0 or inn.size == 0:\n            sig_rel.append(np.nan); continue\n\n        n_oot, n_in = len(oot), len(inn)\n        var_oot = np.nanvar(oot, ddof=1)\n        var_in  = np.nanvar(inn, ddof=1)\n        oot_mean = float(np.nanmean(oot)) if np.isfinite(np.nanmean(oot)) else float(np.nanmean(fgs))\n        sigma_rel = np.sqrt(var_oot / max(n_oot,1) + var_in / max(n_in,1)) / max(oot_mean, eps)\n        sig_rel.append(sigma_rel)\n\n    s = np.asarray(sig_rel, dtype=float)\n    mask = np.isfinite(s) & (s > 0)\n    med = float(np.nanmedian(s[mask])) if mask.any() else 1.0\n\n    k = np.ones_like(s)\n    if med > 0 and np.isfinite(med):\n        k[mask] = np.sqrt(s[mask] / med)\n\n    # --- #4: clipをやや緩める（0.8–1.25 → 0.85–1.30）---\n    k = np.clip(k, 0.85, 1.30)\n\n    sigma_fgs = k * cfg.SIGMA\n\n    # --- #4: 仕上げの全体係数（わずかに拡張）---\n    sigma_fgs *= 1.02\n    return sigma_fgs\n\n\ndef estimate_sigma_air(preprocessed_data, cfg):\n    \"\"\"AIRS用 σ のソフト推定（保守拡張版）\"\"\"\n    sig_rel = []\n    delta = cfg.MODEL_OPTIMIZATION_DELTA\n    eps = 1e-12\n\n    for single in preprocessed_data:\n        white = np.nanmean(single[:, 1:], axis=1)\n        white_s = savgol_filter(white, 20, 2)\n\n        p1, p2 = _phase_detector_signal(white_s, cfg)\n        p1 = max(delta, p1)\n        p2 = min(len(white) - delta - 1, p2)\n\n        oot_left = white[: p1 - delta] if p1 - delta > 0 else np.empty(0, white.dtype)\n        oot_right = white[p2 + delta :] if (p2 + delta) < white.size else np.empty(0, white.dtype)\n        oot = np.concatenate([oot_left, oot_right]) if (oot_left.size + oot_right.size) else oot_left\n        inn = white[p1 + delta : max(p1 + delta, p2 - delta)]\n\n        if oot.size == 0 or inn.size == 0:\n            sig_rel.append(np.nan); continue\n\n        n_oot, n_in = len(oot), len(inn)\n        var_oot = np.nanvar(oot, ddof=1)\n        var_in  = np.nanvar(inn, ddof=1)\n        oot_mean = float(np.nanmean(oot)) if np.isfinite(np.nanmean(oot)) else float(np.nanmean(white))\n\n        sigma_rel = np.sqrt(var_oot / max(n_oot,1) + var_in / max(n_in,1)) / max(oot_mean, eps)\n        sig_rel.append(sigma_rel)\n\n    s = np.asarray(sig_rel, dtype=float)\n    mask = np.isfinite(s) & (s > 0)\n    med = float(np.nanmedian(s[mask])) if mask.any() else 1.0\n\n    k = np.ones_like(s)\n    if med > 0 and np.isfinite(med):\n        k[mask] = np.sqrt(s[mask] / med)\n\n    # --- #4: clipをやや緩める（0.90–1.20 → 0.92–1.22）---\n    k = np.clip(k, 0.92, 1.22)\n\n    sigma_air = k * cfg.SIGMA\n\n    # --- #4: 仕上げの全体係数（わずかに拡張）---\n    sigma_air *= 1.02\n    return sigma_air\n\n\n# ======== 前処理パイプライン ========\nclass SignalProcessor:\n    def __init__(self, config):\n        self.cfg = config\n        self.adc_info = pd.read_csv(f\"{self.cfg.DATA_PATH}/adc_info.csv\")\n        self.planet_ids = pd.read_csv(f'{self.cfg.DATA_PATH}/{self.cfg.DATASET}_star_info.csv', index_col='planet_id').index.astype(int)\n\n    def _apply_linear_corr(self, linear_corr, signal):\n        coeffs = np.flip(linear_corr, axis=0)\n        x = signal.astype(np.float64, copy=False)\n        out = np.empty_like(x, dtype=np.float64)\n        out[...] = coeffs[0]\n        for k in range(1, coeffs.shape[0]):\n            np.multiply(out, x, out=out)\n            out += coeffs[k]\n        return out.astype(signal.dtype, copy=False)\n\n    def _calibrate_single_signal(self, planet_id, sensor):\n        sensor_cfg = self.cfg.SENSOR_CONFIG[sensor]\n    \n        signal = pd.read_parquet(\n            f\"{self.cfg.DATA_PATH}/{self.cfg.DATASET}/{planet_id}/{sensor}_signal_0.parquet\"\n        ).to_numpy()\n        dark = pd.read_parquet(\n            f\"{self.cfg.DATA_PATH}/{self.cfg.DATASET}/{planet_id}/{sensor}_calibration_0/dark.parquet\"\n        ).to_numpy()\n        dead = pd.read_parquet(\n            f\"{self.cfg.DATA_PATH}/{self.cfg.DATASET}/{planet_id}/{sensor}_calibration_0/dead.parquet\"\n        ).to_numpy()\n        flat = pd.read_parquet(\n            f\"{self.cfg.DATA_PATH}/{self.cfg.DATASET}/{planet_id}/{sensor}_calibration_0/flat.parquet\"\n        ).to_numpy()\n        linear_corr = pd.read_parquet(\n            f\"{self.cfg.DATA_PATH}/{self.cfg.DATASET}/{planet_id}/{sensor}_calibration_0/linear_corr.parquet\"\n        ).values.astype(np.float64).reshape(sensor_cfg[\"linear_corr_shape\"])\n    \n        signal = signal.reshape(sensor_cfg[\"raw_shape\"])\n        gain = self.adc_info[f\"{sensor}_adc_gain\"].iloc[0]\n        offset = self.adc_info[f\"{sensor}_adc_offset\"].iloc[0]\n        signal = signal / gain + offset\n    \n        hot = sigma_clip(dark, sigma=5, maxiters=5).mask\n    \n        if sensor == \"AIRS-CH0\":\n            signal = signal[:, :, self.cfg.CUT_INF : self.cfg.CUT_SUP]\n            linear_corr = linear_corr[:, :, self.cfg.CUT_INF : self.cfg.CUT_SUP]\n            dark = dark[:, self.cfg.CUT_INF : self.cfg.CUT_SUP]\n            dead = dead[:, self.cfg.CUT_INF : self.cfg.CUT_SUP]\n            flat = flat[:, self.cfg.CUT_INF : self.cfg.CUT_SUP]\n            hot = hot[:, self.cfg.CUT_INF : self.cfg.CUT_SUP]\n    \n        if sensor == \"FGS1\":\n            y0, y1, x0, x1 = 10, 22, 10, 22\n            signal = signal[:, y0:y1, x0:x1]\n            dark   = dark[y0:y1, x0:x1]\n            dead   = dead[y0:y1, x0:x1]\n            flat   = flat[y0:y1, x0:x1]\n            linear_corr = linear_corr[:, y0:y1, x0:x1]\n            hot    = hot[y0:y1, x0:x1]\n    \n        np.maximum(signal, 0, out=signal)\n    \n        if sensor == \"FGS1\":\n            signal = self._apply_linear_corr(linear_corr, signal)\n        elif sensor == \"AIRS-CH0\":\n            sl = (slice(None), slice(10, 22), slice(None))\n            signal[sl] = self._apply_linear_corr(linear_corr[:, 10:22, :], signal[sl])\n        else:\n            signal = self._apply_linear_corr(linear_corr, signal)\n    \n        base_dt, increment = sensor_cfg[\"dt_pattern\"]\n        even_scale = base_dt\n        odd_scale  = base_dt + increment\n        signal[::2]  -= dark * even_scale\n        signal[1::2] -= dark * odd_scale\n    \n        if sensor == \"FGS1\":\n            flat_roi = flat.astype(signal.dtype, copy=False).copy()\n            bad = (dead) | ~np.isfinite(flat_roi) | (flat_roi == 0)\n            flat_roi[bad] = np.nan\n            signal /= flat_roi\n    \n        elif sensor == \"AIRS-CH0\":\n            y0, y1 = 10, 22\n            flat_roi = flat[y0:y1, :].astype(signal.dtype, copy=False).copy()\n            bad = (dead[y0:y1, :]) | ~np.isfinite(flat_roi) | (flat_roi == 0)\n            flat_roi[bad] = np.nan\n            signal[:, y0:y1, :] /= flat_roi\n    \n        else:\n            flat2 = flat.astype(signal.dtype, copy=False).copy()\n            bad2 = (dead) | ~np.isfinite(flat2) | (flat2 == 0)\n            flat2[bad2] = np.nan\n            signal /= flat2\n    \n        return signal\n\n    def _preprocess_calibrated_signal(self, calibrated_signal, sensor):\n        sensor_cfg = self.cfg.SENSOR_CONFIG[sensor]\n        binning = sensor_cfg[\"binning\"]\n\n        if sensor == \"AIRS-CH0\":\n            signal_roi = calibrated_signal[:, 10:22, :]\n        elif sensor == \"FGS1\":\n            signal_roi = calibrated_signal[:, 10:22, 10:22]\n            signal_roi = signal_roi.reshape(signal_roi.shape[0], -1)\n        \n        mean_signal = np.nanmean(signal_roi, axis=1)\n        cds_signal = mean_signal[1::2] - mean_signal[0::2]\n\n        n_bins = cds_signal.shape[0] // binning\n        binned = np.array([\n            cds_signal[j*binning : (j+1)*binning].mean(axis=0) \n            for j in range(n_bins)\n        ])\n\n        if sensor == \"AIRS-CH0\":\n            q_lo = np.nanpercentile(binned, 5.0, axis=1, keepdims=True)\n            q_hi = np.nanpercentile(binned, 95.0, axis=1, keepdims=True)\n            np.clip(binned, q_lo, q_hi, out=binned)\n\n        if sensor == \"FGS1\":\n            binned = binned.reshape((binned.shape[0], 1))\n\n        if sensor == \"AIRS-CH0\":\n            var = np.nanvar(binned, axis=0, ddof=1)\n            med = np.nanmedian(var)\n            safe_var = np.where(~np.isfinite(var) | (var <= 0), med if (np.isfinite(med) and med > 0) else 1.0, var)\n            w = 1.0 / safe_var\n\n            lo, hi = np.nanpercentile(w, 5.0), np.nanpercentile(w, 95.0)\n            if np.isfinite(lo) and np.isfinite(hi) and lo < hi:\n                w = np.clip(w, lo, hi)\n\n            M = binned.shape[1]\n            s = np.nansum(w)\n            if np.isfinite(s) and s > 0:\n                w = w * (M / s)\n            else:\n                w = np.ones_like(w)\n\n            binned *= w[None, :]\n\n        return binned\n\n    def _process_planet_sensor(self, args):\n        planet_id, sensor = args['planet_id'], args['sensor']\n        calibrated = self._calibrate_single_signal(planet_id, sensor)\n        preprocessed = self._preprocess_calibrated_signal(calibrated, sensor)\n        return preprocessed\n\n    def process_all_data(self):\n        args_fgs1 = [dict(planet_id=planet_id, sensor=\"FGS1\") for planet_id in self.planet_ids]\n        preprocessed_fgs1 = pqdm(args_fgs1, self._process_planet_sensor, n_jobs=self.cfg.N_JOBS)\n\n        args_airs_ch0 = [dict(planet_id=planet_id, sensor=\"AIRS-CH0\") for planet_id in self.planet_ids]\n        preprocessed_airs_ch0 = pqdm(args_airs_ch0, self._process_planet_sensor, n_jobs=self.cfg.N_JOBS)\n\n        preprocessed_signal = np.concatenate(\n            [np.stack(preprocessed_fgs1), np.stack(preprocessed_airs_ch0)], axis=2\n        )\n        return preprocessed_signal\n    \n\n# ======== 1Dトランジット深さ推定 ========\nclass TransitModel:\n    def __init__(self, config):\n        self.cfg = config\n\n    def _phase_detector(self, signal):\n        search_slice = self.cfg.MODEL_PHASE_DETECTION_SLICE\n        min_index = np.argmin(signal[search_slice]) + search_slice.start\n        \n        signal1 = signal[:min_index]\n        signal2 = signal[min_index:]\n\n        grad1 = np.gradient(signal1)\n        grad1 /= grad1.max()\n        \n        grad2 = np.gradient(signal2)\n        grad2 /= grad2.max()\n\n        phase1 = np.argmin(grad1)\n        phase2 = np.argmax(grad2) + min_index\n\n        return phase1, phase2\n    \n    def _objective_function(self, s, signal, phase1, phase2):\n        delta = self.cfg.MODEL_OPTIMIZATION_DELTA\n        power = self.cfg.MODEL_POLYNOMIAL_DEGREE\n\n        if phase1 - delta <= 0 or phase2 + delta >= len(signal) or phase2 - delta - (phase1 + delta) < 5:\n            delta = 2\n\n        y = np.concatenate([\n            signal[: phase1 - delta],\n            signal[phase1 + delta : phase2 - delta] * (1 + s),\n            signal[phase2 + delta :]\n        ])\n        x = np.arange(len(y))\n\n        coeffs = np.polyfit(x, y, deg=power)\n        poly = np.poly1d(coeffs)\n        error = np.abs(poly(x) - y).mean()\n        \n        return error\n\n    def predict(self, single_preprocessed_signal):\n        signal_1d = single_preprocessed_signal[:, 1:].mean(axis=1)\n        signal_1d = savgol_filter(signal_1d, 23, 2)   # #2 は今回は触らない\n        \n        phase1, phase2 = self._phase_detector(signal_1d)\n\n        phase1 = max(self.cfg.MODEL_OPTIMIZATION_DELTA, phase1)\n        phase2 = min(len(signal_1d) - self.cfg.MODEL_OPTIMIZATION_DELTA - 1, phase2)    \n\n        result = minimize(\n            fun=self._objective_function,\n            x0=[0.0001],\n            args=(signal_1d, phase1, phase2),\n            method=\"Nelder-Mead\"\n        )\n        \n        return result.x[0]\n\n    def predict_all(self, preprocessed_signals):\n        predictions = [\n            self.predict(preprocessed_signal)\n            for preprocessed_signal in tqdm(preprocessed_signals)\n        ]\n        return np.array(predictions) * self.cfg.SCALE\n\n\n# ======== メタデータ ========\nStarInfo = pd.read_csv(ROOT_PATH + f\"/{MODE}_star_info.csv\")\nStarInfo[\"planet_id\"] = StarInfo[\"planet_id\"].astype(int)\nPlanetIds = StarInfo[\"planet_id\"].tolist()\nStarInfo = StarInfo.set_index(\"planet_id\")\n\n\n# ======== AIRS 出力（282次元）モデル ========\nclass ResidualBlock2(nn.Module):\n    def __init__(self, dim, p=0.2):\n        super().__init__()\n        self.fc1 = nn.Linear(dim, dim)\n        self.bn1 = nn.BatchNorm1d(dim)\n        self.fc2 = nn.Linear(dim, dim)\n        self.bn2 = nn.BatchNorm1d(dim)\n        self.relu = nn.ReLU()\n        self.dropout = nn.Dropout(p)\n\n    def forward(self, x):\n        identity = x\n        out = self.relu(self.bn1(self.fc1(x)))\n        out = self.dropout(out)\n        out = self.bn2(self.fc2(out))\n        return self.relu(out + identity)\n\n\nclass ResNetMLP2(nn.Module):\n    def __init__(self, input_dim=3, hidden_dim=128, output_dim=282, num_blocks=3, dropout_rate=0.2):\n        super().__init__()\n        self.input_layer = nn.Linear(input_dim, hidden_dim)\n        self.blocks = nn.Sequential(*[ResidualBlock2(hidden_dim, p=dropout_rate) for _ in range(num_blocks)])\n        self.output_layer = nn.Linear(hidden_dim, output_dim)\n\n    def forward(self, x):\n        x = self.input_layer(x)\n        x = self.blocks(x)\n        x = self.output_layer(x)\n        return x\n\n\n# ======== 提出生成 ========\nclass SubmissionGenerator:\n    def __init__(self, config):\n        self.cfg = config\n        self.sample_submission = pd.read_csv(\n            \"/kaggle/input/ariel-data-challenge-2025/sample_submission.csv\",\n            index_col=\"planet_id\"\n        )\n\n    def create(self, predictions1, predictions, sigma_fgs=None, sigma_air=None):\n        # Convert to DataFrames with planet_id as index for safe alignment\n        planet_ids = self.sample_submission.index\n\n        # Align predictions\n        pred_df = pd.DataFrame({\"planet_id\": planet_ids, \"mu\": predictions})\n        pred_df = pred_df.set_index(\"planet_id\").reindex(planet_ids)\n\n        # Align predictions1\n        pred1_df = pd.DataFrame(predictions1, index=planet_ids)\n        pred1_df = pred1_df.reindex(planet_ids)\n\n        # Align sigma_fgs\n        if sigma_fgs is not None:\n            sigma_fgs = pd.Series(sigma_fgs, index=planet_ids).reindex(planet_ids).values\n\n        # Align sigma_air\n        if sigma_air is not None:\n            sigma_air = pd.Series(sigma_air, index=planet_ids).reindex(planet_ids).values\n\n        n_mu = self.sample_submission.shape[1] // 2\n\n        mu = np.tile(pred_df[\"mu\"].values.reshape(-1, 1), (1, n_mu))\n        mu = np.clip(mu, 0, None)\n\n        sigmas = np.full_like(mu, self.cfg.SIGMA, dtype=float)\n        if sigma_fgs is not None:\n            sigmas[:, 0] = np.clip(sigma_fgs, 1e-6, 0.1)\n        if sigma_air is not None:\n            sigmas[:, 1:] = np.clip(sigma_air.reshape(-1, 1), 1e-6, 0.1)\n\n        submission_df = pd.DataFrame(\n            np.concatenate([mu, sigmas], axis=1),\n            columns=self.sample_submission.columns,\n            index=planet_ids\n        )\n\n        # Overwrite μ columns explicitly (safe after reindexing)\n        submission_df.iloc[:, 0] = pred_df[\"mu\"].values\n        submission_df.iloc[:, 1:283] = pred1_df.values\n\n        # Write CSV\n        submission_df.to_csv(\"submission.csv\")\n        print(\"[INFO] submission.csv written successfully with shape:\", submission_df.shape)\n        return submission_df\n\n\n\n# =========================\n# 実行フロー（修正版）\n# =========================\n\nif __name__ == \"__main__\":\n    config = Config()\n    signal_processor = SignalProcessor(config)\n    \n    # --- 前処理 ---\n    preprocessed_data = signal_processor.process_all_data()\n    \n    # --- TransitModel 推論 ---\n    model = TransitModel(config)\n    predictions = model.predict_all(preprocessed_data)\n    \n    # --- σ 推定 ---\n    sigma_fgs_vec = estimate_sigma_fgs(preprocessed_data, config)\n    sigma_air_vec = estimate_sigma_air(preprocessed_data, config)\n    \n    predictions_corrected = predictions.copy()\n    sigma_air_corrected = sigma_air_vec.copy()\n    \n    # --- Mask: TransitModel がほぼトランジットを検出しない惑星 ---\n    mask = predictions < (np.median(predictions) * 0.1)\n    \n    # --- log-model 補正（マスク対象のみ） ---\n    if mask.any():\n        adjusted, adjusted_sigma = log_model_ensemble_adjust(\n            preprocessed_data[mask],\n            predictions[mask],\n            sigma_air_vec[mask],\n            log_threshold=0.75,\n            window_size=4\n        )\n        # 軽くブレンド\n        alpha = 0.5  # 0.0 = TransitModel only, 1.0 = log model only\n        predictions_corrected[mask] = alpha * adjusted + (1 - alpha) * predictions[mask]\n        sigma_air_corrected[mask] = adjusted_sigma\n    \n    # --- メタ特徴作成 & AIRSモデル推論 ---\n    predictions_df = pd.DataFrame({\"planet_id\": PlanetIds,\"transit_depth\": predictions_corrected})\n    input_df = pd.merge(predictions_df, StarInfo, on=\"planet_id\", how=\"left\")\n    input_df[\"transit_depth\"] *= 10000  # scale for AIRS model\n    \n    features = ['transit_depth','Rs','i']\n    X = input_df[features].values.astype(np.float32)\n    X_tensor = torch.tensor(X, dtype=torch.float32)\n\n    resnet2 = ResNetMLP2(num_blocks=80, dropout_rate=0.3)\n    resnet2.load_state_dict(torch.load(\"/kaggle/input/airs/pytorch/default/1/best_model_airs.pth\", map_location=\"cpu\"))\n    resnet2.eval()\n    \n    with torch.no_grad():\n        predictions1 = resnet2(X_tensor).numpy()\n    predictions1 /= 10000\n    # --- 波長方向スムージング 0.7:0.3 ブレンド ---\n    predictions1_smooth = savgol_filter(predictions1, window_length=13, polyorder=2, axis=1)\n    predictions1 = 0.7 * predictions1 + 0.3 * predictions1_smooth \n    \n    # --- 提出生成 ---\n    submission_generator = SubmissionGenerator(config)\n    submission = submission_generator.create(\n        predictions1,\n        predictions_corrected,\n        sigma_fgs=sigma_fgs_vec,\n        sigma_air=sigma_air_corrected\n    )\n    \n    __t1 = time.perf_counter()\n    elapsed = __t1 - __t0\n    print(f\"[TIMING] total runtime: {elapsed:.2f} s ({elapsed/60:.2f} min)\")\n    print(submission.head())","metadata":{"_uuid":"e6844afc-ace1-4ef0-aa01-84dc82ec03aa","_cell_guid":"4dc63a37-342e-487c-8418-cd2741fcfffb","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-09-21T04:02:43.149104Z","iopub.execute_input":"2025-09-21T04:02:43.149522Z","iopub.status.idle":"2025-09-21T04:03:08.184619Z","shell.execute_reply.started":"2025-09-21T04:02:43.149484Z","shell.execute_reply":"2025-09-21T04:03:08.182836Z"}},"outputs":[],"execution_count":null}]}