{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "45727d75",
   "metadata": {},
   "source": [
    "\n",
    "File: 01-nested_solution.py\n",
    "\n",
    "\n",
    "Michel Bierlaire\n",
    "\n",
    "Mon Aug 04 2025, 09:55:09\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ae3564fa",
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "from IPython.core.display_functions import display\n",
    "from biogeme.biogeme import BIOGEME\n",
    "from biogeme.expressions import Beta\n",
    "from biogeme.models import loglogit, lognested\n",
    "from biogeme.nests import NestsForNestedLogit, OneNestForNestedLogit\n",
    "from biogeme.results_processing import (\n",
    "    EstimationResults,\n",
    "    get_pandas_estimated_parameters,\n",
    ")\n",
    "from scipy.stats import chi2\n",
    "\n",
    "from spec_swissmetro import av, v\n",
    "from variables_swissmetro import CHOICE, database\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "911f6c4e",
   "metadata": {},
   "source": [
    "The objective of this laboratory is to compare a nested logit specification with a logit specification."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "65b44f35",
   "metadata": {},
   "source": [
    "## Estimation of a logit model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f06e3adc",
   "metadata": {},
   "outputs": [],
   "source": [
    "logprob = loglogit(v, av, CHOICE)\n",
    "biogeme = BIOGEME(database, logprob)\n",
    "biogeme.model_name = 'logit'\n",
    "logit_results: EstimationResults = biogeme.estimate(recycle=True)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aeea49cb",
   "metadata": {},
   "source": [
    "General statistics"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ffd8ca98",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(logit_results.short_summary())\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1fd9c543",
   "metadata": {},
   "source": [
    "Estimated parameters"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a58bf27e",
   "metadata": {},
   "outputs": [],
   "source": [
    "logit_parameters = get_pandas_estimated_parameters(estimation_results=logit_results)\n",
    "display(logit_parameters)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d6163372",
   "metadata": {},
   "source": [
    "## Nested logit"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2afcf000",
   "metadata": {},
   "source": [
    "There are three possibilities to partition the choice set:\n",
    "\n",
    "- [Car, Train]  and [Swissmetro],\n",
    "- [Train, Swissmetro] and [Car],\n",
    "- [Car, Swissmetro] and [Train].\n",
    "\n",
    "The first one groups existing alternatives together. The second one groups public transportation modes together.\n",
    "The third one being less intuitive, we select the two first specifications.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ccf3c1f9",
   "metadata": {},
   "source": [
    "### Nested logit: existing alternatives"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b41af0ba",
   "metadata": {},
   "source": [
    "There are two nests: existing and future. The second one contains only one alternative. Therefore, its nest parameter\n",
    "is not identified and set to 1."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "18596dd8",
   "metadata": {},
   "outputs": [],
   "source": [
    "mu_existing = Beta('mu_existing', 1, 0, None, 0)\n",
    "existing = OneNestForNestedLogit(\n",
    "    nest_param=mu_existing, list_of_alternatives=[1, 3], name='existing'\n",
    ")\n",
    "future = OneNestForNestedLogit(nest_param=1.0, list_of_alternatives=[2], name='future')\n",
    "nests = NestsForNestedLogit(choice_set=list(v), tuple_of_nests=(existing, future))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8000152f",
   "metadata": {},
   "source": [
    "Estimation of the parameters"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9d5d98c2",
   "metadata": {},
   "outputs": [],
   "source": [
    "logprob = lognested(v, av, nests, CHOICE)\n",
    "biogeme = BIOGEME(database, logprob)\n",
    "biogeme.model_name = 'nested_existing'\n",
    "nested_existing_results: EstimationResults = biogeme.estimate(recycle=True)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "281d39cc",
   "metadata": {},
   "source": [
    "General statistics"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7b0dd336",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(nested_existing_results.short_summary())\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4b2e61d0",
   "metadata": {},
   "source": [
    "Estimated parameters"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "09afab28",
   "metadata": {},
   "outputs": [],
   "source": [
    "nested_existing_table = get_pandas_estimated_parameters(\n",
    "    estimation_results=nested_existing_results\n",
    ")\n",
    "display(nested_existing_table)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ba35560f",
   "metadata": {},
   "source": [
    "The nested parameter is greater than one, consistently with the theory."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9c0b0e27",
   "metadata": {},
   "outputs": [],
   "source": [
    "value_mu_existing = nested_existing_results.get_parameter_value('mu_existing')\n",
    "print(f'Nest parameter: {value_mu_existing:.3g}')\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b382f0ac",
   "metadata": {},
   "source": [
    "If we test the null hypothesis that the true value of the nest parameter is 1, we use a $t$-test.\n",
    "Remember that the t-test provided in the estimation report are against 0, not 1, and cannot be used for this purpose."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "017969fc",
   "metadata": {},
   "outputs": [],
   "source": [
    "mu_stderr = nested_existing_results.get_parameter_std_err('mu_existing')\n",
    "tested_value = 1\n",
    "t_test = (tested_value - value_mu_existing) / mu_stderr\n",
    "print(f't_test against 1: {t_test:.3g}')\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aee38898",
   "metadata": {},
   "source": [
    "Therefore, we can reject the null hypothesis at the 5% level. It means that we reject logit."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7cca661c",
   "metadata": {},
   "source": [
    "We can also test the null hypothesis that the two models are equivalent using a likelihood ratio test:"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b5e5b4d4",
   "metadata": {},
   "source": [
    "First, we calculate the statistic."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ccb4a166",
   "metadata": {},
   "outputs": [],
   "source": [
    "LL_logit = logit_results.final_loglikelihood\n",
    "LL_nested_existing = nested_existing_results.final_loglikelihood\n",
    "LR = -2 * (LL_logit - LL_nested_existing)\n",
    "print(f'Statistic for the LR test: {LR:.3g}')\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d47b3d16",
   "metadata": {},
   "source": [
    "Number of degrees of freedom:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1d337026",
   "metadata": {},
   "outputs": [],
   "source": [
    "dof = (\n",
    "    nested_existing_results.number_of_free_parameters\n",
    "    - logit_results.number_of_free_parameters\n",
    ")\n",
    "print(f'Degrees of freedom: {dof}')\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8e33834f",
   "metadata": {},
   "source": [
    "The threshold value of the $\\chi$-square test with one degree of freedom at 5% level is:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cd386cdc",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(f'Threshold for the test: {chi2.isf(0.05, dof):.3g}')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7a4a3ab5",
   "metadata": {},
   "source": [
    "The same test can also be performed directly as follows."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6b5ab21c",
   "metadata": {},
   "outputs": [],
   "source": [
    "lr_test_existing = nested_existing_results.likelihood_ratio_test(\n",
    "    logit_results, significance_level=0.05\n",
    ")\n",
    "print(f'{lr_test_existing.statistic=:.3g}')\n",
    "print(f'{lr_test_existing.threshold=:.3g}')\n",
    "print(lr_test_existing.message)\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "84851a52",
   "metadata": {},
   "source": [
    "Therefore, the null hypothesis can be rejected, and the nested logit model is preferred."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f9e001a5",
   "metadata": {},
   "source": [
    "### Nested logit: public transportation modes"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e8d39b24",
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "\n",
    "mu_public = Beta('mu_public', 1, 0, None, 0)\n",
    "\n",
    "public = OneNestForNestedLogit(\n",
    "    nest_param=mu_public, list_of_alternatives=[1, 2], name='public'\n",
    ")\n",
    "private = OneNestForNestedLogit(\n",
    "    nest_param=1.0, list_of_alternatives=[3], name='private'\n",
    ")\n",
    "nests = NestsForNestedLogit(choice_set=list(v), tuple_of_nests=(public, private))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "41cbfc92",
   "metadata": {},
   "source": [
    "Estimation of the parameters"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1f841ba1",
   "metadata": {},
   "outputs": [],
   "source": [
    "logprob = lognested(v, av, nests, CHOICE)\n",
    "biogeme = BIOGEME(database, logprob)\n",
    "biogeme.model_name = 'nested_public'\n",
    "nested_public_results: EstimationResults = biogeme.estimate(recycle=True)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13de8177",
   "metadata": {},
   "source": [
    "General statistics"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c4de7808",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(nested_public_results.short_summary())\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3d049dd6",
   "metadata": {},
   "source": [
    "Estimated parameters"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8673a8c9",
   "metadata": {},
   "outputs": [],
   "source": [
    "nested_public_table = get_pandas_estimated_parameters(\n",
    "    estimation_results=nested_public_results\n",
    ")\n",
    "display(nested_public_table)\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "40b7f146",
   "metadata": {},
   "source": [
    "The nest parameter is less than 1. This is inconsistent with the theory. The model is rejected."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9df4b295",
   "metadata": {},
   "outputs": [],
   "source": [
    "value_mu_public = nested_public_results.get_parameter_value('mu_public')\n",
    "print(f'Nest parameter: {value_mu_public:.3g}')\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "32f37b87",
   "metadata": {},
   "source": [
    "In conclusion, among the three models, the nested logit model where the existing alternatives are in the same nest\n",
    "is preferred."
   ]
  }
 ],
 "metadata": {},
 "nbformat": 4,
 "nbformat_minor": 5
}
