{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":22559,"databundleVersionId":1923081,"sourceType":"competition"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os, re, glob, random, time\nfrom collections import defaultdict  # FIX: bạn thiếu dòng này\n\nimport numpy as np\nimport pandas as pd\n\nfrom scipy.interpolate import interp1d\nfrom scipy.sparse import csr_matrix, hstack\nfrom sklearn.neighbors import NearestNeighbors\nfrom sklearn.preprocessing import normalize\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\n\nimport matplotlib.pyplot as plt\n\n# =======================\n# Config\n# =======================\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\n\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"DEVICE:\", DEVICE)\n\nTRAIN_ROOT = \"/kaggle/input/indoor-location-navigation/train\"\nTEST_ROOT  = \"/kaggle/input/indoor-location-navigation/test\"\nSAMPLE_SUB = \"/kaggle/input/indoor-location-navigation/sample_submission.csv\"\n\nassert os.path.isdir(TRAIN_ROOT)\nassert os.path.isdir(TEST_ROOT)\nassert os.path.isfile(SAMPLE_SUB)\n\n# =======================\n# Hyperparams\n# =======================\nFREQ_HZ = 50.0\nWINDOW = 100\nSTRIDE = 20\nBATCH = 256\nEPOCHS = 6\nLR = 1e-3\n\nMAX_TRAIN_FILES = None     # tăng dần 6000/12000/None nếu muốn\nVAL_RATIO = 0.2\n\nWIFI_WIN_MS = 1000         # wifi window around waypoint\nALPHA_WIFI = 0.4           # fusion presence+RSSI in fingerprint\nKNN_K = 30\n\nGATE_SIGMA = 0.25          # fusion weight from WiFi confidence\nprint(\"OK paths\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:21:19.052406Z","iopub.execute_input":"2026-01-10T05:21:19.052678Z","iopub.status.idle":"2026-01-10T05:21:28.353173Z","shell.execute_reply.started":"2026-01-10T05:21:19.052651Z","shell.execute_reply":"2026-01-10T05:21:28.352067Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_files_all = glob.glob(TRAIN_ROOT + \"/*/*/*.txt\")\nprint(\"total train files:\", len(train_files_all))\n\nif MAX_TRAIN_FILES is not None:\n    train_files_all = train_files_all[:MAX_TRAIN_FILES]\nprint(\"using train files:\", len(train_files_all))\n\nrandom.shuffle(train_files_all)\nn_val = int(len(train_files_all) * VAL_RATIO)\n\nval_files = train_files_all[:n_val]\ntr_files  = train_files_all[n_val:]\n\nprint(\"train:\", len(tr_files), \"val:\", len(val_files))\nprint(\"example train file:\", tr_files[0])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:21:28.355140Z","iopub.execute_input":"2026-01-10T05:21:28.355665Z","iopub.status.idle":"2026-01-10T05:21:34.611289Z","shell.execute_reply.started":"2026-01-10T05:21:28.355636Z","shell.execute_reply":"2026-01-10T05:21:34.610586Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TYPE_ACCEL = \"TYPE_ACCELEROMETER\"\nTYPE_GYRO  = \"TYPE_GYROSCOPE\"\nTYPE_WP    = \"TYPE_WAYPOINT\"\nTYPE_WIFI  = \"TYPE_WIFI\"\n\ndef parse_floor_str(floor_str: str) -> int:\n    s = str(floor_str).strip().upper()\n    m = re.match(r\"^([BF])(\\d+)$\", s)\n    if m:\n        sign = -1 if m.group(1) == \"B\" else 1\n        return sign * int(m.group(2))\n    return 0\n\ndef parse_train_path(fp: str):\n    parts = fp.replace(\"\\\\\", \"/\").split(\"/\")\n    idx = parts.index(\"train\")\n    site = parts[idx+1]\n    floor_str = parts[idx+2]\n    path = parts[idx+3].replace(\".txt\",\"\")\n    return site, parse_floor_str(floor_str), path\n\ndef read_txt(fp: str):\n    acc, gyro, wp, wifi = [], [], [], []\n    with open(fp, \"r\", encoding=\"utf-8\") as f:\n        for line in f:\n            line = line.strip()\n            if (not line) or line.startswith(\"#\"):\n                continue\n            v = line.split(\"\\t\")\n            if len(v) < 2:\n                continue\n            ts = int(v[0]); t = v[1]\n            try:\n                if t == TYPE_ACCEL and len(v) >= 5:\n                    acc.append([ts, float(v[2]), float(v[3]), float(v[4])])\n                elif t == TYPE_GYRO and len(v) >= 5:\n                    gyro.append([ts, float(v[2]), float(v[3]), float(v[4])])\n                elif t == TYPE_WP and len(v) >= 4:\n                    wp.append([ts, float(v[2]), float(v[3])])\n                elif t == TYPE_WIFI and len(v) >= 5:\n                    wifi.append([ts, str(v[3]), float(v[4])])  # bssid,rssi\n            except:\n                continue\n\n    return {\n        \"acc\":  pd.DataFrame(acc,  columns=[\"ts\",\"ax\",\"ay\",\"az\"]),\n        \"gyro\": pd.DataFrame(gyro, columns=[\"ts\",\"gx\",\"gy\",\"gz\"]),\n        \"wp\":   pd.DataFrame(wp,   columns=[\"ts\",\"x\",\"y\"]),\n        \"wifi\": pd.DataFrame(wifi, columns=[\"ts\",\"bssid\",\"rssi\"]),\n    }\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:21:34.615149Z","iopub.execute_input":"2026-01-10T05:21:34.615434Z","iopub.status.idle":"2026-01-10T05:21:34.627902Z","shell.execute_reply.started":"2026-01-10T05:21:34.615412Z","shell.execute_reply":"2026-01-10T05:21:34.626493Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class Synchronizer:\n    def __init__(self, freq_hz=50.0):\n        self.dt = 1000.0 / freq_hz\n\n    def _interp(self, df, cols, t_new):\n        if df.empty or len(df) < 2:\n            return None\n        df = df.sort_values(\"ts\")\n        f = interp1d(df[\"ts\"].values, df[cols].values,\n                     axis=0, kind=\"linear\",\n                     fill_value=\"extrapolate\",\n                     bounds_error=False,\n                     assume_sorted=True)\n        return f(t_new).astype(np.float32)\n\n    def sync(self, data):\n        acc, gyro, wp = data[\"acc\"], data[\"gyro\"], data[\"wp\"]\n        if len(acc) < 2 or len(gyro) < 2 or len(wp) < 2:\n            return None\n\n        t0 = max(acc[\"ts\"].min(), gyro[\"ts\"].min(), wp[\"ts\"].min())\n        t1 = min(acc[\"ts\"].max(), gyro[\"ts\"].max(), wp[\"ts\"].max())\n        if t1 <= t0:\n            return None\n\n        t_new = np.arange(t0, t1+1, self.dt).astype(np.int64)\n\n        A = self._interp(acc,  [\"ax\",\"ay\",\"az\"], t_new)\n        G = self._interp(gyro, [\"gx\",\"gy\",\"gz\"], t_new)\n        P = self._interp(wp,   [\"x\",\"y\"],       t_new)\n        if A is None or G is None or P is None:\n            return None\n\n        df = pd.DataFrame({\n            \"ts\": t_new,\n            \"ax\": A[:,0], \"ay\": A[:,1], \"az\": A[:,2],\n            \"gx\": G[:,0], \"gy\": G[:,1], \"gz\": G[:,2],\n            \"x\":  P[:,0], \"y\":  P[:,1],\n        }).dropna()\n\n        if len(df) < WINDOW + 5:\n            return None\n        return df\n\nsyncer = Synchronizer(freq_hz=FREQ_HZ)\nprint(\"syncer ready\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:21:34.629446Z","iopub.execute_input":"2026-01-10T05:21:34.629738Z","iopub.status.idle":"2026-01-10T05:21:34.651642Z","shell.execute_reply.started":"2026-01-10T05:21:34.629714Z","shell.execute_reply":"2026-01-10T05:21:34.650375Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def wifi_fingerprint_at_ts(wifi_df: pd.DataFrame, ts: int, win_ms: int):\n    if wifi_df.empty:\n        return {}\n    w = wifi_df[(wifi_df[\"ts\"] >= ts-win_ms) & (wifi_df[\"ts\"] <= ts+win_ms)]\n    if w.empty:\n        return {}\n    return w.groupby(\"bssid\")[\"rssi\"].mean().to_dict()\n\ndef build_knn_anchors(files, alpha=0.4, k=30):\n    site_rows = defaultdict(list)  # site -> (floor,x,y,fp_dict)\n\n    for i, fp in enumerate(files):\n        site, floor, _ = parse_train_path(fp)\n        d = read_txt(fp)\n        wp = d[\"wp\"]\n        wifi = d[\"wifi\"]\n        if wp.empty or wifi.empty:\n            continue\n\n        for _, r in wp.iterrows():\n            fpd = wifi_fingerprint_at_ts(wifi, int(r[\"ts\"]), WIFI_WIN_MS)\n            if not fpd:\n                continue\n            site_rows[site].append((floor, float(r[\"x\"]), float(r[\"y\"]), fpd))\n\n        if (i+1) % 500 == 0:\n            print(\"anchors processed\", i+1)\n\n    site_data = {}\n    for site, rows in site_rows.items():\n        bssid_set = set()\n        for _,_,_,fpd in rows:\n            bssid_set.update(fpd.keys())\n        b2i = {b:i for i,b in enumerate(sorted(bssid_set))}\n        n, m = len(rows), len(b2i)\n\n        indptr=[0]; idxs=[]; rssi_vals=[]; bin_vals=[]\n        y_floor=np.zeros(n,np.int32)\n        y_xy=np.zeros((n,2),np.float32)\n\n        for j,(floor,x,y,fpd) in enumerate(rows):\n            y_floor[j]=floor\n            y_xy[j]=[x,y]\n            for bssid,rssi in fpd.items():\n                ii=b2i.get(bssid)\n                if ii is None: \n                    continue\n                idxs.append(ii)\n                rssi_vals.append(float(np.clip(rssi,-100,-30)))\n                bin_vals.append(1.0)\n            indptr.append(len(idxs))\n\n        Xr = csr_matrix((np.array(rssi_vals,np.float32), np.array(idxs,np.int32), np.array(indptr,np.int32)), shape=(n,m))\n        Xb = csr_matrix((np.array(bin_vals,np.float32),  np.array(idxs,np.int32), np.array(indptr,np.int32)), shape=(n,m))\n\n        Xr = normalize(Xr, axis=1)\n        Xb = normalize(Xb, axis=1)\n        X  = hstack([Xb.multiply(alpha), Xr.multiply(1-alpha)]).tocsr()\n        X  = normalize(X, axis=1)\n\n        knn = NearestNeighbors(n_neighbors=min(k,n), metric=\"cosine\")\n        knn.fit(X)\n\n        site_data[site] = {\"b2i\": b2i, \"X\": X, \"knn\": knn, \"y_floor\": y_floor, \"y_xy\": y_xy}\n        print(f\"site={site} anchors={n} bssids={m}\")\n\n    return site_data\n\nanchor_knn = build_knn_anchors(tr_files, alpha=ALPHA_WIFI, k=KNN_K)\nprint(\"anchor sites:\", len(anchor_knn))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:21:34.652604Z","iopub.execute_input":"2026-01-10T05:21:34.652888Z","iopub.status.idle":"2026-01-10T05:24:25.650125Z","shell.execute_reply.started":"2026-01-10T05:21:34.652860Z","shell.execute_reply":"2026-01-10T05:24:25.648923Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def vectorize_fusion(fpd, b2i, alpha=0.4):\n    if not fpd:\n        return None\n    idxs=[]; rssi=[]; bins=[]\n    for bssid,val in fpd.items():\n        j=b2i.get(bssid)\n        if j is None: \n            continue\n        idxs.append(j)\n        rssi.append(float(np.clip(val,-100,-30)))\n        bins.append(1.0)\n    if not idxs:\n        return None\n    m=len(b2i)\n    indptr=np.array([0,len(idxs)],np.int32)\n    idxs=np.array(idxs,np.int32)\n    Xr=csr_matrix((np.array(rssi,np.float32), idxs, indptr), shape=(1,m))\n    Xb=csr_matrix((np.array(bins,np.float32), idxs, indptr), shape=(1,m))\n    Xr=normalize(Xr, axis=1)\n    Xb=normalize(Xb, axis=1)\n    X=hstack([Xb.multiply(alpha), Xr.multiply(1-alpha)]).tocsr()\n    X=normalize(X, axis=1)\n    return X\n\ndef knn_anchor_predict(site, wifi_df, ts, k=30, alpha=0.4):\n    d = anchor_knn.get(site)\n    if d is None:\n        return 0, 0.0, 0.0, 0.0, 1.0\n\n    fpd = wifi_fingerprint_at_ts(wifi_df, ts, WIFI_WIN_MS)\n    xvec = vectorize_fusion(fpd, d[\"b2i\"], alpha=alpha)\n    if xvec is None:\n        return 0, 0.0, 0.0, 0.0, 1.0\n\n    k = min(k, d[\"X\"].shape[0])\n    dist, idx = d[\"knn\"].kneighbors(xvec, n_neighbors=k, return_distance=True)\n    dist = dist[0]; idx = idx[0]\n    w = 1.0 / (dist + 1e-6)\n\n    floors = d[\"y_floor\"][idx]\n    xy = d[\"y_xy\"][idx]\n\n    # weighted floor\n    scores={}\n    for f,ww in zip(floors.tolist(), w.tolist()):\n        scores[f]=scores.get(f,0.0)+ww\n    floor_pred = int(max(scores.items(), key=lambda x:x[1])[0])\n\n    x_pred = float(np.sum(xy[:,0]*w)/np.sum(w))\n    y_pred = float(np.sum(xy[:,1]*w)/np.sum(w))\n\n    best = float(dist[0])\n    conf = float(np.exp(-(best*best)/(2*GATE_SIGMA*GATE_SIGMA)))\n    return floor_pred, x_pred, y_pred, conf, best\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:24:25.652733Z","iopub.execute_input":"2026-01-10T05:24:25.653264Z","iopub.status.idle":"2026-01-10T05:24:25.666407Z","shell.execute_reply.started":"2026-01-10T05:24:25.653239Z","shell.execute_reply":"2026-01-10T05:24:25.665320Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"FEATURES = [\"ax\",\"ay\",\"az\",\"gx\",\"gy\",\"gz\"]\n\ndef make_windows_odometry(df, window=100, stride=20):\n    arr = df[FEATURES + [\"x\",\"y\"]].values.astype(np.float32)\n    X_list=[]; y_list=[]\n    for st in range(0, len(df)-window, stride):\n        ed = st+window\n        chunk = arr[st:ed]\n        imu = chunk[:, :6]\n        x0,y0 = chunk[0,6], chunk[0,7]\n        x1,y1 = chunk[-1,6], chunk[-1,7]\n        X_list.append(imu)\n        y_list.append([x1-x0, y1-y0])\n    if not X_list:\n        return None, None\n    return np.stack(X_list), np.array(y_list, np.float32)\n\nclass IMUOdometryDataset(Dataset):\n    def __init__(self, files):\n        self.samples=[]\n        for fp in files:\n            d = read_txt(fp)\n            df = syncer.sync(d)  # FIX: syncer đã tạo\n            if df is None:\n                continue\n            X,y = make_windows_odometry(df, WINDOW, STRIDE)\n            if X is None:\n                continue\n            for i in range(len(X)):\n                self.samples.append((X[i], y[i]))\n        print(\"IMU samples:\", len(self.samples))\n\n    def __len__(self):\n        return len(self.samples)\n\n    def __getitem__(self, idx):\n        X,y = self.samples[idx]\n        return torch.from_numpy(X), torch.from_numpy(y)\n\nimu_train_ds = IMUOdometryDataset(tr_files[:800])   # tăng dần nếu muốn\nimu_val_ds   = IMUOdometryDataset(val_files[:200])\n\ntrain_loader = DataLoader(imu_train_ds, batch_size=BATCH, shuffle=True)\nval_loader   = DataLoader(imu_val_ds, batch_size=BATCH, shuffle=False)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:24:25.667376Z","iopub.execute_input":"2026-01-10T05:24:25.667708Z","iopub.status.idle":"2026-01-10T05:25:21.164963Z","shell.execute_reply.started":"2026-01-10T05:24:25.667685Z","shell.execute_reply":"2026-01-10T05:25:21.163965Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class GRUOdometry(nn.Module):\n    def __init__(self, in_dim=6, hid=128):\n        super().__init__()\n        self.gru = nn.GRU(input_size=in_dim, hidden_size=hid, batch_first=True)\n        self.head = nn.Sequential(\n            nn.Linear(hid, 128),\n            nn.ReLU(),\n            nn.Linear(128, 2)\n        )\n\n    def forward(self, x):\n        out, _ = self.gru(x)\n        feat = out[:, -1, :]\n        return self.head(feat)\n\nmodel = GRUOdometry().to(DEVICE)\nopt = torch.optim.Adam(model.parameters(), lr=LR)\nloss_fn = nn.MSELoss()\n\nmodel\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:25:21.165961Z","iopub.execute_input":"2026-01-10T05:25:21.166309Z","iopub.status.idle":"2026-01-10T05:25:24.830935Z","shell.execute_reply.started":"2026-01-10T05:25:21.166279Z","shell.execute_reply":"2026-01-10T05:25:24.830072Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_epoch(model, loader, train=True):\n    model.train(train)\n    total=0.0; n=0\n    for xb,yb in loader:\n        xb=xb.to(DEVICE); yb=yb.to(DEVICE)\n        if train:\n            opt.zero_grad(set_to_none=True)\n        pred = model(xb)\n        loss = loss_fn(pred, yb)\n        if train:\n            loss.backward()\n            opt.step()\n        total += loss.item()*xb.size(0)\n        n += xb.size(0)\n    return total/max(n,1)\n\ntrain_losses=[]; val_losses=[]\nfor ep in range(1, EPOCHS+1):\n    tr = run_epoch(model, train_loader, train=True)\n    va = run_epoch(model, val_loader, train=False)\n    train_losses.append(tr); val_losses.append(va)\n    print(f\"Epoch {ep:02d} | train_mse={tr:.6f} | val_mse={va:.6f}\")\n\nplt.figure()\nplt.plot(train_losses, marker=\"o\", label=\"train\")\nplt.plot(val_losses, marker=\"o\", label=\"val\")\nplt.title(\"GRU Odometry Loss (MSE)\")\nplt.xlabel(\"epoch\"); plt.ylabel(\"mse\")\nplt.legend(); plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:25:24.831828Z","iopub.execute_input":"2026-01-10T05:25:24.832281Z","iopub.status.idle":"2026-01-10T05:34:24.571972Z","shell.execute_reply.started":"2026-01-10T05:25:24.832256Z","shell.execute_reply":"2026-01-10T05:34:24.570989Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@torch.no_grad()\ndef eval_on_file(fp, max_points=300):\n    site, _, _ = parse_train_path(fp)\n    d = read_txt(fp)\n    df = syncer.sync(d)\n    if df is None:\n        return None\n\n    if len(df) > max_points:\n        df = df.iloc[:max_points].copy()\n\n    gt_xy = df[[\"x\",\"y\"]].values.astype(np.float32)\n    ts_arr = df[\"ts\"].values.astype(np.int64)\n\n    Xw, _ = make_windows_odometry(df, WINDOW, STRIDE)\n    if Xw is None:\n        return None\n    dxy = model(torch.from_numpy(Xw).to(DEVICE)).cpu().numpy()\n\n    starts = list(range(0, len(df)-WINDOW, STRIDE))\n    pred_gru=[]; pred_knn=[]; pred_fus=[]; gt_end=[]\n\n    wifi_df = d[\"wifi\"]\n\n    for i, st in enumerate(starts):\n        ed = st + WINDOW - 1\n        x0,y0 = gt_xy[st]\n        xg,yg = gt_xy[ed]\n        dx,dy = dxy[i]\n        x_gru, y_gru = x0+dx, y0+dy\n\n        fl_w, x_w, y_w, conf, best = knn_anchor_predict(site, wifi_df, int(ts_arr[ed]), k=KNN_K, alpha=ALPHA_WIFI)\n\n        a = conf\n        x_f = a*x_w + (1-a)*x_gru\n        y_f = a*y_w + (1-a)*y_gru\n\n        pred_gru.append([x_gru,y_gru])\n        pred_knn.append([x_w,y_w])\n        pred_fus.append([x_f,y_f])\n        gt_end.append([xg,yg])\n\n    pred_gru=np.array(pred_gru); pred_knn=np.array(pred_knn); pred_fus=np.array(pred_fus); gt_end=np.array(gt_end)\n\n    def rmse(a,b): return float(np.sqrt(np.mean((a-b)**2)))\n    return {\n        \"site\": site,\n        \"rmse_knn\": rmse(pred_knn, gt_end),\n        \"rmse_gru\": rmse(pred_gru, gt_end),\n        \"rmse_fus\": rmse(pred_fus, gt_end),\n        \"pred_knn\": pred_knn,\n        \"pred_gru\": pred_gru,\n        \"pred_fus\": pred_fus,\n        \"gt\": gt_end\n    }\n\nresults=[]\nfor fp in val_files[:20]:\n    out = eval_on_file(fp)\n    if out is not None:\n        results.append(out)\n\ndf_res = pd.DataFrame([{k:v for k,v in r.items() if not isinstance(v, np.ndarray)} for r in results])\nprint(df_res.describe())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:34:24.573076Z","iopub.execute_input":"2026-01-10T05:34:24.573479Z","iopub.status.idle":"2026-01-10T05:34:28.294799Z","shell.execute_reply.started":"2026-01-10T05:34:24.573455Z","shell.execute_reply":"2026-01-10T05:34:28.293943Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if len(results):\n    r = results[0]\n    gt = r[\"gt\"]\n    pk = r[\"pred_knn\"]\n    pg = r[\"pred_gru\"]\n    pf = r[\"pred_fus\"]\n\n    plt.figure()\n    plt.plot(gt[:,0], gt[:,1], marker=\"o\", label=\"GT\")\n    plt.plot(pk[:,0], pk[:,1], marker=\"x\", label=\"WiFi-kNN\")\n    plt.plot(pg[:,0], pg[:,1], marker=\"^\", label=\"GRU\")\n    plt.plot(pf[:,0], pf[:,1], marker=\"s\", label=\"Fusion\")\n    plt.title(\"Trajectory comparison (window endpoints)\")\n    plt.xlabel(\"x\"); plt.ylabel(\"y\")\n    plt.legend()\n    plt.show()\n\n    print(\"RMSE:\", {\"kNN\": r[\"rmse_knn\"], \"GRU\": r[\"rmse_gru\"], \"Fusion\": r[\"rmse_fus\"]})\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:34:28.295821Z","iopub.execute_input":"2026-01-10T05:34:28.296087Z","iopub.status.idle":"2026-01-10T05:34:28.518449Z","shell.execute_reply.started":"2026-01-10T05:34:28.296066Z","shell.execute_reply":"2026-01-10T05:34:28.517353Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %%\ndef wifi_fingerprint_window(wifi_df: pd.DataFrame, ts: int, win_ms: int):\n    if wifi_df.empty:\n        return {}\n    w = wifi_df[(wifi_df[\"ts\"] >= ts-win_ms) & (wifi_df[\"ts\"] <= ts+win_ms)]\n    if w.empty:\n        return {}\n    return w.groupby(\"bssid\")[\"rssi\"].mean().to_dict()\n\ndef wifi_fingerprint_nearest(wifi_df: pd.DataFrame, ts: int, k_rows=40):\n    \"\"\"Take k WiFi rows closest in time to ts.\"\"\"\n    if wifi_df.empty:\n        return {}\n    w = wifi_df.copy()\n    w[\"dt\"] = (w[\"ts\"] - ts).abs()\n    w = w.nsmallest(k_rows, \"dt\")\n    if w.empty:\n        return {}\n    return w.groupby(\"bssid\")[\"rssi\"].mean().to_dict()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:34:28.519433Z","iopub.execute_input":"2026-01-10T05:34:28.519728Z","iopub.status.idle":"2026-01-10T05:34:28.527956Z","shell.execute_reply.started":"2026-01-10T05:34:28.519706Z","shell.execute_reply":"2026-01-10T05:34:28.526897Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %%\ndef knn_anchor_predict_v2(site, wifi_df, ts, win_ms, k=30, alpha=0.4, sigma=0.25):\n    \"\"\"\n    Return: floor, x, y, conf, best_dist\n    - conf computed from best_dist (Gaussian)\n    - xy weighted by Gaussian(dist) for stability\n    \"\"\"\n    d = anchor_knn.get(site)\n    if d is None:\n        return 0, 0.0, 0.0, 0.0, 1.0\n\n    # 1) fingerprint by window\n    fpd = wifi_fingerprint_window(wifi_df, ts, win_ms)\n    xvec = vectorize_fusion(fpd, d[\"b2i\"], alpha=alpha)\n\n    # 2) if no overlap -> nearest-by-time fallback\n    if xvec is None:\n        fpd2 = wifi_fingerprint_nearest(wifi_df, ts, k_rows=40)\n        xvec = vectorize_fusion(fpd2, d[\"b2i\"], alpha=alpha)\n\n    # 3) final fallback\n    if xvec is None:\n        # site missing or no overlap\n        return 0, 0.0, 0.0, 0.0, 1.0\n\n    k = min(k, d[\"X\"].shape[0])\n    dist, idx = d[\"knn\"].kneighbors(xvec, n_neighbors=k, return_distance=True)\n    dist = dist[0]; idx = idx[0]\n\n    # Gaussian weights (more stable than inverse-dist)\n    w = np.exp(-(dist**2) / (2*sigma*sigma)) + 1e-9\n\n    floors = d[\"y_floor\"][idx]\n    xy = d[\"y_xy\"][idx]\n\n    # weighted floor vote\n    scores={}\n    for f, ww in zip(floors.tolist(), w.tolist()):\n        scores[f] = scores.get(f, 0.0) + ww\n    floor_pred = int(max(scores.items(), key=lambda x: x[1])[0])\n\n    # xy weighted mean\n    x_pred = float(np.sum(xy[:,0]*w) / np.sum(w))\n    y_pred = float(np.sum(xy[:,1]*w) / np.sum(w))\n\n    best = float(dist[0])\n    conf = float(np.exp(-(best*best) / (2*sigma*sigma)))\n    return floor_pred, x_pred, y_pred, conf, best\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:34:28.528840Z","iopub.execute_input":"2026-01-10T05:34:28.529140Z","iopub.status.idle":"2026-01-10T05:34:28.544045Z","shell.execute_reply.started":"2026-01-10T05:34:28.529117Z","shell.execute_reply":"2026-01-10T05:34:28.543008Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %%\ndef predict_ensemble(site, wifi_df, ts,\n                     windows=(1000, 2000, 4000),\n                     k=30, alpha=0.4, sigma=0.25):\n    \"\"\"\n    Try multiple time windows; pick prediction with highest confidence.\n    \"\"\"\n    best = None\n    for w in windows:\n        fl, x, y, conf, dist = knn_anchor_predict_v2(site, wifi_df, ts, win_ms=w, k=k, alpha=alpha, sigma=sigma)\n        cand = (conf, fl, x, y, w, dist)\n        if best is None or cand[0] > best[0]:\n            best = cand\n    # unpack\n    conf, fl, x, y, w, dist = best\n    return fl, x, y, conf, w, dist\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:34:28.545098Z","iopub.execute_input":"2026-01-10T05:34:28.545450Z","iopub.status.idle":"2026-01-10T05:34:28.571636Z","shell.execute_reply.started":"2026-01-10T05:34:28.545421Z","shell.execute_reply":"2026-01-10T05:34:28.570418Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %%\nsample_sub = pd.read_csv(SAMPLE_SUB)\nid_col = \"site_path_timestamp\" if \"site_path_timestamp\" in sample_sub.columns else sample_sub.columns[0]\n\ndef parse_site_path_timestamp(s: str):\n    a, ts = s.rsplit(\"_\", 1)\n    site, path = a.split(\"_\", 1)\n    return site, path, int(ts)\n\nreq = sample_sub[id_col].apply(parse_site_path_timestamp)\nsample_sub[\"site\"] = req.apply(lambda t: t[0])\nsample_sub[\"path\"] = req.apply(lambda t: t[1])\nsample_sub[\"ts\"]   = req.apply(lambda t: t[2])\n\n# Map test files by PATH only\ntest_fp_by_path = {}\nfor root, _, files in os.walk(TEST_ROOT):\n    for fn in files:\n        if fn.endswith(\".txt\"):\n            fp = os.path.join(root, fn)\n            path = os.path.basename(fp).replace(\".txt\",\"\")\n            test_fp_by_path[path] = fp\n\nmissing = (sample_sub[\"path\"].map(lambda p: p not in test_fp_by_path)).sum()\nprint(f\"Missing rows by PATH: {missing}/{len(sample_sub)} = {missing/len(sample_sub)*100:.2f}%\")\n\n# Hyperparams to try (start with these)\nENSEMBLE_WINS = (1000, 2000, 4000)\nSIGMA = 0.25\nK = KNN_K          # use your KNN_K\nALPHA = ALPHA_WIFI # use your ALPHA_WIFI\n\npred_floor, pred_x, pred_y = [], [], []\nconf_list, win_used = [], []\nwifi_cache = {}\n\nfor i, row in sample_sub.iterrows():\n    site, path, ts = row[\"site\"], row[\"path\"], int(row[\"ts\"])\n    fp = test_fp_by_path.get(path)\n    if fp is None:\n        pred_floor.append(0); pred_x.append(0.0); pred_y.append(0.0)\n        conf_list.append(0.0); win_used.append(-1)\n        continue\n\n    if fp not in wifi_cache:\n        d = read_txt(fp)\n        wifi_cache[fp] = d[\"wifi\"]\n    wifi_df = wifi_cache[fp]\n\n    fl, x, y, conf, w, dist = predict_ensemble(site, wifi_df, ts, windows=ENSEMBLE_WINS, k=K, alpha=ALPHA, sigma=SIGMA)\n    pred_floor.append(int(fl)); pred_x.append(float(x)); pred_y.append(float(y))\n    conf_list.append(float(conf)); win_used.append(int(w))\n\n    if (i+1) % 5000 == 0:\n        print(f\"pred {i+1}/{len(sample_sub)}\")\n\nsubmission = pd.DataFrame({id_col: sample_sub[id_col].values, \"floor\": pred_floor, \"x\": pred_x, \"y\": pred_y})\nsubmission.to_csv(\"submission.csv\", index=False)\nprint(\"Saved submission.csv | rows:\", len(submission))\nsubmission.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:34:28.572991Z","iopub.execute_input":"2026-01-10T05:34:28.573306Z","iopub.status.idle":"2026-01-10T05:36:54.030813Z","shell.execute_reply.started":"2026-01-10T05:34:28.573281Z","shell.execute_reply":"2026-01-10T05:36:54.029711Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %%\nplt.figure()\nplt.hist(pred_floor, bins=30)\nplt.title(\"Predicted floor distribution\")\nplt.xlabel(\"floor\"); plt.ylabel(\"count\")\nplt.show()\n\nplt.figure()\nplt.hist(conf_list, bins=40)\nplt.title(\"Confidence distribution\")\nplt.xlabel(\"conf\"); plt.ylabel(\"count\")\nplt.show()\n\nplt.figure()\nplt.hist(win_used, bins=10)\nplt.title(\"Chosen window (ms) distribution\")\nplt.xlabel(\"win\"); plt.ylabel(\"count\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T05:36:54.031674Z","iopub.execute_input":"2026-01-10T05:36:54.032076Z","iopub.status.idle":"2026-01-10T05:36:54.665405Z","shell.execute_reply.started":"2026-01-10T05:36:54.032052Z","shell.execute_reply":"2026-01-10T05:36:54.664597Z"}},"outputs":[],"execution_count":null}]}