{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":14247324,"sourceType":"datasetVersion","datasetId":9090093}],"dockerImageVersionId":31234,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"%pip -q install pandas numpy scikit-learn polars scipy joblib grpcio\nimport warnings\nwarnings.filterwarnings('ignore')\n\nimport os\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nfrom pathlib import Path\nimport joblib\nfrom scipy import stats\nfrom scipy.spatial.transform import Rotation as R","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-21T18:26:48.298484Z","iopub.execute_input":"2025-12-21T18:26:48.298848Z","iopub.status.idle":"2025-12-21T18:26:51.361787Z","shell.execute_reply.started":"2025-12-21T18:26:48.298767Z","shell.execute_reply":"2025-12-21T18:26:51.360891Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Paths configuration\nIS_KAGGLE = Path('/kaggle/input').exists()\n\nif IS_KAGGLE:\n    ARTIFACT_PATH = Path('/kaggle/input/joydeep-gesture-model/joydeep_model_artifacts.joblib')\nelse:\n    ARTIFACT_PATH = Path(r\"C:\\Users\\Joydeep\\Desktop\\BigData\\Final Project\\cmi-detect-behavior-with-sensor-data\\joydeep_model_artifacts.joblib\")\n\nprint(f'Kaggle: {IS_KAGGLE}, Artifact: {ARTIFACT_PATH.name}')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T18:26:51.363369Z","iopub.execute_input":"2025-12-21T18:26:51.363618Z","iopub.status.idle":"2025-12-21T18:26:51.368482Z","shell.execute_reply.started":"2025-12-21T18:26:51.363592Z","shell.execute_reply":"2025-12-21T18:26:51.367909Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Inference functions and model artifacts (accuracy-oriented, still inference-only)\n\n# Global state (lazy-loaded)\n_artifacts_loaded = False\n_artifacts = None\n\nbest_thr = None\nfallback_non_target = None\nle_gesture_classes = None\n\nbin_models_all = None\nbin_models_imu = None\nmulti_models_all = None\nmulti_models_imu = None\n\nfeature_columns_all = None\nfeature_columns_imu = None\n\nuse_stacking = None\nstack_bin = None\nstack_multi = None\ncalibrator = None\nsubject_norm = None\n\nIMU_COLS = None\nTHM_COLS = None\nTOF_COLS = None\nDERIVED_COLS = None\nDEMO_COLS = None\nSENSOR_COLS_ALL = None\nNEG1_MISSING_ALL = None\n\n# Flags to avoid unnecessary compute when models don't use those features\nNEED_SKEW_KURT = False\nNEED_TEMP_SPECTRAL = False\n\n# Defaults (used if not found in artifacts)\nIMU_COLS_DEFAULT = [\"acc_x\", \"acc_y\", \"acc_z\", \"rot_w\", \"rot_x\", \"rot_y\", \"rot_z\"]\nTHM_COLS_DEFAULT = [f\"thm_{i}\" for i in range(1, 6)]\nTOF_COLS_DEFAULT = [f\"tof_{s}_v{v}\" for s in range(1, 6) for v in range(64)]\nDEMO_COLS_DEFAULT = [\n    \"adult_child\",\n    \"age\",\n    \"sex\",\n    \"handedness\",\n    \"height_cm\",\n    \"shoulder_to_wrist_cm\",\n    \"elbow_to_wrist_cm\",\n]\n\nTEMP_HEAD_N = 150\nTEMP_TAIL_N = 150\nTEMP_EPS = 1e-6\n\nSPECTRAL_CONFIG = {\n    \"fs\": 200.0,\n    \"bands\": [(0, 20), (20, 40), (40, 60), (60, 100)],\n}\n\n\ndef load_artifacts() -> None:\n    global _artifacts_loaded, _artifacts\n    global best_thr, fallback_non_target, le_gesture_classes\n    global bin_models_all, bin_models_imu, multi_models_all, multi_models_imu\n    global feature_columns_all, feature_columns_imu\n    global use_stacking, stack_bin, stack_multi, calibrator, subject_norm\n    global IMU_COLS, THM_COLS, TOF_COLS, DERIVED_COLS, DEMO_COLS, SENSOR_COLS_ALL, NEG1_MISSING_ALL\n    global NEED_SKEW_KURT, NEED_TEMP_SPECTRAL\n\n    if _artifacts_loaded:\n        return\n\n    _artifacts = joblib.load(ARTIFACT_PATH)\n\n    best_thr = float(_artifacts.get(\"best_thr\", 0.5))\n    fallback_non_target = str(_artifacts.get(\"fallback_non_target\", \"Text on phone\"))\n    le_gesture_classes = [str(x) for x in list(_artifacts.get(\"le_gesture_classes\", []))]\n\n    bin_models_all = _artifacts.get(\"bin_models_all\", [])\n    bin_models_imu = _artifacts.get(\"bin_models_imu\", [])\n    multi_models_all = _artifacts.get(\"multi_models_all\", [])\n    multi_models_imu = _artifacts.get(\"multi_models_imu\", [])\n\n    feature_columns_all = list(_artifacts.get(\"feature_columns_all\", []))\n    feature_columns_imu = list(_artifacts.get(\"feature_columns_imu\", []))\n\n    use_stacking = bool(_artifacts.get(\"use_stacking\", False))\n    stack_bin = _artifacts.get(\"stack_bin\", None)\n    stack_multi = _artifacts.get(\"stack_multi\", None)\n    calibrator = _artifacts.get(\"calibrator\", None)\n    subject_norm = _artifacts.get(\"subject_norm\", None)\n\n    IMU_COLS = list(_artifacts.get(\"imu_cols\", IMU_COLS_DEFAULT))\n    THM_COLS = list(_artifacts.get(\"thm_cols\", THM_COLS_DEFAULT))\n    TOF_COLS = list(_artifacts.get(\"tof_cols\", TOF_COLS_DEFAULT))\n    DERIVED_COLS = list(_artifacts.get(\"derived_cols\", []))\n    DEMO_COLS = list(_artifacts.get(\"demo_cols\", DEMO_COLS_DEFAULT))\n\n    SENSOR_COLS_ALL = IMU_COLS + THM_COLS + TOF_COLS\n    NEG1_MISSING_ALL = set(THM_COLS + TOF_COLS)\n\n    # Decide whether to compute more expensive features\n    all_feats = set(feature_columns_all) | set(feature_columns_imu)\n    NEED_SKEW_KURT = any(f.endswith(\"__skew\") or f.endswith(\"__kurt\") for f in all_feats)\n\n    spectral_markers = (\n        \"temp__acc_w_norm_entropy\",\n        \"temp__acc_w_norm_centroid\",\n        \"temp__acc_w_norm_bandwidth\",\n        \"temp__acc_w_norm_dominant_freq\",\n        \"temp__acc_w_norm_autocorr\",\n        \"temp__ang_v_norm_entropy\",\n        \"temp__ang_v_norm_centroid\",\n        \"temp__ang_v_norm_bandwidth\",\n        \"temp__ang_v_norm_dominant_freq\",\n        \"temp__ang_v_norm_autocorr\",\n        \"band_0_20\",\n        \"band_20_40\",\n        \"band_40_60\",\n        \"band_60_100\",\n    )\n    NEED_TEMP_SPECTRAL = any(any(m in f for m in spectral_markers) for f in all_feats)\n\n    _artifacts_loaded = True\n\n\ndef _is_left_handed(handedness: object) -> bool:\n    if handedness is None:\n        return False\n    s = str(handedness).strip().lower()\n    return s.startswith(\"l\")\n\n\ndef _select_numpy(sequence: pl.DataFrame, cols: list[str], fill_value: float = np.nan) -> np.ndarray:\n    exprs = []\n    for c in cols:\n        if c in sequence.columns:\n            exprs.append(pl.col(c))\n        else:\n            exprs.append(pl.lit(fill_value).alias(c))\n    return sequence.select(exprs).to_numpy()\n\n\ndef handle_quaternion_missing_values(rot_wxyz: np.ndarray) -> np.ndarray:\n    rot = rot_wxyz.astype(np.float64, copy=True)\n    out = rot.copy()\n    for i in range(rot.shape[0]):\n        row = rot[i]\n        missing = np.isnan(row)\n        missing_count = int(missing.sum())\n        if missing_count == 0:\n            n = np.linalg.norm(row)\n            out[i] = row / n if n > 1e-8 else np.array([1.0, 0.0, 0.0, 0.0])\n        elif missing_count == 1:\n            mi = int(np.where(missing)[0][0])\n            valid = row[~missing]\n            s2 = float(np.sum(valid * valid))\n            if s2 <= 1.0:\n                mv = float(np.sqrt(max(0.0, 1.0 - s2)))\n                if i > 0 and np.isfinite(out[i - 1, mi]) and out[i - 1, mi] < 0:\n                    mv = -mv\n                out[i] = row\n                out[i, mi] = mv\n                n = np.linalg.norm(out[i])\n                out[i] = out[i] / n if n > 1e-8 else np.array([1.0, 0.0, 0.0, 0.0])\n            else:\n                out[i] = np.array([1.0, 0.0, 0.0, 0.0])\n        else:\n            out[i] = np.array([1.0, 0.0, 0.0, 0.0])\n    return out\n\n\ndef compute_world_acceleration(acc_xyz: np.ndarray, rot_wxyz: np.ndarray) -> np.ndarray:\n    try:\n        quat_xyzw = rot_wxyz[:, [1, 2, 3, 0]]\n        r = R.from_quat(quat_xyzw)\n        return r.apply(acc_xyz)\n    except Exception:\n        return acc_xyz.copy()\n\n\ndef compute_rot_step_features(rot_wxyz: np.ndarray, time_delta: float = 1 / 200) -> tuple[np.ndarray, np.ndarray]:\n    T = rot_wxyz.shape[0]\n    if T < 2:\n        return np.zeros((T, 3), dtype=np.float64), np.zeros((T,), dtype=np.float64)\n    try:\n        quat_xyzw = rot_wxyz[:, [1, 2, 3, 0]]\n        r = R.from_quat(quat_xyzw)\n        delta = r[:-1].inv() * r[1:]\n        rotvec = delta.as_rotvec()\n        angle = np.linalg.norm(rotvec, axis=1)\n        rotvec_full = np.vstack([np.zeros((1, 3)), rotvec])\n        angle_full = np.concatenate([[0.0], angle])\n        return rotvec_full, angle_full\n    except Exception:\n        return np.zeros((T, 3), dtype=np.float64), np.zeros((T,), dtype=np.float64)\n\n\ndef compute_angular_velocity(rot_wxyz: np.ndarray, time_delta: float = 1 / 200) -> np.ndarray:\n    rotvec_step, _ = compute_rot_step_features(rot_wxyz, time_delta=time_delta)\n    return rotvec_step / float(time_delta)\n\n\ndef mirror_acc_and_quaternion(acc_xyz: np.ndarray, rot_wxyz: np.ndarray, axis: str = \"x\") -> tuple[np.ndarray, np.ndarray]:\n    if axis != \"x\":\n        raise ValueError(\"Only axis='x' implemented\")\n    M = np.diag([-1.0, 1.0, 1.0]).astype(np.float64)\n    acc_m = (acc_xyz @ M.T).astype(np.float64, copy=False)\n    try:\n        quat_xyzw = rot_wxyz[:, [1, 2, 3, 0]]\n        r = R.from_quat(quat_xyzw)\n        mats = r.as_matrix()\n        mats_m = M[None, :, :] @ mats @ M[None, :, :]\n        quat_xyzw_m = R.from_matrix(mats_m).as_quat()\n        rot_wxyz_m = quat_xyzw_m[:, [3, 0, 1, 2]].astype(np.float64, copy=False)\n        return acc_m, rot_wxyz_m\n    except Exception:\n        return acc_m, rot_wxyz.copy()\n\n\ndef compute_spectral_features(signal: np.ndarray, fs: float = 200.0) -> dict:\n    out = {\n        \"entropy\": 0.0,\n        \"centroid\": 0.0,\n        \"bandwidth\": 0.0,\n        \"dominant_freq\": 0.0,\n        \"band_0_20\": 0.0,\n        \"band_20_40\": 0.0,\n        \"band_40_60\": 0.0,\n        \"band_60_100\": 0.0,\n    }\n    if signal.size < 8:\n        return out\n    try:\n        sig = np.nan_to_num(signal - np.nanmean(signal), nan=0.0)\n        fft_vals = np.fft.rfft(sig)\n        power = (np.abs(fft_vals) ** 2).astype(np.float64, copy=False)\n        freqs = np.fft.rfftfreq(sig.size, d=1.0 / fs).astype(np.float64, copy=False)\n\n        total_power = float(np.sum(power))\n        if total_power < 1e-12:\n            return out\n        pdf = power / total_power\n\n        out[\"entropy\"] = float(stats.entropy(pdf))\n\n        centroid = float(np.sum(freqs * pdf))\n        out[\"centroid\"] = centroid\n\n        out[\"bandwidth\"] = float(np.sqrt(np.sum(((freqs - centroid) ** 2) * pdf)))\n        out[\"dominant_freq\"] = float(freqs[int(np.argmax(power))])\n\n        for low, high in SPECTRAL_CONFIG[\"bands\"]:\n            idx_low = int(np.searchsorted(freqs, low))\n            idx_high = int(np.searchsorted(freqs, high))\n            out[f\"band_{low}_{high}\"] = float(np.sum(power[idx_low:idx_high]) / total_power)\n\n        return out\n    except Exception:\n        return out\n\n\ndef compute_autocorr_lag1(signal: np.ndarray) -> float:\n    if signal.size < 3:\n        return 0.0\n    try:\n        x = np.nan_to_num(signal, nan=0.0)\n        x = x - float(np.mean(x))\n        c0 = float(np.dot(x, x))\n        if c0 < 1e-12:\n            return 0.0\n        c1 = float(np.dot(x[:-1], x[1:]))\n        return float(c1 / c0)\n    except Exception:\n        return 0.0\n\n\ndef _compute_temporal_features_from_arrays(acc_w: np.ndarray, ang_v: np.ndarray, time_delta: float) -> dict:\n    acc_w_norm = np.linalg.norm(acc_w, axis=1)\n    ang_v_norm = np.linalg.norm(ang_v, axis=1)\n\n    n = int(acc_w.shape[0])\n    head_n = min(TEMP_HEAD_N, n)\n    tail_n = min(TEMP_TAIL_N, n)\n\n    def _mean_std(x: np.ndarray) -> tuple[float, float]:\n        if x.size == 0:\n            return 0.0, 0.0\n        return float(np.mean(x)), float(np.std(x))\n\n    h_acc_m, h_acc_s = _mean_std(acc_w_norm[:head_n])\n    h_ang_m, h_ang_s = _mean_std(ang_v_norm[:head_n])\n    t_acc_m, t_acc_s = _mean_std(acc_w_norm[-tail_n:])\n    t_ang_m, t_ang_s = _mean_std(ang_v_norm[-tail_n:])\n\n    # jerk\n    if n >= 2:\n        jerk = np.diff(acc_w, axis=0) / float(time_delta)\n        jerk_norm = np.linalg.norm(jerk, axis=1)\n        j_mean, j_std = _mean_std(jerk_norm)\n        j_max = float(np.max(jerk_norm)) if jerk_norm.size else 0.0\n        j_energy = float(np.mean(jerk_norm * jerk_norm)) if jerk_norm.size else 0.0\n    else:\n        j_mean = j_std = j_max = j_energy = 0.0\n\n    # peaks\n    def _peaks(x: np.ndarray) -> float:\n        if x.size < 3:\n            return 0.0\n        return float(np.sum((x[1:-1] > x[:-2]) & (x[1:-1] > x[2:]))) / max(1.0, float(x.size))\n\n    p0 = _peaks(acc_w_norm)\n    p1 = _peaks(ang_v_norm)\n\n    # zcr on axes\n    axes = np.column_stack([acc_w[:, 0], acc_w[:, 1], acc_w[:, 2], ang_v[:, 0], ang_v[:, 1], ang_v[:, 2]])\n\n    def _zcr(col: np.ndarray) -> float:\n        if col.size < 2:\n            return 0.0\n        s = np.sign(col)\n        s[np.abs(col) <= TEMP_EPS] = 0\n        nz = s[s != 0]\n        if nz.size < 2:\n            return 0.0\n        return float(np.sum(nz[1:] != nz[:-1])) / max(1.0, float(col.size))\n\n    out = {\n        \"temp__acc_w_norm_head_mean\": h_acc_m,\n        \"temp__acc_w_norm_head_std\": h_acc_s,\n        \"temp__ang_v_norm_head_mean\": h_ang_m,\n        \"temp__ang_v_norm_head_std\": h_ang_s,\n        \"temp__acc_w_norm_tail_mean\": t_acc_m,\n        \"temp__acc_w_norm_tail_std\": t_acc_s,\n        \"temp__ang_v_norm_tail_mean\": t_ang_m,\n        \"temp__ang_v_norm_tail_std\": t_ang_s,\n        \"temp__jerk_mean\": j_mean,\n        \"temp__jerk_std\": j_std,\n        \"temp__jerk_max\": j_max,\n        \"temp__jerk_energy\": j_energy,\n        \"temp__acc_w_norm_peaks_per_step\": p0,\n        \"temp__ang_v_norm_peaks_per_step\": p1,\n        \"temp__corr_acc_ang\": float(np.corrcoef(acc_w_norm, ang_v_norm)[0, 1])\n        if n > 1 and np.std(acc_w_norm) > 1e-9 and np.std(ang_v_norm) > 1e-9\n        else 0.0,\n    }\n\n    ax_names = [\"acc_w_x\", \"acc_w_y\", \"acc_w_z\", \"ang_v_x\", \"ang_v_y\", \"ang_v_z\"]\n    for j, nm in enumerate(ax_names):\n        out[f\"temp__{nm}_zcr\"] = _zcr(axes[:, j])\n\n    # Spectral + autocorr (only if the models ask for it)\n    if NEED_TEMP_SPECTRAL:\n        spec_acc = compute_spectral_features(acc_w_norm, fs=float(SPECTRAL_CONFIG[\"fs\"]))\n        spec_ang = compute_spectral_features(ang_v_norm, fs=float(SPECTRAL_CONFIG[\"fs\"]))\n\n        for k, v in spec_acc.items():\n            out[f\"temp__acc_w_norm_{k}\"] = float(v)\n        for k, v in spec_ang.items():\n            out[f\"temp__ang_v_norm_{k}\"] = float(v)\n\n        out[\"temp__acc_w_norm_autocorr\"] = float(compute_autocorr_lag1(acc_w_norm))\n        out[\"temp__ang_v_norm_autocorr\"] = float(compute_autocorr_lag1(ang_v_norm))\n\n    return out\n\n\ndef featurize_sequence_df(\n    sequence: pl.DataFrame,\n    demos: pl.DataFrame,\n    use_all_sensors: bool,\n    time_delta: float = 1 / 200,\n    mirror: bool = False,\n) -> pd.DataFrame:\n    load_artifacts()\n\n    base_cols = SENSOR_COLS_ALL if use_all_sensors else IMU_COLS\n    sensor_cols = list(base_cols) + list(DERIVED_COLS)\n\n    n_rows = int(sequence.height)\n\n    base_vals = _select_numpy(sequence, base_cols).astype(np.float64, copy=True)\n\n    if use_all_sensors:\n        base_is_missing = np.array([c in NEG1_MISSING_ALL for c in base_cols], dtype=bool)\n        if base_is_missing.any():\n            base_vals[:, base_is_missing] = np.where(base_vals[:, base_is_missing] <= -0.5, np.nan, base_vals[:, base_is_missing])\n\n    acc = _select_numpy(sequence, [\"acc_x\", \"acc_y\", \"acc_z\"]).astype(np.float64, copy=True)\n    rot = _select_numpy(sequence, [\"rot_w\", \"rot_x\", \"rot_y\", \"rot_z\"]).astype(np.float64, copy=True)\n    rot = handle_quaternion_missing_values(rot)\n\n    if mirror:\n        acc, rot = mirror_acc_and_quaternion(acc, rot, axis=\"x\")\n        rot = handle_quaternion_missing_values(rot)\n\n    quat_norm = np.linalg.norm(rot, axis=1)\n    acc_norm = np.linalg.norm(acc, axis=1)\n\n    acc_w = compute_world_acceleration(acc, rot)\n    acc_w_norm = np.linalg.norm(acc_w, axis=1)\n\n    ang_v = compute_angular_velocity(rot, time_delta=time_delta)\n    ang_v_norm = np.linalg.norm(ang_v, axis=1)\n\n    rotvec_step, rot_angle = compute_rot_step_features(rot, time_delta=time_delta)\n    rotvec_step_norm = np.linalg.norm(rotvec_step, axis=1)\n\n    derived = np.column_stack(\n        [\n            acc_norm,\n            acc_w[:, 0],\n            acc_w[:, 1],\n            acc_w[:, 2],\n            acc_w_norm,\n            ang_v[:, 0],\n            ang_v[:, 1],\n            ang_v[:, 2],\n            ang_v_norm,\n            quat_norm,\n            rot_angle,\n            rotvec_step[:, 0],\n            rotvec_step[:, 1],\n            rotvec_step[:, 2],\n            rotvec_step_norm,\n        ]\n    ).astype(np.float64, copy=False)\n\n    vals = np.concatenate([base_vals, derived], axis=1)\n\n    counts = (~np.isnan(vals)).sum(axis=0).astype(np.float64)\n    means = np.divide(np.nansum(vals, axis=0), counts, out=np.zeros_like(counts), where=counts > 0)\n    stds = np.nanstd(vals, axis=0)\n    vmin = np.nanmin(vals, axis=0)\n    vmax = np.nanmax(vals, axis=0)\n\n    vmin = np.where(np.isnan(vmin), 0.0, vmin)\n    vmax = np.where(np.isnan(vmax), 0.0, vmax)\n    miss_frac = 1.0 - np.divide(counts, float(max(n_rows, 1)), out=np.zeros_like(counts), where=True)\n\n    if n_rows >= 2:\n        v1 = vals[:-1]\n        v2 = vals[1:]\n        valid_pair = (~np.isnan(v1)) & (~np.isnan(v2))\n        diffs = np.abs(v2 - v1)\n        diff_sum = np.nansum(np.where(valid_pair, diffs, np.nan), axis=0)\n        diff_cnt = valid_pair.sum(axis=0).astype(np.float64)\n        diff_mean = np.divide(diff_sum, diff_cnt, out=np.zeros_like(diff_sum), where=diff_cnt > 0)\n    else:\n        diff_mean = np.zeros(vals.shape[1], dtype=np.float64)\n\n    skew_arr = None\n    kurt_arr = None\n    if NEED_SKEW_KURT:\n        # Compute only when models actually contain __skew/__kurt columns\n        try:\n            skew_arr = stats.skew(vals, axis=0, nan_policy=\"omit\", bias=False)\n            kurt_arr = stats.kurtosis(vals, axis=0, nan_policy=\"omit\", fisher=True, bias=False)\n            skew_arr = np.where(np.isfinite(skew_arr), skew_arr, 0.0)\n            kurt_arr = np.where(np.isfinite(kurt_arr), kurt_arr, 0.0)\n        except Exception:\n            skew_arr = None\n            kurt_arr = None\n\n    feats: dict[str, object] = {\"sequence_len\": int(n_rows)}\n    for i, c in enumerate(sensor_cols):\n        feats[f\"{c}__mean\"] = float(means[i]) if np.isfinite(means[i]) else 0.0\n        feats[f\"{c}__std\"] = float(stds[i]) if np.isfinite(stds[i]) else 0.0\n        feats[f\"{c}__min\"] = float(vmin[i]) if np.isfinite(vmin[i]) else 0.0\n        feats[f\"{c}__max\"] = float(vmax[i]) if np.isfinite(vmax[i]) else 0.0\n        feats[f\"{c}__diff_mean\"] = float(diff_mean[i]) if np.isfinite(diff_mean[i]) else 0.0\n        feats[f\"{c}__miss_frac\"] = float(miss_frac[i]) if np.isfinite(miss_frac[i]) else 0.0\n        if NEED_SKEW_KURT and skew_arr is not None and kurt_arr is not None:\n            feats[f\"{c}__skew\"] = float(skew_arr[i])\n            feats[f\"{c}__kurt\"] = float(kurt_arr[i])\n\n    feats.update(_compute_temporal_features_from_arrays(acc_w=acc_w, ang_v=ang_v, time_delta=time_delta))\n\n    # Demographics\n    if demos is not None and demos.height:\n        for c in DEMO_COLS:\n            try:\n                v = demos.select(c).item(0)\n                feats[f\"demo__{c}\"] = float(v) if v is not None else 0.0\n            except Exception:\n                feats[f\"demo__{c}\"] = 0.0\n    else:\n        for c in DEMO_COLS:\n            feats[f\"demo__{c}\"] = 0.0\n\n    return pd.DataFrame([feats])\n\n\ndef _has_extra_sensors(sequence: pl.DataFrame) -> bool:\n    load_artifacts()\n\n    cols = set(sequence.columns)\n\n    # THM: if any THM column exists and any value is present (>-0.5)\n    thm_present = [c for c in THM_COLS if c in cols]\n    if thm_present:\n        thm_vals = sequence.select(thm_present).to_numpy()\n        if np.any(thm_vals > -0.5):\n            return True\n\n    # TOF: probe subset for speed\n    tof_present = [c for c in TOF_COLS[:64] if c in cols]\n    if tof_present:\n        tof_vals = sequence.select(tof_present).to_numpy()\n        if np.any(tof_vals > -0.5):\n            return True\n\n    return False\n\n\ndef _avg_proba(models, X: pd.DataFrame) -> np.ndarray:\n    probs = None\n    for m in models:\n        p = m.predict_proba(X)\n        probs = p if probs is None else (probs + p)\n    return probs / float(len(models))\n\n\ndef _apply_subject_norm_row(X: pd.DataFrame, subject: str, use_all: bool) -> pd.DataFrame:\n    if subject_norm is None:\n        return X\n\n    Xn = X.copy()\n    if use_all:\n        cols = subject_norm.get(\"cols_all\", [])\n        mean_df = subject_norm.get(\"mean_all\", None)\n        std_df = subject_norm.get(\"std_all\", None)\n        gmean = subject_norm.get(\"gmean_all\", None)\n        gstd = subject_norm.get(\"gstd_all\", None)\n    else:\n        cols = subject_norm.get(\"cols_imu\", [])\n        mean_df = subject_norm.get(\"mean_imu\", None)\n        std_df = subject_norm.get(\"std_imu\", None)\n        gmean = subject_norm.get(\"gmean_imu\", None)\n        gstd = subject_norm.get(\"gstd_imu\", None)\n\n    if mean_df is None or std_df is None or gmean is None or gstd is None or len(cols) == 0:\n        return Xn\n\n    cols = [c for c in cols if c in Xn.columns]\n    if len(cols) == 0:\n        return Xn\n\n    if subject in mean_df.index:\n        m = mean_df.loc[subject, cols].to_numpy(dtype=np.float64, copy=False).reshape(1, -1)\n        s = std_df.loc[subject, cols].to_numpy(dtype=np.float64, copy=False).reshape(1, -1)\n    else:\n        m = gmean[cols].to_numpy(dtype=np.float64, copy=False).reshape(1, -1)\n        s = gstd[cols].to_numpy(dtype=np.float64, copy=False).reshape(1, -1)\n\n    s = np.where(np.isfinite(s) & (s > 1e-12), s, 1.0)\n    Xv = Xn[cols].to_numpy(dtype=np.float64, copy=True)\n    Xn[cols] = (Xv - m) / s\n    return Xn\n\n\ndef _predict_proba_views(sequence: pl.DataFrame, demos: pl.DataFrame, time_delta: float, mirror: bool) -> tuple[float, np.ndarray]:\n    load_artifacts()\n\n    use_all_flag = _has_extra_sensors(sequence)\n\n    X_all = featurize_sequence_df(sequence, demos, use_all_sensors=True, time_delta=time_delta, mirror=mirror)\n    X_imu = featurize_sequence_df(sequence, demos, use_all_sensors=False, time_delta=time_delta, mirror=mirror)\n\n    X_all = X_all.reindex(columns=feature_columns_all, fill_value=0.0)\n    X_imu = X_imu.reindex(columns=feature_columns_imu, fill_value=0.0)\n\n    subj = \"\"\n    if \"subject\" in sequence.columns and sequence.height:\n        try:\n            subj = str(sequence.select(\"subject\").to_series()[0])\n        except Exception:\n            subj = \"\"\n\n    X_all = _apply_subject_norm_row(X_all, subj, use_all=True)\n    X_imu = _apply_subject_norm_row(X_imu, subj, use_all=False)\n\n    p_all = float(_avg_proba(bin_models_all, X_all)[:, 1][0])\n    p_imu = float(_avg_proba(bin_models_imu, X_imu)[:, 1][0])\n\n    if use_stacking and stack_bin is not None:\n        p_bin = float(stack_bin.predict_proba([[p_all, p_imu, int(use_all_flag)]])[:, 1][0])\n    else:\n        p_bin = p_all if use_all_flag else p_imu\n\n    if calibrator is not None:\n        try:\n            p_bin = float(calibrator.predict([p_bin])[0])\n        except Exception:\n            p_bin = float(p_bin)\n\n    proba_all = _avg_proba(multi_models_all, X_all)\n    proba_imu = _avg_proba(multi_models_imu, X_imu)\n\n    if use_stacking and stack_multi is not None:\n        stack_x = np.concatenate([proba_all, proba_imu, np.array([[float(use_all_flag)]])], axis=1)\n        p_mul = stack_multi.predict_proba(stack_x)\n    else:\n        p_mul = proba_all if use_all_flag else proba_imu\n\n    return p_bin, p_mul[0]\n\n\ndef predict_gesture(sequence: pl.DataFrame, demos: pl.DataFrame, time_delta: float = 1 / 200) -> str:\n    load_artifacts()\n\n    is_left = False\n    if demos is not None and demos.height and (\"handedness\" in demos.columns):\n        try:\n            is_left = _is_left_handed(demos.select(\"handedness\").to_series()[0])\n        except Exception:\n            is_left = False\n\n    if is_left:\n        p0, m0 = _predict_proba_views(sequence, demos, time_delta=time_delta, mirror=False)\n        p1, m1 = _predict_proba_views(sequence, demos, time_delta=time_delta, mirror=True)\n        p_bin = 0.5 * (p0 + p1)\n        p_mul = 0.5 * (m0 + m1)\n    else:\n        p_bin, p_mul = _predict_proba_views(sequence, demos, time_delta=time_delta, mirror=False)\n\n    if float(p_bin) < float(best_thr):\n        return str(fallback_non_target)\n\n    if not le_gesture_classes:\n        return str(fallback_non_target)\n\n    pred_idx = int(np.argmax(p_mul))\n    pred_idx = max(0, min(pred_idx, len(le_gesture_classes) - 1))\n    return str(le_gesture_classes[pred_idx])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T18:26:51.369580Z","iopub.execute_input":"2025-12-21T18:26:51.370049Z","iopub.status.idle":"2025-12-21T18:26:51.426763Z","shell.execute_reply.started":"2025-12-21T18:26:51.370029Z","shell.execute_reply":"2025-12-21T18:26:51.426157Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Kaggle Inference Server Setup\ntry:\n    import kaggle_evaluation.cmi_inference_server\n    \n    def predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n        \"\"\"\n        Main prediction entry point for the evaluation server.\n        Wraps the optimized predict_gesture function.\n        \"\"\"\n        try:\n            # Use the optimized prediction function\n            return predict_gesture(sequence, demographics)\n        except Exception as e:\n            # Fallback in case of any error\n            print(f\"Prediction error: {e}\")\n            return \"Text on phone\"\n    \n    # Create inference server\n    inference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n    \n    # Run based on environment\n    if os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n        # Competition evaluation mode\n        inference_server.serve()\n    else:\n        # Kaggle notebook testing mode\n        inference_server.run_local_gateway(\n            data_paths=(\n                '/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv',\n                '/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv',\n            )\n        )\n    \nexcept ModuleNotFoundError:\n    print(\"✅ Ready for Kaggle - upload 'joydeep_model_artifacts.joblib' as dataset 'joydeep-gesture-model'\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T18:26:51.428077Z","iopub.execute_input":"2025-12-21T18:26:51.428310Z","iopub.status.idle":"2025-12-21T18:27:01.805650Z","shell.execute_reply.started":"2025-12-21T18:26:51.428291Z","shell.execute_reply":"2025-12-21T18:27:01.804939Z"}},"outputs":[],"execution_count":null}]}