{
  "id": 336090,
  "title": "2nd place solution - colum2131 Part",
  "content": "<p>Thank you very much for organizing this interesting and exciting competition, and thank you for teaming up with me, <a href=\"https://www.kaggle.com/no9more9ria10\" target=\"_blank\">@no9more9ria10</a>, <a href=\"https://www.kaggle.com/tomyanabe\" target=\"_blank\">@tomyanabe</a>, <a href=\"https://www.kaggle.com/tkm2261\" target=\"_blank\">@tkm2261</a>, <a href=\"https://www.kaggle.com/yukia18\" target=\"_blank\">@yukia18</a> .</p>\n<p>This post is part of the 2nd place solution in LB 0.949 (LB 0.971 with leak).</p>\n<p><a href=\"https://www.kaggle.com/competitions/foursquare-location-matching/discussion/336062\" target=\"_blank\">Brief Summary</a></p>\n<p>Other part</p>\n<ul>\n<li><a href=\"https://www.kaggle.com/competitions/foursquare-location-matching/discussion/336072\" target=\"_blank\">Candidate Generation</a></li>\n</ul>\n<h1>Stack-part 1: Binary classification of matches</h1>\n<p>Using the candidate matches created in the previous stage, we ensemble three models: GBDT (XGBoost, lightGBM), BERT(xlm-roberta-base).<br>\nFor this id-pair (e.g., src_id, dst_id), GBDT mainly used the edit distance feature, while BERT used text created in particular columns. These models classified whether the pair matched or not.</p>\n<h2>GBDT</h2>\n<h3>Features</h3>\n<p>XGBoost features were created by colum2131 ( <a href=\"https://www.kaggle.com/columbia2131\" target=\"_blank\">@columbia2131</a> ), and lightGBM features by ria ( <a href=\"https://www.kaggle.com/no9more9ria10\" target=\"_blank\">@no9more9ria10</a> ).<br>\nAlthough there are some differences in each feature, the following XGBoost’s features were below:</p>\n<ul>\n<li>Features generated independently with src_id and dst_id<ul>\n<li>latitude / longitude</li>\n<li>country with ordinal-encoding</li>\n<li>categories with ordinal-encoding</li></ul></li>\n<li>Features generated dependently with src_id and dst_id<ul>\n<li>Edit distance (name, categories, address, city, state, zip, url, phone)<ul>\n<li>gesh</li>\n<li>leven</li>\n<li>jaro</li>\n<li>simple_ratio (RapidFuzz)</li>\n<li>partial_ratio (RapidFuzz)</li>\n<li>token_set_ratio (RapidFuzz)</li>\n<li>token_sort_ratio (RapidFuzz)</li>\n<li>token_ratio (RapidFuzz)</li>\n<li>partial_token_ratio (RapidFuzz)</li>\n<li>wratio (RapidFuzz)</li>\n<li>qratio (RapidFuzz)</li></ul></li>\n<li>Set similarity with .split(“ ” or “, “) (name, cateogires)<ul>\n<li>jaccard</li>\n<li>dice</li>\n<li>sympson</li></ul></li>\n<li>Cosine similarity of vectors encoded by tf-idf (name, categories, address, all)<ul>\n<li>tfidf(ngram_range=(1, 1), analyzer=‘word’)</li>\n<li>tfidf(ngram_range=(1, 3), analyzer=‘char’)</li></ul></li></ul></li>\n<li>Others<ul>\n<li>probability by transformer</li></ul></li>\n</ul>\n<h4>Modeling</h4>\n<p>The cross-validation of the train data was purposely split so that leakage would occur.<br>\nSpecifically, instead of the folds with GroupKFold(n_splits=2, group=‘point_of_interest’), we created new folds with GroupKFold(n_splits=5, group=‘src_id’).<br>\nThis leakage improved the CV and Public LB because train and test data contained common records.<br>\nWe suspect that the latitude and longitude variables and the ordinal categories worked effectively as features to represent the common id’s records.</p>\n<h2>BERT</h2>\n<h3>Text &amp; Numerical-Features</h3>\n<p>The text was defined as each lowercased name and cateogires connected by a SEP-token and country.<br>\nAdditionally, the numerical feature was logarithmic variables of haversine distance.</p>\n<pre><code>def get_basearray(cfg, df):\n    cfg.idx2id = df['id'].fillna('').to_numpy()\n    cfg.id2idx = {c: i for i, c in enumerate(cfg.idx2id)}\n\n    cfg.idx2name = df['name'].fillna('').str.lower().to_numpy()\n    cfg.idx2categories = df['categories'].fillna('').str.lower().to_numpy()\n    cfg.idx2address = df['address'].fillna('').str.lower().to_numpy()    \n    return cfg\n\ndef get_text(cfg, i, j): # {i: index of src_id, j: index of dst_id}\n    text = cfg.idx2country[i]+cfg.sep+\\\n            'name1 '+cfg.idx2name[i]+cfg.sep+'category1 '+cfg.idx2categories[i]+\\\n            cfg.sep + cfg.sep +\\\n            'name2 '+cfg.idx2name[j]+cfg.sep+'category2 '+cfg.idx2categories[j]\n    return text\n</code></pre>\n<h3>Model Training</h3>\n<p>The architecture of BERT is below:</p>\n<pre><code>class CustomModel(nn.Module):\n    def __init__(self, cfg, criterion):\n        super().__init__()\n        self.cfg = cfg\n        self.criterion = criterion\n        self.config = AutoConfig.from_pretrained(\n            cfg.MODEL_PATH,\n            output_hidden_states=True\n        )\n        self.config.attention_probs_dropout_prob = 0.0\n        self.config.hidden_dropout_prob = 0.0\n        self.backbone = AutoModel.from_pretrained(\n            cfg.MODEL_PATH, # xlm-roberta-base\n            config=self.config\n        )\n        self.linear1 = nn.Sequential(\n            nn.Linear(self.config.hidden_size+len(cfg.num_cols), 1024),\n            nn.SELU(),\n            nn.Linear(1024, 1)\n        )\n\n    def forward(self, inputs, labels=None):\n        outputs = self.backbone(\n            inputs['input_ids'],\n            attention_mask=inputs['attention_mask']\n        )[\"last_hidden_state\"]\n        outputs = outputs[:, 0, :]\n        outputs = torch.cat((\n            outputs,\n            inputs['num_features']\n        ), dim=1)\n        logits = self.linear1(outputs).flatten()\n        loss = self.criterion(logits, labels)\n        return logits, loss\n</code></pre>\n<p>This model was trained for 5 epochs on all train data.</p>\n<h1>Leak Post Process</h1>\n<p>As mentioned in other discussions, there was a duplication of train data in the test data.<br>\nTherefore, the some test data was merged with the train data, and matches were created with the same poi. The prediction of LB0.949 was overwritten.<br>\nWe processed this matches overwriting the matches of LB0.949.<br>\nNote that train.csv is replaced on submit, so we created a separate train.csv dataset.</p>\n<pre><code>import cudf\ntrain = cudf.read_feather(‘../input/4sq-train-data-copy-feather/train.ftr’).fillna(‘[NAN]‘)\ntest = cudf.read_csv(‘../input/foursquare-location-matching/test.csv’, dtype=str).fillna(‘[NAN]’)\ncols = [‘name’, ‘latitude’, ‘longitude’]\ntest = test.merge(train[cols+[‘point_of_interest’]], on=cols, how=‘left’)\ntest[[‘id’, ‘point_of_interest’]].to_pandas().to_csv(‘id2poi.csv’, index=False)\n\nid2poi = pd.read_csv('id2poi.csv')\n\nmatch_dict = {}\nfor poi, group_df in id2poi.groupby('point_of_interest'):\n    ids = set(group_df['id'].tolist())\n    for id_str in ids:\n        match_dict[id_str] = ids\n\nsub = pd.DataFrame({\n    ‘id’: sub_dict.keys(),\n    ‘matches’: [\n        match_dict.get(key, value)\n        for key, value in sub_dict.items()\n    ]\n})\nsub[‘matches’] = sub[‘matches’].map(lambda x: ' ’.join(x))\nhttp://sub.to_csv(‘submission.csv’, index=False)\n</code></pre>",
  "comments": [
    {
      "id": 1849944,
      "author_name": "The Devastator",
      "author_url": "",
      "post_date": "2022-07-10T00:28:29.110000",
      "content": "<p>Thanks for sharing your insights on this competition!<br>\nGreat job on your success in this competition!</p>",
      "votes": 1,
      "replies": []
    },
    {
      "id": 1849269,
      "author_name": "nadare",
      "author_url": "",
      "post_date": "2022-07-09T11:01:48.137000",
      "content": "<p>I couldn't find the RapidFuzz library during the competition. <br>\nThank you for sharing the solution and congrats for your 2nd place in this competition.</p>",
      "votes": 1,
      "replies": []
    }
  ],
  "comment_count": 2,
  "messages": [
    {
      "id": 1849227,
      "postDate": "2022-07-09T10:15:20.463Z",
      "content": "<p>Thank you very much for organizing this interesting and exciting competition, and thank you for teaming up with me, <a href=\"https://www.kaggle.com/no9more9ria10\" target=\"_blank\">@no9more9ria10</a>, <a href=\"https://www.kaggle.com/tomyanabe\" target=\"_blank\">@tomyanabe</a>, <a href=\"https://www.kaggle.com/tkm2261\" target=\"_blank\">@tkm2261</a>, <a href=\"https://www.kaggle.com/yukia18\" target=\"_blank\">@yukia18</a> .</p>\n<p>This post is part of the 2nd place solution in LB 0.949 (LB 0.971 with leak).</p>\n<p><a href=\"https://www.kaggle.com/competitions/foursquare-location-matching/discussion/336062\" target=\"_blank\">Brief Summary</a></p>\n<p>Other part</p>\n<ul>\n<li><a href=\"https://www.kaggle.com/competitions/foursquare-location-matching/discussion/336072\" target=\"_blank\">Candidate Generation</a></li>\n</ul>\n<h1>Stack-part 1: Binary classification of matches</h1>\n<p>Using the candidate matches created in the previous stage, we ensemble three models: GBDT (XGBoost, lightGBM), BERT(xlm-roberta-base).<br>\nFor this id-pair (e.g., src_id, dst_id), GBDT mainly used the edit distance feature, while BERT used text created in particular columns. These models classified whether the pair matched or not.</p>\n<h2>GBDT</h2>\n<h3>Features</h3>\n<p>XGBoost features were created by colum2131 ( <a href=\"https://www.kaggle.com/columbia2131\" target=\"_blank\">@columbia2131</a> ), and lightGBM features by ria ( <a href=\"https://www.kaggle.com/no9more9ria10\" target=\"_blank\">@no9more9ria10</a> ).<br>\nAlthough there are some differences in each feature, the following XGBoost’s features were below:</p>\n<ul>\n<li>Features generated independently with src_id and dst_id<ul>\n<li>latitude / longitude</li>\n<li>country with ordinal-encoding</li>\n<li>categories with ordinal-encoding</li></ul></li>\n<li>Features generated dependently with src_id and dst_id<ul>\n<li>Edit distance (name, categories, address, city, state, zip, url, phone)<ul>\n<li>gesh</li>\n<li>leven</li>\n<li>jaro</li>\n<li>simple_ratio (RapidFuzz)</li>\n<li>partial_ratio (RapidFuzz)</li>\n<li>token_set_ratio (RapidFuzz)</li>\n<li>token_sort_ratio (RapidFuzz)</li>\n<li>token_ratio (RapidFuzz)</li>\n<li>partial_token_ratio (RapidFuzz)</li>\n<li>wratio (RapidFuzz)</li>\n<li>qratio (RapidFuzz)</li></ul></li>\n<li>Set similarity with .split(“ ” or “, “) (name, cateogires)<ul>\n<li>jaccard</li>\n<li>dice</li>\n<li>sympson</li></ul></li>\n<li>Cosine similarity of vectors encoded by tf-idf (name, categories, address, all)<ul>\n<li>tfidf(ngram_range=(1, 1), analyzer=‘word’)</li>\n<li>tfidf(ngram_range=(1, 3), analyzer=‘char’)</li></ul></li></ul></li>\n<li>Others<ul>\n<li>probability by transformer</li></ul></li>\n</ul>\n<h4>Modeling</h4>\n<p>The cross-validation of the train data was purposely split so that leakage would occur.<br>\nSpecifically, instead of the folds with GroupKFold(n_splits=2, group=‘point_of_interest’), we created new folds with GroupKFold(n_splits=5, group=‘src_id’).<br>\nThis leakage improved the CV and Public LB because train and test data contained common records.<br>\nWe suspect that the latitude and longitude variables and the ordinal categories worked effectively as features to represent the common id’s records.</p>\n<h2>BERT</h2>\n<h3>Text &amp; Numerical-Features</h3>\n<p>The text was defined as each lowercased name and cateogires connected by a SEP-token and country.<br>\nAdditionally, the numerical feature was logarithmic variables of haversine distance.</p>\n<pre><code>def get_basearray(cfg, df):\n    cfg.idx2id = df['id'].fillna('').to_numpy()\n    cfg.id2idx = {c: i for i, c in enumerate(cfg.idx2id)}\n\n    cfg.idx2name = df['name'].fillna('').str.lower().to_numpy()\n    cfg.idx2categories = df['categories'].fillna('').str.lower().to_numpy()\n    cfg.idx2address = df['address'].fillna('').str.lower().to_numpy()    \n    return cfg\n\ndef get_text(cfg, i, j): # {i: index of src_id, j: index of dst_id}\n    text = cfg.idx2country[i]+cfg.sep+\\\n            'name1 '+cfg.idx2name[i]+cfg.sep+'category1 '+cfg.idx2categories[i]+\\\n            cfg.sep + cfg.sep +\\\n            'name2 '+cfg.idx2name[j]+cfg.sep+'category2 '+cfg.idx2categories[j]\n    return text\n</code></pre>\n<h3>Model Training</h3>\n<p>The architecture of BERT is below:</p>\n<pre><code>class CustomModel(nn.Module):\n    def __init__(self, cfg, criterion):\n        super().__init__()\n        self.cfg = cfg\n        self.criterion = criterion\n        self.config = AutoConfig.from_pretrained(\n            cfg.MODEL_PATH,\n            output_hidden_states=True\n        )\n        self.config.attention_probs_dropout_prob = 0.0\n        self.config.hidden_dropout_prob = 0.0\n        self.backbone = AutoModel.from_pretrained(\n            cfg.MODEL_PATH, # xlm-roberta-base\n            config=self.config\n        )\n        self.linear1 = nn.Sequential(\n            nn.Linear(self.config.hidden_size+len(cfg.num_cols), 1024),\n            nn.SELU(),\n            nn.Linear(1024, 1)\n        )\n\n    def forward(self, inputs, labels=None):\n        outputs = self.backbone(\n            inputs['input_ids'],\n            attention_mask=inputs['attention_mask']\n        )[\"last_hidden_state\"]\n        outputs = outputs[:, 0, :]\n        outputs = torch.cat((\n            outputs,\n            inputs['num_features']\n        ), dim=1)\n        logits = self.linear1(outputs).flatten()\n        loss = self.criterion(logits, labels)\n        return logits, loss\n</code></pre>\n<p>This model was trained for 5 epochs on all train data.</p>\n<h1>Leak Post Process</h1>\n<p>As mentioned in other discussions, there was a duplication of train data in the test data.<br>\nTherefore, the some test data was merged with the train data, and matches were created with the same poi. The prediction of LB0.949 was overwritten.<br>\nWe processed this matches overwriting the matches of LB0.949.<br>\nNote that train.csv is replaced on submit, so we created a separate train.csv dataset.</p>\n<pre><code>import cudf\ntrain = cudf.read_feather(‘../input/4sq-train-data-copy-feather/train.ftr’).fillna(‘[NAN]‘)\ntest = cudf.read_csv(‘../input/foursquare-location-matching/test.csv’, dtype=str).fillna(‘[NAN]’)\ncols = [‘name’, ‘latitude’, ‘longitude’]\ntest = test.merge(train[cols+[‘point_of_interest’]], on=cols, how=‘left’)\ntest[[‘id’, ‘point_of_interest’]].to_pandas().to_csv(‘id2poi.csv’, index=False)\n\nid2poi = pd.read_csv('id2poi.csv')\n\nmatch_dict = {}\nfor poi, group_df in id2poi.groupby('point_of_interest'):\n    ids = set(group_df['id'].tolist())\n    for id_str in ids:\n        match_dict[id_str] = ids\n\nsub = pd.DataFrame({\n    ‘id’: sub_dict.keys(),\n    ‘matches’: [\n        match_dict.get(key, value)\n        for key, value in sub_dict.items()\n    ]\n})\nsub[‘matches’] = sub[‘matches’].map(lambda x: ' ’.join(x))\nhttp://sub.to_csv(‘submission.csv’, index=False)\n</code></pre>",
      "rawMarkdown": "Thank you very much for organizing this interesting and exciting competition, and thank you for teaming up with me, @no9more9ria10, @tomyanabe, @tkm2261, @yukia18 .\n\nThis post is part of the 2nd place solution in LB 0.949 (LB 0.971 with leak).\n\n[Brief Summary](https://www.kaggle.com/competitions/foursquare-location-matching/discussion/336062)\n\nOther part\n\n* [Candidate Generation](https://www.kaggle.com/competitions/foursquare-location-matching/discussion/336072)\n\n# Stack-part 1: Binary classification of matches\n\nUsing the candidate matches created in the previous stage, we ensemble three models: GBDT (XGBoost, lightGBM), BERT(xlm-roberta-base).\nFor this id-pair (e.g., src_id, dst_id), GBDT mainly used the edit distance feature, while BERT used text created in particular columns. These models classified whether the pair matched or not.\n\n\n## GBDT\n\n### Features\n\nXGBoost features were created by colum2131 ( @columbia2131 ), and lightGBM features by ria ( @no9more9ria10 ).\nAlthough there are some differences in each feature, the following XGBoost’s features were below:\n\n* Features generated independently with src_id and dst_id\n   * latitude / longitude\n   * country with ordinal-encoding\n   * categories with ordinal-encoding\n* Features generated dependently with src_id and dst_id\n    * Edit distance (name, categories, address, city, state, zip, url, phone)\n        * gesh\n        * leven\n        * jaro\n        * simple_ratio (RapidFuzz)\n        * partial_ratio (RapidFuzz)\n        * token_set_ratio (RapidFuzz)\n        * token_sort_ratio (RapidFuzz)\n        * token_ratio (RapidFuzz)\n        * partial_token_ratio (RapidFuzz)\n        * wratio (RapidFuzz)\n        * qratio (RapidFuzz)\n    * Set similarity with .split(“ ” or “, “) (name, cateogires)\n        * jaccard\n        * dice\n        * sympson\n    * Cosine similarity of vectors encoded by tf-idf (name, categories, address, all)\n        * tfidf(ngram_range=(1, 1), analyzer=‘word’)\n        * tfidf(ngram_range=(1, 3), analyzer=‘char’)\n* Others\n    * probability by transformer\n\n\n#### Modeling\n\nThe cross-validation of the train data was purposely split so that leakage would occur.\nSpecifically, instead of the folds with GroupKFold(n_splits=2, group=‘point_of_interest’), we created new folds with GroupKFold(n_splits=5, group=‘src_id’).\nThis leakage improved the CV and Public LB because train and test data contained common records.\nWe suspect that the latitude and longitude variables and the ordinal categories worked effectively as features to represent the common id’s records.\n\n## BERT\n\n### Text & Numerical-Features\n\nThe text was defined as each lowercased name and cateogires connected by a SEP-token and country.\nAdditionally, the numerical feature was logarithmic variables of haversine distance.\n\n```python\ndef get_basearray(cfg, df):\n    cfg.idx2id = df['id'].fillna('').to_numpy()\n    cfg.id2idx = {c: i for i, c in enumerate(cfg.idx2id)}\n    \n    cfg.idx2name = df['name'].fillna('').str.lower().to_numpy()\n    cfg.idx2categories = df['categories'].fillna('').str.lower().to_numpy()\n    cfg.idx2address = df['address'].fillna('').str.lower().to_numpy()    \n    return cfg\n\ndef get_text(cfg, i, j): # {i: index of src_id, j: index of dst_id}\n    text = cfg.idx2country[i]+cfg.sep+\\\n            'name1 '+cfg.idx2name[i]+cfg.sep+'category1 '+cfg.idx2categories[i]+\\\n            cfg.sep + cfg.sep +\\\n            'name2 '+cfg.idx2name[j]+cfg.sep+'category2 '+cfg.idx2categories[j]\n    return text\n```\n\n### Model Training\n\nThe architecture of BERT is below:\n\n```python\nclass CustomModel(nn.Module):\n    def __init__(self, cfg, criterion):\n        super().__init__()\n        self.cfg = cfg\n        self.criterion = criterion\n        self.config = AutoConfig.from_pretrained(\n            cfg.MODEL_PATH,\n            output_hidden_states=True\n        )\n        self.config.attention_probs_dropout_prob = 0.0\n        self.config.hidden_dropout_prob = 0.0\n        self.backbone = AutoModel.from_pretrained(\n            cfg.MODEL_PATH, # xlm-roberta-base\n            config=self.config\n        )\n        self.linear1 = nn.Sequential(\n            nn.Linear(self.config.hidden_size+len(cfg.num_cols), 1024),\n            nn.SELU(),\n            nn.Linear(1024, 1)\n        )\n        \n    def forward(self, inputs, labels=None):\n        outputs = self.backbone(\n            inputs['input_ids'],\n            attention_mask=inputs['attention_mask']\n        )[\"last_hidden_state\"]\n        outputs = outputs[:, 0, :]\n        outputs = torch.cat((\n            outputs,\n            inputs['num_features']\n        ), dim=1)\n        logits = self.linear1(outputs).flatten()\n        loss = self.criterion(logits, labels)\n        return logits, loss\n```\n\nThis model was trained for 5 epochs on all train data.\n\n# Leak Post Process\nAs mentioned in other discussions, there was a duplication of train data in the test data.\nTherefore, the some test data was merged with the train data, and matches were created with the same poi. The prediction of LB0.949 was overwritten.\nWe processed this matches overwriting the matches of LB0.949.\nNote that train.csv is replaced on submit, so we created a separate train.csv dataset.\n\n```python\nimport cudf\ntrain = cudf.read_feather(‘../input/4sq-train-data-copy-feather/train.ftr’).fillna(‘[NAN]‘)\ntest = cudf.read_csv(‘../input/foursquare-location-matching/test.csv’, dtype=str).fillna(‘[NAN]’)\ncols = [‘name’, ‘latitude’, ‘longitude’]\ntest = test.merge(train[cols+[‘point_of_interest’]], on=cols, how=‘left’)\ntest[[‘id’, ‘point_of_interest’]].to_pandas().to_csv(‘id2poi.csv’, index=False)\n\nid2poi = pd.read_csv('id2poi.csv')\n\nmatch_dict = {}\nfor poi, group_df in id2poi.groupby('point_of_interest'):\n    ids = set(group_df['id'].tolist())\n    for id_str in ids:\n        match_dict[id_str] = ids\n\nsub = pd.DataFrame({\n    ‘id’: sub_dict.keys(),\n    ‘matches’: [\n        match_dict.get(key, value)\n        for key, value in sub_dict.items()\n    ]\n})\nsub[‘matches’] = sub[‘matches’].map(lambda x: ' ’.join(x))\nhttp://sub.to_csv(‘submission.csv’, index=False)\n```",
      "votes": 27
    },
    {
      "id": 1849944,
      "postDate": "2022-07-10T00:28:29.110Z",
      "content": "<p>Thanks for sharing your insights on this competition!<br>\nGreat job on your success in this competition!</p>",
      "rawMarkdown": "Thanks for sharing your insights on this competition!\nGreat job on your success in this competition!\n",
      "votes": 1
    },
    {
      "id": 1849269,
      "postDate": "2022-07-09T11:01:48.137Z",
      "content": "<p>I couldn't find the RapidFuzz library during the competition. <br>\nThank you for sharing the solution and congrats for your 2nd place in this competition.</p>",
      "rawMarkdown": "I couldn't find the RapidFuzz library during the competition. \nThank you for sharing the solution and congrats for your 2nd place in this competition.",
      "votes": 1
    }
  ],
  "complete": true,
  "source": "live",
  "fetched_at": "2026-10-24T13:20:58Z",
  "competition_id": 35476,
  "competition_slug": "foursquare-location-matching",
  "team_id": 8525766,
  "solution_url": "/competitions/foursquare-location-matching/writeups/2-30-2nd-place-solution-colum2131-part"
}