diff --git a/README.md b/README.md index 8a614595..76d0c248 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,21 @@ This repository contains a private version of the package. ## Table of Contents - [choice-learn-private](#choice-learn-private) + - [Introduction - Discrete Choice Modelling](#introduction---discrete-choice-modelling) - [Table of Contents](#table-of-contents) - [What's in there ?](#whats-in-there) + - [Getting Started](#getting-started---fast-track) - [Installation](#installation) - [Usage](#usage) - [Documentation](#documentation) - [Citation](#citation) +## Introduction - Discrete Choice Modelling + +Discrete choice models aim at explaining or predicting a choice from a set of alternatives. Well known use-cases include analyzing people choice of mean of transport or products purchases in stores. + +If you are new to choice modelling, you can check this [resource](https://www.publichealth.columbia.edu/research/population-health-methods/discrete-choice-model-and-analysis). The different notebooks from the [Getting Started](#getting-started---fast-track) section can also help you understand choice modelling and more importantly help you for your usecase. + ## What's in there ? ### Data @@ -41,7 +49,7 @@ This repository contains a private version of the package. ### Models - Ready to use models: - Conditional MultiNomialLogit, Train, K.; McFadden, D.; Ben-Akiva, M. (1987) - - RUMnet, Aouad A.; Désir A. (2022) + - RUMnet, Aouad A.; Désir A. (2022) [1] - Ready to use models to be implemented: - Nested MultiNomialLogit - MultiNomialLogit with latent variables (MixedLogit) @@ -100,3 +108,11 @@ A detailed documentation of this project is available [here](https://artefactory ### Contributors ## References + +### Papers +[1][Representing Random Utility Choice Models with Neural Networks](https://arxiv.org/abs/2207.12877), Aouad A.; Désir A. (2022) + +### Code and Repositories +- [PyLogit](https://github.com/timothyb0912/pylogit) +- [Torch Choice](https://gsbdbi.github.io/torch-choice/) +- [1][RUMnet](https://github.com/antoinedesir/rumnet) diff --git a/choice_learn/models/conditional_mnl.py b/choice_learn/models/conditional_mnl.py index f1374181..daf935d6 100644 --- a/choice_learn/models/conditional_mnl.py +++ b/choice_learn/models/conditional_mnl.py @@ -127,10 +127,20 @@ def add_weight(self, weight_name, weight_index): if weight_name not in self.coefficients.keys(): raise ValueError(f"Weight {weight_name} not in coefficients") - self.feature_to_weight[self.coefficients[weight_name]["feature_name"]] = ( - weight_name, - weight_index, - ) + if self.coefficients[weight_name]["feature_name"] in self.feature_to_weight.keys(): + self.feature_to_weight[self.coefficients[weight_name]["feature_name"]].append( + ( + weight_name, + weight_index, + ) + ) + else: + self.feature_to_weight[self.coefficients[weight_name]["feature_name"]] = [ + ( + weight_name, + weight_index, + ), + ] def list_features_with_weights(self): """Get a list of the features that have a weight to be estimated. @@ -157,8 +167,12 @@ def get_weight_item_indexes(self, feature_name): int The index of the weight in the conditionalMNL weights. """ - weight_name, weight_index = self.feature_to_weight[feature_name] - return self.coefficients[weight_name]["items_indexes"], weight_index + weights_info = self.feature_to_weight[feature_name] + weight_names = [weight_info[0] for weight_info in weights_info] + weight_indexs = [weight_info[1] for weight_info in weights_info] + return [ + self.coefficients[weight_name]["items_indexes"] for weight_name in weight_names + ], weight_indexs @property def coefficients_list(self): @@ -366,161 +380,194 @@ def compute_utility_from_specification( num_choices = availabilities_batch.shape[0] contexts_items_utilities = [] # Items features - for i, feat_tuple in enumerate(self._items_features_names): - for j, feat in enumerate(feat_tuple): - if feat in self.params.list_features_with_weights(): - item_index, weight_index = self.params.get_weight_item_indexes(feat) - - s_i_u = tf.zeros((num_items,)) - for q, idx in enumerate(item_index): - if isinstance(idx, list): - for k in idx: - s_i_u = tf.concat( - [ - s_i_u[:k], - tf.multiply( - items_batch[i][k, j], self.weights[weight_index][:, q] - ), - s_i_u[k + 1 :], - ], - axis=0, - ) - else: - s_i_u = tf.concat( - [ - s_i_u[:idx], - tf.multiply( - items_batch[i][idx, j], self.weights[weight_index][:, q] - ), - s_i_u[idx + 1 :], - ], - axis=0, - ) - s_i_u = tf.stack([s_i_u] * num_choices, axis=0) - - ### Need reshaping here - contexts_items_utilities.append(s_i_u) - else: - if verbose > 1: - print( - f"Feature {feat} is in dataset but has no weight assigned in utility\ - computations" + if self._items_features_names is not None: + for i, feat_tuple in enumerate(self._items_features_names): + for j, feat in enumerate(feat_tuple): + if feat in self.params.list_features_with_weights(): + item_index_list, weight_index_list = self.params.get_weight_item_indexes( + feat ) - - # Context features - for i, feat_tuple in enumerate(self._contexts_features_names): - for j, feat in enumerate(feat_tuple): - if feat in self.params.list_features_with_weights(): - item_index, weight_index = self.params.get_weight_item_indexes(feat) - - s_i_u = tf.zeros((num_choices, num_items)) - - for q, idx in enumerate(item_index): - if isinstance(idx, list): - for k in idx: - s_i_u = tf.concat( - [ - s_i_u[:, :k], - tf.expand_dims( + for item_index, weight_index in zip(item_index_list, weight_index_list): + s_i_u = tf.zeros((num_items,)) + for q, idx in enumerate(item_index): + if isinstance(idx, list): + for k in idx: + s_i_u = tf.concat( + [ + s_i_u[:k], + tf.multiply( + items_batch[i][k, j], + self.weights[weight_index][:, q], + ), + s_i_u[k + 1 :], + ], + axis=0, + ) + else: + s_i_u = tf.concat( + [ + s_i_u[:idx], tf.multiply( - contexts_batch[i][:, j], + items_batch[i][idx, j], self.weights[weight_index][:, q], ), - axis=-1, - ), - s_i_u[:, k + 1 :], - ], - axis=1, - ) - else: - s_i_u = tf.concat( - [ - s_i_u[:, :idx], - tf.expand_dims( - tf.multiply( + s_i_u[idx + 1 :], + ], + axis=0, + ) + s_i_u = tf.stack([s_i_u] * num_choices, axis=0) + + ### Need reshaping here + contexts_items_utilities.append(tf.cast(s_i_u, tf.float32)) + else: + if verbose > 1: + print( + f"Feature {feat} is in dataset but has no weight assigned\ + in utility computations" + ) + # Context features + if self._contexts_features_names is not None: + for i, feat_tuple in enumerate(self._contexts_features_names): + for j, feat in enumerate(feat_tuple): + if feat in self.params.list_features_with_weights(): + print("found feat", feat) + item_index_list, weight_index_list = self.params.get_weight_item_indexes( + feat + ) + for item_index, weight_index in zip(item_index_list, weight_index_list): + s_i_u = tf.zeros((num_choices, num_items)) + s_i_u = [tf.zeros(num_choices) for _ in range(num_items)] + + for q, idx in enumerate(item_index): + if isinstance(idx, list): + for k in idx: + """ + s_i_u = tf.concat( + [ + s_i_u[:, :k], + tf.expand_dims( + tf.multiply( + contexts_batch[i][:, j], + self.weights[weight_index][:, q], + ), + axis=-1, + ), + s_i_u[:, k + 1 :], + ], + axis=1, + ) + """ + contexts_batch[i][:, j] + compute = tf.multiply( contexts_batch[i][:, j], self.weights[weight_index][:, q], - ), - axis=-1, - ), - s_i_u[:, idx + 1 :], - ], - axis=1, + ) + s_i_u[k] += compute + else: + """ + s_i_u = tf.concat( + [ + s_i_u[:, :idx], + tf.expand_dims( + tf.multiply( + contexts_batch[i][:, j], + self.weights[weight_index][:, q], + ), + axis=-1, + ), + s_i_u[:, idx + 1 :], + ], + axis=1, + ) + """ + compute = tf.multiply( + contexts_batch[i][:, j], self.weights[weight_index][:, q] + ) + s_i_u[idx] += compute + + contexts_items_utilities.append( + tf.cast(tf.stack(s_i_u, axis=1), tf.float32) ) - - contexts_items_utilities.append(s_i_u) - else: - print( - f"Feature {feat} is in dataset but has no weight assigned in utility\ - computations" - ) + else: + print( + f"Feature {feat} is in dataset but has no weight assigned\ + in utility computations" + ) # context Items features - for i, feat_tuple in enumerate(self._contexts_items_features_names): - for j, feat in enumerate(feat_tuple): - if feat in self.params.list_features_with_weights(): - item_index, weight_index = self.params.get_weight_item_indexes(feat) - s_i_u = tf.zeros((num_choices, num_items)) - - for q, idx in enumerate(item_index): - if isinstance(idx, list): - for k in idx: - s_i_u = tf.concat( - [ - s_i_u[:, :k], - tf.expand_dims( - tf.multiply( - contexts_items_batch[i][:, k, j], - self.weights[weight_index][:, q], + if self._contexts_items_features_names is not None: + for i, feat_tuple in enumerate(self._contexts_items_features_names): + for j, feat in enumerate(feat_tuple): + if feat in self.params.list_features_with_weights(): + print("found feat", feat) + + item_index_list, weight_index_list = self.params.get_weight_item_indexes( + feat + ) + for item_index, weight_index in zip(item_index_list, weight_index_list): + s_i_u = tf.zeros((num_choices, num_items)) + + for q, idx in enumerate(item_index): + if isinstance(idx, list): + for k in idx: + s_i_u = tf.concat( + [ + s_i_u[:, :k], + tf.expand_dims( + tf.multiply( + contexts_items_batch[i][:, k, j], + self.weights[weight_index][:, q], + ), + axis=-1, + ), + s_i_u[:, k + 1 :], + ], + axis=1, + ) + else: + s_i_u = tf.concat( + [ + s_i_u[:, :idx], + tf.expand_dims( + tf.multiply( + contexts_items_batch[i][:, idx, j], + self.weights[weight_index][:, q], + ), + axis=-1, ), - axis=-1, - ), - s_i_u[:, k + 1 :], - ], - axis=1, - ) - else: - s_i_u = tf.concat( - [ - s_i_u[:, :idx], - tf.expand_dims( - tf.multiply( - contexts_items_batch[i][:, idx, j], - self.weights[weight_index][:, q], - ), - axis=-1, - ), - s_i_u[:, idx + 1 :], - ], - axis=1, - ) + s_i_u[:, idx + 1 :], + ], + axis=1, + ) - contexts_items_utilities.append(s_i_u) - else: - print( - f"Feature {feat} is in dataset but has no weight assigned in utility\ - computations" - ) + contexts_items_utilities.append(tf.cast(s_i_u, tf.float32)) + else: + print( + f"Feature {feat} is in dataset but has no weight assigned\ + in utility computations" + ) if "intercept" in self.params.list_features_with_weights(): - item_index, weight_index = self.params.get_weight_item_indexes("intercept") - - s_i_u = tf.zeros((num_items,)) - for q, idx in enumerate(item_index): - s_i_u = tf.concat( - [ - s_i_u[:idx], - self.weights[weight_index][:, q], - s_i_u[idx + 1 :], - ], - axis=0, - ) + print("found feat", "intercept") + item_index_list, weight_index_list = self.params.get_weight_item_indexes("intercept") + + for item_index, weight_index in zip(item_index_list, weight_index_list): + s_i_u = tf.zeros((num_items,)) + for q, idx in enumerate(item_index): + s_i_u = tf.concat( + [ + s_i_u[:idx], + self.weights[weight_index][:, q], + s_i_u[idx + 1 :], + ], + axis=0, + ) - s_i_u = tf.stack([s_i_u] * num_choices, axis=0) + s_i_u = tf.stack([s_i_u] * num_choices, axis=0) - ### Need reshaping here + ### Need reshaping here - contexts_items_utilities.append(s_i_u) + contexts_items_utilities.append(tf.cast(s_i_u, tf.float32)) return tf.reduce_sum(contexts_items_utilities, axis=0) diff --git a/mkdocs.yaml b/mkdocs.yaml index 93d3cd97..f3edefda 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -54,11 +54,15 @@ plugins: nav: - HomePage: index.md - - tutorials.md + - Tutorials: + - Introduction: tutorials/introduction.md + - Getting Started with Data: notebooks/choice_learn_introduction_data.md + - Getting Started with Conditional Logit: notebooks/choice_learn_introduction_clogit.md - How-To Guides: - Introduction: how-to-guides.md - ChoiceDataset Usage: notebooks/choice_learn_introduction_data.md - Conditional Logit Estimation: notebooks/choice_learn_introduction_clogit.md + - RUMnet Estimation: notebooks/rumnet_example.md - Custom Choice Model Creation: notebooks/custom_model.md - References: - Data: diff --git a/notebooks/choice_learn_introduction_clogit.ipynb b/notebooks/choice_learn_introduction_clogit.ipynb index 60edd0d5..08735f5c 100644 --- a/notebooks/choice_learn_introduction_clogit.ipynb +++ b/notebooks/choice_learn_introduction_clogit.ipynb @@ -30,13 +30,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### ChoiceModel - Getting Started\n", + "### Getting Started with the ConditionalMNL\n", "\n", - "choice-learn package offers a high level API to conceive and estimate discrete choice models.\n", + "The choice-learn package offers a high level API to conceive and estimate discrete choice models. Several models are ready to be used, you can check the list [here](../README.md). If you want to create your own model or another one that is not in the list, the lower level API can help you. Check the notebook [here](./custom_model.ipynb).\n", "\n", - "We begin this tutorial with the estimation of a Conditional Logit Model on the ModeCanada dataset.\n", + "We begin this tutorial with the estimation of a Conditional Logit Model on the ModeCanada dataset[1]. We try to reproduce the example [Torch-Choice](https://gsbdbi.github.io/torch-choice/conditional_logit_model_mode_canada/).\n", + "We also reproduce the example from [PyLogit](https://github.com/timothyb0912/pylogit/blob/master/examples/notebooks/Main%20PyLogit%20Example.ipynb) [here](#example-2-swissmetro).\n", "\n", - "First, we download our data as a ChoiceDataset. See the data management tutorial first if needed." + "First, we download our data as a ChoiceDataset. See the [data management tutorial](./choice_learn_introduction_data.ipynb) first if needed." ] }, { @@ -53,12 +54,6 @@ "transport_df = transport_df.loc[transport_df.noalt == 4]\n", "\n", "items = [\"air\", \"bus\", \"car\", \"train\"]\n", - "\n", - "transport_df[\"oh_air\"] = transport_df.apply(lambda row: 1. if row.alt == items[0] else 0., axis=1)\n", - "transport_df[\"oh_bus\"] = transport_df.apply(lambda row: 1. if row.alt == items[1] else 0., axis=1)\n", - "transport_df[\"oh_car\"] = transport_df.apply(lambda row: 1. if row.alt == items[2] else 0., axis=1)\n", - "transport_df[\"oh_train\"] = transport_df.apply(lambda row: 1. if row.alt == items[3] else 0., axis=1)\n", - "\n", "transport_df.income = transport_df.income.astype(\"float32\")" ] }, @@ -68,9 +63,9 @@ "metadata": {}, "outputs": [], "source": [ + "# Initialization of the ChoiceDataset\n", "from choice_learn.data import ChoiceDataset\n", "dataset = ChoiceDataset.from_single_df(df=transport_df,\n", - " fixed_items_features_columns=[\"oh_air\", \"oh_bus\", \"oh_car\", \"oh_train\"],\n", " contexts_features_columns=[\"income\"],\n", " contexts_items_features_columns=[\"cost\", \"freq\", \"ovt\", \"ivt\"],\n", " items_id_column=\"alt\",\n", @@ -83,7 +78,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now, we can import the needed modules:" + "Now, we can import the model from choice_learn.models:" ] }, { @@ -99,7 +94,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The conditional MNL \\ref{Train} specifies a linear utility for each item i during the session s with regards to the features:\n", + "The conditional MNL[2] specifies a linear utility for each item i during the session s with regards to the features:\n", "$$\n", "U(i, s) = \\sum_{features} a(i, s) * feat(i, s)\n", "$$\n", @@ -107,7 +102,7 @@ "We will use a ModelSpecification object to define our model with regards to our ChoiceDataset.\n", "For each feature in the choice dataset we can specify how it must be specified in the utility.\n", "\n", - "Let's re-use a common example from \\ref{} on the ModeCanda dataset:\n", + "Let's re-use a common example from on the ModeCanda[1] dataset:\n", "$$\n", "U(i, s) = \\beta^{inter}_i + \\beta^{price} \\cdot price(i, s) + \\beta^{freq} \\cdot freq(i, s) + \\beta^{ovt} \\cdot ovt(i, s) + \\beta^{income}_i \\cdot income(s) + \\beta^{ivt}_i \\cdot ivt(i, t) + \\epsilon(i, t)\n", "$$" @@ -213,21 +208,12 @@ }, "outputs": [ { - "data": { - "text/plain": [ - "[,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ]" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "The average neg-loglikelihood is: 0.674474\n", + "The total neg-loglikelihood is: 1874.3632485866547\n" + ] } ], "source": [ @@ -239,7 +225,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A faster specification can be done using a dictionnary. It follows torch-choice \\ref{} method to create conditional logit models.\n", + "A faster specification can be done using a dictionnary. It follows torch-choice method to create conditional logit models.\n", "The parameters dict needs to be as follows:\n", "- The key is the feature name\n", "- The value is the mode. Currently three modes are available:\n", @@ -261,17 +247,16 @@ "name": "stdout", "output_type": "stream", "text": [ - "The average neg-loglikelihood is: 0.674474\n", - "The total neg-loglikelihood is: 1874.3632485866547\n" + "Using L-BFGS optimizer, setting up .fit() function\n" ] } ], "source": [ "# Instantiation of the parameters dictionnary\n", "params = {\"income\": \"item\",\n", - " \"cost\": \"constant\", \n", + " \"cost\": \"constant\",\n", " \"freq\": \"constant\",\n", - " \"ovt\": \"constant\", \n", + " \"ovt\": \"constant\",\n", " \"ivt\": \"item-full\",\n", " \"intercept\": \"item\"}\n", "\n", @@ -293,7 +278,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We can compare the estimated coefficients and the negative log-likelihood obtained in \\ref{} and \\ref{torch-choice}, and it is similar !" + "We can compare the estimated coefficients and the negative log-likelihood obtained in torch-choice example, and it is similar !" ] }, { @@ -303,18 +288,35 @@ "keep_output": true }, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Feature oh_air is in dataset but has no weight assigned in utility computations\n", + "Feature oh_bus is in dataset but has no weight assigned in utility computations\n", + "Feature oh_car is in dataset but has no weight assigned in utility computations\n", + "Feature oh_train is in dataset but has no weight assigned in utility computations\n" + ] + }, { "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 1/1 [00:01<00:00, 1.30s/it]\n" + "100%|██████████| 1/1 [00:01<00:00, 1.70s/it]" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "'Ground Truth' Negative LogLikelihood: tf.Tensor(1874.363, shape=(), dtype=float32)\n" + "'Ground Truth' Negative LogLikelihood: tf.Tensor(1874.3633, shape=(), dtype=float32)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" ] } ], @@ -357,11 +359,11 @@ "output_type": "stream", "text": [ "Purchase probability of each item for the first 5 sessions: tf.Tensor(\n", - "[[0.19061294 0.00353295 0.40536764 0.40048242]\n", - " [0.34869465 0.00069692 0.36830828 0.28229645]\n", - " [0.14418268 0.00651326 0.4056784 0.4436212 ]\n", - " [0.34869465 0.00069692 0.36830828 0.28229645]\n", - " [0.34869465 0.00069692 0.36830828 0.28229645]], shape=(5, 4), dtype=float32)\n" + "[[0.1906135 0.00353266 0.4053667 0.4004831 ]\n", + " [0.34869286 0.00069682 0.36830992 0.28229675]\n", + " [0.14418365 0.00651285 0.40567666 0.44362238]\n", + " [0.34869286 0.00069682 0.36830992 0.28229675]\n", + " [0.34869286 0.00069682 0.36830992 0.28229675]], shape=(5, 4), dtype=float32)\n" ] } ], @@ -413,14 +415,14 @@ { "data": { "text/plain": [ - "[,\n", - " ,\n", - " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", - " ,\n", - " ,\n", - " ]" + " ]" ] }, "execution_count": null, @@ -477,9 +479,9 @@ "source": [ "# Instantiation of the parameters dictionnary\n", "params = {\"income\": \"item\",\n", - " \"cost\": \"constant\", \n", + " \"cost\": \"constant\",\n", " \"freq\": \"constant\",\n", - " \"ovt\": \"constant\", \n", + " \"ovt\": \"constant\",\n", " \"ivt\": \"item-full\",\n", " \"intercept\": \"item\"}\n", "\n", @@ -517,12 +519,218 @@ "print(cmnl.weights)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example 2: SwissMetro\n", + "\n", + "We reproduce the [PyLogit](https://github.com/timothyb0912/pylogit/blob/master/examples/notebooks/Main%20PyLogit%20Example.ipynb) example of ConditionalMNL, that is reproduction of a Biogeme example. It uses the SwissMetro dataset[3]." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from choice_learn.datasets import load_swissmetro\n", + "\n", + "swiss_df = load_swissmetro(as_frame=True)\n", + "\n", + "# Removing unknown choices\n", + "swiss_df = swiss_df.loc[swiss_df.CHOICE != 0]\n", + "# Keep only commute an dbusiness trips\n", + "swiss_df = swiss_df.loc[swiss_df.PURPOSE.isin([1, 3])]\n", + "\n", + "# Normalizing values\n", + "swiss_df[[\"TRAIN_TT\", \"SM_TT\", \"CAR_TT\"]] = swiss_df[[\"TRAIN_TT\", \"SM_TT\", \"CAR_TT\"]] / 60.\n", + "swiss_df[[\"TRAIN_HE\", \"SM_HE\"]] = swiss_df[[\"TRAIN_HE\", \"SM_HE\"]] / 60.\n", + "\n", + "swiss_df[\"train_free_ticket\"] = swiss_df.apply(lambda row: ((row[\"GA\"]==1 or row[\"WHO\"]==2) > 0).astype(int), axis=1)\n", + "swiss_df[\"sm_free_ticket\"] = swiss_df.apply(lambda row: ((row[\"GA\"]==1 or row[\"WHO\"]==2) > 0).astype(int), axis=1)\n", + "swiss_df[\"car_free_ticket\"] = 0\n", + "\n", + "swiss_df[\"train_travel_cost\"] = swiss_df.apply(lambda row: (row[\"TRAIN_CO\"] * (1 - row[\"train_free_ticket\"])) / 100, axis=1)\n", + "swiss_df[\"sm_travel_cost\"] = swiss_df.apply(lambda row: (row[\"SM_CO\"] * (1 - row[\"sm_free_ticket\"])) / 100, axis=1)\n", + "swiss_df[\"car_travel_cost\"] = swiss_df.apply(lambda row: row[\"CAR_CO\"] / 100, axis=1)\n", + "\n", + "swiss_df[\"single_luggage_piece\"] = swiss_df.apply(lambda row: (row[\"LUGGAGE\"] == 1).astype(int), axis=1)\n", + "swiss_df[\"multiple_luggage_piece\"] = swiss_df.apply(lambda row: (row[\"LUGGAGE\"] == 3).astype(int), axis=1)\n", + "swiss_df[\"regular_class\"] = swiss_df.apply(lambda row: 1 - row[\"FIRST\"], axis=1)\n", + "swiss_df[\"train_survey\"] = swiss_df.apply(lambda row: 1 - row[\"SURVEY\"], axis=1)\n", + "swiss_df.head()[[\"train_travel_cost\", \"TRAIN_TT\", \"TRAIN_HE\",\n", + " \"sm_travel_cost\", \"SM_TT\", \"SM_HE\", \"SM_SEATS\",\n", + " \"car_travel_cost\", \"CAR_TT\",\n", + " \"TRAIN_AV\", \"SM_AV\", \"CAR_AV\",\n", + " \"train_survey\", \"regular_class\", \"single_luggage_piece\", \"multiple_luggage_piece\",\n", + " \"CHOICE\"]]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "contexts_features = swiss_df[[\"train_survey\", \"regular_class\", \"single_luggage_piece\", \"multiple_luggage_piece\"]].to_numpy()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "train_features = swiss_df[[\"train_travel_cost\", \"TRAIN_TT\", \"TRAIN_HE\"]].to_numpy()\n", + "sm_features = swiss_df[[\"sm_travel_cost\", \"SM_TT\", \"SM_HE\", \"SM_SEATS\"]].to_numpy()\n", + "car_features = swiss_df[[\"car_travel_cost\", \"CAR_TT\"]].to_numpy()\n", + "\n", + "# We need to have the same number of features for each item, we create dummy ones:\n", + "car_features = np.concatenate([car_features, np.zeros((len(car_features), 2))], axis=1)\n", + "train_features = np.concatenate([train_features, np.zeros((len(train_features), 1))], axis=1)\n", + "assert train_features.shape == car_features.shape == sm_features.shape\n", + "\n", + "contexts_items_features = np.stack([train_features, sm_features, car_features], axis=1)\n", + "print(contexts_items_features.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "contexts_items_availabilities = swiss_df[[\"TRAIN_AV\", \"SM_AV\", \"CAR_AV\"]].to_numpy()\n", + "# Re-Indexing choices from 1 to 3 to 0 to 2\n", + "choices = swiss_df.CHOICE.to_numpy() - 1" + ] + }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "swiss_dataset = ChoiceDataset(contexts_features=(contexts_features, ),\n", + " contexts_items_features=(contexts_items_features, ),\n", + " contexts_items_availabilities=contexts_items_availabilities,\n", + " contexts_features_names=([\"train_survey\", \"regular_class\", \"single_luggage_piece\", \"multiple_luggage_piece\"], ),\n", + " contexts_items_features_names=([\"cost\", \"travel_time\", \"headway\", \"seats\"], ),\n", + " choices=choices\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "swiss_dataset.summary()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Initialization of the model\n", + "swiss_model = ConditionalMNL(optimizer=\"lbfgs\")\n", + "\n", + "swiss_model.add_coefficients(coefficient_name=\"beta_inter\", feature_name=\"intercept\", items_indexes=[0, 1])\n", + "swiss_model.add_shared_coefficient(coefficient_name=\"beta_tt_transit\", feature_name=\"travel_time\", items_indexes=[0, 1])\n", + "swiss_model.add_coefficients(coefficient_name=\"beta_tt_car\", feature_name=\"travel_time\", items_indexes=[2])\n", + "swiss_model.add_coefficients(coefficient_name=\"beta_tc\", feature_name=\"cost\", items_indexes=[0, 1, 2])\n", + "swiss_model.add_coefficients(coefficient_name=\"beta_hw\", feature_name=\"headway\", items_indexes=[0, 1])\n", + "swiss_model.add_coefficients(coefficient_name=\"beta_seat\", feature_name=\"seats\", items_indexes=[1])\n", + "swiss_model.add_shared_coefficient(coefficient_name=\"beta_survey\", feature_name=\"train_survey\", items_indexes=[0, 1])\n", + "swiss_model.add_coefficients(coefficient_name=\"beta_first_class\", feature_name=\"regular_class\", items_indexes=[0])\n", + "swiss_model.add_coefficients(coefficient_name=\"beta_luggage=1\", feature_name=\"single_luggage_piece\", items_indexes=[2])\n", + "swiss_model.add_coefficients(coefficient_name=\"beta_luggage>1\", feature_name=\"multiple_luggage_piece\", items_indexes=[2])\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "history = swiss_model.fit(swiss_dataset, n_epochs=10000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "keep_output": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " 1:0' shape=(1, 1) dtype=float32, numpy=array([[1.413982]], dtype=float32)>]" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "swiss_model.weights" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "keep_output": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(swiss_dataset) * swiss_model.evaluate(swiss_dataset)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We find the same results (estimation of parameters and negative log-likelihood) as the PyLogit package." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### References\n", + "\n", + "[1] ModeCanada dataset in *Application and interpretation of nested logit models of intercity mode choice*, Christophier, V. F.; Koppelman, S. (1993)\\\n", + "[2] Conditional MultiNomialLogit, Train, K.; McFadden, D.; Ben-Akiva, M. (1987)\\\n", + "[3] Siwssmetro dataset in *The acceptance of modal innovation: The case of Swissmetro*, Bierlaire, M.; Axhausen, K.; Abay, G (2001)\\" + ] } ], "metadata": { diff --git a/notebooks/choice_learn_introduction_data.ipynb b/notebooks/choice_learn_introduction_data.ipynb index 23424bed..ba559f1d 100644 --- a/notebooks/choice_learn_introduction_data.ipynb +++ b/notebooks/choice_learn_introduction_data.ipynb @@ -1069,7 +1069,7 @@ "source": [ "### References\n", "[1] Koppelman et al. (1993), *Application and Interpretation of Nested Logit Models of Intercity Mode Choice*\\\n", - "[2] Bierlaire, M., Axhausen, K. and Abay, G. (2001), *The Acceptance of Modal Innovation: The Case of SwissMetro\"*" + "[2] Bierlaire, M., Axhausen, K. and Abay, G. (2001), *The Acceptance of Modal Innovation: The Case of SwissMetro*" ] }, { diff --git a/notebooks/rumnet_example.ipynb b/notebooks/rumnet_example.ipynb new file mode 100644 index 00000000..cd1945da --- /dev/null +++ b/notebooks/rumnet_example.ipynb @@ -0,0 +1,23 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Introduction to modelling with RUMnet" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}