{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.11"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":514.400872,"end_time":"2025-06-14T09:53:36.250389","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-06-14T09:45:01.849517","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# **Imports**","metadata":{"papermill":{"duration":0.003884,"end_time":"2025-06-14T09:45:05.931312","exception":false,"start_time":"2025-06-14T09:45:05.927428","status":"completed"},"tags":[]}},{"cell_type":"code","source":"import pandas as pd\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nimport pandas as pd\nimport warnings\nfrom matplotlib.ticker import MaxNLocator\nfrom mpl_toolkits.mplot3d import Axes3D\nimport os\nimport polars as pl\nimport kaggle_evaluation.cmi_inference_server\nfrom sklearn.metrics import accuracy_score, f1_score\nimport joblib\nfrom scipy.spatial.transform import Rotation as R","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:37:46.487572Z","iopub.execute_input":"2025-08-18T02:37:46.487901Z","iopub.status.idle":"2025-08-18T02:37:46.493156Z","shell.execute_reply.started":"2025-08-18T02:37:46.487877Z","shell.execute_reply":"2025-08-18T02:37:46.492316Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import random\nimport numpy as np\nimport torch\nimport os\n\ndef seed_everything(seed=42):\n    \"\"\"Set all random seeds for reproducibility\"\"\"\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    os.environ['CUBLAS_WORKSPACE_CONFIG'] = ':4096:8'\n    torch.use_deterministic_algorithms(True, warn_only=True)\n\nSEED = 42\nseed_everything(seed=SEED)\n\nimport pandas as pd\nimport polars as pl\nfrom sklearn.metrics import f1_score\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.utils.class_weight import compute_class_weight\nimport joblib\nfrom tqdm import tqdm\n\nfrom torch import nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom tensorflow.keras.preprocessing.sequence import pad_sequences as keras_pad_sequences\n\nimport kaggle_evaluation.cmi_inference_server\nfrom matplotlib import pyplot as plt","metadata":{"papermill":{"duration":20.411251,"end_time":"2025-06-14T09:45:26.3457","exception":false,"start_time":"2025-06-14T09:45:05.934449","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:37:46.494270Z","iopub.execute_input":"2025-08-18T02:37:46.494837Z","iopub.status.idle":"2025-08-18T02:38:07.014261Z","shell.execute_reply.started":"2025-08-18T02:37:46.494819Z","shell.execute_reply":"2025-08-18T02:38:07.013630Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **Read data**","metadata":{"papermill":{"duration":0.002878,"end_time":"2025-06-14T09:45:26.352143","exception":false,"start_time":"2025-06-14T09:45:26.349265","status":"completed"},"tags":[]}},{"cell_type":"code","source":"print(\"Loading datasets...\")\ntrain_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\")\ntrain_dem_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv\")\ntest_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv\")\ntest_dem_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv\")\nprint(f\"Train rows: {len(train_df)}, Test rows: {len(test_df)}\")\n\n# Encode labels\nlabel_encoder = LabelEncoder()\ntrain_df['gesture'] = label_encoder.fit_transform(train_df['gesture'].astype(str))\ngesture_classes = label_encoder.classes_\n\nbfrb_gestures = [\n    'Above ear - pull hair',\n    'Forehead - pull hairline',\n    'Forehead - scratch',\n    'Eyebrow - pull hair',\n    'Eyelash - pull hair',\n    'Neck - pinch skin',\n    'Neck - scratch',\n    'Cheek - pinch skin'\n]\nbfrb_indices = label_encoder.transform(bfrb_gestures)\n\nimu_cols = ['acc_x', 'acc_y', 'acc_z', 'rot_w', 'rot_x', 'rot_y', 'rot_z']\ntof_thm_cols = [c for c in train_df.columns if c.startswith('thm_') or c.startswith('tof_')]\n\n# Reorder so that IMU features come first\nfeature_cols = imu_cols + tof_thm_cols\nimu_dim = len(imu_cols)\ntof_thm_dim = len(tof_thm_cols)\n\nprint(f\"IMU features: {imu_dim}, TOF/Thermal features: {tof_thm_dim}, Total features: {len(feature_cols)}\")\n\n# Check for missing values\nnan_counts = train_df[feature_cols].isna().sum().sum()\nprint(\"Total NaNs in train features:\", nan_counts)\n\n\n# to remove hand dependency in IMU data\n# im not sure if the rotation is on the x axis but this give me the best CV\ndef apply_symmetry(data):\n    transformed = data.copy()\n    transformed['acc_z'] = -transformed['acc_z']\n    transformed['acc_y'] = -transformed['acc_y']\n    \n    transformed['rot_w'] = transformed['rot_w']\n    transformed['rot_x'] = transformed['rot_x']\n    transformed['rot_y'] = -transformed['rot_y']\n    transformed['rot_z'] = -transformed['rot_z']\n    return transformed\n\n\ntrain_df = train_df.merge(\n    train_dem_df,\n    on='subject',\n    how='left',\n    validate='many_to_one'\n)\n\nright_handed_mask = train_df['handedness'] == 1\ntrain_df.loc[right_handed_mask, imu_cols] = apply_symmetry(train_df.loc[right_handed_mask, imu_cols])","metadata":{"papermill":{"duration":32.078977,"end_time":"2025-06-14T09:45:58.434039","exception":false,"start_time":"2025-06-14T09:45:26.355062","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:38:07.015272Z","iopub.execute_input":"2025-08-18T02:38:07.015808Z","iopub.status.idle":"2025-08-18T02:38:40.892310Z","shell.execute_reply.started":"2025-08-18T02:38:07.015788Z","shell.execute_reply":"2025-08-18T02:38:40.891731Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Tổng quan dữ liệu","metadata":{}},{"cell_type":"markdown","source":"## Khám phá dữ liệu","metadata":{}},{"cell_type":"markdown","source":"### Tóm tắt thống kê các tính năng","metadata":{}},{"cell_type":"code","source":"datasets = {\n    \"Train Data\": train_df,\n    \"Train Demographics\": train_dem_df,\n    \"Test Data\": test_df,\n    \"Test Demographics\": test_dem_df,\n}\n\nfor name, df in datasets.items():\n    print(f\"Missing values in {name}:\")\n    missing = df.isnull().sum()\n    missing = missing[missing > 0]\n    if not missing.empty:\n        print(missing)\n    else:\n        print(\"  ✅ No missing values.\")\n    print()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:38:40.893128Z","iopub.execute_input":"2025-08-18T02:38:40.893374Z","iopub.status.idle":"2025-08-18T02:38:41.617004Z","shell.execute_reply.started":"2025-08-18T02:38:40.893357Z","shell.execute_reply":"2025-08-18T02:38:41.616284Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### kiểm tra giá trị trùng lặp","metadata":{}},{"cell_type":"code","source":"#Kiểm tra trùng lặp (Duplicate Rows): .duplicated(): Trả về Series boolean, dòng nào trùng lặp sẽ là True.\n#                                     .sum(): Đếm tổng số dòng trùng lặp.\n#Kiểm tra xem dữ liệu có bản ghi nào bị trùng không — quan trọng cho việc làm sạch dữ liệu.\n\n# Đếm các hàng trùng lặp trong train_df\ntrain_duplicates = train_df.duplicated().sum()\n\n# Đếm các hàng trùng lặp trong  test_df\ntest_duplicates = test_df.duplicated().sum()\n\n# Đếm các hàng trùng lặp trong train_dem_df (optional)\ntrain_dem_duplicates = train_dem_df.duplicated().sum()\n# Đếm các hàng trùng lặp trong test_dem_df (optional)\ntest_dem_duplicates = test_dem_df.duplicated().sum()\n\n# In số lượng dòng trùng:\nprint(f\"Number of duplicate rows in train_df: {train_duplicates}\")\nprint(f\"Number of duplicate rows in test_df: {test_duplicates}\")\nprint(f\"Number of duplicate rows in train_dem_df: {train_dem_duplicates}\")\nprint(f\"Number of duplicate rows in test_dem_df: {test_dem_duplicates}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:38:41.618441Z","iopub.execute_input":"2025-08-18T02:38:41.618663Z","iopub.status.idle":"2025-08-18T02:38:46.000177Z","shell.execute_reply.started":"2025-08-18T02:38:41.618646Z","shell.execute_reply":"2025-08-18T02:38:45.999501Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Tiền xử lý dữ liệu","metadata":{}},{"cell_type":"markdown","source":"## Tỷ lệ phần trăm Null","metadata":{}},{"cell_type":"markdown","source":"UTILS/HELPERS","metadata":{}},{"cell_type":"code","source":"def null_percent(df):\n    per=((df.isnull().sum()/len(df))*100).round(2)\n    return per[per>0]\n\nprint(\"Nan Values in Train data\")\nprint(null_percent(train_df))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:38:46.000873Z","iopub.execute_input":"2025-08-18T02:38:46.001145Z","iopub.status.idle":"2025-08-18T02:38:46.720072Z","shell.execute_reply.started":"2025-08-18T02:38:46.001128Z","shell.execute_reply":"2025-08-18T02:38:46.719414Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Quan sát hoặc xu hướng ban đầu","metadata":{}},{"cell_type":"code","source":"# --- 2.4 Hợp nhất dữ liệu nhân khẩu học và cảm biến ---\nmerged_train = pd.merge(train_df, train_dem_df, on=\"subject\", how=\"left\")\nmerged_test = pd.merge(test_df, test_dem_df, on=\"subject\", how=\"left\")\n\nprint(\"Merged Train Shape:\", merged_train.shape)\nprint(\"Merged Test Shape:\", merged_test.shape)\ndisplay(merged_train.head(2))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:38:46.720751Z","iopub.execute_input":"2025-08-18T02:38:46.721001Z","iopub.status.idle":"2025-08-18T02:38:48.367806Z","shell.execute_reply.started":"2025-08-18T02:38:46.720963Z","shell.execute_reply":"2025-08-18T02:38:48.367019Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Thống kê mô tả toàn bộ merged_train ---\nmerged_train.describe().T","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:38:48.368624Z","iopub.execute_input":"2025-08-18T02:38:48.368934Z","iopub.status.idle":"2025-08-18T02:38:56.448480Z","shell.execute_reply.started":"2025-08-18T02:38:48.368913Z","shell.execute_reply":"2025-08-18T02:38:56.447847Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Kiểm tra thiếu giá trị & thống kê cảm biến: \n#Danh sách các cột không thuộc cảm biến (có thể là ID, thông tin khác). \n\nexcluded_prefixes = ('acc_', 'rot_', 'thm_', 'tof_')\nsensor_cols = [col for col in train_df.columns if not col.startswith(excluded_prefixes)]\n\n# Sensor Data Summary for TRAIN\n#isnull().sum(): Đếm số giá trị bị thiếu.\nmissing_sensor_train = pd.DataFrame({\n    'Feature': sensor_cols,\n    '[TRAIN] Missing Count': train_df[sensor_cols].isnull().sum().values,\n    '[TRAIN] Missing %': (train_df[sensor_cols].isnull().sum().values / len(train_df)) * 100\n})\n\n#nunique(): Đếm số lượng giá trị duy nhất.\nunique_sensor_train = pd.DataFrame({\n    'Feature': sensor_cols,\n    'Unique Values [TRAIN]': train_df[sensor_cols].nunique().values\n})\n\n#dtypes: Lấy kiểu dữ liệu của từng cột.\ndtypes_sensor = pd.DataFrame({\n    'Feature': sensor_cols,\n    'Data Type': train_df[sensor_cols].dtypes.values\n})\n\n# Merge all summaries (NO test set)\n#merge: Gộp các bảng thống kê thành bảng duy nhất theo Feature.\nsensor_summary = missing_sensor_train \\\n    .merge(unique_sensor_train, on='Feature', how='left') \\\n    .merge(dtypes_sensor, on='Feature', how='left')\n\n# Display styled DataFrame (mask NaNs just for styling)\n#fillna(0): Điền giá trị thiếu bằng 0 (cho đẹp mắt khi hiển thị).\n#.style.background_gradient: Tô màu nền theo giá trị giúp dễ nhìn.\nstyled_df = sensor_summary.fillna(0)\nstyled_df.style.background_gradient(cmap='viridis')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:38:56.449147Z","iopub.execute_input":"2025-08-18T02:38:56.449338Z","iopub.status.idle":"2025-08-18T02:38:57.397448Z","shell.execute_reply.started":"2025-08-18T02:38:56.449324Z","shell.execute_reply":"2025-08-18T02:38:57.396671Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Thống kê tương tự cho nhân khẩu học: Tương tự như phần thống kê cảm biến nhưng áp dụng cho dữ liệu nhân khẩu học.\n\n# Cột nhân khẩu học (không loại trừ)\ndem_cols = train_dem_df.columns\n\n# Giá trị bị thiếu trong dữ liệu nhân khẩu học của train \nmissing_demo_train = pd.DataFrame({\n    'Feature': dem_cols,\n    '[TRAIN DEMO] Missing Count': train_dem_df[dem_cols].isnull().sum().values,\n    '[TRAIN DEMO] Missing %': (train_dem_df[dem_cols].isnull().sum().values / len(train_dem_df)) * 100\n})\n\n# Giá trị duy nhất được tính trong dữ liệu nhân khẩu học của train \nunique_demo_train = pd.DataFrame({\n    'Feature': dem_cols,\n    'Unique Values [TRAIN DEMO]': train_dem_df[dem_cols].nunique().values\n})\n\n# Data types\ndtypes_demo = pd.DataFrame({\n    'Feature': dem_cols,\n    'Data Type': train_dem_df[dem_cols].dtypes.values\n})\n\n# Tóm tắt hợp nhất (chỉ dành cho đào tạo)\ndemo_summary = (\n    missing_demo_train\n    .merge(unique_demo_train, on='Feature', how='left')\n    .merge(dtypes_demo, on='Feature', how='left')\n)\n\n# Hiển thị tóm tắt theo phong cách\ndemo_summary.style.background_gradient(cmap='viridis')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:38:57.398282Z","iopub.execute_input":"2025-08-18T02:38:57.399185Z","iopub.status.idle":"2025-08-18T02:38:57.421545Z","shell.execute_reply.started":"2025-08-18T02:38:57.399165Z","shell.execute_reply":"2025-08-18T02:38:57.420786Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# EDA","metadata":{}},{"cell_type":"markdown","source":"## Numerical Feature Distribution Analysis (Phân tích phân phối tính năng số) (Phân tích đơn biến)","metadata":{}},{"cell_type":"markdown","source":"### Sensor-Based Features (Tính năng dựa trên cảm biến)","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\n\n# 1) Sao chép đào tạo và kiểm tra để chúng ta không sửa đổi DataFrame gốc\ntrain_temp = train_df.copy()\ntest_temp  = test_df.copy()\n\n# 2) GIA TỐC KẾ: tính toán độ lớn tại mỗi dấu thời gian\ntrain_temp['acc_mag'] = np.sqrt(\n    train_temp['acc_x']**2 + train_temp['acc_y']**2 + train_temp['acc_z']**2\n)\ntest_temp['acc_mag'] = np.sqrt(\n    test_temp['acc_x']**2 + test_temp['acc_y']**2 + test_temp['acc_z']**2\n)\n\n# 3) ROTATION: tính toán “góc quay” từ thành phần quaternion w\n# (Lưu ý: rot_w nằm trong [-1,1], do đó arccos hợp lệ. Chúng tôi bỏ qua NaN nếu có.)\ntrain_temp['rot_angle'] = 2 * np.arccos(train_temp['rot_w'].clip(-1,1))\ntest_temp['rot_angle']  = 2 * np.arccos(test_temp['rot_w'].clip(-1,1))\n\n# 4) Nhóm theo sequence_id và tổng hợp các tóm tắt gia tốc kế\nacc_agg_funcs = {\n    'acc_mag': ['mean', 'std', 'max']\n}\ntrain_acc_summary = train_temp.groupby('sequence_id').agg(acc_agg_funcs)\ntest_acc_summary  = test_temp.groupby('sequence_id').agg(acc_agg_funcs)\n\n# Làm phẳng cột MultiIndex\ntrain_acc_summary.columns = ['acc_mag_' + stat for stat in ['mean', 'std', 'max']]\ntest_acc_summary.columns  = ['acc_mag_' + stat for stat in ['mean', 'std', 'max']]\n\n# 5) Nhóm theo sequence_id và tổng hợp tóm tắt vòng quay\nrot_agg_funcs = {\n    'rot_angle': ['mean', 'std', 'max']\n}\ntrain_rot_summary = train_temp.groupby('sequence_id').agg(rot_agg_funcs)\ntest_rot_summary  = test_temp.groupby('sequence_id').agg(rot_agg_funcs)\n\ntrain_rot_summary.columns = ['rot_angle_' + stat for stat in ['mean', 'std', 'max']]\ntest_rot_summary.columns  = ['rot_angle_' + stat for stat in ['mean', 'std', 'max']]\n\n# 6) NHIỆT ĐỘ: năm cảm biến thm_1 … thm_5\nthm_cols = [f\"thm_{i}\" for i in range(1, 6)]\n\n# Xác định hàm tổng hợp: trung bình + độ lệch chuẩn\nthm_agg_funcs = {col: ['mean', 'std'] for col in thm_cols}\n\ntrain_thm_summary = train_temp.groupby('sequence_id').agg(thm_agg_funcs)\ntest_thm_summary  = test_temp.groupby('sequence_id').agg(thm_agg_funcs)\n\n# Làm phẳng các cột MultiIndex\nflattened_thm_cols = []\nfor sensor in thm_cols:\n    for stat in ['mean','std']:\n        flattened_thm_cols.append(f\"{sensor}_{stat}\")\n\ntrain_thm_summary.columns = flattened_thm_cols\ntest_thm_summary.columns  = flattened_thm_cols\n\n# 7) THỜI GIAN BAY: mỗi cảm biến i có 64 cột pixel: tof_i_v0 … tof_i_v63\n# Chúng ta sẽ tạo một “tof_i_mean_at_ts” cho mỗi dấu thời gian, sau đó tổng hợp theo từng chuỗi.\n\ndef compute_tof_sequence_summary(df):\n    # Khởi tạo một dict để giữ DataFrames theo từng chuỗi\n    seq_summaries = {}\n\n    for i in range(1, 6):\n        # Xây dựng danh sách các cột cho cảm biến i\n        tof_cols = [f\"tof_{i}_v{pix}\" for pix in range(64)]\n        # Thay thế -1 bằng NaN để chúng không làm lệch giá trị trung bình; chuyển đổi sang float\n        ts_grid = df[tof_cols].replace(-1, np.nan).astype(float)\n        # Tính toán “trung bình trên tất cả 64 pixel” cho mỗi dấu thời gian\n        df[f\"tof_{i}_mean_at_ts\"] = ts_grid.mean(axis=1)\n    \n   # Bây giờ, nhóm theo id chuỗi và tính giá trị trung bình và độ lệch chuẩn của các giá trị trung bình đó\n    agg_dict = {f\"tof_{i}_mean_at_ts\": ['mean','std'] for i in range(1, 6)}\n    summary = df.groupby('sequence_id').agg(agg_dict)\n    # Làm phẳng các cột MultiIndex\n    flat_cols = [f\"tof_{i}_{stat}\" for i in range(1, 6) for stat in ['mean','std']]\n    summary.columns = flat_cols\n    return summary\n\ntrain_tof_summary = compute_tof_sequence_summary(train_temp)\ntest_tof_summary  = compute_tof_sequence_summary(test_temp)\n\n# 8) Hợp nhất các tóm tắt accel, rotation, thm, tof (trên sequence_id)\ntrain_sensor_summary = (\n    train_acc_summary\n    .join(train_rot_summary, how='outer')\n    .join(train_thm_summary, how='outer')\n    .join(train_tof_summary, how='outer')\n)\n\ntest_sensor_summary = (\n    test_acc_summary\n    .join(test_rot_summary, how='outer')\n    .join(test_thm_summary, how='outer')\n    .join(test_tof_summary, how='outer')\n)\n\n# 9) Thêm cột “Dataset” để chúng ta có thể thực hiện box+hist song song\ntrain_sensor_summary['Dataset'] = 'Train'\ntest_sensor_summary['Dataset']  = 'Test'\n\n# 10) Ghép nối thành một DataFrame để vẽ đồ thị\ncombined_sensor_summary = pd.concat(\n    [train_sensor_summary, test_sensor_summary],\n    axis=0\n).reset_index(drop=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:38:57.423835Z","iopub.execute_input":"2025-08-18T02:38:57.424257Z","iopub.status.idle":"2025-08-18T02:39:03.312939Z","shell.execute_reply.started":"2025-08-18T02:38:57.424238Z","shell.execute_reply":"2025-08-18T02:39:03.312374Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Target Feature Analysis (Univariate Analysis)","metadata":{}},{"cell_type":"markdown","source":"### Sensor‐Based Differentiation Across Demographics and Physiology Features","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport matplotlib.pyplot as plt\n\n# (1) Sáp nhập train_demographics vào train_df nếu chưa thực hiện\ntrain_df = train_df.merge(\n    train_dem_df,\n    on=\"subject\",\n    how=\"left\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:39:03.313670Z","iopub.execute_input":"2025-08-18T02:39:03.313952Z","iopub.status.idle":"2025-08-18T02:39:04.923124Z","shell.execute_reply.started":"2025-08-18T02:39:03.313925Z","shell.execute_reply":"2025-08-18T02:39:04.922546Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Xử lý missing values","metadata":{}},{"cell_type":"markdown","source":"##  IMU – Gia tốc/quaternion (acc_*, rot_w/x/y/z)","metadata":{}},{"cell_type":"code","source":"def fix_imu(df):\n    imu_cols = [c for c in df.columns if c.startswith((\"acc_\", \"rot_\"))]\n    def _seq_fix(g):\n        g[imu_cols] = g[imu_cols].interpolate(limit_direction=\"both\")\n        # renormalize quaternion\n        q = g[[\"rot_w\",\"rot_x\",\"rot_y\",\"rot_z\"]].to_numpy(float)\n        n = (q**2).sum(1)**0.5\n        n[n==0] = 1.0\n        q = q / n[:,None]\n        g[[\"rot_w\",\"rot_x\",\"rot_y\",\"rot_z\"]] = q\n        return g\n    return df.groupby(\"sequence_id\", group_keys=False).apply(_seq_fix)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:39:04.923785Z","iopub.execute_input":"2025-08-18T02:39:04.923987Z","iopub.status.idle":"2025-08-18T02:39:04.929551Z","shell.execute_reply.started":"2025-08-18T02:39:04.923953Z","shell.execute_reply":"2025-08-18T02:39:04.928973Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"##  Thermopile (thm_1..thm_5)","metadata":{}},{"cell_type":"code","source":"def fix_thm(df):\n    thm = [f\"thm_{i}\" for i in range(1,6)]\n    return df.groupby(\"sequence_id\", group_keys=False)[thm].apply(\n        lambda g: g.interpolate(limit_direction=\"both\").ffill().bfill()\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:39:04.930135Z","iopub.execute_input":"2025-08-18T02:39:04.930361Z","iopub.status.idle":"2025-08-18T02:39:04.949262Z","shell.execute_reply.started":"2025-08-18T02:39:04.930347Z","shell.execute_reply":"2025-08-18T02:39:04.948573Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## ToF – 5 cảm biến × 64 pixel (tof_i_vj)","metadata":{}},{"cell_type":"code","source":"def fix_tof_and_reduce(df):\n    # 1) sentinel -> NaN + flag\n    tof_pix = [c for c in df.columns if c.startswith(\"tof_\") and \"_v\" in c]\n    df[tof_pix] = df[tof_pix].replace(-1, np.nan)\n    # optional flags per sensor\n    for i in range(1,6):\n        cols = [f\"tof_{i}_v{p}\" for p in range(64) if f\"tof_{i}_v{p}\" in df]\n        df[f\"tof_{i}_missing_frac\"] = df[cols].isna().mean(axis=1)\n        df[f\"tof_{i}_mean\"] = df[cols].mean(axis=1)  # dimension reduction\n    # 2) interpolate the 5 means\n    mean_cols = [f\"tof_{i}_mean\" for i in range(1,6)]\n    df[mean_cols] = df.groupby(\"sequence_id\", group_keys=False)[mean_cols].apply(\n        lambda g: g.interpolate(limit_direction=\"both\").ffill().bfill()\n    )\n    # 3) drop heavy pixel columns to save RAM\n    df.drop(columns=tof_pix, inplace=True)\n    return df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:39:04.950032Z","iopub.execute_input":"2025-08-18T02:39:04.950706Z","iopub.status.idle":"2025-08-18T02:39:04.966889Z","shell.execute_reply.started":"2025-08-18T02:39:04.950684Z","shell.execute_reply":"2025-08-18T02:39:04.966081Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Pha/nhãn & metadata","metadata":{}},{"cell_type":"code","source":"from sklearn.preprocessing import LabelEncoder\nle_gesture = LabelEncoder().fit(train_df[\"gesture\"].astype(str))\ntrain_df[\"gesture_id\"] = le_gesture.transform(train_df[\"gesture\"].astype(str))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:39:04.967389Z","iopub.execute_input":"2025-08-18T02:39:04.967611Z","iopub.status.idle":"2025-08-18T02:39:05.329827Z","shell.execute_reply.started":"2025-08-18T02:39:04.967596Z","shell.execute_reply":"2025-08-18T02:39:05.329234Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Pipeline tiền xử lý","metadata":{}},{"cell_type":"code","source":"def minimal_preprocess(df):\n    df = df.copy()\n    # 1) IMU\n    df = fix_imu(df)\n    # 2) Thermopile\n    thm_fixed = fix_thm(df)  # returns only thm cols\n    df[thm_fixed.columns] = thm_fixed\n    # 3) ToF -> means + flags, drop pixels\n    df = fix_tof_and_reduce(df)\n    # 4) Feature cơ bản (ví dụ):\n    df[\"acc_mag\"] = np.sqrt(df[\"acc_x\"]**2 + df[\"acc_y\"]**2 + df[\"acc_z\"]**2)\n    df[\"rot_angle\"] = 2*np.arccos(np.clip(df[\"rot_w\"], -1, 1))\n    return df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:39:05.330589Z","iopub.execute_input":"2025-08-18T02:39:05.330819Z","iopub.status.idle":"2025-08-18T02:39:05.335410Z","shell.execute_reply.started":"2025-08-18T02:39:05.330804Z","shell.execute_reply":"2025-08-18T02:39:05.334750Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Create dataset","metadata":{"papermill":{"duration":0.003008,"end_time":"2025-06-14T09:45:58.440588","exception":false,"start_time":"2025-06-14T09:45:58.43758","status":"completed"},"tags":[]}},{"cell_type":"code","source":"sequences = train_df.groupby('sequence_id')\nX_list = []\nlengths = []\ny_list = []\n\nsequence_info = []\nfor i, (seq_id, seq) in enumerate(sequences):\n    seq_data = seq[feature_cols].ffill().bfill().fillna(0).values\n    X_list.append(seq_data)\n    lengths.append(seq_data.shape[0])\n    sequence_info.append({\n        'sequence_id': seq_id,\n        'subject': seq['subject'].iloc[0],\n        'gesture': seq['gesture'].iloc[0]\n    })\n\npad_len = int(np.percentile(lengths, 90))\nprint(f\"Pad/truncate all sequences to length {pad_len} (90th percentile).\")\n\nseq_df = pd.DataFrame(sequence_info)\nX_array = keras_pad_sequences(\n    X_list,\n    maxlen=pad_len,\n    dtype='float32',\n    padding='post',\n    truncating='post'\n)  # shape: (n_samples, pad_len, total_features)\n\ny_array = seq_df['gesture'].values  # shape: (n_samples,)\n\nnum_classes = len(np.unique(y_array))\ny_array = np.eye(num_classes)[y_array]  # shape: (n_samples, num_classes)\n\n# Transpose to (n_samples, features, seq_len) for PyTorch\nX_array = np.transpose(X_array, (0, 2, 1))\n\n\nclass SequenceDataset(Dataset):\n    def __init__(self, X, y=None):\n        \"\"\"\n        X: np.ndarray of shape (n_samples, features, seq_len)\n        y: np.ndarray of shape (n_samples, num_classes) or None for test\n        \"\"\"\n        self.X = torch.from_numpy(X).float()\n        self.y = torch.from_numpy(y).float() if y is not None else None\n\n    def __len__(self):\n        return self.X.size(0)\n\n    def __getitem__(self, idx):\n        if self.y is not None:\n            return self.X[idx], self.y[idx]\n        else:\n            return self.X[idx]","metadata":{"papermill":{"duration":10.6658,"end_time":"2025-06-14T09:46:09.109534","exception":false,"start_time":"2025-06-14T09:45:58.443734","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:39:05.336265Z","iopub.execute_input":"2025-08-18T02:39:05.336498Z","iopub.status.idle":"2025-08-18T02:39:16.336892Z","shell.execute_reply.started":"2025-08-18T02:39:05.336475Z","shell.execute_reply":"2025-08-18T02:39:16.336133Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **Model**","metadata":{"papermill":{"duration":0.003078,"end_time":"2025-06-14T09:46:09.116145","exception":false,"start_time":"2025-06-14T09:46:09.113067","status":"completed"},"tags":[]}},{"cell_type":"code","source":"class SEBlock(nn.Module):\n    def __init__(self, channels, reduction=8):\n        super(SEBlock, self).__init__()\n        self.fc1 = nn.Linear(channels, channels // reduction, bias=True)\n        self.relu = nn.ReLU(inplace=True)\n        self.fc2 = nn.Linear(channels // reduction, channels, bias=True)\n        self.sigmoid = nn.Sigmoid()\n\n    def forward(self, x):\n        # x: (batch, channels, seq_len)\n        # Squeeze: global average pooling over time dimension\n        se = x.mean(dim=2)                      # (batch, channels)\n        se = self.relu(self.fc1(se))            # (batch, channels//reduction)\n        se = self.sigmoid(self.fc2(se))         # (batch, channels)\n        se = se.unsqueeze(2)                    # (batch, channels, 1)\n        return x * se                           # scale channels\n\nclass ResidualSEBlock(nn.Module):\n    def __init__(self, in_channels, out_channels, kernel_size=3, pool_size=2, dropout_rate=0.3):\n        super(ResidualSEBlock, self).__init__()\n        self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size,\n                               padding=kernel_size//2, bias=False)\n        self.bn1 = nn.BatchNorm1d(out_channels)\n        self.relu = nn.ReLU(inplace=True)\n\n        self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size,\n                               padding=kernel_size//2, bias=False)\n        self.bn2 = nn.BatchNorm1d(out_channels)\n\n        self.se = SEBlock(out_channels, reduction=8)\n\n        if in_channels != out_channels:\n            self.shortcut = nn.Sequential(\n                nn.Conv1d(in_channels, out_channels, kernel_size=1, bias=False),\n                nn.BatchNorm1d(out_channels)\n            )\n        else:\n            self.shortcut = nn.Identity()\n\n        self.pool = nn.MaxPool1d(kernel_size=pool_size)\n        self.dropout = nn.Dropout(dropout_rate)\n\n    def forward(self, x):\n        # x: (batch, in_channels, seq_len)\n        shortcut = self.shortcut(x)                                 # (batch, out_channels, seq_len)\n        out = self.conv1(x)                                          # (batch, out_channels, seq_len)\n        out = self.bn1(out)\n        out = self.relu(out)\n\n        out = self.conv2(out)                                        # (batch, out_channels, seq_len)\n        out = self.bn2(out)\n\n        out = self.se(out)                                           # SE scaling\n\n        out = out + shortcut                                         # skip connection\n        out = self.relu(out)\n\n        out = self.pool(out)                                         # (batch, out_channels, seq_len//pool_size)\n        out = self.dropout(out)\n        return out\n\nclass Attention(nn.Module):\n    def __init__(self, input_dim):\n        super(Attention, self).__init__()\n        self.score_fc = nn.Linear(input_dim, 1)\n\n    def forward(self, x):\n        # x: (batch, seq_len, features)\n        scores = torch.tanh(self.score_fc(x))            # (batch, seq_len, 1)\n        scores = scores.squeeze(2)                       # (batch, seq_len)\n        weights = F.softmax(scores, dim=1)               # (batch, seq_len)\n        weights = weights.unsqueeze(2)                   # (batch, seq_len, 1)\n        weighted = x * weights                           # (batch, seq_len, features)\n        context = weighted.sum(dim=1)                    # (batch, features)\n        return context\n        \nclass IMU_HARModel(nn.Module):\n    def __init__(self, total_features, imu_dim, pad_len, num_classes):\n        super(IMU_HARModel, self).__init__()\n        # IMU branch\n        self.resblock1 = ResidualSEBlock(imu_dim, 64, kernel_size=3, pool_size=2, dropout_rate=0.1)\n        self.resblock2 = ResidualSEBlock(64, 128, kernel_size=3, pool_size=2, dropout_rate=0.1)\n\n        # After pooling twice, seq_len reduced by factor of 4\n        reduced_len = pad_len // 4\n        merged_channels = 128 #+ 128  # from IMU and TTF here we use IMU only\n\n        self.bigru = nn.GRU(\n            input_size=merged_channels,\n            hidden_size=merged_channels//2,\n            num_layers=2,\n            batch_first=True,\n            bidirectional=True,\n            dropout=0.1,\n        )\n\n        # Attention\n        self.attention = Attention(input_dim=merged_channels)\n\n        # Dense head\n        self.fc1 = nn.Linear(merged_channels, 128, bias=True)\n        self.bn_fc1 = nn.BatchNorm1d(128)\n        self.drop_fc1 = nn.Dropout(0.1)\n\n        self.fc2 = nn.Linear(128, 64, bias=True)\n        self.bn_fc2 = nn.BatchNorm1d(64)\n        self.drop_fc2 = nn.Dropout(0.1)\n\n        self.out = nn.Linear(64, num_classes)\n\n    def forward(self, x):\n        # x: (batch, total_features, seq_len)\n        x_imu = x[:, :imu_dim, :]           # (batch, imu_dim, seq_len)\n        x_ttf = x[:, imu_dim:, :]           # (batch, rest_dim, seq_len)\n\n        # IMU branch\n        b1 = self.resblock1(x_imu)          # (batch, 64, seq_len/2)\n        b1 = self.resblock2(b1)             # (batch, 128, seq_len/4)\n\n        # b2 is reserved for tof branch when will be using it\n\n        # Concatenate branches along channel dimension\n        merged = b1 # torch.cat([b1, b2], dim=1)  # (batch, 256, seq_len/4)\n\n        # Prepare for GRU: (batch, seq_len/4, 256)\n        merged = merged.permute(0, 2, 1)\n\n        # BiGRU\n        lstm_out, _ = self.bigru(merged)       # (batch, seq_len/4, 256)\n\n        # Attention\n        context = self.attention(lstm_out)    # (batch, 256)\n\n        # Dense head\n        x = self.fc1(context)                 # (batch, 256)\n        x = self.bn_fc1(x)\n        x = F.relu(x)\n        x = self.drop_fc1(x)\n\n        x = self.fc2(x)                       # (batch, 128)\n        x = self.bn_fc2(x)\n        x = F.relu(x)\n        x = self.drop_fc2(x)\n\n        out = self.out(x)                     # (batch, num_classes)\n        return out\n\n# Device setup\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)","metadata":{"papermill":{"duration":0.083553,"end_time":"2025-06-14T09:46:09.202903","exception":false,"start_time":"2025-06-14T09:46:09.11935","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:39:16.337822Z","iopub.execute_input":"2025-08-18T02:39:16.338444Z","iopub.status.idle":"2025-08-18T02:39:16.426556Z","shell.execute_reply.started":"2025-08-18T02:39:16.338401Z","shell.execute_reply":"2025-08-18T02:39:16.425885Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **Utils**","metadata":{"papermill":{"duration":0.003115,"end_time":"2025-06-14T09:46:09.209456","exception":false,"start_time":"2025-06-14T09:46:09.206341","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def soft_cross_entropy(pred, soft_targets):\n    \"\"\"\n    pred: (batch, num_classes) raw scores (no softmax)\n    soft_targets: (batch, num_classes) probabilities\n    \"\"\"\n    log_probs = F.log_softmax(pred, dim=1)\n    loss = -torch.sum(soft_targets * log_probs, dim=1).mean()\n    return loss\n\ndef mixup_data(x, y, alpha=0.2):\n    \"\"\"\n    Return mixed inputs and mixed targets (one-hot) for mixup.\n    x: Tensor of shape (batch_size, features, seq_len)\n    y: Tensor of shape (batch_size, num_classes)\n    \"\"\"\n    if alpha > 0:\n        lam = np.random.beta(alpha, alpha)\n    else:\n        lam = 1.0\n    batch_size = x.size(0)\n    index = torch.randperm(batch_size).to(x.device)\n\n    mixed_x = lam * x + (1 - lam) * x[index, :]\n    mixed_y = lam * y + (1 - lam) * y[index, :]\n    return mixed_x, mixed_y","metadata":{"papermill":{"duration":0.010109,"end_time":"2025-06-14T09:46:09.222665","exception":false,"start_time":"2025-06-14T09:46:09.212556","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:39:16.427344Z","iopub.execute_input":"2025-08-18T02:39:16.427539Z","iopub.status.idle":"2025-08-18T02:39:16.450399Z","shell.execute_reply.started":"2025-08-18T02:39:16.427525Z","shell.execute_reply":"2025-08-18T02:39:16.449737Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **Training loop**","metadata":{"papermill":{"duration":0.003048,"end_time":"2025-06-14T09:46:09.228893","exception":false,"start_time":"2025-06-14T09:46:09.225845","status":"completed"},"tags":[]}},{"cell_type":"code","source":"seed_everything(seed=SEED)\n\ncriterion = soft_cross_entropy\n\nn_splits = 5\nbatch_size = 128\ngkf = GroupKFold(n_splits=n_splits)\n\nfold_metrics = []\nbest_fold_metrics = []\nbest_models = []\n\nfor fold, (train_idx, val_idx) in enumerate(gkf.split(X_array, y_array, groups=seq_df['subject'])):\n    print(f\"\\n{'='*50}\")\n    print(f\"Fold {fold + 1}/{n_splits}\")\n    print(f\"Train subjects: {len(np.unique(seq_df.iloc[train_idx]['subject']))}\")\n    print(f\"Val subjects: {len(np.unique(seq_df.iloc[val_idx]['subject']))}\")\n    print(f\"{'='*50}\")\n    \n    train_dataset = SequenceDataset(X_array[train_idx], y_array[train_idx])\n    val_dataset = SequenceDataset(X_array[val_idx], y_array[val_idx])\n    \n    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, drop_last=True)\n    val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)\n    \n    seed_everything(seed=SEED + fold)\n    model = IMU_HARModel(\n        total_features=len(feature_cols),\n        imu_dim=imu_dim,\n        pad_len=pad_len,\n        num_classes=num_classes,\n    ).to(device)\n    \n    # Optimizer et scheduler\n    optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)\n    steps_per_epoch = len(train_loader)\n    scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(\n        optimizer,\n        T_0=5 * steps_per_epoch,\n        T_mult=2,\n        eta_min=1e-5,\n    )\n    \n    # Early stopping\n    best_metric = -np.inf\n    best_binary_f1 = -np.inf\n    best_macro_f1 = -np.inf\n    patience = 15\n    epochs_no_improve = 0\n    num_epochs = 100\n    \n    for epoch in range(1, num_epochs + 1):\n        # Training phase\n        model.train()\n        train_loss = 0.0\n        total = 0\n        for batch_x, batch_y in train_loader:\n            batch_x = batch_x.to(device)\n            batch_y = batch_y.to(device)\n    \n            # Apply mixup\n            mixed_x, mixed_y = mixup_data(batch_x, batch_y, alpha=0.2)\n    \n            optimizer.zero_grad()\n            outputs = model(mixed_x)\n            loss = criterion(outputs, mixed_y)\n            loss.backward()\n            optimizer.step()\n            scheduler.step()\n    \n            train_loss += loss.item() * batch_x.size(0)\n            total += batch_x.size(0)\n        train_loss /= total\n    \n        # Validation phase\n        model.eval()\n        val_loss = 0.0\n        total = 0\n        all_true = []\n        all_pred = []\n        \n        with torch.no_grad():\n            for batch_x, batch_y in val_loader:\n                batch_x = batch_x.to(device)\n                batch_y = batch_y.to(device)\n                \n                outputs = model(batch_x)\n                loss = criterion(outputs, batch_y)\n                val_loss += loss.item() * batch_x.size(0)\n                total += batch_x.size(0)\n                \n                # Get predicted class indices\n                preds = torch.argmax(outputs, dim=1).cpu().numpy()\n                # Get true class indices from one-hot\n                trues = torch.argmax(batch_y, dim=1).cpu().numpy()\n                \n                all_true.append(trues)\n                all_pred.append(preds)\n        \n        val_loss /= total\n        all_true = np.concatenate(all_true)\n        all_pred = np.concatenate(all_pred)\n        \n        # Compute competition metrics\n        # Binary classification: BFRB (1) vs non-BFRB (0)\n        binary_true = np.isin(all_true, bfrb_indices).astype(int)\n        binary_pred = np.isin(all_pred, bfrb_indices).astype(int)\n        binary_f1 = f1_score(binary_true, binary_pred)\n        \n        # Collapse non-BFRB gestures into a single class\n        collapsed_true = np.where(\n            np.isin(all_true, bfrb_indices),\n            all_true,\n            len(bfrb_gestures)  # Single non-BFRB class\n        )\n        collapsed_pred = np.where(\n            np.isin(all_pred, bfrb_indices),\n            all_pred,\n            len(bfrb_gestures)  # Single non-BFRB class\n        )\n        \n        # Macro F1 on collapsed classes\n        macro_f1 = f1_score(collapsed_true, collapsed_pred, average='macro')\n        final_metric = (binary_f1 + macro_f1) / 2\n        \n        print(f\"Epoch {epoch:02d}: Train Loss = {train_loss:.4f}, Val Loss = {val_loss:.4f}\")\n        print(f\"  Binary F1 = {binary_f1:.4f}, Macro F1 = {macro_f1:.4f}, Final Metric = {final_metric:.4f}\")\n        \n        if final_metric > best_metric:\n            best_metric = final_metric\n            best_binary_f1 = binary_f1\n            best_macro_f1 = macro_f1\n            epochs_no_improve = 0\n            best_model_state = model.state_dict()\n            print(f\"  New best metric! Saving model...\")\n        else:\n            epochs_no_improve += 1\n            if epochs_no_improve >= patience:\n                print(f\"Early stopping triggered at epoch {epoch}\")\n                model.load_state_dict(best_model_state)\n                break\n    \n    torch.save(best_model_state, f\"best_model_fold{fold}.pth\")\n    best_models.append(best_model_state)\n    \n    fold_metrics.append({\n        'binary_f1': binary_f1,\n        'macro_f1': macro_f1,\n        'final_metric': final_metric\n    })\n    \n    best_fold_metrics.append({\n        'binary_f1': best_binary_f1,\n        'macro_f1': best_macro_f1,\n        'final_metric': best_metric\n    })\n    \n    print(f\"\\nFold {fold + 1} completed.\")\n    print(f\"Final validation metrics - Binary F1: {binary_f1:.4f}, Macro F1: {macro_f1:.4f}, Final: {final_metric:.4f}\")\n    print(f\"Best validation metrics - Binary F1: {best_binary_f1:.4f}, Macro F1: {best_macro_f1:.4f}, Final: {best_metric:.4f}\")\n\nprint(\"\\n\" + \"=\"*50)\nprint(\"Cross-Validation Results\")\nprint(\"=\"*50)\n\n# Statistiques pour les meilleures métriques\nbest_binary_f1 = [m['binary_f1'] for m in best_fold_metrics]\nbest_macro_f1 = [m['macro_f1'] for m in best_fold_metrics]\nbest_metrics = [m['final_metric'] for m in best_fold_metrics]\n\nprint(\"\\nBest Fold-wise Metrics:\")\nfor i, (bf1, mf1, fm) in enumerate(zip(best_binary_f1, best_macro_f1, best_metrics)):\n    print(f\"Fold {i+1}: Binary F1 = {bf1:.4f}, Macro F1 = {mf1:.4f}, Final = {fm:.4f}\")\n\nprint(\"\\nGlobal Statistics (Best Metrics):\")\nprint(f\"Mean Best Final Metric: {np.mean(best_metrics):.4f} ± {np.std(best_metrics):.4f}\")\nprint(f\"Mean Best Binary F1: {np.mean(best_binary_f1):.4f} ± {np.std(best_binary_f1):.4f}\")\nprint(f\"Mean Best Macro F1: {np.mean(best_macro_f1):.4f} ± {np.std(best_macro_f1):.4f}\")","metadata":{"papermill":{"duration":441.705085,"end_time":"2025-06-14T09:53:30.937184","exception":false,"start_time":"2025-06-14T09:46:09.232099","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:39:16.451413Z","iopub.execute_input":"2025-08-18T02:39:16.451667Z","iopub.status.idle":"2025-08-18T02:46:38.785458Z","shell.execute_reply.started":"2025-08-18T02:39:16.451646Z","shell.execute_reply":"2025-08-18T02:46:38.784533Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Reloading best model","metadata":{"papermill":{"duration":0.013055,"end_time":"2025-06-14T09:53:30.964331","exception":false,"start_time":"2025-06-14T09:53:30.951276","status":"completed"},"tags":[]}},{"cell_type":"code","source":"model_ensemble = []\nfor fold in range(5):\n    model = IMU_HARModel(\n        total_features=len(feature_cols),\n        imu_dim=imu_dim,\n        pad_len=pad_len,\n        num_classes=num_classes,\n    ).to(device)\n    checkpoint = torch.load(f\"best_model_fold{fold}.pth\", map_location=device)\n    model.load_state_dict(checkpoint)\n    model.eval()\n    model_ensemble.append(model)","metadata":{"papermill":{"duration":0.145456,"end_time":"2025-06-14T09:53:31.123179","exception":false,"start_time":"2025-06-14T09:53:30.977723","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:46:38.786356Z","iopub.execute_input":"2025-08-18T02:46:38.786920Z","iopub.status.idle":"2025-08-18T02:46:38.922566Z","shell.execute_reply.started":"2025-08-18T02:46:38.786901Z","shell.execute_reply":"2025-08-18T02:46:38.922006Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **Submission**","metadata":{"papermill":{"duration":0.012887,"end_time":"2025-06-14T09:53:31.149767","exception":false,"start_time":"2025-06-14T09:53:31.13688","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def preprocess_sequence(df_seq: pd.DataFrame):\n    \"\"\"\n    Process a single sequence DataFrame (pandas):\n    - Forward/backward fill missing\n    - Scale using loaded scaler\n    - Pad/truncate to pad_len\n    - Return torch.Tensor of shape (1, features, seq_len)\n    \"\"\"\n    data = df_seq[feature_cols].ffill().bfill().fillna(0).values\n    # Pad/truncate\n    padded = keras_pad_sequences(\n        [data],\n        maxlen=pad_len,\n        dtype='float32',\n        padding='post',\n        truncating='post'\n    )[0]  # (pad_len, total_features)\n    # Transpose to (features, pad_len)\n    tensor = torch.from_numpy(padded.T).unsqueeze(0).float()  # (1, features, pad_len)\n    return tensor\n    \ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    \"\"\"\n    Kaggle evaluation API will call this for each sequence.\n    sequence: polars DataFrame for a single sequence\n    demographics: unused in this model\n    Returns: predicted gesture string\n    \"\"\"\n    df_seq = sequence.to_pandas()\n    df_demo = demographics.to_pandas()\n    \n    df_seq = df_seq.merge(\n    df_demo,\n    on='subject',\n    how='left',\n    validate='many_to_one',\n    )\n    right_handed_mask = df_seq['handedness'] == 1\n    df_seq.loc[right_handed_mask, imu_cols] = apply_symmetry(df_seq.loc[right_handed_mask, imu_cols])\n\n    x_tensor = preprocess_sequence(df_seq).to(device)\n    \n    all_outputs = []\n    with torch.no_grad():\n        for model in model_ensemble:\n            outputs = model(x_tensor).softmax(dim=-1)\n            all_outputs.append(outputs)\n\n    avg_outputs = torch.mean(torch.stack(all_outputs), dim=0)\n    pred_idx = torch.argmax(avg_outputs, dim=1).item()\n    \n    return str(gesture_classes[pred_idx])","metadata":{"papermill":{"duration":0.02145,"end_time":"2025-06-14T09:53:31.184073","exception":false,"start_time":"2025-06-14T09:53:31.162623","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:46:38.923272Z","iopub.execute_input":"2025-08-18T02:46:38.923479Z","iopub.status.idle":"2025-08-18T02:46:38.930263Z","shell.execute_reply.started":"2025-08-18T02:46:38.923464Z","shell.execute_reply":"2025-08-18T02:46:38.929534Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"inference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\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    )","metadata":{"papermill":{"duration":1.492266,"end_time":"2025-06-14T09:53:32.689615","exception":false,"start_time":"2025-06-14T09:53:31.197349","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T02:46:38.931066Z","iopub.execute_input":"2025-08-18T02:46:38.931290Z","iopub.status.idle":"2025-08-18T02:46:40.650615Z","shell.execute_reply.started":"2025-08-18T02:46:38.931270Z","shell.execute_reply":"2025-08-18T02:46:40.650016Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}