{"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":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":4294,"databundleVersionId":33829,"isSourceIdPinned":false},{"sourceType":"datasetVersion","sourceId":15624990,"datasetId":10000332,"databundleVersionId":16559525}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import roc_auc_score\nimport joblib\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# Reproducibility\nSEED = 42\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\n\n# Device\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'Using device: {device}')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-04-30T13:13:31.973195Z","iopub.execute_input":"2026-04-30T13:13:31.973433Z","iopub.status.idle":"2026-04-30T13:13:42.928262Z","shell.execute_reply.started":"2026-04-30T13:13:31.973409Z","shell.execute_reply":"2026-04-30T13:13:42.927537Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load the files - data is already mounted on Kaggle\nbids = pd.read_csv('/kaggle/input/datasets/ragavendransara/facebookbot/bids.csv')\ntrain = pd.read_csv('/kaggle/input/datasets/ragavendransara/facebookbot/train.csv')\ntest = pd.read_csv('/kaggle/input/datasets/ragavendransara/facebookbot/test.csv')\n\nprint('bids shape:', bids.shape)\nprint('train shape:', train.shape)\nprint('test shape:', test.shape)\nprint()\nprint(bids.head())\nprint()\nprint(train.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T13:13:42.930187Z","iopub.execute_input":"2026-04-30T13:13:42.930597Z","iopub.status.idle":"2026-04-30T13:14:01.307755Z","shell.execute_reply.started":"2026-04-30T13:13:42.930571Z","shell.execute_reply":"2026-04-30T13:14:01.306980Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Get all bidder IDs we care about\ntrain_bidders = set(train['bidder_id'].unique())\ntest_bidders = set(test['bidder_id'].unique())\n\n# Filter bids to only bidders we need\nbids_train = bids[bids['bidder_id'].isin(train_bidders)].copy()\nbids_test = bids[bids['bidder_id'].isin(test_bidders)].copy()\n\nprint('bids for train bidders:', bids_train.shape)\nprint('bids for test bidders:', bids_test.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T13:14:01.308793Z","iopub.execute_input":"2026-04-30T13:14:01.309142Z","iopub.status.idle":"2026-04-30T13:14:03.900156Z","shell.execute_reply.started":"2026-04-30T13:14:01.309112Z","shell.execute_reply":"2026-04-30T13:14:03.899222Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_aggregate_features(bids_df):\n    features = []\n    \n    for bidder_id, group in bids_df.groupby('bidder_id'):\n        # Sort by time\n        group = group.sort_values('time')\n        times = group['time'].values\n        \n        # Inter-bid time deltas\n        deltas = np.diff(times)\n        \n        if len(deltas) > 0:\n            mean_delta = np.mean(deltas)\n            std_delta = np.std(deltas)\n            min_delta = np.min(deltas)\n            frac_zero = np.mean(deltas == 0)\n        else:\n            mean_delta = 0\n            std_delta = 0\n            min_delta = 0\n            frac_zero = 0\n        \n        # Unique counts\n        n_unique_ips = group['ip'].nunique()\n        n_unique_countries = group['country'].nunique()\n        n_unique_devices = group['device'].nunique()\n        n_unique_auctions = group['auction'].nunique()\n        \n        # IP entropy\n        ip_counts = group['ip'].value_counts(normalize=True)\n        ip_entropy = -np.sum(ip_counts * np.log2(ip_counts + 1e-9))\n        \n        # Country entropy\n        country_counts = group['country'].value_counts(normalize=True)\n        country_entropy = -np.sum(country_counts * np.log2(country_counts + 1e-9))\n        \n        # Bids per auction\n        bids_per_auction = group.groupby('auction').size()\n        mean_bpa = bids_per_auction.mean()\n        std_bpa = bids_per_auction.std() if len(bids_per_auction) > 1 else 0\n        \n        # Total bids\n        total_bids = np.log1p(len(group))\n        \n        features.append({\n            'bidder_id': bidder_id,\n            'mean_delta': mean_delta,\n            'std_delta': std_delta,\n            'min_delta': min_delta,\n            'frac_zero': frac_zero,\n            'n_unique_ips': n_unique_ips,\n            'n_unique_countries': n_unique_countries,\n            'n_unique_devices': n_unique_devices,\n            'n_unique_auctions': n_unique_auctions,\n            'ip_entropy': ip_entropy,\n            'country_entropy': country_entropy,\n            'mean_bpa': mean_bpa,\n            'std_bpa': std_bpa,\n            'total_bids': total_bids\n        })\n    \n    return pd.DataFrame(features)\n\nprint('Building train aggregate features...')\ntrain_agg = build_aggregate_features(bids_train)\nprint('Building test aggregate features...')\ntest_agg = build_aggregate_features(bids_test)\n\nprint('Done')\nprint(train_agg.shape)\nprint(train_agg.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T13:14:03.901280Z","iopub.execute_input":"2026-04-30T13:14:03.901924Z","iopub.status.idle":"2026-04-30T13:14:34.101813Z","shell.execute_reply.started":"2026-04-30T13:14:03.901891Z","shell.execute_reply":"2026-04-30T13:14:34.101035Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# First encode categorical columns\nle_country = LabelEncoder()\nle_device = LabelEncoder()\nle_url = LabelEncoder()\n\n# Fit on all bids so both train and test have consistent encoding\nall_bids = pd.concat([bids_train, bids_test])\nle_country.fit(all_bids['country'].astype(str))\nle_device.fit(all_bids['device'].astype(str))\nle_url.fit(all_bids['url'].astype(str))\n\nbids_train['country_enc'] = le_country.transform(bids_train['country'].astype(str))\nbids_train['device_enc'] = le_device.transform(bids_train['device'].astype(str))\nbids_train['url_enc'] = le_url.transform(bids_train['url'].astype(str))\n\nbids_test['country_enc'] = le_country.transform(bids_test['country'].astype(str))\nbids_test['device_enc'] = le_device.transform(bids_test['device'].astype(str))\nbids_test['url_enc'] = le_url.transform(bids_test['url'].astype(str))\n\nprint('Encoding done')\nprint('Unique countries:', bids_train['country'].nunique())\nprint('Unique devices:', bids_train['device'].nunique())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T13:14:34.102827Z","iopub.execute_input":"2026-04-30T13:14:34.103183Z","iopub.status.idle":"2026-04-30T13:14:51.291919Z","shell.execute_reply.started":"2026-04-30T13:14:34.103156Z","shell.execute_reply":"2026-04-30T13:14:51.290978Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"MAX_SEQ_LEN = 50  \n# 50 bids per bidder, truncate if more, pad if less\n\ndef build_sequences(bids_df):\n    sequences = {}\n    seq_lengths = {}\n    \n    for bidder_id, group in bids_df.groupby('bidder_id'):\n        group = group.sort_values('time')\n        times = group['time'].values\n        \n        # Time deltas - normalize to 0-1 range\n        deltas = np.diff(times, prepend=times[0])\n        max_delta = deltas.max()\n        if max_delta > 0:\n            deltas = deltas / max_delta\n        \n        country = group['country_enc'].values\n        device = group['device_enc'].values\n        url = group['url_enc'].values\n        \n        # IP change flag\n        ips = group['ip'].values\n        ip_changed = np.array([0] + [1 if ips[i] != ips[i-1] else 0 \n                                      for i in range(1, len(ips))], dtype=float)\n        \n        # Stack into (T, 5) matrix\n        seq = np.stack([deltas, country, device, url, ip_changed], axis=1).astype(np.float32)\n        \n        # Truncate or pad to MAX_SEQ_LEN\n        if len(seq) >= MAX_SEQ_LEN:\n            seq = seq[:MAX_SEQ_LEN]\n        else:\n            pad = np.zeros((MAX_SEQ_LEN - len(seq), seq.shape[1]), dtype=np.float32)\n            seq = np.vstack([seq, pad])\n            \n        seq_lengths[bidder_id] = min(len(group), MAX_SEQ_LEN)\n        sequences[bidder_id] = seq\n    \n    return sequences, seq_lengths\n\nprint('Building train sequences...')\ntrain_seqs, train_lengths = build_sequences(bids_train)\nprint('Building test sequences...')\ntest_seqs, test_lengths = build_sequences(bids_test)\n\nprint('Done')\nprint('Sample sequence shape:', list(train_seqs.values())[0].shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T13:14:51.292897Z","iopub.execute_input":"2026-04-30T13:14:51.293234Z","iopub.status.idle":"2026-04-30T13:15:05.131109Z","shell.execute_reply.started":"2026-04-30T13:14:51.293208Z","shell.execute_reply":"2026-04-30T13:15:05.130362Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class Attention(nn.Module):\n    def __init__(self, hidden_dim):\n        super(Attention, self).__init__()\n        self.attn = nn.Linear(hidden_dim * 2, 1)\n    \n    def forward(self, lstm_out, lengths):\n        attn_scores = self.attn(lstm_out).squeeze(-1)  # (batch, seq_len)\n        mask = torch.arange(attn_scores.size(1), device=lstm_out.device).unsqueeze(0) >= lengths.unsqueeze(1)\n        attn_scores = attn_scores.masked_fill(mask, float('-inf'))\n        attn_weights = torch.softmax(attn_scores, dim=1).unsqueeze(-1)\n        context = (attn_weights * lstm_out).sum(dim=1)\n        return context, attn_weights\n\n\nclass BiLSTMClassifier(nn.Module):\n    def __init__(self, input_dim, hidden_dim, num_layers, agg_feature_dim, dropout, use_attention=True):\n        super(BiLSTMClassifier, self).__init__()\n        self.use_attention = use_attention\n        \n        self.lstm = nn.LSTM(\n            input_size=input_dim,\n            hidden_size=hidden_dim,\n            num_layers=num_layers,\n            batch_first=True,\n            bidirectional=True,\n            dropout=dropout if num_layers > 1 else 0\n        )\n        \n        self.attention = Attention(hidden_dim)\n        self.dropout = nn.Dropout(dropout)\n        \n        # After attention: hidden*2 (bidirectional)\n        # After concat with agg features: hidden*2 + agg_feature_dim\n        self.fc1 = nn.Linear(hidden_dim * 2 + agg_feature_dim, 64)\n        self.fc2 = nn.Linear(64, 32)\n        self.fc3 = nn.Linear(32, 1)\n        self.relu = nn.ReLU()\n    \n    def forward(self, seq, agg, lengths):\n        # seq shape: (batch, seq_len, input_dim)\n        lstm_out, _ = self.lstm(seq)\n        \n        # Attention pooling\n        if self.use_attention:\n            context, attn_weights = self.attention(lstm_out, lengths)\n        else:\n            context = lstm_out[torch.arange(lstm_out.size(0)), lengths - 1]\n            attn_weights = None\n        # Concatenate with aggregate features\n        combined = torch.cat([context, agg], dim=1)\n        \n        # Dense head\n        out = self.relu(self.fc1(combined))\n        out = self.dropout(out)\n        out = self.relu(self.fc2(out))\n        out = self.fc3(out)\n        \n        return out, attn_weights\n\n\n# Model hyperparameters\nINPUT_DIM = 5        # features per bid timestep\nHIDDEN_DIM = 64      # LSTM hidden size\nNUM_LAYERS = 2       # LSTM layers\nAGG_DIM = 13         # number of aggregate features (excluding bidder_id)\nDROPOUT = 0.4\n\nmodel = BiLSTMClassifier(INPUT_DIM, HIDDEN_DIM, NUM_LAYERS, AGG_DIM, DROPOUT).to(device)\nprint(model)\nprint()\nprint('Total parameters:', sum(p.numel() for p in model.parameters()))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T13:15:05.133085Z","iopub.execute_input":"2026-04-30T13:15:05.133834Z","iopub.status.idle":"2026-04-30T13:15:05.671437Z","shell.execute_reply.started":"2026-04-30T13:15:05.133797Z","shell.execute_reply":"2026-04-30T13:15:05.670721Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.utils.data import Dataset, DataLoader\n\nclass BidderDataset(Dataset):\n    def __init__(self, bidder_ids, sequences, seq_lengths, agg_features, labels=None):\n        self.bidder_ids = bidder_ids\n        self.sequences = sequences\n        self.agg_features = agg_features\n        self.labels = labels\n        self.seq_lengths = seq_lengths\n    \n    def __len__(self):\n        return len(self.bidder_ids)\n    \n    def __getitem__(self, idx):\n        bidder_id = self.bidder_ids[idx]\n        seq = torch.tensor(self.sequences[bidder_id], dtype=torch.float32)\n        agg = torch.tensor(self.agg_features[idx], dtype=torch.float32)\n        length = torch.tensor(self.seq_lengths[bidder_id], dtype=torch.long)\n        \n        if self.labels is not None:\n            label = torch.tensor(self.labels[idx], dtype=torch.float32)\n            return seq, length, agg, label\n        return seq, length, agg\n\n\n# Merge aggregate features with train labels\ntrain_df = train.merge(train_agg, on='bidder_id', how='inner')\n\n# Drop non-feature columns\nagg_cols = [c for c in train_agg.columns if c != 'bidder_id']\n\n# Normalize aggregate features\nfrom sklearn.preprocessing import StandardScaler\nscaler = StandardScaler()\n\ntrain_agg_vals = scaler.fit_transform(train_df[agg_cols].values)\nlabels = train_df['outcome'].values.astype(np.float32)\nbidder_ids_train = train_df['bidder_id'].values\n\n# Train/validation split\nfrom sklearn.model_selection import train_test_split\n(train_ids, val_ids, \n train_agg_data, val_agg_data,\n train_labels, val_labels) = train_test_split(\n    bidder_ids_train, train_agg_vals, labels,\n    test_size=0.2, random_state=SEED, stratify=labels\n)\n\nval_seqs = train_seqs  # val bidders are a subset of train bidders, same dict is fine\n# Create datasets\ntrain_dataset = BidderDataset(train_ids, train_seqs, train_lengths, train_agg_data, train_labels)\nval_dataset = BidderDataset(val_ids, train_seqs, train_lengths, val_agg_data, val_labels)\n\n# Dataloaders\ntrain_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)\nval_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)\n\nprint('Train size:', len(train_dataset))\nprint('Val size:', len(val_dataset))\nprint('Bot ratio in train:', train_labels.mean().round(3))\nprint('Bot ratio in val:', val_labels.mean().round(3))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T13:15:54.859460Z","iopub.execute_input":"2026-04-30T13:15:54.859741Z","iopub.status.idle":"2026-04-30T13:15:54.880228Z","shell.execute_reply.started":"2026-04-30T13:15:54.859720Z","shell.execute_reply":"2026-04-30T13:15:54.879508Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Calculate pos_weight to handle class imbalance\n# If 5% are bots, pos_weight = 95/5 = ~18\nn_neg = (train_labels == 0).sum()\nn_pos = (train_labels == 1).sum()\npos_weight = torch.tensor([n_neg / n_pos], dtype=torch.float32).to(device)\nprint(f'pos_weight: {pos_weight.item():.2f}')\n\n# Loss and optimizer\ncriterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\noptimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5, factor=0.5)\n\n# Training loop\nEPOCHS = 50\nbest_val_auc = 0\nbest_model_state = None\npatience_counter = 0\nEARLY_STOP_PATIENCE = 10\n\nfor epoch in range(EPOCHS):\n    # Training phase\n    model.train()\n    train_losses = []\n    \n    for seq, length, agg, labels_batch in train_loader:\n        seq = seq.to(device)\n        agg = agg.to(device)\n        labels_batch = labels_batch.to(device)\n        length = length.to(device)        \n        \n        optimizer.zero_grad()\n        output, _ = model(seq, agg, length)\n        loss = criterion(output.squeeze(), labels_batch)\n        loss.backward()\n        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n        optimizer.step()\n        train_losses.append(loss.item())\n    \n    # Validation phase\n    model.eval()\n    val_preds = []\n    val_true = []\n    \n    with torch.no_grad():\n        for seq,length, agg, labels_batch in val_loader:\n            seq = seq.to(device)\n            agg = agg.to(device)\n            length = length.to(device)\n            output, _ = model(seq, agg, length)\n            preds = torch.sigmoid(output.squeeze()).cpu().numpy()\n            val_preds.extend(preds)\n            val_true.extend(labels_batch.numpy())\n    \n    val_auc = roc_auc_score(val_true, val_preds)\n    avg_train_loss = np.mean(train_losses)\n    scheduler.step(1 - val_auc)\n    \n    \n    print(f'Epoch {epoch+1}/{EPOCHS} | Loss: {avg_train_loss:.4f} | Val AUC: {val_auc:.4f}')\n    \n    # Save best model\n    if val_auc > best_val_auc:\n        best_val_auc = val_auc\n        best_model_state = model.state_dict().copy()\n        patience_counter = 0\n    else:\n        patience_counter += 1\n    \n    if patience_counter >= EARLY_STOP_PATIENCE:\n        print(f'Early stopping at epoch {epoch+1}')\n        break\n\nprint(f'\\nBest Val AUC: {best_val_auc:.4f}')\njoblib.dump(scaler, 'scaler.pkl')\ntorch.save(best_model_state, 'best_model.pth')\nprint('Scaler and model saved.')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T13:15:58.682571Z","iopub.execute_input":"2026-04-30T13:15:58.683278Z","iopub.status.idle":"2026-04-30T13:16:12.444963Z","shell.execute_reply.started":"2026-04-30T13:15:58.683248Z","shell.execute_reply":"2026-04-30T13:16:12.444352Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load best model\nmodel.load_state_dict(best_model_state)\n\n# Prepare test aggregate features\ntest_df = test.merge(test_agg, on='bidder_id', how='inner')\ntest_agg_vals = scaler.transform(test_df[agg_cols].values)  # use same scaler as train\ntest_bidder_ids = test_df['bidder_id'].values\n\n# Create test dataset\ntest_dataset = BidderDataset(test_bidder_ids, test_seqs, test_lengths, test_agg_vals, labels=None)\ntest_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)\n\n# Generate predictions\nmodel.eval()\ntest_preds = []\n\nwith torch.no_grad():\n    for seq, length, agg in test_loader:\n        seq = seq.to(device)\n        agg = agg.to(device)\n        length = length.to(device)\n        output, _ = model(seq, agg, length)\n        preds = torch.sigmoid(output.squeeze()).cpu().numpy()\n        test_preds.extend(preds)\n\n# Build submission\nsubmission = pd.DataFrame({\n    'bidder_id': test_bidder_ids,\n    'prediction': test_preds\n})\n\n# Make sure all test bidders are included, fill missing with 0\nsubmission_full = test[['bidder_id']].merge(submission, on='bidder_id', how='left')\nsubmission_full['prediction'] = submission_full['prediction'].fillna(0)\n\nsubmission_full.to_csv('submission.csv', index=False)\nprint('Submission saved')\nprint(submission_full.shape)\nprint(submission_full['prediction'].describe())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-30T13:16:12.446045Z","iopub.execute_input":"2026-04-30T13:16:12.446515Z","iopub.status.idle":"2026-04-30T13:16:12.702604Z","shell.execute_reply.started":"2026-04-30T13:16:12.446489Z","shell.execute_reply":"2026-04-30T13:16:12.701676Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}