{"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":"gpu","dataSources":[{"sourceId":60095,"databundleVersionId":6542333,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":13611908,"sourceType":"datasetVersion","datasetId":8650300},{"sourceId":629687,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":474393,"modelId":490278}],"dockerImageVersionId":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Model Evaluation Notebook\nThis notebook is for evaluation only. It loads a pre-trained RNN model and its corresponding scalers to evaluate performance on the validation dataset.","metadata":{}},{"cell_type":"code","source":"import os\nimport glob\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import RobustScaler\nimport matplotlib.pyplot as plt\nimport re\nimport joblib\nfrom sklearn.metrics import mean_squared_error\n\n# (Optional) Suppress TensorFlow logging\nos.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:37:17.294200Z","iopub.execute_input":"2025-11-04T15:37:17.294739Z","iopub.status.idle":"2025-11-04T15:37:17.299363Z","shell.execute_reply.started":"2025-11-04T15:37:17.294715Z","shell.execute_reply":"2025-11-04T15:37:17.298595Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# IMPORTANT: Update this path if your data is in a different location\nDATA_DIR = '/kaggle/input/smartphone-decimeter-2023/sdc2023'\n\n# This MUST match the sequence length the model was trained on\nSEQUENCE_LENGTH = 50","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:37:17.300922Z","iopub.execute_input":"2025-11-04T15:37:17.301146Z","iopub.status.idle":"2025-11-04T15:37:17.317118Z","shell.execute_reply.started":"2025-11-04T15:37:17.301131Z","shell.execute_reply":"2025-11-04T15:37:17.316569Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TRAIN_DATA_DIR = os.path.join(DATA_DIR, 'train')\ntrace_paths = glob.glob(os.path.join(TRAIN_DATA_DIR, \"*\", \"*\"))\nunique_traces = sorted([path for path in trace_paths if os.path.isdir(path)])\nprint(f\"Found {len(unique_traces)} total traces.\")\n\n# Split to get the same validation set as during training\ntrain_paths, val_paths = train_test_split(\n    unique_traces,\n    test_size=0.2,  # Must be same as training\n    random_state=42 # Must be same as training\n)\nprint(f\"Using {len(val_paths)} validation traces for evaluation.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:37:17.317781Z","iopub.execute_input":"2025-11-04T15:37:17.317938Z","iopub.status.idle":"2025-11-04T15:37:17.528857Z","shell.execute_reply.started":"2025-11-04T15:37:17.317926Z","shell.execute_reply":"2025-11-04T15:37:17.528273Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 1. Utility Functions\nThis section defines all the necessary helper functions for the pipeline:\n* `load_and_preprocess_trace`: Loads and merges the raw data files and creates the `trip_id`.\n* `create_sliding_windows`: Creates sequences for the RNN.\n* `ecef_to_lla`: Converts coordinates.\n* `calculate_gsdc_score`: The official competition metric.","metadata":{}},{"cell_type":"code","source":"def _safe_column_lookup(df, pattern_list):\n    cols = df.columns.tolist()\n    found = []\n    for pat in pattern_list:\n        pat_re = re.compile(pat, re.I)\n        match = next((c for c in cols if pat_re.search(c)), None)\n        if match is None:\n            return None\n        found.append(match)\n        cols.remove(match)\n    return found\n\ndef _extract_from_message_style(imu_df):\n    accel_df = pd.DataFrame()\n    gyro_df = pd.DataFrame()\n    if 'MessageType' in imu_df.columns and any(c.lower().startswith('measurement') for c in imu_df.columns):\n        meas_cols = {\n            'x': next((c for c in imu_df.columns if re.match(r'measurement.*x', c, re.I)), None),\n            'y': next((c for c in imu_df.columns if re.match(r'measurement.*y', c, re.I)), None),\n            'z': next((c for c in imu_df.columns if re.match(r'measurement.*z', c, re.I)), None)\n        }\n        if all(meas_cols.values()) and 'utcTimeMillis' in imu_df.columns:\n            mx, my, mz = meas_cols['x'], meas_cols['y'], meas_cols['z']\n            accel_rows = imu_df[imu_df['MessageType'].str.contains('accel', na=False, case=False)]\n            if not accel_rows.empty:\n                accel_df = accel_rows[['utcTimeMillis', mx, my, mz]].copy()\n                accel_df.columns = ['utcTimeMillis', 'accel_x', 'accel_y', 'accel_z']\n            gyro_rows = imu_df[imu_df['MessageType'].str.contains('gyro', na=False, case=False)]\n            if not gyro_rows.empty:\n                gyro_df = gyro_rows[['utcTimeMillis', mx, my, mz]].copy()\n                gyro_df.columns = ['utcTimeMillis', 'gyro_x', 'gyro_y', 'gyro_z']\n    return accel_df, gyro_df\n\ndef load_and_preprocess_trace(trace_path):\n    trace_name = os.path.basename(os.path.dirname(trace_path)) + \"/\" + os.path.basename(trace_path)\n    try:\n        gnss_df = pd.read_csv(os.path.join(trace_path, 'device_gnss.csv'), low_memory=False)\n        imu_df = pd.read_csv(os.path.join(trace_path, 'device_imu.csv'))\n        ground_truth_df = pd.read_csv(os.path.join(trace_path, 'ground_truth.csv'))\n        \n        # --- TRIP ID LOGIC (PART 1) ---\n        # Create a unique ID from the folder name (drive_id) and subfolder name (phone)\n        drive_id = os.path.basename(os.path.dirname(trace_path))\n        phone = os.path.basename(trace_path)\n        ground_truth_df['trip_id'] = f\"{drive_id}_{phone}\" # <--- THIS IS THE TRIP ID LOGIC\n        # --- END OF TRIP ID LOGIC (PART 1) ---\n        \n        if 'UnixTimeMillis' in ground_truth_df.columns:\n            ground_truth_df.rename(columns={'UnixTimeMillis': 'utcTimeMillis'}, inplace=True)\n        accel_df, gyro_df = _extract_from_message_style(imu_df)\n        if accel_df.empty or gyro_df.empty:\n            accel_patterns = [\n                ['UncalibratedAccelerometerMps2_x', 'UncalibratedAccelerometerMps2_y', 'UncalibratedAccelerometerMps2_z'],\n                ['AccelerometerMps2_x', 'AccelerometerMps2_y', 'AccelerometerMps2_z'],\n                [r'accel.*_x', r'accel.*_y', r'accel.*_z']\n            ]\n            gyro_patterns = [\n                ['UncalibratedGyroscopeRps_x', 'UncalibratedGyroscopeRps_y', 'UncalibratedGyroscopeRps_z'],\n                ['GyroscopeRps_x', 'GyroscopeRps_y', 'GyroscopeRps_z'],\n                [r'gyro.*_x', r'gyro.*_y', r'gyro.*_z']\n            ]\n            time_col = next((c for c in imu_df.columns if 'time' in c.lower()), None)\n            if not time_col: return pd.DataFrame()\n            accel_cols = next((cols for pat in accel_patterns if (cols := _safe_column_lookup(imu_df, pat))), None)\n            gyro_cols = next((cols for pat in gyro_patterns if (cols := _safe_column_lookup(imu_df, pat))), None)\n            if accel_cols:\n                accel_df = imu_df[[time_col] + accel_cols].copy()\n                accel_df.columns = ['utcTimeMillis', 'accel_x', 'accel_y', 'accel_z']\n            if gyro_cols:\n                gyro_df = imu_df[[time_col] + gyro_cols].copy()\n                gyro_df.columns = ['utcTimeMillis', 'gyro_x', 'gyro_y', 'gyro_z']\n        if accel_df.empty and gyro_df.empty: return pd.DataFrame()\n        if not accel_df.empty and gyro_df.empty:\n            gyro_df = accel_df[['utcTimeMillis']].copy()\n            gyro_df[['gyro_x', 'gyro_y', 'gyro_z']] = 0.0\n        if accel_df.empty and not gyro_df.empty:\n            accel_df = gyro_df[['utcTimeMillis']].copy()\n            accel_df[['accel_x', 'accel_y', 'accel_z']] = 0.0\n        for df in [accel_df, gyro_df]:\n            df['utcTimeMillis'] = df['utcTimeMillis'].astype(np.int64)\n            df.sort_values('utcTimeMillis', inplace=True)\n        imu_merged = pd.merge_asof(accel_df, gyro_df, on='utcTimeMillis', direction='nearest', tolerance=20).dropna()\n        required_gnss_cols = ['utcTimeMillis', 'WlsPositionXEcefMeters', 'WlsPositionYEcefMeters', 'WlsPositionZEcefMeters']\n        if not all(col in gnss_df.columns for col in required_gnss_cols): return pd.DataFrame()\n        gnss_df = gnss_df[required_gnss_cols].copy()\n        gnss_df.rename(columns={\n            'WlsPositionXEcefMeters': 'WlsPositionEcefMeters_x',\n            'WlsPositionYEcefMeters': 'WlsPositionEcefMeters_y',\n            'WlsPositionZEcefMeters': 'WlsPositionEcefMeters_z'\n        }, inplace=True)\n        if 'utcTimeMillis' not in ground_truth_df.columns: return pd.DataFrame()\n        ground_truth_df.sort_values('utcTimeMillis', inplace=True)\n        gnss_df.sort_values('utcTimeMillis', inplace=True)\n        merged_df = pd.merge_asof(ground_truth_df, gnss_df, on='utcTimeMillis', direction='nearest', tolerance=1000)\n        merged_df = pd.merge_asof(merged_df, imu_merged, on='utcTimeMillis', direction='nearest', tolerance=50)\n        essential_cols = ['LatitudeDegrees', 'LongitudeDegrees', 'accel_x', 'accel_y', 'accel_z', 'gyro_x', 'gyro_y', 'gyro_z']\n        merged_df.dropna(subset=essential_cols, inplace=True)\n        return merged_df.reset_index(drop=True)\n    except Exception as e:\n        print(f\"Warning: Error processing trace {trace_name}: {e}\")\n        return pd.DataFrame()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:37:17.529606Z","iopub.execute_input":"2025-11-04T15:37:17.529851Z","iopub.status.idle":"2025-11-04T15:37:17.548546Z","shell.execute_reply.started":"2025-11-04T15:37:17.529835Z","shell.execute_reply":"2025-11-04T15:37:17.547932Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_sliding_windows(feature_data, target_data, sequence_length):\n    X, y = [], []\n    for i in range(len(feature_data) - sequence_length):\n        X.append(feature_data[i:(i + sequence_length)])\n        y.append(target_data[i + sequence_length])\n    return np.array(X), np.array(y)\n\ndef ecef_to_lla(ecef_coords):\n    a = 6378137.0\n    e = 8.1819190842622e-2\n    x, y, z = ecef_coords[:, 0], ecef_coords[:, 1], ecef_coords[:, 2]\n    p = np.sqrt(x**2 + y**2)\n    lon = np.arctan2(y, x)\n    lat_initial = np.arctan2(z, p * (1 - e**2))\n    lat = lat_initial\n    for _ in range(5):\n        N = a / np.sqrt(1 - e**2 * np.sin(lat)**2)\n        h = p / np.cos(lat) - N\n        lat = np.arctan2(z, p * (1 - e**2 * N / (N + h)))\n    N = a / np.sqrt(1 - e**2 * np.sin(lat)**2)\n    alt = p / np.cos(lat) - N\n    lat_deg = np.rad2deg(lat)\n    lon_deg = np.rad2deg(lon)\n    return lat_deg, lon_deg, alt","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:37:17.550301Z","iopub.execute_input":"2025-11-04T15:37:17.550569Z","iopub.status.idle":"2025-11-04T15:37:17.565580Z","shell.execute_reply.started":"2025-11-04T15:37:17.550554Z","shell.execute_reply":"2025-11-04T15:37:17.564934Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def calculate_gsdc_score(ground_truth, predictions):\n    R = 6371000\n    lat_gt, lon_gt = np.deg2rad(ground_truth[:, 0]), np.deg2rad(ground_truth[:, 1])\n    lat_pred, lon_pred = np.deg2rad(predictions[:, 0]), np.deg2rad(predictions[:, 1])\n    dlon, dlat = lon_pred - lon_gt, lat_pred - lat_gt\n    a = np.sin(dlat/2)**2 + np.cos(lat_gt) * np.cos(lat_pred) * np.sin(dlon/2)**2\n    c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1-a))\n    distances = R * c\n    return np.mean([np.percentile(distances, 50), np.percentile(distances, 95)])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:37:17.566266Z","iopub.execute_input":"2025-11-04T15:37:17.566601Z","iopub.status.idle":"2025-11-04T15:37:17.583093Z","shell.execute_reply.started":"2025-11-04T15:37:17.566579Z","shell.execute_reply":"2025-11-04T15:37:17.582300Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Feature definitions based on your 3-scaler logic ---\ncoords_features = ['WlsLatitudeDegrees', 'WlsLongitudeDegrees']\nimu_features = ['accel_x', 'accel_y', 'accel_z', 'gyro_x', 'gyro_y', 'gyro_z']\ntarget_cols = ['LatOffsetDegrees', 'LonOffsetDegrees']\necef_cols = ['WlsPositionEcefMeters_x', 'WlsPositionEcefMeters_y', 'WlsPositionEcefMeters_z']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:37:17.583882Z","iopub.execute_input":"2025-11-04T15:37:17.584080Z","iopub.status.idle":"2025-11-04T15:37:17.596944Z","shell.execute_reply.started":"2025-11-04T15:37:17.584058Z","shell.execute_reply":"2025-11-04T15:37:17.596218Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2. Load Pre-trained Artifacts\nNow, we load the saved `.keras` model and the three `.joblib` scalers (for coordinates, IMU, and targets) that were generated during training.","metadata":{}},{"cell_type":"code","source":"print(\"Loading pre-trained artifacts...\")\n\ntry:\n    # 1. Load the Coords scaler\n    coords_scaler = joblib.load(\"/kaggle/input/finalweight/coords_scaler.joblib\")\n    print(\"✅ Coords scaler loaded successfully.\")\n    \n    # 2. Load the IMU scaler\n    imu_scaler = joblib.load(\"/kaggle/input/finalweight/imu_scaler.joblib\")\n    print(\"✅ IMU scaler loaded successfully.\")\n\n    # 3. Load the Target scaler\n    target_scaler = joblib.load(\"/kaggle/input/finalweight/target_scaler (2).joblib\")\n    print(\"✅ Target scaler loaded successfully.\")\n\n    # 4. Load the pre-trained Keras model\n    model_path = \"/kaggle/input/gnss-prediction/keras/default/1/gnss_correction_model_final.keras\"\n    model = tf.keras.models.load_model(model_path)\n    print(f\"✅ Model loaded successfully from {model_path}\")\n    model.summary()\n\nexcept Exception as e:\n    print(f\"❌ Error loading artifacts: {e}\")\n    print(\"Please check the file paths and ensure the files are available in the input directory.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:37:17.597704Z","iopub.execute_input":"2025-11-04T15:37:17.597950Z","iopub.status.idle":"2025-11-04T15:37:17.843907Z","shell.execute_reply.started":"2025-11-04T15:37:17.597929Z","shell.execute_reply":"2025-11-04T15:37:17.843120Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\nLoading and preprocessing all validation traces...\")\nall_traces_df_list_val = []\nfor path in val_paths:\n    trace_df = load_and_preprocess_trace(path)\n    if not trace_df.empty:\n        all_traces_df_list_val.append(trace_df)\nprint(f\"Successfully loaded and preprocessed {len(all_traces_df_list_val)} validation traces.\")\n\nfull_val_df = pd.concat(all_traces_df_list_val, ignore_index=True)\n\n# --- Feature Engineering (as per your snippet) ---\n# 1. Convert ECEF to LLA\necef_coords_val = full_val_df[ecef_cols].values\nlat_wls_val, lon_wls_val, alt_wls_val = ecef_to_lla(ecef_coords_val)\nfull_val_df['WlsLatitudeDegrees'] = lat_wls_val\nfull_val_df['WlsLongitudeDegrees'] = lon_wls_val\n\n# 2. Calculate Target Offsets\nfull_val_df['LatOffsetDegrees'] = full_val_df['LatitudeDegrees'] - full_val_df['WlsLatitudeDegrees']\nfull_val_df['LonOffsetDegrees'] = full_val_df['LongitudeDegrees'] - full_val_df['WlsLongitudeDegrees']\n\n# 3. Filter outliers\nMAX_OFFSET = 0.01 # Use the same 0.01 threshold\ninitial_val_rows = len(full_val_df)\nfull_val_df = full_val_df[\n    (full_val_df['LatOffsetDegrees'].abs() < MAX_OFFSET) &\n    (full_val_df['LonOffsetDegrees'].abs() < MAX_OFFSET)\n].reset_index(drop=True)\nprint(f\"Removing {initial_val_rows - len(full_val_df)} rows with extreme offsets.\")\n\nprint(\"\\nCreating validation sequences...\")\nX_val_list = []\ny_val_list = []\nground_truth_coords_list = []\nwls_coords_list = []\n\n# --- TRIP ID LOGIC (PART 2) ---\n# We loop over the DataFrame grouped by the 'trip_id' we created in Cell 4.\nfor trip_id, trip_df in full_val_df.groupby('trip_id'): # <--- THIS IS THE TRIP ID LOGIC\n    \n    # --- Apply all 3 loaded scalers ---\n    scaled_coords = coords_scaler.transform(trip_df[coords_features])\n    scaled_imu = imu_scaler.transform(trip_df[imu_features])\n    scaled_target = target_scaler.transform(trip_df[target_cols])\n    \n    # 4. Stack SCALED Coords + SCALED IMU (matching 3-scaler logic)\n    scaled_features = np.hstack([scaled_coords, scaled_imu])\n    scaled_features = np.clip(scaled_features, -100.0, 100.0)\n\n    X_trace, y_trace = create_sliding_windows(\n        scaled_features.astype(np.float32), \n        scaled_target.astype(np.float32), \n        SEQUENCE_LENGTH\n    )\n    \n    if len(X_trace) > 0:\n        X_val_list.append(X_trace)\n        y_val_list.append(y_trace)\n        ground_truth_coords_list.append(trip_df[['LatitudeDegrees', 'LongitudeDegrees']].iloc[SEQUENCE_LENGTH:].values)\n        wls_coords_list.append(trip_df[['WlsLatitudeDegrees', 'WlsLongitudeDegrees']].iloc[SEQUENCE_LENGTH:].values)\n# --- END OF TRIP ID LOGIC (PART 2) ---\n\nplot_len = 0 # Initialize plot length\nif len(X_val_list) > 0:\n    # --- FIX FOR PLOTTING SPIKE ---\n    # Store the length of the *first trip* and limit it to 500 for plotting\n    len_first_trip = len(X_val_list[0]) \n    plot_len = min(len_first_trip, 500) # <--- THIS IS THE FIX (added this line)\n    # --- END OF FIX ---\n    \n    X_val = np.concatenate(X_val_list, axis=0)\n    y_val = np.concatenate(y_val_list, axis=0)\n    ground_truth_coords_for_eval = np.concatenate(ground_truth_coords_list, axis=0)\n    wls_coords_for_eval = np.concatenate(wls_coords_list, axis=0)\n    \n    print(f\"\\nFinal X_val (evaluation) shape: {X_val.shape}\")\n    print(f\"Final y_val (evaluation) shape: {y_val.shape}\")\nelse:\n    print(\"No validation data available after processing. Check paths and filters.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:37:17.844764Z","iopub.execute_input":"2025-11-04T15:37:17.845045Z","iopub.status.idle":"2025-11-04T15:37:51.640718Z","shell.execute_reply.started":"2025-11-04T15:37:17.845022Z","shell.execute_reply":"2025-11-04T15:37:51.640061Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 3. Run Evaluation\nUsing the loaded model and scalers, we process the entire validation dataset, create the sequences, and run `model.predict()` to get the final predictions.","metadata":{}},{"cell_type":"code","source":"print(\"\\nEvaluating model on validation set...\")\npredicted_scaled_offsets = model.predict(X_val)\n\n# Inverse transform to get offsets in degrees\npredicted_offsets = target_scaler.inverse_transform(predicted_scaled_offsets)\n\n# Add offsets to the baseline WLS coordinates\nfinal_predictions = wls_coords_for_eval + predicted_offsets\n\n# Compute GSDC score\nscore = calculate_gsdc_score(ground_truth_coords_for_eval, final_predictions)\nprint(f\"\\nValidation GSDC Score: {score:.4f} meters\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:37:51.641387Z","iopub.execute_input":"2025-11-04T15:37:51.641602Z","iopub.status.idle":"2025-11-04T15:38:03.190887Z","shell.execute_reply.started":"2025-11-04T15:37:51.641586Z","shell.execute_reply":"2025-11-04T15:38:03.190221Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Plotting sample trajectory...\")\n\n# --- FIX FOR PLOTTING ---\n# Use plot_len to only plot the first 500 points (or less) of the *first trip*.\nplot_len = min(len_first_trip, 500) \n# --- END OF FIX ---\n\nplt.figure(figsize=(12, 8))\nplt.plot(ground_truth_coords_for_eval[:plot_len, 1], ground_truth_coords_for_eval[:plot_len, 0], '.-', label='Ground Truth', alpha=0.7)\nplt.plot(final_predictions[:plot_len, 1], final_predictions[:plot_len, 0], '.-', label='Prediction', alpha=0.7)\nplt.xlabel(\"Longitude\")\nplt.ylabel(\"Latitude\")\nplt.title(\"Predicted vs. Ground Truth Trajectory (Sample)\")\nplt.legend()\nplt.grid(True)\nplt.axis('equal')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:38:03.191556Z","iopub.execute_input":"2025-11-04T15:38:03.191764Z","iopub.status.idle":"2025-11-04T15:38:03.394739Z","shell.execute_reply.started":"2025-11-04T15:38:03.191749Z","shell.execute_reply":"2025-11-04T15:38:03.393993Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"R = 6371000 # Earth radius\nlat_gt = np.deg2rad(ground_truth_coords_for_eval[:, 0])\nlon_gt = np.deg2rad(ground_truth_coords_for_eval[:, 1])\nlat_pred = np.deg2rad(final_predictions[:, 0])\nlon_pred = np.deg2rad(final_predictions[:, 1])\n\ndlon = lon_pred - lon_gt\ndlat = lat_pred - lat_gt\na = np.sin(dlat/2)**2 + np.cos(lat_gt) * np.cos(lat_pred) * np.sin(dlon/2)**2\nc = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a))\ndistances = R * c\n\n# Model performance\nmae_distance = np.mean(distances)\nrmse_distance = np.sqrt(np.mean(distances**2))\np50 = np.percentile(distances, 50)\np95 = np.percentile(distances, 95)\ngsdc_score_calculated = np.mean([p50, p95])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:38:03.395622Z","iopub.execute_input":"2025-11-04T15:38:03.395846Z","iopub.status.idle":"2025-11-04T15:38:03.405918Z","shell.execute_reply.started":"2025-11-04T15:38:03.395830Z","shell.execute_reply":"2025-11-04T15:38:03.405258Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"lat_wls_rad = np.deg2rad(wls_coords_for_eval[:, 0])\nlon_wls_rad = np.deg2rad(wls_coords_for_eval[:, 1])\ndlon_base = lon_wls_rad - lon_gt\ndlat_base = lat_wls_rad - lat_gt\na_base = np.sin(dlat_base/2)**2 + np.cos(lat_gt) * np.cos(lat_wls_rad) * np.sin(dlon_base/2)**2\nc_base = 2 * np.arctan2(np.sqrt(a_base), np.sqrt(1 - a_base))\nbaseline_distances = R * c_base\n\n# Baseline performance\nbaseline_p50 = np.percentile(baseline_distances, 50)\nbaseline_p95 = np.percentile(baseline_distances, 95)\nbaseline_gsdc = np.mean([baseline_p50, baseline_p95])\nbaseline_mae = np.mean(baseline_distances)\nbaseline_rmse = np.sqrt(np.mean(baseline_distances**2))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:38:03.406615Z","iopub.execute_input":"2025-11-04T15:38:03.406910Z","iopub.status.idle":"2025-11-04T15:38:03.417075Z","shell.execute_reply.started":"2025-11-04T15:38:03.406893Z","shell.execute_reply":"2025-11-04T15:38:03.416546Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"metrics_data = {\n    'Metric': ['GSDC Score', 'Median Error (p50)', '95th Percentile (p95)', 'MAE', 'RMSE'],\n    'Baseline GNSS (meters)': [\n        baseline_gsdc,\n        baseline_p50,\n        baseline_p95,\n        baseline_mae,\n        baseline_rmse\n    ],\n    'Final Model (meters)': [\n        gsdc_score_calculated,\n        p50,\n        p95,\n        mae_distance,\n        rmse_distance\n    ]\n}\n\ndf_results = pd.DataFrame(metrics_data)\ndf_results['Improvement (meters)'] = df_results['Baseline GNSS (meters)'] - df_results['Final Model (meters)']\nprint(\"--- Performance Comparison: Baseline vs. Final Model ---\")\nprint(df_results.to_string(index=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:38:03.419706Z","iopub.execute_input":"2025-11-04T15:38:03.419945Z","iopub.status.idle":"2025-11-04T15:38:03.428018Z","shell.execute_reply.started":"2025-11-04T15:38:03.419923Z","shell.execute_reply":"2025-11-04T15:38:03.427224Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 4. Performance Visualization\nThe following plots provide a visual analysis of the model's performance by comparing the baseline (WLS) error to the final corrected model error.","metadata":{}},{"cell_type":"code","source":"print(\"\\nPlotting error distributions...\")\n\n# --- FIX FOR PLOTTING ---\n# Slice the data to only show the first trip for a cleaner histogram\nplot_len = min(len_first_trip, 500)\n# --- END OF FIX ---\n\nplt.figure(figsize=(12, 7))\nplt.hist(baseline_distances[:plot_len], bins=100, label='Baseline GNSS Error', alpha=0.7, color='darkorange', range=[0, 15])\nplt.hist(distances[:plot_len], bins=100, label='Final Model Error', alpha=0.7, color='royalblue', range=[0, 15])\nplt.title('Distribution of Baseline Error vs. Final Model Error (Sample)')\nplt.xlabel('Position Error (meters)')\nplt.ylabel('Frequency')\nplt.legend()\nplt.grid(True)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:38:03.428718Z","iopub.execute_input":"2025-11-04T15:38:03.428917Z","iopub.status.idle":"2025-11-04T15:38:03.851404Z","shell.execute_reply.started":"2025-11-04T15:38:03.428896Z","shell.execute_reply":"2025-11-04T15:38:03.850707Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\nPlotting metrics comparison bar chart...\")\n\n# Get data from the DataFrame created in Cell 15\nlabels = df_results['Metric']\nbaseline_values = df_results['Baseline GNSS (meters)']\nmodel_values = df_results['Final Model (meters)']\n\nx = np.arange(len(labels)) # the label locations\nwidth = 0.35 # the width of the bars\n\nfig, ax = plt.subplots(figsize=(14, 8))\nrects1 = ax.bar(x - width/2, baseline_values, width, label='Baseline GNSS Error', color='darkorange')\nrects2 = ax.bar(x + width/2, model_values, width, label='Final Model Error', color='royalblue')\n\n# Add text for labels, title, and axes ticks\nax.set_ylabel('Error (meters)')\nax.set_title('Model Performance Improvement Over Baseline', fontsize=16)\nax.set_xticks(x)\nax.set_xticklabels(labels, rotation=15)\nax.legend()\nax.set_ylim(0, max(baseline_values.max(), model_values.max()) * 1.15) # Dynamic y-limit\nax.bar_label(rects1, padding=3, fmt='%.2f')\nax.bar_label(rects2, padding=3, fmt='%.2f')\nfig.tight_layout()\nplt.grid(axis='y', linestyle='--', alpha=0.7)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:38:03.852270Z","iopub.execute_input":"2025-11-04T15:38:03.852551Z","iopub.status.idle":"2025-11-04T15:38:04.123715Z","shell.execute_reply.started":"2025-11-04T15:38:03.852533Z","shell.execute_reply":"2025-11-04T15:38:04.122784Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 5. Diagnostic Plots\nThese plots help diagnose *how* and *where* the model is making errors.\n* **Predicted vs. True:** Shows any systematic bias in the model's offset predictions.\n* **Residuals Plot:** Shows the model's error relative to the true offset.\n* **Error Over Time:** Visualizes the error for a single, continuous trip.","metadata":{}},{"cell_type":"code","source":"print(\"\\nPlotting Predicted vs. True values...\")\n\n# Calculate the true offsets (Ground Truth - WLS Baseline)\ntrue_offsets_for_eval = ground_truth_coords_for_eval - wls_coords_for_eval\n\n# --- FIX FOR PLOTTING ---\nplot_len = min(len_first_trip, 500)\n# --- END OF FIX ---\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 7))\nfig.suptitle('Predicted vs. True Offset Comparison (Sample)', fontsize=16)\n\n# --- Latitude Plot ---\nax1.scatter(true_offsets_for_eval[:plot_len, 0], predicted_offsets[:plot_len, 0], alpha=0.1, s=10)\nlims = [min(ax1.get_xlim()[0], ax1.get_ylim()[0]), max(ax1.get_xlim()[1], ax1.get_ylim()[1])]\nax1.plot(lims, lims, 'r--', alpha=0.75, zorder=5, label='Perfect Prediction (y=x)')\nax1.set_xlabel('True Latitude Offset (Degrees)')\nax1.set_ylabel('Predicted Latitude Offset (Degrees)')\nax1.set_title('Latitude Offset')\nax1.grid(True)\nax1.legend()\nax1.axis('equal')\n\n# --- Longitude Plot ---\nax2.scatter(true_offsets_for_eval[:plot_len, 1], predicted_offsets[:plot_len, 1], alpha=0.1, s=10)\nlims = [min(ax2.get_xlim()[0], ax2.get_ylim()[0]), max(ax2.get_xlim()[1], ax2.get_ylim()[1])]\nax2.plot(lims, lims, 'r--', alpha=0.75, zorder=5, label='Perfect Prediction (y=x)')\nax2.set_xlabel('True Longitude Offset (Degrees)')\nax2.set_ylabel('Predicted Longitude Offset (Degrees)')\nax2.set_title('Longitude Offset')\nax2.grid(True)\nax2.legend()\nax2.axis('equal')\n\nplt.tight_layout(rect=[0, 0.03, 1, 0.95])\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:38:04.124294Z","iopub.execute_input":"2025-11-04T15:38:04.124508Z","iopub.status.idle":"2025-11-04T15:38:04.599881Z","shell.execute_reply.started":"2025-11-04T15:38:04.124493Z","shell.execute_reply":"2025-11-04T15:38:04.598929Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\nPlotting Residuals...\")\n\n# Calculate the true offsets (Ground Truth - WLS Baseline)\ntrue_offsets_for_eval = ground_truth_coords_for_eval - wls_coords_for_eval\n\n# Calculate residuals (Error = True - Predicted)\nresiduals = true_offsets_for_eval - predicted_offsets\n\n# --- FIX FOR PLOTTING ---\nplot_len = min(len_first_trip, 500)\n# --- END OF FIX ---\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 7))\nfig.suptitle('Residuals Plot (Error vs. True Value) (Sample)', fontsize=16)\n\n# --- Latitude Residuals ---\nax1.scatter(true_offsets_for_eval[:plot_len, 0], residuals[:plot_len, 0], alpha=0.1, s=10)\nax1.axhline(0, color='r', linestyle='--', label='y = 0 (No Error)')\nax1.set_xlabel('True Latitude Offset (Degrees)')\nax1.set_ylabel('Residual (Error) in Degrees')\nax1.set_title('Latitude Residuals')\nax1.grid(True)\nax1.legend()\n\n# --- Longitude Residuals ---\nax2.scatter(true_offsets_for_eval[:plot_len, 1], residuals[:plot_len, 1], alpha=0.1, s=10)\nax2.axhline(0, color='r', linestyle='--', label='y = 0 (No Error)')\nax2.set_xlabel('True Longitude Offset (Degrees)')\nax2.set_ylabel('Residual (Error) in Degrees')\nax2.set_title('Longitude Residuals')\nax2.grid(True)\nax2.legend()\n\nplt.tight_layout(rect=[0, 0.03, 1, 0.95])\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:38:04.600772Z","iopub.execute_input":"2025-11-04T15:38:04.601011Z","iopub.status.idle":"2025-11-04T15:38:05.072928Z","shell.execute_reply.started":"2025-11-04T15:38:04.600994Z","shell.execute_reply":"2025-11-04T15:38:05.072149Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\nPlotting Error (in meters) over time for a sample...\")\n\n# --- FIX FOR PLOTTING SPIKE ---\n# Get the 'distances' (in meters) from Cell 12\n# Slice using the plot_len (defined in Cell 9) to only show the first trip.\nsample_distances = distances[:plot_len] \n# --- END OF FIX ---\ntime_steps = np.arange(len(sample_distances))\n\nplt.figure(figsize=(14, 6))\nplt.plot(time_steps, sample_distances, label='Model Error (meters)', color='b', alpha=0.8)\nplt.xlabel(f'Time Step (for first {plot_len} points)') # <--- Label is now dynamic\nplt.ylabel('Position Error (meters)')\nplt.title('Model Error Over Time (Sample)')\nplt.legend()\nplt.grid(True)\nplt.show()\n\nprint(\"\\n--- Evaluation Complete ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-04T15:38:05.073698Z","iopub.execute_input":"2025-11-04T15:38:05.073911Z","iopub.status.idle":"2025-11-04T15:38:05.277674Z","shell.execute_reply.started":"2025-11-04T15:38:05.073885Z","shell.execute_reply":"2025-11-04T15:38:05.276956Z"}},"outputs":[],"execution_count":null}]}