{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# H&M: Which Algorithm is the Best? Ask RecBole!\nThere are many recommendation algorithms out there, and it is difficult to tell which one is the best. In fact, it is often the case that an algorithm that is optimal for one dataset does not work well for another dataset. In such case, [RecBole](https://recbole.io/), a set of standardized recommendation algorithms, is very useful to compare different algorithms. So, in the following, I use RecBole to quickly get an idea of which is the best algorithm for H&M recommendations.\n\n## Acknowledgements\nThis work is inspired from: https://techlife.cookpad.com/entry/2021/11/04/090000","metadata":{}},{"cell_type":"markdown","source":"## Setups","metadata":{}},{"cell_type":"code","source":"! pip install recbole","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2022-02-21T13:31:20.806994Z","iopub.execute_input":"2022-02-21T13:31:20.807244Z","iopub.status.idle":"2022-02-21T13:31:39.220769Z","shell.execute_reply.started":"2022-02-21T13:31:20.807184Z","shell.execute_reply":"2022-02-21T13:31:39.219815Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import os\nfrom datetime import datetime\nimport time\n\nimport numpy as np\nimport pandas as pd\nfrom recbole.quick_start import run_recbole\nfrom tqdm import tqdm","metadata":{"execution":{"iopub.status.busy":"2022-02-21T13:31:39.222947Z","iopub.execute_input":"2022-02-21T13:31:39.223211Z","iopub.status.idle":"2022-02-21T13:31:39.228846Z","shell.execute_reply.started":"2022-02-21T13:31:39.223180Z","shell.execute_reply":"2022-02-21T13:31:39.228143Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Create Atomic Files\nFirst, you need to transform the CSV files to a specific file format called Atomic Files (see [user guide](https://recbole.io/docs/user_guide/usage/running_new_dataset.html)). But if you use [this pre-computed dataset](https://www.kaggle.com/shionhonda/hm-recbole-atomic-files), you can skip running the following cells.\n\nNOTE: I use the last-two-week data for valid & test sets and the last-month data for training. I discard the rest of the data to reduce computation. I further downsampled users by 1/6 to avoid OOM.\n\nNOTE: For simplicity, I only use columns `customer_id`, `article_id`, and `t_dat`. You can include other columns by editing `HMDataset`.","metadata":{}},{"cell_type":"code","source":"# https://github.com/RUCAIBox/RecSysDatasets/blob/master/conversion_tools/src/base_dataset.py\n\n\nclass BaseDataset(object):\n    def __init__(self, input_path, output_path):\n        super(BaseDataset, self).__init__()\n\n        self.dataset_name = ''\n        self.input_path = input_path\n        self.output_path = output_path\n        self.check_output_path()\n\n        # input file\n        self.inter_file = os.path.join(self.input_path, 'inters.dat')\n        self.item_file = os.path.join(self.input_path, 'items.dat')\n        self.user_file = os.path.join(self.input_path, 'users.dat')\n        self.sep = '\\t'\n\n        # output file\n        self.output_inter_file, self.output_item_file, self.output_user_file = self.get_output_files()\n\n        # selected feature fields\n        self.inter_fields = {}\n        self.item_fields = {}\n        self.user_fields = {}\n\n    def check_output_path(self):\n        if not os.path.isdir(self.output_path):\n            os.makedirs(self.output_path)\n\n    def get_output_files(self):\n        output_inter_file = os.path.join(self.output_path, self.dataset_name + '.inter')\n        output_item_file = os.path.join(self.output_path, self.dataset_name + '.item')\n        output_user_file = os.path.join(self.output_path, self.dataset_name + '.user')\n        return output_inter_file, output_item_file, output_user_file\n\n    def load_inter_data(self) -> pd.DataFrame():\n        raise NotImplementedError\n\n    def load_item_data(self) -> pd.DataFrame():\n        raise NotImplementedError\n\n    def load_user_data(self) -> pd.DataFrame():\n        raise NotImplementedError\n\n    def convert_inter(self):\n        try:\n            input_inter_data = self.load_inter_data()\n            self.convert(input_inter_data, self.inter_fields, self.output_inter_file)\n        except NotImplementedError:\n            print('This dataset can\\'t be converted to inter file\\n')\n\n    def convert_item(self):\n        try:\n            input_item_data = self.load_item_data()\n            self.convert(input_item_data, self.item_fields, self.output_item_file)\n        except NotImplementedError:\n            print('This dataset can\\'t be converted to item file\\n')\n\n    def convert_user(self):\n        try:\n            input_user_data = self.load_user_data()\n            self.convert(input_user_data, self.user_fields, self.output_user_file)\n        except NotImplementedError:\n            print('This dataset can\\'t be converted to user file\\n')\n\n    @staticmethod\n    def convert(input_data, selected_fields, output_file):\n        output_data = pd.DataFrame()\n        for column in selected_fields:\n            output_data[column] = input_data.iloc[:, column]\n        with open(output_file, 'w') as fp:\n            fp.write('\\t'.join([selected_fields[column] for column in output_data.columns]) + '\\n')\n            for i in tqdm(range(output_data.shape[0])):\n                fp.write('\\t'.join([str(output_data.iloc[i, j])\n                                    for j in range(output_data.shape[1])]) + '\\n')\n\n    def parse_json(self, data_path):\n        with open(data_path, 'rb') as g:\n            for l in g:\n                yield eval(l)\n\n    def getDF(self, data_path):\n        i = 0\n        df = {}\n        for d in self.parse_json(data_path):\n            df[i] = d\n            i += 1\n        data = pd.DataFrame.from_dict(df, orient='index')\n        \n        return data","metadata":{"execution":{"iopub.status.busy":"2022-02-21T13:31:39.230892Z","iopub.execute_input":"2022-02-21T13:31:39.231503Z","iopub.status.idle":"2022-02-21T13:31:39.254578Z","shell.execute_reply.started":"2022-02-21T13:31:39.231466Z","shell.execute_reply":"2022-02-21T13:31:39.253835Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"class HMDataset(BaseDataset):\n    def __init__(self, input_path, output_path):\n        super(HMDataset, self).__init__(input_path, output_path)\n        self.dataset_name = \"hm\"\n\n        self.inter_file = os.path.join(self.input_path, \"transactions_train.csv\")\n        self.item_file = os.path.join(self.input_path, \"articles.csv\")\n        self.user_file = os.path.join(self.input_path, \"customers.csv\")\n\n        self.sep = \",\"\n\n        # output_path\n        output_files = self.get_output_files()\n        self.output_inter_file = output_files[0]\n        self.output_item_file = output_files[1]\n        self.output_user_file = output_files[2]\n\n        # selected feature fields\n        self.inter_fields = {\n            0: \"t_dat:float\",\n            1: \"customer_id:token\",\n            2: \"article_id:token\",\n        }\n\n        self.item_fields = {\n            0: \"article_id:token\",\n        }\n\n        self.user_fields = {\n            0: \"customer_id:token\",\n        }\n\n    def load_inter_data(self):\n        df = pd.read_csv(self.inter_file,\n            dtype={\"t_dat\": \"object\", \"customer_id\": \"object\", \"article_id\": \"object\", \"price\": float, \"sales_channel_id\": int}\n           )\n        # approx. 1 month + 2 weeks\n        df = df[-len(df)*3//48:].reset_index(drop=True)\n        # Further downsampling to avoid OOM\n        uus = df[\"customer_id\"].unique()\n        sampled_users = np.random.choice(uus, len(uus)//6)\n        df = df.query('customer_id in @sampled_users')\n        df['t_dat'] = df['t_dat'].apply(lambda x: datetime.timestamp(datetime.strptime(x, \"%Y-%m-%d\")))\n        return df\n\n    def load_item_data(self):\n        return pd.read_csv(self.item_file, delimiter=self.sep, engine=\"python\")\n\n    def load_user_data(self):\n        return pd.read_csv(self.user_file, delimiter=self.sep, engine=\"python\")","metadata":{"execution":{"iopub.status.busy":"2022-02-21T13:31:39.256599Z","iopub.execute_input":"2022-02-21T13:31:39.257474Z","iopub.status.idle":"2022-02-21T13:31:39.270372Z","shell.execute_reply.started":"2022-02-21T13:31:39.257417Z","shell.execute_reply":"2022-02-21T13:31:39.269545Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"%%time\nhmds = HMDataset(\"../input/h-and-m-personalized-fashion-recommendations\", \"./hm\")\nhmds.convert_inter()\nhmds.convert_user()\nhmds.convert_item()\ndel hmds","metadata":{"execution":{"iopub.status.busy":"2022-02-21T13:31:39.271831Z","iopub.execute_input":"2022-02-21T13:31:39.272080Z","iopub.status.idle":"2022-02-21T13:34:18.475956Z","shell.execute_reply.started":"2022-02-21T13:31:39.272047Z","shell.execute_reply":"2022-02-21T13:34:18.475297Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Configurations\nNOTE: Here I train each model for only 5 epochs to save time. If you train a model for submission, you should train hundreds of epochs (defaults to 500) with a lower learning rate (defaults to 0.001).\n\nNOTE: `MAP@12` that I specify here is different from the competiton's evaluation metric. To define the same MAP@12 as the competition's evaluation metric, see [here](https://recbole.io/docs/developer_guide/customize_metrics.html). And I'd appreciate it if you kindly share the code for the correct MAP@12.\n\nNOTE: Of course you can change the following configurations, but it can cause errors that are not really understandable (the most common one is: \"some feat is empty, please check the filtering settings\"). I worked hard and finally got to the following configurations.","metadata":{}},{"cell_type":"code","source":"cfg_str = \"\"\"\ndata_path: ./\ndataset: hm\nfield_separator: \"\\\\t\"\nUSER_ID_FIELD: customer_id\nITEM_ID_FIELD: article_id\nRATING_FIELD: ~\nTIME_FIELD: t_dat\nshow_progress: false\n\nload_col:\n    inter: [customer_id, article_id, t_dat]\n    user: [customer_id]\n    item: [article_id]\n\nepochs: 5\nlearning_rate: 0.01\nuser_inter_num_interval: \"[0,inf)\"\nitem_inter_num_interval: \"[0,inf)\"\nfilter_inter_by_user_or_item: false\nneg_sampling:\n    uniform: 1\neval_args:\n    split: {'RS': [4, 1, 1]}\n    group_by: None\n    order: TO\n    mode: uni50\nmetrics: ['Recall', 'MRR', 'NDCG', 'Hit', 'Precision', 'MAP']\ntopk: 12\nvalid_metric: MAP@12\n\"\"\"\n\n\nwith open(\"hm/config.yaml\", \"w\") as f:\n    f.write(cfg_str)","metadata":{"execution":{"iopub.status.busy":"2022-02-21T13:34:18.477305Z","iopub.execute_input":"2022-02-21T13:34:18.478026Z","iopub.status.idle":"2022-02-21T13:34:18.483730Z","shell.execute_reply.started":"2022-02-21T13:34:18.477986Z","shell.execute_reply":"2022-02-21T13:34:18.483067Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Run Experiments\nI selected 10 major algorithms from general, context-aware, and sequatial recommmenders. `Pop` means popularity ranking.","metadata":{}},{"cell_type":"code","source":"def run(model_name):\n    if model_name in [\n        \"MultiVAE\",\n        \"MultiDAE\",\n        \"MacridVAE\",\n        \"RecVAE\",\n        \"GRU4Rec\",\n        \"NARM\",\n        \"STAMP\",\n        \"NextItNet\",\n        \"TransRec\",\n        \"SASRec\",\n        \"BERT4Rec\",\n        \"SRGNN\",\n        \"GCSAN\",\n        \"GRU4RecF\",\n        \"FOSSIL\",\n        \"SHAN\",\n        \"RepeatNet\",\n        \"HRM\",\n        \"NPE\",\n    ]:\n        parameter_dict = {\n            \"neg_sampling\": None,\n        }\n        return run_recbole(\n            model=model_name,\n            dataset='hm',\n            config_file_list=['hm/config.yaml'],\n            config_dict=parameter_dict,\n        )\n    else:\n        return run_recbole(\n            model=model_name,\n            dataset='hm',\n            config_file_list=['hm/config.yaml'],\n        )\n","metadata":{"execution":{"iopub.status.busy":"2022-02-21T13:34:18.485160Z","iopub.execute_input":"2022-02-21T13:34:18.485653Z","iopub.status.idle":"2022-02-21T13:34:20.475262Z","shell.execute_reply.started":"2022-02-21T13:34:18.485615Z","shell.execute_reply":"2022-02-21T13:34:20.474526Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"%%time\nmodel_list = [\"Pop\", \"ItemKNN\", \"BPR\", \"NeuMF\", \"RecVAE\", \"LightGCN\"] # General\nmodel_list += [\"FFM\", \"DeepFM\"] # Context-aware\nmodel_list += [\"GRU4Rec\", \"SHAN\"] # Sequential\nfor model_name in model_list:\n    print(f\"running {model_name}...\")\n    start = time.time()\n    result = run(model_name)\n    t = time.time() - start\n    print(f\"It took {t/60:.2f} mins\")\n    print(result)","metadata":{"execution":{"iopub.status.busy":"2022-02-21T13:47:24.876253Z","iopub.execute_input":"2022-02-21T13:47:24.877230Z","iopub.status.idle":"2022-02-21T14:44:50.835277Z","shell.execute_reply.started":"2022-02-21T13:47:24.877188Z","shell.execute_reply":"2022-02-21T14:44:50.834554Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]}