From 2dfd72bc5687eab48cf6cd31da35ffd0ff9b5fe8 Mon Sep 17 00:00:00 2001 From: knaaptime Date: Fri, 19 Jun 2026 13:08:46 -0700 Subject: [PATCH 1/7] refactor to choicemodel class and add sar style --- docs/source/_static/references.bib | 96 +- .../user-guide/spatial_models_demo.ipynb | 476 ++++- locpick/__init__.pyi | 17 +- locpick/_jax/sar_kernels.py | 163 ++ locpick/_jax/transforms.py | 31 + locpick/_kernels/mnl_numpy.py | 23 +- locpick/_solvers/optimistix.py | 8 - locpick/data/arrays.py | 7 - locpick/data/choicetable.py | 60 +- locpick/dgp.py | 323 +++- locpick/models/__init__.pyi | 6 + locpick/models/_spatial_weights.py | 132 ++ locpick/models/base.py | 7 +- locpick/models/choice_model.py | 1686 +++++++++++++++++ locpick/models/mixed.py | 100 +- locpick/models/mnl.py | 3 - locpick/models/nested.py | 71 - locpick/models/sar_mnl.py | 344 ++++ locpick/models/scl.py | 193 +- locpick/spec/__init__.pyi | 3 - locpick/spec/model_spec.py | 43 +- tests/test_estimation_problem.py | 20 +- tests/test_inference.py | 26 +- tests/test_marginal_effects_wtp.py | 28 +- tests/test_mixed_nested.py | 48 +- tests/test_mnl.py | 105 +- tests/test_nested_and_mixed.py | 32 +- tests/test_param_recovery.py | 34 +- tests/test_prediction_all_models.py | 69 +- tests/test_prediction_simulation.py | 24 +- tests/test_qmc_draws.py | 8 +- tests/test_sampling.py | 8 +- tests/test_sar_mnl.py | 180 ++ tests/test_solvers.py | 18 +- tests/test_sparse_design.py | 146 -- tests/test_spatial_models.py | 54 +- 36 files changed, 3702 insertions(+), 890 deletions(-) create mode 100644 locpick/_jax/sar_kernels.py create mode 100644 locpick/models/_spatial_weights.py create mode 100644 locpick/models/choice_model.py create mode 100644 locpick/models/sar_mnl.py create mode 100644 tests/test_sar_mnl.py delete mode 100644 tests/test_sparse_design.py diff --git a/docs/source/_static/references.bib b/docs/source/_static/references.bib index 663ddde..69f18f7 100644 --- a/docs/source/_static/references.bib +++ b/docs/source/_static/references.bib @@ -25,7 +25,6 @@ @article{abbe2007NormalizationCorrelation keywords = {Behavior models,Correlations,Generalized extreme-value,Logit models,Model estimation,Route choice}, } - @article{al-haideri2026CyclistsCrossing, title = {Cyclists' Crossing Behaviour at Roundabouts: {{A Generalized Spatially Correlated Nested Logit}} Model}, @@ -70,7 +69,6 @@ @article{al-haideri2026CyclistsCrossing behavioural mechanisms underlying cyclists' crossing decisions.}, langid = {english}, } - @article{anselin1988spatial, title = {Spatial Econometrics: Methods and Models}, author = {Anselin, Luc}, @@ -246,6 +244,69 @@ @article{dugundji2013StructureEmergence networks,Spatial interaction,Transportation demand}, } +@article{krisztin2021BayesianSpatial, + title = {A {{Bayesian}} Spatial Autoregressive Logit Model with an Empirical + Application to {{European}} Regional {{FDI}} Flows}, + author = {Krisztin, Tamás and Piribauer, Philipp}, + date = {2021-07-01}, + journaltitle = {Empirical Economics}, + shortjournal = {Empir Econ}, + volume = {61}, + number = {1}, + pages = {231--257}, + issn = {1435-8921}, + doi = {10.1007/s00181-020-01856-w}, + url = {https://doi.org/10.1007/s00181-020-01856-w}, + urldate = {2026-06-18}, + abstract = {In this paper, we propose a Bayesian estimation approach for a + spatial autoregressive logit specification. Our approach relies on + recent advances in Bayesian computing, making use of Pólya–Gamma + sampling for Bayesian Markov-chain Monte Carlo algorithms. The + proposed specification assumes that the involved log-odds of the + model follow a spatial autoregressive process. Pólya–Gamma sampling + involves a computationally efficient treatment of the spatial + autoregressive logit model, allowing for extensions to the existing + baseline specification in an elegant and straightforward way. In a + Monte Carlo study we demonstrate that our proposed approach + markedly outperforms alternative specifications in terms of + parameter precision. The paper moreover illustrates the performance + of the proposed spatial autoregressive logit specification using + pan-European regional data on foreign direct investments. Our + empirical results highlight the importance of accounting for + spatial dependence when modelling European regional FDI flows.}, + langid = {english}, + keywords = {Bayesian MCMC estimation,C11,C21,C25,European regions,F23,FDI + flows,R11,R30,Spatial autoregressive logit}, +} + +@article{krisztin2022SpatialMultinomial, + title = {A Spatial Multinomial Logit Model for Analysing Urban Expansion}, + author = {Krisztin, Tamás and Piribauer, Philipp and Wögerer, Michael}, + date = {2022-04-03}, + journaltitle = {Spatial Economic Analysis}, + volume = {17}, + number = {2}, + pages = {223--244}, + publisher = {Routledge}, + issn = {1742-1772}, + doi = {10.1080/17421772.2021.1933579}, + url = {https://doi.org/10.1080/17421772.2021.1933579}, + urldate = {2026-06-18}, + abstract = {The paper proposes a Bayesian multinomial logit model to analyse + spatial patterns of urban expansion. The specification assumes that + the log-odds of each class follow a spatial autoregressive process. + Using recent advances in Bayesian computing, our model allows for a + computationally efficient treatment of the spatial multinomial + logit model. This allows us to assess spillovers between regions + and across land-use classes. In a series of Monte Carlo studies, we + benchmark our model against other competing specifications. The + paper also showcases the performance of the proposed specification + using European regional data. Our results indicate that spatial + dependence plays a key role in the land-sealing process of cropland + and grassland. Moreover, we uncover land-sealing spillovers across + multiple classes of arable land.}, +} + @article{mcfadden1978goodness, title = {Goodness-of-Fit for the Multinomial Logit Model}, author = {McFadden, Daniel}, @@ -304,6 +365,37 @@ @article{perez-lopez2022SpatiallyCorrelated langid = {english}, } +@article{smirnov2010ModelingSpatial, + title = {Modeling Spatial Discrete Choice}, + author = {Smirnov, Oleg A.}, + date = {2010-09}, + journaltitle = {Regional Science and Urban Economics}, + volume = {40}, + number = {5}, + pages = {292--298}, + publisher = {Elsevier B.V.}, + issn = {01660462}, + doi = {10.1016/j.regsciurbeco.2009.09.004}, + url = {http://dx.doi.org/10.1016/j.regsciurbeco.2009.09.004}, + abstract = {The paper presents a basic spatial discrete choice modeling + framework obtained by applying random utility theory to discrete + choices made by heterogeneous spatially dependent individuals. The + newly developed framework has two main advantages over existing + approaches. First, individual decision-makers are no longer assumed + to be independent and non-interacting but spatially interdependent + in their preferences facilitating the development of applied + discrete choice models using a wide range of spatial data. Second, + pseudo maximum likelihood estimator is developed for this model + that is consistent and computationally feasible for large datasets. + The performance of the pseudo maximum likelihood estimator for the + spatial discrete choice model is illustrated using simulated data. + © 2009 Elsevier B.V.}, + isbn = {0166-0462}, + keywords = {Discrete choice,Maximum likelihood,Spatial interdependence,Spatial + random utility,urban-modeling}, + annotation = {44 citations (Crossref) [2022-08-11]}, +} + @book{train2009discrete, title = {Discrete Choice Methods with Simulation}, author = {Train, Kenneth E.}, diff --git a/docs/source/user-guide/spatial_models_demo.ipynb b/docs/source/user-guide/spatial_models_demo.ipynb index 443c22f..ae08d0f 100644 --- a/docs/source/user-guide/spatial_models_demo.ipynb +++ b/docs/source/user-guide/spatial_models_demo.ipynb @@ -18,7 +18,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -31,6 +31,95 @@ "from locpick.models.nested import NestingTree, NestSpec" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "import geosnap as gsp" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "datasets = gsp.DataStore()" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "dc = gsp.io.get_acs(datasets, years=2019, level='tract', state_fips='11')" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAx0AAAM6CAYAAADkMC4dAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjksIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvJkbTWQAAAAlwSFlzAAAewgAAHsIBbtB1PgABAABJREFUeJzsnQd4I+X19a8sy7LcrXXvW9heWdilJUACIdQECITO7lJD+ZME0hMSQr5USAIp1GXpvbdQQ8vCrrf3Xtx777Zsfc957TGyrDIzmhmN5Pt7oqyx5dFoJNvvee+951jcbrebGIZhGIZhGIZhdCJGrwMzDMMwDMMwDMOw6GAYhmEYhmEYRne40sEwDMMwDMMwjK6w6GAYhmEYhmEYRldYdDAMwzAMwzAMoyssOhiGYRiGYRiG0RUWHQzDMAzDMAzD6AqLDoZhGIZhGIZhdIVFB8MwDMMwDMMwusKig2EYhmEYhmEYXWHRwTAMwzAMwzCMrrDoYBiGYRiGYRhGV1h0MAzDMAzDMAyjKyw6GIZhGIZhGIbRFRYdDMMwDMMwDMPoCosOhmEYhmEYhmF0JVbfwzORSm9vL23btk18nJmZSbGx/FZhGIZhGIYJhsvlooaGBvHxvHnzKD4+ni8aiw7GHxAcS5Ys4QvEMAzDMAyjktLSUjr66KP5+nF7FcMwDMMwDMMwesM9M4xP0FLlqdJzc3P5SjEMwzAMwwShpqZmtFvEcz010WHRwfh+Y3jMcEBwFBQU8JViGIZhGIZRstDmmdhR2L2KYRiGYRiGYRhdYdHBMAzDMAzDMIyusOhgGIZhGIZhGEZXWHQwDMMwDMMwDKMrLDoYhmEYhmEYhtEVFh0MwzAMwzAMw+gKiw6GYRiGYRiGYXSFRQfDMAzDMAzDMJEpOtrb2+nZZ5+lW2+9lU488USaNm0apaamUlxcHGVlZdFJJ51Ef/7zn6mpqSnosQ4dOkQ/+MEPaO7cuZScnEyJiYk0ffp0uvHGG2nHjh2anjfO59e//jUtWLBAnG9KSor4GJ+Tc64SX3zxBV1++eVUUlJC8fHxImDvm9/8prgmcnG5XPTAAw/QV7/6VZFo6XA4xHW8/vrraefOnSqfIcMwDMMwDMMYjFsn3n//fTcOH+yWkZHhfuedd/we54EHHnDHxcX5/X587d///rcm51xaWurOzc31+1h5eXnudevWBT3OHXfc4Y6JifF7nLPPPtvd09MT8BiNjY3upUuX+j2G3W53r1y50q0XFRUVo4+FjxmGYRiGYRheQ6lF1/aqwsJCuuKKK+iee+6hl19+Wez+r169mp577jm64IILyGq1UmNjI51zzjm0devWcd+PqsB1111H/f39ourw29/+lv73v//RunXr6MEHHxS7/vgaKh4vvvhiSOdaVVVFZ599NtXU1IjI+h//+Mf06aefihs+xueqq6vprLPOEvf1x8MPPyyqIkNDQzR16lRauXIllZaW0quvvkonn3yyuM8bb7xBV199td9jDA4O0nnnnUdr164V/42P//Of/4j/vvfee0WlqK+vj6699lp69913Q3reDMMwDMMwDKM7bp1wuVxB7/PKK6+M7qafd955Y77W1dXlzsrKEl9LSkpyb9u2bdz3t7W1uefNmyfuk5OT4+7s7FR9vldeeeXouTz//PPjvo7PSV9fvny5z2O0tLS409LSxH2KiorcDQ0N464JqhzScT755BOfx1m1atXofW644YZxX9+3b587JSVFfP2II45wDwwMuLWGKx0MwzAMwzC8hjJ9pQNVjGB8+9vfppkzZ4qPUVHwBDv79fX14uNbbrlFzHN4g3mLv/71r+Lj2tpaevTRR1Wda11dHT355JPi49NOO01UYbzB5/A18Pjjj4vv8eahhx6i1tZW8fGf/vQnysjIGHdN/v3vf49em7/85S8+z0f6fHp6us/7oMLzs5/9THy8b98+eu211xQ/Z4ZhGIZhGIaZMO5VGAoHvb29Yz6PFiqJ008/3e/3YyAdg9pAbYvV66+/LlqawPLly/3eb9myZeJf3Bff4w1aqCQxhJYoXxQUFNApp5wiPn7//feps7NzzNchIqQh8e9+97uUkJAQ8FwAWtcYhmEYhmEYxqyEVXTs2rWLNm/eLD6WKh4Szc3Nox9nZ2f7PQZmLZxOp/j4888/F45PSvnss89GP4bTlj88v4bZEk8wW4LZDXDssccKl65gx8Fchqe4UnIuOTk5wsHL17kwDMMwDMMwjJmINfoBu7u7xSA2hqlhmStVGNBC5asCAtra2vwez+12C3teaeG/f//+cQJGjvgBGFbHYt4fsL1FFQOPJ32PZ4VCEjzBHt/z6ziONGDueS5yj7N3716qqKigrq6uMdcsGJWVlQG/joF6hmEYhmEYhokY0YFZ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pFctmlUq93x3miRw0jgREYURgw4iohhd6Vg0s1T+77vTGXAQEVHYMeggIopyZtPIoOM0BBxnMOAgIqLIwKCDiCjKDY85Tp1RIr9hwEFERBGEQQcRUQw5ZUaJ/PbMGUypIiKiiMKgg4goyvX129W/J08vkd9yhYOIiCIQgw4ioijXP2CXE6cVy71nTvfYyYqIiChcEsP2zEREpItjpxTJBQdWMuAgIqKIxaCDiCjKXXJIdbg3gYiIyCOuwxMRERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaESjX14ilZ9fX2O6zt37gzrthARERFFC+fjJufjqXjHoINc2rt3r+P6vHnz+C4RERERBXA8VVVVxfeN6VVERERERGQ0k91utxv+LBR1urq6ZNWqVep6fn6+JCZyUczXJVVtZWjp0qVSXFxs6H6iyML9H9+4/+Mb9398c97/n3zyieO4aerUqZKcnBzmrYsMPJIkl/ABmTt3Lt+dICDgKCsr43sYp7j/4xv3f3zj/o9v5eXl/PvvArtXERERERGRoRh0EBERERGRoRh0EBERERGRoRh0EBERERGRoRh0EBERERGRoRh0EBERERGRoRh0EBERERGRoTgckIiIiIiIDMWVDiIiIiIiMhSDDiIiIiIiMhSDDiIiIiIiMhSDDiIiIiIiMhSDDiIiIiIiMhSDDiIiIiIiMhSDDiIiIiIiMhSDDiIiIiIiMhSDDiIiIiIiMhSDDiIiIiIiMhSDDoort956q5hMJr8u+J7hqqqq/H6cLVu2BLTN3d3d8tlnn8nvf/97Of/882X8+PFiNpsdj+uvbdu2yQ033CATJ06U1NRUycnJkXnz5slvfvMb6ejokFgWjftf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zZdISUlRzzF27FivXc7c4ec/dvf/kgj6+8+gg+La73//e8eHEQN2AjV79mz1GImJifbdu3f7/H233HKL4/kff/xxw4IOOPTQQx3b+Mknn4y4/Z577nE8JrYrHkTD/n/00Ucd98EAuOE2bNhgz8jIcLQ57O3tHXGff/7zny6/7q5lbjDvRTSJl/0P/Pzru/+PO+44xz531aJ6x44d9rKyMkc70+3btwe0/9H+dGBgwOVt+FmbNm2a4zHeeustl/fj5z++938kff4ZdFBcmzt3rvqQJSQkqAOvQKxZs8bxYcWEUn/48ktn586d6jbny/jx44d8n/Nl/fr1Lh/nyy+/dJwRSUtLs99xxx32Tz/91P7f//7XfsUVVzgeD32/A52gGm2iYf+jVzsm5Gr3O/300+1vvvmmOnuFP5oFBQXq65iS+/rrr7t8jMrKSntJSYn92muvtT/77LPqj87y5cvVH6lf/OIX9tzcXMfjH3LIIfauri57PIiX/Q/8/Ou7/9etW6fOCOP7bTab+hzh4BPzGB588EF7aWmpY5/dfffdAe9/BJrV1dVq0v1//vMftR/ffvttNZfF+XOL293h5z++938kff4ZdFDcWrt2reODdvzxxwf8OD/5yU8CTm3x5ZfOu+++67iPLxdPKyY446WdFXV1wS8cd0FLrImW/Q979+51/IF0dcGZtP/3//6fx4MOX352cEDraYhYLImn/a/h51/f/Y+DzMLCQrf7xWQy2X/5y1+6/X5fDzo9fWZxIHnvvfd63E5+/uN7/0fS55/dqyhu6VFAOjAwIM8884y6jhZ2J598skQyDBH66quv5P7771etUdFCDwPhxowZI2eccYZ8//vfF5vNJvEgmvY/Wh5+8skn8sgjj6iiQbRHRFEgir3RC/76669XHVXcefLJJ1WR4KeffiqbNm1SxY0ockQvdnQ7Ouigg1THtQMPPFDiRTztfw0///rufxTofv3112qYJmYlbN68WbXgRav2hQsXyrXXXiszZ86UYFx55ZVqEBw+v1u2bFGtWvG5xfyGE044QS677DJ13RN+/uN7/0fS59+EyMPQZyAiIiIiorjGlrlERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERMSgg4iIiIiIohdXOoiIiIiIyFAMOoiIiIiIyFAMOoiIiIiISIz0/wEGcVMI+ACIngAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": { + "image/png": { + "height": 413, + "width": 398 + } + }, + "output_type": "display_data" + } + ], + "source": [ + "dc.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "from libpysal.graph import Graph\n", + "\n", + "dc_graph = Graph.build_contiguity(dc, rook=False)\n", + "\n", + "adj = dc_graph.sparse.todense()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "markdown", "metadata": {}, @@ -46,13 +135,81 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[31mSignature:\u001b[39m\n", + "simulate_scl(\n", + " n_obs: \u001b[33m'int'\u001b[39m = \u001b[32m10000\u001b[39m,\n", + " n_alts: \u001b[33m'int'\u001b[39m = \u001b[32m12\u001b[39m,\n", + " alt_params: \u001b[33m'dict[str, float] | None'\u001b[39m = \u001b[38;5;28;01mNone\u001b[39;00m,\n", + " rho: \u001b[33m'float'\u001b[39m = \u001b[32m0.7\u001b[39m,\n", + " adjacency: \u001b[33m'np.ndarray | None'\u001b[39m = \u001b[38;5;28;01mNone\u001b[39;00m,\n", + " seed: \u001b[33m'int'\u001b[39m = \u001b[32m1234\u001b[39m,\n", + ") -> \u001b[33m'SCLDataset'\u001b[39m\n", + "\u001b[31mDocstring:\u001b[39m\n", + "Generate synthetic SCL choice data with known parameters.\n", + "\n", + "The default DGP creates a circular adjacency graph with 12 zones,\n", + "includes both alternative-level and chooser×alternative interaction\n", + "terms, and simulates choices using the SCL probability formula.\n", + "\n", + "Parameters\n", + "----------\n", + "n_obs : int, default 10000\n", + " Number of observations (decision-makers).\n", + "n_alts : int, default 12\n", + " Number of alternatives (zones).\n", + "alt_params : dict, optional\n", + " Mapping of alternative-level column name → true coefficient.\n", + " Default: ``{\"cost\": -0.5, \"time\": -0.1}``.\n", + "rho : float, default 0.7\n", + " True dissimilarity parameter ρ ∈ (0, 1].\n", + "adjacency : np.ndarray, optional\n", + " Binary adjacency matrix of shape ``(n_alts, n_alts)``.\n", + " Default: circular graph where zone *i* is adjacent to\n", + " ``(i-1) % n_alts`` and ``(i+1) % n_alts``.\n", + "seed : int, default 1234\n", + " Random seed for reproducibility.\n", + "\n", + "Returns\n", + "-------\n", + "SCLDataset\n", + "\u001b[31mFile:\u001b[39m ~/Dropbox/projects/locpick/locpick/dgp.py\n", + "\u001b[31mType:\u001b[39m function" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "simulate_scl?" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Observations: 2000\n", + "Alternatives: 179\n", + "True rho: 0.7\n", + "Adjacency shape: (179, 179)\n" + ] + } + ], "source": [ "# Generate synthetic SCL data with spatial correlation\n", "n_obs = 2000\n", - "n_alts = 20\n", + "n_alts = dc.shape[0]\n", "\n", "scl_dataset = simulate_scl(\n", " n_obs=n_obs,\n", @@ -60,6 +217,7 @@ " alt_params={\"cost\": -0.5, \"time\": -0.2},\n", " rho=0.7,\n", " seed=42,\n", + " adjacency=adj\n", ")\n", "\n", "ct = scl_dataset.choice_table\n", @@ -87,9 +245,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Models configured:\n", + " Spatial MNL: MNL\n", + " Spatial NestedMNL: NestedMNL\n", + " Spatial MixedMNL: MixedMNL\n", + " Spatial MixedNestedMNL: MixedNestedMNL\n" + ] + } + ], "source": [ "# Common formula for all models\n", "formula = \"cost + time - 1\"\n", @@ -160,9 +330,38 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 32, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== Spatial MNL ===\n", + "============================================================\n", + "Spatially Correlated Logit Estimation Results\n", + "============================================================\n", + "Observations: 2000\n", + "Alternatives: 179\n", + "Parameters: 3\n", + "DF residual: 1997\n", + "------------------------------------------------------------\n", + "Log-likelihood: -7883.9502\n", + "LL (null): -10374.7716\n", + "AIC: 15773.9004\n", + "BIC: 15790.7031\n", + "Rho-squared: 0.2401\n", + "Adj. rho-sq: 0.2398\n", + "------------------------------------------------------------\n", + "Parameter Coef Std.Err t P>|t|\n", + "------------------------------------------------------------\n", + "cost -0.4940 0.0172 -28.659 0.0000\n", + "time -0.1993 0.0070 -28.287 0.0000\n", + "rho 0.7088 0.1051 6.743 0.0000\n", + "============================================================\n" + ] + } + ], "source": [ "# Fit spatial MNL\n", "result_scl = model_scl.fit()\n", @@ -172,9 +371,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== Spatial NestedMNL ===\n", + "============================================================\n", + "Nested Spatially Correlated Logit Estimation Results\n", + "============================================================\n", + "Observations: 2000\n", + "Alternatives: 179\n", + "Parameters: 6\n", + "DF residual: 1994\n", + "------------------------------------------------------------\n", + "Log-likelihood: -7887.6327\n", + "LL (null): -10374.7716\n", + "AIC: 15787.2654\n", + "BIC: 15820.8708\n", + "Rho-squared: 0.2397\n", + "Adj. rho-sq: 0.2392\n", + "------------------------------------------------------------\n", + "Parameter Coef Std.Err t P>|t|\n", + "------------------------------------------------------------\n", + "cost -0.5268 0.0143 -36.768 0.0000\n", + "time -0.2140 0.0051 -42.008 0.0000\n", + "rho_urban 1.0000 nan nan 1.0000\n", + "rho_suburban 1.0000 0.0060 166.820 0.0000\n", + "lambda_urban 1.0000 nan nan 1.0000\n", + "lambda_suburban 0.9989 0.0985 10.140 0.0000\n", + "============================================================\n" + ] + } + ], "source": [ "# Fit spatial NestedMNL\n", "result_nested_scl = model_nested_scl.fit()\n", @@ -184,9 +415,39 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== Spatial MixedMNL ===\n", + "============================================================\n", + "Mixed Spatially Correlated Logit Estimation Results\n", + "============================================================\n", + "Observations: 2000\n", + "Alternatives: 179\n", + "Parameters: 4\n", + "DF residual: 1996\n", + "------------------------------------------------------------\n", + "Log-likelihood: -7883.9503\n", + "LL (null): -10374.7716\n", + "AIC: 15775.9006\n", + "BIC: 15798.3042\n", + "Rho-squared: 0.2401\n", + "Adj. rho-sq: 0.2397\n", + "------------------------------------------------------------\n", + "Parameter Coef Std.Err t P>|t|\n", + "------------------------------------------------------------\n", + "cost -0.4940 0.0172 -28.659 0.0000\n", + "rho 0.7089 0.1051 6.744 0.0000\n", + "mean_time -0.1993 0.0070 -28.287 0.0000\n", + "sd_time -11.5144 101.0278 -0.114 0.9093\n", + "============================================================\n" + ] + } + ], "source": [ "# Fit spatial MixedMNL\n", "result_mixed_scl = model_mixed_scl.fit()\n", @@ -196,9 +457,50 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 35, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/knaaptime/Dropbox/projects/locpick/locpick/models/mixed.py:214: UserWarning: The balance properties of Sobol' points require n to be a power of 2.\n", + " uniform = sampler.random(n=n_total)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== Spatial MixedNestedMNL ===\n", + "============================================================\n", + "Mixed Nested Spatially Correlated Logit Estimation Results\n", + "============================================================\n", + "Observations: 2000\n", + "Alternatives: 179\n", + "Parameters: 7\n", + "DF residual: 1993\n", + "------------------------------------------------------------\n", + "Log-likelihood: -7887.6327\n", + "LL (null): -10374.7716\n", + "AIC: 15789.2655\n", + "BIC: 15828.4718\n", + "Rho-squared: 0.2397\n", + "Adj. rho-sq: 0.2391\n", + "------------------------------------------------------------\n", + "Parameter Coef Std.Err t P>|t|\n", + "------------------------------------------------------------\n", + "cost -0.5267 0.0140 -37.620 0.0000\n", + "rho_urban 1.0000 nan nan 1.0000\n", + "rho_suburban 1.0000 0.0027 376.534 0.0000\n", + "lambda_urban 0.9920 0.8306 1.194 0.2324\n", + "lambda_suburban 1.0000 nan nan 1.0000\n", + "mean_time -0.2139 0.0051 -42.057 0.0000\n", + "sd_time 114.1125 3108247384245457486086144.0000 0.000 1.0000\n", + "============================================================\n" + ] + } + ], "source": [ "# Fit spatial MixedNestedMNL\n", "result_mixed_nested_scl = model_mixed_nested_scl.fit()\n", @@ -217,9 +519,27 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 36, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Log-Likelihood Null LL AIC BIC \\\n", + "Spatial MNL -7883.9502 -10374.7716 15773.9004 15790.7031 \n", + "Spatial NestedMNL -7887.6327 -10374.7716 15787.2654 15820.8708 \n", + "Spatial MixedMNL -7883.9503 -10374.7716 15775.9006 15798.3042 \n", + "Spatial MixedNestedMNL -7887.6327 -10374.7716 15789.2655 15828.4718 \n", + "\n", + " Rho² Adj. Rho² n_params \n", + "Spatial MNL 0.2401 0.2398 3.0 \n", + "Spatial NestedMNL 0.2397 0.2392 6.0 \n", + "Spatial MixedMNL 0.2401 0.2397 4.0 \n", + "Spatial MixedNestedMNL 0.2397 0.2391 7.0 \n" + ] + } + ], "source": [ "# Collect fit statistics for comparison\n", "results = {\n", @@ -258,9 +578,74 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 37, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Spatial MNL probabilities shape: (2000, 179)\n", + "Probabilities sum to 1: True\n", + "\n", + "First 3 decision-makers' probabilities:\n", + "[[0.0008 0.0038 0.0002 0.0428 0.0206 0. 0.0199 0.0003 0.0014 0.0001\n", + " 0.0002 0. 0.0066 0.0127 0.0146 0. 0.0073 0.009 0.0109 0.0043\n", + " 0.0002 0.0043 0.0003 0.0011 0.0015 0.0003 0.0009 0.0005 0.0002 0.0008\n", + " 0. 0.0001 0.0004 0.0003 0.0757 0.0018 0.0012 0. 0.0048 0.0025\n", + " 0.0018 0.0125 0.0004 0.0003 0.0002 0.0001 0. 0.0003 0.0008 0.\n", + " 0.0002 0.0816 0.0004 0.0226 0.0004 0.0001 0.0071 0.0009 0.0002 0.0001\n", + " 0.0011 0.0028 0. 0.0004 0.0208 0.0019 0. 0.001 0.0002 0.0232\n", + " 0.0002 0.0293 0.0021 0.0574 0.039 0.0006 0.0005 0.0005 0.0046 0.0015\n", + " 0.002 0.0045 0.0016 0.0081 0.0043 0.0003 0. 0.0007 0.0002 0.0039\n", + " 0.0016 0.0068 0.0012 0. 0.0001 0. 0.014 0.0023 0.0003 0.0189\n", + " 0.0003 0.0004 0.0004 0.0012 0.0036 0.0003 0.0047 0.0003 0. 0.0039\n", + " 0.0002 0.0017 0.0003 0.0026 0.0051 0.0111 0.0138 0.013 0.0003 0.0003\n", + " 0.0008 0.0013 0.0001 0.0003 0.0075 0.0157 0.0278 0.0001 0.0178 0.0013\n", + " 0.0008 0.0003 0.0001 0.0024 0. 0.0015 0. 0.0016 0.005 0.\n", + " 0.0017 0. 0.0018 0.0038 0.0002 0.0052 0.0008 0.0006 0.0191 0.002\n", + " 0.0004 0.0409 0.0001 0.0012 0.003 0.0014 0. 0.0005 0.0219 0.0265\n", + " 0.0056 0.0001 0. 0.0463 0.0012 0.0005 0.0006 0. 0.0018 0.0017\n", + " 0.0033 0.0004 0.0004 0.0004 0.0151 0.0011 0.0006 0.0001 0.0011]\n", + " [0.0008 0.0038 0.0002 0.0428 0.0206 0. 0.0199 0.0003 0.0014 0.0001\n", + " 0.0002 0. 0.0066 0.0127 0.0146 0. 0.0073 0.009 0.0109 0.0043\n", + " 0.0002 0.0043 0.0003 0.0011 0.0015 0.0003 0.0009 0.0005 0.0002 0.0008\n", + " 0. 0.0001 0.0004 0.0003 0.0757 0.0018 0.0012 0. 0.0048 0.0025\n", + " 0.0018 0.0125 0.0004 0.0003 0.0002 0.0001 0. 0.0003 0.0008 0.\n", + " 0.0002 0.0816 0.0004 0.0226 0.0004 0.0001 0.0071 0.0009 0.0002 0.0001\n", + " 0.0011 0.0028 0. 0.0004 0.0208 0.0019 0. 0.001 0.0002 0.0232\n", + " 0.0002 0.0293 0.0021 0.0574 0.039 0.0006 0.0005 0.0005 0.0046 0.0015\n", + " 0.002 0.0045 0.0016 0.0081 0.0043 0.0003 0. 0.0007 0.0002 0.0039\n", + " 0.0016 0.0068 0.0012 0. 0.0001 0. 0.014 0.0023 0.0003 0.0189\n", + " 0.0003 0.0004 0.0004 0.0012 0.0036 0.0003 0.0047 0.0003 0. 0.0039\n", + " 0.0002 0.0017 0.0003 0.0026 0.0051 0.0111 0.0138 0.013 0.0003 0.0003\n", + " 0.0008 0.0013 0.0001 0.0003 0.0075 0.0157 0.0278 0.0001 0.0178 0.0013\n", + " 0.0008 0.0003 0.0001 0.0024 0. 0.0015 0. 0.0016 0.005 0.\n", + " 0.0017 0. 0.0018 0.0038 0.0002 0.0052 0.0008 0.0006 0.0191 0.002\n", + " 0.0004 0.0409 0.0001 0.0012 0.003 0.0014 0. 0.0005 0.0219 0.0265\n", + " 0.0056 0.0001 0. 0.0463 0.0012 0.0005 0.0006 0. 0.0018 0.0017\n", + " 0.0033 0.0004 0.0004 0.0004 0.0151 0.0011 0.0006 0.0001 0.0011]\n", + " [0.0008 0.0038 0.0002 0.0428 0.0206 0. 0.0199 0.0003 0.0014 0.0001\n", + " 0.0002 0. 0.0066 0.0127 0.0146 0. 0.0073 0.009 0.0109 0.0043\n", + " 0.0002 0.0043 0.0003 0.0011 0.0015 0.0003 0.0009 0.0005 0.0002 0.0008\n", + " 0. 0.0001 0.0004 0.0003 0.0757 0.0018 0.0012 0. 0.0048 0.0025\n", + " 0.0018 0.0125 0.0004 0.0003 0.0002 0.0001 0. 0.0003 0.0008 0.\n", + " 0.0002 0.0816 0.0004 0.0226 0.0004 0.0001 0.0071 0.0009 0.0002 0.0001\n", + " 0.0011 0.0028 0. 0.0004 0.0208 0.0019 0. 0.001 0.0002 0.0232\n", + " 0.0002 0.0293 0.0021 0.0574 0.039 0.0006 0.0005 0.0005 0.0046 0.0015\n", + " 0.002 0.0045 0.0016 0.0081 0.0043 0.0003 0. 0.0007 0.0002 0.0039\n", + " 0.0016 0.0068 0.0012 0. 0.0001 0. 0.014 0.0023 0.0003 0.0189\n", + " 0.0003 0.0004 0.0004 0.0012 0.0036 0.0003 0.0047 0.0003 0. 0.0039\n", + " 0.0002 0.0017 0.0003 0.0026 0.0051 0.0111 0.0138 0.013 0.0003 0.0003\n", + " 0.0008 0.0013 0.0001 0.0003 0.0075 0.0157 0.0278 0.0001 0.0178 0.0013\n", + " 0.0008 0.0003 0.0001 0.0024 0. 0.0015 0. 0.0016 0.005 0.\n", + " 0.0017 0. 0.0018 0.0038 0.0002 0.0052 0.0008 0.0006 0.0191 0.002\n", + " 0.0004 0.0409 0.0001 0.0012 0.003 0.0014 0. 0.0005 0.0219 0.0265\n", + " 0.0056 0.0001 0. 0.0463 0.0012 0.0005 0.0006 0. 0.0018 0.0017\n", + " 0.0033 0.0004 0.0004 0.0004 0.0151 0.0011 0.0006 0.0001 0.0011]]\n" + ] + } + ], "source": [ "# Predict probabilities using model.probabilities() (no arguments uses fitted parameters)\n", "probs_scl = model_scl.probabilities()\n", @@ -282,9 +667,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 38, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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K9rbCy+8kI08DVFhLD6kF48e0hqSzmbgwIFpm222yeYnT56cnn/++SaXv+666woVQ/fbb7922UeaFl/4//jHPxaSDDfddNNmD1n+5Tx06+ajZykRlPnRj36UllxyybJG7BaPxoygTinRLiIuxIS4WHD22WeXXPbggw8utIaI1kwR+KBlxowZk/r06VN4v/7kk08cSgAAaAfiqjSMIzU3ODa+/+YDYyPpodRAwaaMHDmy0PI44iA//OEPF4kH4gtf+EL2/TcXhROgFjg3oW1j/i0R14aitXKPHv9r6BnVDI877riK39vjdtFtrrnrq//4xz+y+RisENemqilaSufHLETMvdS1j6997Wtpyy23zOYjRhxVFZuz2267Zf9/9NFHWfVEgM7O1WGoQXn7g7DaaqtVbb0xKiUS3GK65557CtuKVo1RjSySImI0R7QgPPLII7PWkOWIYMtf//rXrDXkqFGj0korrZRVeowL8tG6MD70RZnuDz/8sOQ6Yn/yfSuW/67h1HDUafHfmvKf//wnXXLJJdkH2PXXXz+7zzFaZsCAAdn9juS/uC+1oDho9tWvfjV95jOfydp35m6++eZCu+9yRSXOww8/PEs8WXzxxbMP4xtuuGHWaqOSyljFj1ecV81tM0bL7rzzztkon/79+2fHfNlll822ffTRR7eohHp8gI8vHzGKOdYb7U9jvTEqOkYpxzkcZdLLKbcfiTuxH/E8iP2KSmIrrLBCFlSMymLljEiKRKGG52c8v2LUVAQ04xzLn19HHHFE1lKmre/vY489VtinWD5P8ivni2icH/lty30tKEfxqPPmAsP53+OL6l577VXRdqKtfZzXX//619Pqq6+e+vXrlz2u0e72K1/5Sjr55JPTtGnTmlxHfv8nTZpU+F28xjX2mtSw/XBj50N8GY7txmtPnGfxRTXOuXJfy+I8HDx4cOHvUfa/OWeddVZh+TgHKz3vGpo4cWKh/UEkzpf6sl38flO8zXhPa3jsikX131LHNDd27NjCMjEf4tweP3582mqrrbLXynis4+///Oc/F7htvA9F64No9RDLxXke71fxXhWPRbQcinZLU6dObXSb8fjn4rxo7FyIx749xCjMXFOVGPKLKyEuksQ5VErsf3HrkxjJWg3x+MR+xDkbIz7jeRiPUTwv43jF+0O0sIg2760Rx6Hc8674s1CxV199NbuAFO+7sZ+9evXK3rdiHcOHD88+J0SbkebeM+P1fvTo0dn8G2+8kf72t7+16r4BAADlEVf9fxbluGp87887z1x++eVNxgevv/767MJ/S6sl5qKFcy4G+pVTqbEr2GijjQrz0eWn+BpAxHbycyuPN0SMM5IsdthhhyxmF9cQ4u/F8Ytqx20bDoSO8yO/hrHKKqtk8cM4Dypp21pODKuhKEIQMdx4/kSb1Hj+REeMqAQWz6uINxTHb1t7zaRhvOP000/PEo7ivke8I57DsS+RGFRp/DcGwcZzPuIl8RjGYxKxql/+8pdNXgfqKudmXBOL18Mdd9wxO4di2XgsI25/4IEHpjvuuKPF+x2vHcccc0z63Oc+lw1gzs+R6Ijy+uuv10wsLtYfxyrWF8+lOAciFhbH9tNPP2329o3Fz1uiscey4d+Kr7NGzLDUc6hUnLHceO8777yTtd6N4xAx2Dgu8R4ZbYSjPW+51QHjtSiOb1wTiXMqzoH82kacF7H+eM+J18dyjnW1Yv7VENdm4lpycYw9Wj2XI45HntRY7rWl1r63lxJVG/Njv9Zaa2X3qynxepkr9X5XLK5T5OJzDECnVw/UnG233Ta+AWfTbrvtVrX1jhw5srDeu+++u/6mm26qHzBgQOF3DadevXrV//KXv2xyndOmTatfZpllSq6jeBo4cGD97bff3uh6Yn/KWUc+vfTSSwvcvvhvpZx//vn13bt3L2v9m2++ef2MGTOavO+XXHJJYfn99tuvvpo+/fTT+tVWW62w/osuuqjwt7XWWqvw+1/96ldlrzPuf8+ePUve5xVXXLH+/vvvL+t+FT9ecV6Vsuuuu5Z1vOvq6uqPOuqo+vnz55d1X+69994FjkNT0wknnFByPf/973/rv/GNbzS7jnieXHfddU3u0yqrrLLA+XnDDTfUL7nkkiXX2adPn/qbb765ze/vBhtsUPjb5ZdfXtb2LrzwwsJtNt100/pqve7EeufMmVO/9NJLZz+vuuqq2bnemDiGcV7Ectttt132u913372wrtNOO63kNocNG1bWsVpsscXqx40bV3I9lbwmxfOmqfPht7/9bX3v3r0Xut16661XcpuNueeee+q7detWWOaWW24puf8PPfRQdh/zZa+44or61tppp50K67vmmmvKetybm4rFa06pY5qLx774PHj66afrP/e5zzW67scee6xwu3/84x/1K6+8ctn7NW/evEa32dwUj317iPfwfJuLL754yeU22mijwnJnnnlms+v9+9//Xlg+3jNnz57dqv2cPHlyfb9+/co6dvGa8Oijjza5vng+lTrWxX8rZ4r3smK/+c1vstfmcm678cYbN3vfzzvvvMLy3/3ud1t4BAEAgEqIqy6acdXi9X35y1/OfnfooYcWfve3v/2t5G0322yzwnfgN954o/62224r6zt+cQwjj8dFDCv/XcSxyom9NRVbLY6vNBULq6bi+1XOY/O73/1ugcf/tddeK/k4x98i1tjYeROx1LaK24b33nuvfquttmpyXWPGjMmWa3gdpbnjVCqGlXv11VfrR48eXdbzJz9/q3HNJHzyySf1p5xySqNxyeKpR48e9SeddFLJWG2x448/foH4ZMPps5/9bBavaxjD6yrnZsS61lhjjWYfj3jM//Of/zS5jw1jyL///e+z63Kl1hnXG5o736sdi2t4Lej999+v33HHHZtcb8Tni493Ofe9pc+zpt5Piv9WzlRuDLIxca2uqetBMcX1jv/7v//LrpGU8uabby4Qz21uuuOOO+rbK+Zf6rWpUvFeW3ytslTctPg1JH9PzT/nxbF84YUXGr1dvI6tvvrq2XJxLSqOd7nvu+Xac889C+v79re/3ezyr7/++gLH7Pnnn29y+bgP+XW0eL19++23W73PAB3pf2nlQE0ZMmTIAlXxouJcJe2cyxEjc6JyV4yIipFxUd0q/o8KYjESL34/Z86crLJbjI459NBDG13PrFmzslFAISqyxWidGCEWI69iHS+99FLWsjVG20R1oaieGCNgGo4eWXnllbMRQ+HXv/514ff57xqKEUKVitFkeSvFGPEWxzRGWcaopXfffTc9+eST6amnnsr+HpWNorR27HuMHmxv9957b3bsQuzfLrvsUvhbVE2MKnx5e8hSx6hYHNMYjZmL+xSVv6JKWIwWjPsbFZ1idOhRRx1VtfuRV6SLUUwxKiwqNcYIsWgnGqOsY0TXa6+9lo0A+/nPf56dc1GtrCkxajFGOBW3h40RSV/60peyEYRR7S8ex5hixFI+0quhN998M22++ebZ8ysX50SMvo3qWLF/MYo1zts4P6J0eox2/uY3v9ns/Y7KhdESNc63qMoWozPjnI3HNJ5f8+fPz0Ytxjr/9a9/NVkZtbX399vf/na2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//tCe6Jo81x8vrdGfNEubvNd/FpT65qryNpYm5HGKr22x7kQr0nRGqu48mW0emqpYcOGZVMeOG3qvT/aRucjY/v379/qthflvvfGfn3729+uWhJtpY9VvBbmQd3WniPF7rvvvsL8VlttVdb6AQCA1hFXXfTiquUaOnRoevTRRwvxp7wLRXs45phjCkkk8b27nKSmRVW147YxaDcfpB2thc8+++ySt49YYnSsqIaoyHnooYcucL9aU42s0phUXI+JAeF5ImFx7LAlxzGeP8VVIGMAbanuTJGcVXxtqSuImHPErooHETf1OMT1sj//+c+Fnw855JBmtxFV784555wm45uR2JyLQesNOwa1Rywu9rGpuHt09SmuFhkD6TtaXBPJk8iixXa1khOjjXKe4B6JuPGcL/f6WjyeDZPUy41hz5w5c4FrRc3FKGsl5v+Xv/wljRkzpnAtefHFF18gSbElvva1ry1wbemkk05K7SGuwxdfe40YfqnHPqrW5pVr43W5nNeDWFdeRCYeoy222KJq+w7QESQmQg2aMmVKGj16dNpggw2yAMkbb7xRctlIvonKasVf0ONLbnMjQqKKXozgiS+IDb98xPbiw2E+GiOWbeyLa3xYjQ+OIb6IxBehd999d4Fl3n///ewLTmyrb9++Zd3/ddddtzBfrdYWxSOXzjrrrEaDPxGYGjlyZBagKHdfqyGCdrfddlvh54bV45qz6qqrZpURS43qjKTVvBVnJClFkmnD4EIkZcSo4VtvvbVQYbE14vzbdtttCz8fcMAB2XndUDy+sVx8ASvnmP/iF78o3JcIqESFuMbWG+I+xpeaaM/a0N577134shVfgmIE6bhx49KsWbMaXVd8yYvRiHGbKJXfXqp1fxsqDqIUt4g48MADU0eIL++RXJdPedCsUjFKNB85FoGxSLpsGPCL3//617/OKkfGdhq2a2ruNen666+vSvJua1xwwQWFpMp43kSwp7gdRgS643UgT9KM86harSTivaEtWoy3tagiHOd9tEwoVZUgRjUefvjhhZ+LX8OKg3v5a1W06C31fGypiy++eIGLOIcddlj22tRaP/nJTwrzcb6ceOKJCwXgIlm3uCpjfD6I51RrxGtrfh5Ggnqss2FyfLx2feMb38gCttV8763ks8QjjzySVllllSy4G1UOGxPnTRy74gtXjZ0jxe6+++5G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ZNOVV16ZXfiIqgwAAACwKIlWzQ3bNVcqqihWUkkxkhBjAgAAAAAAFqRiYhU8/vjjabfddsuSEvv165fOOOOM9I9//CPddddd6Vvf+la2zHPPPZe222679OGHH7Z4OwceeGAhKXHUqFHpxhtvTFOmTEnjx49Pa6yxRvr000/TqaeemiVBAgAAsAiqq6vdCQAAAAAAWGSomFgFRx11VFYdsUePHun2229PG220UeFvm2++eVpzzTXT8ccfn5599tl07rnnZsmDlZo0aVJWETGMGTMm3XDDDal79+7Zz8OGDUvbb7992mCDDbJ20rGtXXbZJQ0YMKAadw8AAAAAAAAAgBozd+7crEDWrFmzsvkoaAUserp375569+6dllhiidS3b99UVyPFAiQmtlK0ab7nnnsKFQ2LkxJzxx57bLrkkkvSM888k37+85+nE088MS222GIVbefss88unEgXXHBBISkxN3DgwDRu3Li05557ppkzZ2ZVFGO7AAAAAAAAAAB0HfX19WnGjBnZBDB//vw0Z86crNtvnz590mc+85nUrVvHN1KWmNhK0U45d8ABBzS6TDzQ++67b5aQGEmDkcg4evTosrcR2e3RFjrE7QYNGtTocjvvvHOW+fr++++niRMnSkwEAAAAAAAAAOhi3njjjSwBqVhUSGtY5ApYNHzyySdZwnL4+OOPs467q6yySodXTpSY2Er33Xdf9n+UwYxWyqWMHDmyMH///fdXlJg4ZcqULKu14Xoa6tmzZxoxYkTWTjpuM2/evIorMwIAANCJ1XX731RranGfAAAAAKATmj179gJJicsss0xWxKpXr14dnoQEdIxPP/00K3z35ptvZkmKkZwYLd779evXoQ+JKwOtFO2Zw5AhQ1KPHqXzPNdee+2FblPpNhqup6ntRInO559/vqLtAAAAAAAAAABQu959993C/HLLLZdNvXv3lpQIi7Bu3bplCcorrLBC4XcffPBB6mgqJrYyC33GjBnZfKn2yrmllloqq6oY2aivvvpqRdspXr657QwePHiB2w0dOrSsbUyfPr3ZMsAAAAAAAAAAAHScjz76qDA/YMAADwVQEBUSo3JqtHWOqokdTWJiKxRnlpZT+jJPTIzSmW21ndhGrpLtFCc0AgAA0ElFq5ZabNdSi/sEAAAAAJ1QtGkN0dWze/fuHb07QI1VTuzevXvWaTd/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pr/85S/pkUceyaoZRrJj3mn1jjvuaLSl9JNPPpn++te/Fn7+8Y9/nP2ueDr00EPLvi/REjnaG++3337Z9u+777706KOPZu2vf/SjH2VJf5FMefjhh6e//e1vqb3k7Zn//Oc/p3fffXeBv7333nvZ74uXq9SJJ55Y6Jx7yimntHp/u7IeHb0DAAAAAAAAAAAAteaII45Ixx13XDb//e9/P1144YVpzJgxWRLgl7/85bTGGmtUtL5IcIxkx2iBHK2Pc1/96lfT1ltvnbbaaqss4S8SC6Pdc7Hu3bun5557rtG20iNHjsxaM5922mlZUmBUBYwWy9HuOff5z38+9evXr/BztH+O37VUVBzca6+9Ckl6uaj6uN1226Xvfve72f164oknsv2KhMr2EI/PgAEDsqTE6667Ln3rW98q/C1+nj17dvb3WK4lll9++ezxieTQu+66K917773Z/WRhKiYCAABAV1HXrXYnAAAAAIBO5uijj07/93//V/j5lVdeSb/61a/SN7/5zSxBMJILo31yVOHLKxo255xzzlkgKTE3atSoQhLdww8/vFBiYl1dXaNJicVOPfXUrFJh7Eu0dm5LkdjYMCmx2JJLLpklSYb7778/vfPOO6k9ROXJ3XbbLZu//PLLG23jvOuuu2bLtdTxxx9fSPJUNbE0VwYAAAAAAAAAAAAaJlZ165bGjx+fbrvttjR69Ojs52JvvfVWuuaaa9L222+fhg8fnl544YUmj2G0Pt5hhx1K/r04CfLOO+9scl3RVvr111/Pqij+61//yqZnnnkmDRo0qFCdsT3NmjUrvfzyy+mpp54q7M9iiy1W+Ht77k/epjnaS8c+5Uml8XPx31sqkj+jImSIionNPVaLKq2cAQAAAAAAAAAASvja176WTTNnzkx///vfs4qGjzzySJbo9t5772XLxO823XTT7Pcrrrhio+uJNsc9epRO1/riF7+YevbsmebOnZsl9jUUlRCvuOKKLFnywQcfTB9//HHJdc2YMaPNH8/Yxrnnnpv++Mc/pueff77JqpHtsT+5TTbZJK2++urpxRdfzI7XySefnFVLjP1bddVVs7+3VrT4/vWvf509/lE1ccstt6zKvnclKiYCAABAV1FXV7sTAACLnAMnPLTQBAAAnVlUPPz617+exo4dm7VvjoqJF198cfb78MYbbzTZ2ne55ZZrcv2RtLj00ktn8//9738X+Nvs2bPTdtttl1X7u+eee5pMSgzN/b21IgFz7bXXTj/5yU/Sv//972ZbWbf1/jS09957L9C+OW/rHL+PttitFY95tPoOkydPzqpqsiCJiQAAAAAAAAAAABXq1atXOuCAA9JVV11V+N3EiROzNsuNKSchrlSC3xlnnFFIfhs5cmS69tpr09SpU9OHH36YPvnkk+x2MUXVxqbWUw1R0XG33XZL77zzTtau+ZhjjkmTJk3KEjMjgTLfl+LW1m25P43J2zVHq+tf/epX2f/FCYvVEImJeVLqqaeeWrX1dhVaOVOxz2/3g1TXs1/NHrmZD/2qo3cBANrVUsMOd8QBAAAAAACgg2y99dZp8ODB6dVXX83aPUfC3rLLLrvQclFhsSnz58/Pbh/yyol5Ut9FF12UzUcb4r/97W+pW7fG69Hlt29Lsf1okxyinfG3vvWtDtuXUoYMGZI22mij9MADD6Tvfe972e+GDx+ePvvZz1ZtG0sssUTW0jlaRUcr7z/96U9pvfXWq9r6OzsVEwEAAKCrqOtWuxMAAAAAQBe20korFeZLJQ3+85//zJIPS3n88cezaoTh85//fOH30db5zTffzOajUmGp9Uf1xLwyYGOq0cI4PPXUU4X5PfbYo+RykazXkfKqiVHFsfjnavrud7+bBg4cmM2fdtpp7V4Zspa5MgAAAAAAAAAAANBCH330UXr66acLVfSKqx0WiwTDP//5zyXXc/HFFxfmt9xyy8J8cTJjbKuU8ePHp3nz5pX8e+/evQvzc+bMSS1Vzv5EO+vf/e53qSPtvvvuqX///lnL7X79+jWZRNlSsd4TTjihkFj6xz/+serb6KwkJgIAAAAAAAAAADSoPvjlL3853XzzzVmSXSnxtyOOOCJ98MEH2c/bb799k5UJjznmmEZbOk+aNKmQyLfBBhukYcOGFf4WbaEHDBiQzV999dWFqorFHnroofSDH/ygycdwmWWWST179szmX3jhhRY/3muuuWZh/tJLL210mRNPPDE9+uijqSNFguj777+fVUyMxyevbFhthx12WFphhRWy+XHjxrXJNjqjHh29AwAAAECVRLCrFtsmV6k9CNSKadOmpV/84hfplltuyeZjxPWQIUOyNjqHHnpoWnzxxauynQgyX3LJJemJJ55IM2fOzIKbm266aRboHDFiRLO3j7Yx119/fbriiivSI488kt5+++2szU+sZ/jw4emAAw5IW2+9dVX2FQAAAKArmjJlShozZkxaeeWV04477pg22mijtMoqq2RV+N5999302GOPZVUOn3zyyWz5JZdcMp1++ukl17feeutllRUj8TAS9yJGE5ULb7311nTeeedllQh79OiRfv3rXy9wu4jpfPOb38x+H+2gI0Z09NFHZzGp9957L7v9BRdckFXvi5bS//73vxvdfqw7Eh7//ve/Z/u9/vrrpy9+8YtpscUWKyTylar2WCxiSsstt1z6z3/+k04++eT0yiuvZAmZkfg3derU9Pvf/z7dddddaeONN8621dX16dMnff/7309HHXVUmjFjRtm3e/PNN9OECROaXS4e50022SR1NhITAQAAAKBMkYwYQeAI+Ba3q4kR6TFddNFFWSB49dVXb/ExjRHcu+66azYav1gEeGO68sor09ixY9Mpp5xSch2xfxEsv+eeexb620svvZRN11xzTdbO5rLLLiuMlAcAAADg/yXxxQDPSB577bXXsqTAhgmDDasIXnXVVWnVVVctuUwkAR5++OHpO9/5TvZ/QxGjiQqEUamxoTPOOCNL8ovExEiY3HPPPRf4eyQURhvhU089tWRiYoiEyEi2fOedd9Jee+21wN9OO+20LO7UnL59+2YxpYg/RSwrkiJjKrbZZpulX/3qV+nzn/98WhQcfPDB6ac//Wl2rpTrueeeywYPN2e//fbrlImJNVhGAQAAAABqz+OPP55VRYykvxh9HsHgf/zjH9no729961uFYOJ2222XtfppqQMPPLCQlDhq1Kh04403ZsHm8ePHpzXWWCNrDxQB5kiCLCUC03lS4mqrrZYFhu+77770t7/9LQuQ5m1rIjkxRtcDAAAAsKDevXtnSWaRDPjDH/4wbbPNNtlg1EjK6969e1piiSXS2muvnQ38jIGk//rXv7JKiM056KCDsjhNxJmiumEkI0ZFxn333TerwLjHHns0eruoxhj7EhUZ11133Wz/Ika1zjrrpOOOOy6LXX31q19tdvsRu4p41g477JBtP6+WWKmomvjwww+nvffeu7CeaDk9cuTIrCV1bCOO1aIiHo+oHsn/U1cfPV2gGdOnT0+DBw/O5nsN3S/V9exXs8ds5kO/6uhdAIB2tdSwhUdTURmfH6BrfGdZc7X/fWd59dVX06BBg9Ii+X1txLGprveSqdbUz34vzZl8ziL5+NC1RJJgJPvFaPl77703a91TLBL+jj/++Gw+gtWRPFipSZMmZaPJQ4xcv+GGG7JAdy5awUSAO1pIL7XUUunFF19MAwYMWGAd0bZ5ww03zOYjWB6j6KO9ULG4fbQOipZD0QooRv5H4LgtXps87wEWXQdOeGih343ff1iH7AsAQFfx/PPPF9r9RoU+OoeoohidMKLyXTmte6E9XyPaKpanYiIAAAAANCPaNOcVCKOiYcOkxHDsscdmI9TDz3/+8zRv3ryKj+vZZ5+d/R/JiFHlsDgpMUSlw3HjxmXzM2fOzKooNhQj53NHHXXUQkmJ4TOf+UyhTUxUYHzwwQcr3lcAAAAAgFIkJgIAAABAM6Kdci5P6Fso0NatW9ZyJ08azBMZyxXtn6PFTRg9enTJkck777xz1iooTJw4caG/z507tzAfFRNLibbQuTlz5lS0rwAAAAAATZGYCAAAAF1FXbfanaCTu++++7L/+/btm7VSLmXkyJGF+fvvv7+ibUyZMqWQIFi8noZ69uyZRowYUbhNw8qMa621VmE+Wj2X8sILLzR6GwAAAACA1nJlAAAAAACa8cwzz2T/DxkyJPXo0aPkcmuvvfZCtylX8fLF62lqO/Pnz0/PP//8An/beuut06qrrprNn3/++WnWrFkL3X769OlpwoQJ2Xy0pV533XUr2lcAAAAAgKaUjqICAAAAAGn27NlpxowZ2ZEo1V45t9RSS2VVFSMZ8NVXX63o6BUv39x2Bg8evMDthg4dWvi5V69e6YorrkhjxozJqiKut9566Xvf+176/Oc/n1VXfOSRR9LZZ5+dtZuOBMZLLrmk4kc5Ehub8sYbb1S8TgAAAACg65CYCAAAAABN+OCDDwrz/fr1a/ZY5YmJH374YZttJ7aRa2w7X/nKV9Jjjz2WfvGLX2TTIYccssDfY/0//OEP06GHHpoGDhyYKlWcGAkAAABA015++WWHiEWOVs4AAADQVdTV1e4EnbxiYq5nz57NLh8VC8PHH3/cZtvJt1FqO/X19en666/PpqiS2FAkM1599dXplltuqWgfAQAAAADKoWIiAAAAADShd+/ehfm5c+c2e6zmzJmT/d+nT582206+jca28+mnn6Y99tgjXXfdddnPBx54YDrssMPSOuuskz755JP0z3/+M2vlfNNNN6X9998/PfHEE+mcc86paF+ba1MdrZyHDx9e0ToBAAAAgK5DYiIAAAAANKF///6F+XLaM0cb53LbPrd0O/k2GtvOBRdcUEhKHDt2bDrttNMW+PvGG2+c/vSnP6V99903/eEPf0jnnntu2nzzzdN2221X9r4OGjSo7GUBAAAAgEWPVs4AAADQVdR1q90JOrGoZDhw4MBsfvr06U0uO3PmzELS4ODBgyvaTnGyX3PbKa5Y2HA748ePLyQ6fv/73y+5jjPPPLMwf9FFF1W0rwAAAAAATXFlAAAAAACaEW2Qw9SpU9P8+fNLLvfss88udJtyDR06tNH1NLWdHj16pCFDhizwt2eeeaawvl69ejWZCLn88suXtT0AAAAAgEpITAQAAACAZmyyySbZ/1EN8ZFHHim53KRJkxZomVyJYcOGpZ49ey60nobmzp2bJk+evNBtcpGsGJpKoMzNmzdvgdsAAAAAta179+7Z/5988kmqr6/v6N0Bakh9fX322hC6dev4tMCO3wMAAACgOurqaneCTm7HHXcszF9yySWNLvPpp5+myy67LJsfMGBAGjVqVEXbiNbLW2yxRTZ/5513lmznPHHixPT+++9n8zvttNNCf19ttdWy///1r3+ld999t+T24u///e9/F7gNAAAAUNvyAYqRgPTRRx919O4ANWTOnDmFhOWGg5k7gsREAAAAAGjG8OHD06abbprNjx8/Pj3wwAMLLXPOOecU2igfeeSRabHFFlvg7xMmTEh1dXXZNHbs2Ea3c9xxxxWqHR522GGFEc65GTNmpBNOOKGQ/HjQQQcttI4xY8YUApHHHHNMo9UTZs+enb773e8Wfv7617/uHAAAAIBOYIkllijMx4BDVROBXD6YOfTt2zd1NImJAAAAAFCG888/P/Xp0ydLGtxqq63ST37yk6yl8t13350OPvjgdPzxx2fLrbXWWunYY49t0THdfPPN0x577JHN33TTTWn06NHZ/w8//HBWqXHEiBFp2rRp2d/POuustNRSSy20jkhGXG655bL5uM1Xv/rVdMUVV2QtqKdMmZJ++9vfpg022CDb77DOOuuk/fff3zkAAAAAnUC/fv2yQY/hww8/zDouzJo1S4IiLMI++eST9M4772RT8WtFR+vR0TsAAAAAVEnWNrkGxyBq5UwXsf7666drrrkm7b333tno45NOOmmhZSIp8ZZbbsnaMrfUxRdfnK3/1ltvzZIH8wTCXLdu3dIpp5ySJUM2ZuDAgemvf/1r2nnnndNLL72U7r///mxqzBe/+MV044031kRrFwAAAKB5ERdYeeWV02uvvZYlI0ZyYkyRrNi9e3eHEBYx9fX1C3VdWXbZZWsi3icxEQAAAADKFG2Sn3jiiax6YiQgRlWCCPINGTIk7brrrunwww9Piy++eKuOZ1RljHVfeeWVWfvnxx9/PL377rtp+eWXz9pJxzY22mijJtcRCYdPPvlkuvTSS9Of/vSnbJ+jvVNcpIhqipFkGfu7++67L9RyGgAAAKhtMSCyODkxxP/R5QFYtC255JJpmWWWSbVAYiIAAAAAVGCVVVZJ5557bjZVItolV9Iyea+99sqmlurbt2869NBDswkAAADoesmJ0bkhqiVG54W5c+cuVDUNWDR07949Gyw9YMCA1Lt371QrJCYCAABAl2rlXJdqTi3uEwAAAAB0gbbOSyyxRDYB1JpuHb0DAAAAAAAAAAAAQNchMREAAAAAAAAAAACoGomJAAAAAAAAAAAAQNX0qN6qAAAAgA5V1+1/U62pxX0CAAAAAADajCsDAAAAAAAAAAAAQNVITAQAAAAAAAAAAACqRitnAAAA6Crq6v431Zpa3CcAAAAAAKDNqJgIAAAAAAAAAAAAVI3ERAAAAAAAAAAAAKBqtHIGAACALqNbSnW1OAaxFvcJAAAAAABoK64MAAAAAAAAAAAAAFUjMREAAAAAAAAAAACoGq2cAQAAoKuoq/vfVGtqcZ8AAAAAAIA2IzGRiv3rlh+nQYMGOXIAUCNmPvSrVMuWGnZ4R+8CAAAAAAAAAO1IK2cAAAAAAAAAAACgalRMBAAAgC7VyrkGxyBq5QwAAAAAAIuUGrxaAQAAAAAAAAAAAHRWEhMBAAAAAAAAAACAqtHKGQAAALqKaONck62ca3CfAAAAAACANuPKAAAAAAAAAAAAAFA1EhMBAAAAAAAAAACAqpGYCAAAAAAAAAAAAFRNj+qtCgAAAOhQdXX/m2pNLe4TAAAAAADQZlRMBAAAAAAAAAAAAKpGYiIAAAAAAAAAAABQNVo5AwAAQFdR1+1/U62pxX0CAAAAAADajCsDAAAAAAAAAAAAQNVITAQAAAAAAAAAAACqRitnAAAA6Crq6v431Zpa3CcAoNM5cMJDC/1u/P7DOmRfAAAAgKapmAgAAAAAAAAAAABUjcREAAAAAAAAAAAAoGq0cgYAAIAu1cq5BscgauUMAAAAAACLlBq8WgEAAAAAAAAAAAB0VhITAQAAAAAAAAAAgKrRyhkAAAC6VCvnulRzanGfAAAAAACANqNiIgAAAAAAAAAAAFA1EhMBAAAAAAAAAACAqtHKGQAAALqIuvhXk22Ta3GfAAAAAACAtqJiIgAAAAAAAAAAAFA1EhMBAAAAAAAAAACAqpGYCAAAAAAAAAAAAFRNj+qtCgAAAOhQdXWprq6u5h6E+hrcJwAAAAAAoO2omAgAAAAAAAAAAABUjcREAAAAAAAAAAAAoGq0cgYAAICuIjom12LX5FrcJwAAAAAAoM2omNhKjz76aDrzzDPTNttskwYPHpx69eqV+vXrl9Zaa620//77p/vuu68qD9TYsWNTXV1dWdM999xTlW0CAAAAAAAAAABApVRMbIWRI0eme++9d6Hfz507Nz3//PPZdOmll6Z99tknXXTRRalnz56t2RwAAAAAAAAAAADUPImJrfDaa69l/6+00kpp1113TZtuumn6zGc+kz755JP0wAMPpHPOOSdb5g9/+EOaP39+uvLKK6vyoD355JNN/n211VarynYAAADoXPJK+rWmFvcJAAAAAABoOxITW2HttdfO2jh/4xvfSN27d1/gbyNGjMgqJW688cbp3//+d7rqqqvSd77znSx5sbU+//nPt3odAAAAAAAAAAAA0Ba6tclaFxE333xz2m233RZKSswNHDgwq5qYu/7669tx7wAAAAAAAAAAAKD9qZjYxjbbbLPC/AsvvNDWmwMAAGARppUzAAAAAABQC1RMbGNz5879fwe7m8MNAAAAAAAAAABA1yZTro1NmjSpML/22mtXZZ2jR49OyyyzTOrZs2dabrnlsqqMZ511Vpo5c2ZV1g8AAAAAAAAAAAAtpZVzG/r000+zhMHcbrvtVpX13nnnnYX5t99+O0t+jGncuHFpwoQJaYcddqh4ndOnT2/y72+88UaL9hUAAID2o5UzAAAAAABQCyQmtqHzzjsvTZkyJZvfaaed0oYbbtiq9a277rppxx13TMOHD08rrbRSmjdvXnruuefSFVdckW6//fb07rvvpm984xvpz3/+c9pmm20qWvfgwYNbtW8AAKXMfOhXDg50EUsNOzzVqvq5H3b0LgAAAAAAAPD/k5jYRqKC4fe///1sPtotX3jhha1a31FHHZXGjh270O+//OUvp3333Tf99re/TYccckj65JNP0kEHHZSmTp2a+vTp06ptAgAAAAAAAAAAQKUkJraBp556KquQOH/+/NSrV6907bXXpuWXX75V6xwwYECTfz/44IPTww8/nC666KL0+uuvp4kTJ6ZvfvObZa//1VdfbbaVc1RqBAAAoHZp5QwAAAAAANQCiYlV9tJLL6WtttoqzZw5M3Xv3j1dddVVaeTIkak9RHJiJCbmFRsrSUwcNGhQG+4ZAAAAAAAAAAAAi4puHb0DXUlUKtxyyy2z/6NKxcUXX5xVTmwvQ4cOLcy/9tpr7bZdAAAAAAAAAAAAyElMrJIZM2ak0aNHpxdffDH7+Ze//GXad999U3uqr69v1+0BAAAAAAAAAABAQ1o5V8F7772Xtt566/T0009nP5911lnpsMMOS+0t335YaaWV2n37AAAAdLC6/3+qNbW4TwAAAAAAQJtRMbGVPvroo7TddtulRx99NPv55JNPTieccELqCL/97W8L8yNHjuyQfQAAAAAAAAAAAGDRJjGxFebOnZt22mmn9Pe//z37+cgjj0w//vGPK17PhAkTUl1dXTaNHTt2ob8/+eSTaerUqc0mJY4fPz6bX2GFFbL9AgAAAAAAAAAAgPamlXMr7Lnnnun222/P5jfffPN04IEHpn/9618ll+/Zs2daa621Kt7OI488kg466KA0atSotM0226R11103LbPMMmn+/Pnp2WefTZdffnm64447smW7d++eJSn27du3FfcMAACAzigf9FZranGfAAAAAACAtiMxsRUmTpxYmP/b3/6WvvCFLzS5/CqrrJJefvnlFm3rk08+SXfeeWc2lRLJilE1cfvtt2/RNgAAAAAAAAAAAKC1JCZ2Attuu22WcPjAAw+kxx57LL311lvpnXfeSfX19WnppZdO6623Xvra176W9t9//7TEEkt09O4CAAAAAAAAAACwCJOY2AqRGFgNkVAYUynLLbdc+r//+79sAgAAgFKiY3Ittk2uwV0CAAAAAADaULe2XDkAAAAAAAAAAACwaJGYCAAAAAAAAAAAAFSNVs4AAADQRdTFvxrsmxz7BQAAAAAALDpUTAQAAAAAAAAAAACqRmIiAAAAAAAAAAAAUDUSEwEAAKCLiDbOtTpBVzJt2rR03HHHpXXWWSf17ds3Lb300mn48OHpZz/7Wfroo4+qtp2rr746bb311mnFFVdMvXv3TquuumraZ5990uTJk5u83f7771/xc3TChAlV228AAAAAgB4OAQAAAACU55Zbbknf/OY303vvvVf4XSQjPvTQQ9l00UUXpVtvvTWtvvrqLT6ks2fPTrvuumu6+eabF/j9K6+8kk1XXnllGjt2bDrllFOq9rB99rOfrdq6AAAAAAAkJgIAAABAGR5//PG02267ZYmI/fr1SyeeeGIaNWpU+vjjj7Pqhr///e/Tc889l7bbbrssSTGWaYkDDzywkJQY6z/yyCPTSiutlJ588sl05plnphdeeCGdeuqpWSXFgw46aKHbn3HGGVlFx6bMnDkzbbbZZunTTz9Na665Ztpoo42cAwAAAABA1UhMBAAAAIAyHHXUUVlSYo8ePdLtt9++QDLf5ptvniX4HX/88enZZ59N5557bpY8WKlJkyZlFRHDmDFj0g033JC6d++e/Txs2LC0/fbbpw022CBrJx3b2mWXXdKAAQMWWMfKK6+cTU258MILs6TEsO+++3r8AQAAAICq6lbd1QEAAAAdpq6GJ+jkogLiPffcU6ho2FiFwWOPPTats8462fzPf/7zNG/evIq3c/bZZ2f/RzLiBRdcUEhKzA0cODCNGzeuUPVw/PjxLbo/l112WfZ/XV1d2meffVq0DgAAAACAUiQmAgAAAEAzbrzxxsL8AQcc0HigrVu3QvXBSBrMExnL9eGHH6a77rormx89enQaNGhQo8vtvPPOaYkllsjmJ06cWPFj9/zzz6fJkydn8yNHjkyrrLJKxesAAAAAAGiKxEQAAAAAaMZ9992X/d+3b9+slXIpkeiXu//++ys6rlOmTElz5sxZaD0N9ezZM40YMaJwm0orM+bVEoM2zgAAAABAW5CYCAAAAF1FXV3WlrXWptgv6OyeeeaZ7P8hQ4akHj16lFxu7bXXXug2lW6j4Xqa2s78+fOzCojlqq+vT5dffnk2v/jii6dddtmlon0EAAAAAChH6SgqAAAAAJBmz56dZsyYkR2JUu2Vc0sttVRWVXHWrFnp1VdfrejoFS/f3HYGDx68wO2GDh1a1jbuvffe9PLLL2fzO+20U+rfv39qienTpzf59zfeeKNF6wUAAAAAugaJiQAAAADQhA8++KAw369fv2aPVZ6Y+OGHH7bZdmIbuUq2U9zGeb/99kstVZwYCQAAAADQkMREAAAA6CIKrZNrTC3uE1RaMTHXs2fPZpfv1atX9v/HH3/cZtvJt1HJdmL9119/fTa/8sorpy222KKi/QMAAAAAKJfERAAAAABoQu/evQvzc+fObfZYzZkzJ/u/T58+bbadfBuVbOfGG29M77//fja/9957p27duqWWaq5NdbRyHj58eIvXDwAAAAB0bhITAQAAAKAJ/fv3r6htcrRxLrftc0u3k2+jku0Ut3Hed999U2sMGjSoVbcHAAAAALq2lg+LBgAAAGqylXMtTu1l2rRp6bjjjkvrrLNO6tu3b1p66aWzqm0/+9nP0kcffVSVbTz99NPpiCOOSOuuu25aYoklspa7yy67bBo1alQ677zz0gcffFCV7VA7opLhwIEDs/np06c3uezMmTMLSYODBw9ucbJfc9sprlhYznbeeuutdPvtt2fzG2ywQRo6dGhF+wYAAAAAUAmJiQAAAECXcMstt6QvfOEL6ZxzzknPPvtslogYSWIPPfRQ+t73vpe+9KUvpRdffLFV24h1r7feeulXv/pV+te//pUlIc6bNy/NmDEj3XPPPemYY47JEhafeOKJqt0vakMku4apU6em+fPnl1wuzr2GtylXcbJg8Xqa2k6PHj3SkCFDml33FVdckT755JNsfr/99qtovwAAAAAAKiUxEQAAAOj0Hn/88bTbbrul9957L2tre8YZZ6R//OMf6a677krf+ta3smWee+65tN1225XVircx1157bVaNMZLSokri0UcfnSVDPvjgg+nKK69Mm2yySbbcK6+8kr72ta9l+0LXkT++UQ3xkUceKbncpEmTCvMbb7xxRdsYNmxYdm41XE9Dc+fOTZMnT17oNuW0cV5sscXSnnvuWdF+AQAAAABUSmIiAAAAdBV1NTy1saOOOiqrkBjV46Jd7UknnZQ22mijtPnmm6ff/e536eyzzy5UmTv33HNbtI3TTz+9MD9x4sRsPdtuu23WKjoSve6777608847Z39/44030vjx46t076gFO+64Y2H+kksuaXSZTz/9tJAAOGDAgKy9dyX69++ftthii2z+zjvvLNnOOc6/999/P5vfaaedml3vk08+mSXvhjhn87bUAAAAAABtRWIiAAAA0KlFq+ZooxwOPPDALCGxoWOPPbbQVvfnP/951n65EpEEFq2bQ7SEjsqLjTnttNMK81Gxka4jElA33XTTbD6STh944IFGW30/88wz2fyRRx6ZVScsNmHChFRXV5dNY8eObXQ7UZUzRGXOww47rNB+ORdtw0844YRC8uNBBx3U7L5feumlhfl99923jHsLAAAAANA6EhMBAACATu3GG28szB9wwAGNLtOtW7dCQtbMmTMLiYzlita5udVXX73kcmussUZhfs6cORVtg9p3/vnnpz59+mRJg1tttVX6yU9+krVUvvvuu9PBBx+cjj/++Gy5tdZaK0uGbYmo8rnHHntk8zfddFMaPXp09v/DDz+cVWocMWJEmjZtWvb3s846Ky211FJNri8SG6PVeFh66aXT17/+9RbtFwAAAABAJXpUtDQAAABQs/JKbLWmrfcpWiiHvn37pg022KDkciNHjizM33///VnCV7mi9W0kdf33v/9NL774YsnlXnjhhcJ8JKfRtay//vrpmmuuSXvvvXdWRTNahjcUj/stt9yStWVuqYsvvjhb/6233polPcbUMNH2lFNOyZIhm3PHHXdkrcVDJDz27NmzxfsFAAAAAFAuFRMBAACATi1vnTtkyJDUo0fpMZhrr732QrepxLe//e3s/0cffTTddtttjS5z+umnZ/937969rBa7dD5jxoxJTzzxRDr66KOzJMTFF188a6m84YYbpnHjxqXHHnssOxdbI6oyRnLjFVdckSXQLrfccllC4eDBg9Nee+2VJdaWagXd0B/+8IfCvDbOAAAAAEB7UTERAAAAaDd55bamDBo0qOz1zZ49O82YMaOs20XL26iqOGvWrPTqq6+mSp188slZO90777wz7bTTTunwww9PW2yxRVZNMaooXnjhhWnSpElZUuIvfvGLtM4661S8DTqHVVZZJZ177rnZVIn9998/m8oVSYgxtUYkN8YEAAAAANCeJCYCAAAA7Wb48OHNLlNfX1/2+j744IPCfL9+/ZpdPk9M/PDDD8veRvH6o1LihAkT0llnnZXOOeecbCq28847p+OPPz59+ctfrnj9AAAAAADQVUhMBAAAqBFLDTs81bKZD/0q1arp06enNVe7NC3q6urqsqnWtOU+RcXEXLS6bU6vXr2y/z/++OMWbS8qJl511VVZhcTGRDXF5ZdfPquWuMQSS7RoGwAAAAAA0Nl16+gdAAAAABYdU6ZMydooNzVVonfv3oX5uXPnNrv8nDlzsv/79OlT8b5ff/31abPNNkt/+9vf0rrrrptuuOGG9M4772TbfeGFF9KZZ56Z5s2bl7V0/spXvpLefPPNircBAAAAAABdgYqJAAAAQLtZccUV06BBg6q2vv79+xfmy2nPHG2cy237XOytt95K+++/f5bY+LnPfS794x//yNpC51ZfffV04oknZq2qR48enZ566ql0xBFHpOuuu66i7QAAAAAAQFegYiIAAAB0EXXxr64Gp9R2rZyjYuLAgQMLLb2bMnPmzEJi4uDBgyvaztVXX1247UknnbRAUmKxLbbYIpvCxIkTs20CAAAAAMCiRmIiAAAA0Kmts8462f9Tp05N8+fPL7ncs88+u9BtyvXMM88U5r/0pS81uewGG2yQ/f/pp5+mf//73xVtBwAAAAAAugKJiQAAAECntskmm2T/R0XDRx55pORykyZNKsxvvPHGFW2jR48ehfmmkh/DvHnzGr0dAAAAAAAsKiQmAgAAQBfR4S2bm5ja0o477liYv+SSSxpdJqoXXnbZZdn8gAED0qhRoyraxmqrrVaYv++++5pc9t57783+j/u96qqrVrQdAAAAAADoCiQmAgAAAJ3a8OHD06abbprNjx8/Pj3wwAMLLXPOOecU2jEfeeSRabHFFlvg7xMmTCgkUY4dO3ah22+33XaFBMszzjgjvfbaa43uy+9+97v08MMPZ/MjRoxIyyyzTBXuIQAAAAAAdC76CQEAAACd3vnnn5+1Z/7444/TVlttlU466aSsKmL8fPXVV2cJg2GttdZKxx57bMXrX3vttdMBBxyQLr744iwpcf31109HHXVUlhDZv3//9Oqrr2bbufLKK7Plu3fvns4888yq308AAAAAAOgMJCYCAABAVxEF/dq2a3LLtMM+RaLgNddck/bee+/0/vvvZ4mJDUVS4i233JIlErbEBRdckGbNmpVt5+23304nn3xyo8v17ds3S4TcbLPNWrQdAAAAAADo7LRyBgAAALqEMWPGpCeeeCIdffTRWRLi4osvngYMGJA23HDDNG7cuPTYY4+lIUOGtHj9vXr1yqoi/u1vf0v77rtvto1IQuzRo0daeuml00YbbZROOeWU9Oyzz6a99tqrqvcNAAAAAAA6ExUTAQAAgC5jlVVWSeeee242VWL//ffPpnJEi+iYAAAAAACAxklMBAAAgC6iri6m2uvlXIO7BAAAAAAAtCGtnAEAAAAAAAAAAICqkZgIAAAAAAAAAAAAVI1WzgAAANBFRBvn2mzlXHv7BAAAAAAAtB0VEwEAAAAAAAAAAICqkZgIAAAAAAAAAAAAVI3ERAAAAAAAAAAAAKBqelRvVQAAAECHqqtLdXV1tfcg1OI+AQAAAAAAbUbFRAAAAAAAAAAAAKBqJCYCAAAAAAAAAAAAVaOVMwAAAHQV0TG5Frsm1+I+AQAAAAAAbUbFRAAAAAAAAAAAAKBqJCYCAAAAAAAAAAAAVaOVMwAAAHQRdXV12VRranGfAAAAAACAtqNiIgAAAAAAAAAAAFA1EhMBAAAAAAAAAACAqtHKGQAAALoIrZwBAAAAAIBaoGIiAAAAAAAAAAAAUDUSEwEAAAAAAAAAAICq0coZAAAAuoi6+FdXl2pxvwAAAAAAgEWHiokAAAAAAAAAAABA1UhMBAAAAAAAAAAAAKpGYiIAAAAAAAAAAABQNT2qtyoAAACgI9XV1WVTranFfQIAAAAAANqOiokAAAAAAAAAAABA1UhMBAAAAAAAAAAAAKpGK2cAAADoKqJjci12Ta7FfQIAAAAAANqMiokAAAAAAAAAAABA1aiYCAAAUCNmPvSrjt4FAAAAAAAAaDWJiQAAANBF1NXVZVOtqcV9AgAAAAAA2o5WzgAAAAAAAAAAAEDVSEwEAAAAAAAAAAAAqkYrZwAAAOgitHIGAAAAAABqgYqJAAAAAAAAAAAAQNVITAQAAAAAAAAAAACqRitnAAAA6CLq6v431Zpa3CcAAAAAAKDtqJgIAAAAAAAAAAAAVI3ERAAAAAAAAAAAAKBqtHIGAACALtXKufb6JtfgLgEAAAAAAG1IxcRWigs+5UybbbZZVR6wq6++Om299dZpxRVXTL17906rrrpq2meffdLkyZOrsn4AAAAAAAAAAABoDRUTO4nZs2enXXfdNd18880L/P6VV17JpiuvvDKNHTs2nXLKKR22jwAAAAAAAAAAACAxsUq+853vpEMPPbTk3/v27duq9R944IGFpMRRo0alI488Mq200krpySefTGeeeWZ64YUX0qmnnppVUjzooINatS0AAAAAAAAAAABoKYmJVbLccsulz3/+86ktTJo0KauIGMaMGZNuuOGG1L179+znYcOGpe233z5tsMEGadq0aen4449Pu+yySxowYECb7AsAAAA1rC6lurpUe2pxnwAAAAAAgDbTre1WTbWcffbZ2f+RjHjBBRcUkhJzAwcOTOPGjcvmZ86cmcaPH+/gAwAAAAAAAAAA0CEkJta4Dz/8MN11113Z/OjRo9OgQYMaXW7nnXdOSyyxRDY/ceLEdt1HAAAAAAAAAAAAyElMrHFTpkxJc+bMyeZHjhxZcrmePXumESNGFG4zb968dttHAAAAakNdXV3NTgAAAAAAwKJDYmKVXHfddemzn/1s6tOnT+rfv39ac80103777ZfuvvvuVq33mWeeKcyvvfbaTS6b/33+/Pnp+eefb9V2AQAAAAAAAAAAoCV6tOhWLOTpp59e4OepU6dm02WXXZZ23HHHNGHChLTkkktWfOReffXVwnypNs65wYMHL3C7oUOHlr2d6dOnN/n3N954o+x1AQAAAAAAAAAAsOiSmNhKiy++eNp+++3TFltskVUs7NevX3r77bfTpEmT0m9+85v0zjvvpBtvvDHtsMMO6Y477kiLLbZYRev/4IMPCvOx7qb07du3MP/hhx9WtJ3ipEYAAAA6p2iYXItdk2twlwAAAAAAgDYkMbGVXnvttTRgwICFfj969Oh0xBFHpG222SY99thjWaLihRdemL773e9WtP7Zs2cX5nv27Nnksr169SrMf/zxxxVtBwAAAAAAAAAAAKpBYmIrNZaUmFt++eXT9ddfn9ZZZ500d+7c9Mtf/rLixMTevXsX5mMdTZkzZ05hvk+fPi1uGV2qlfPw4cMrWicAAAAAAAAAAACLHomJbWz11VfPqifecsstaerUqen1119PK620Utm379+/f9ntmWfNmlV22+eGBg0aVNHyAAAA1J5u3eqyqdbU4j4BAAAAAABtp1sbrpv/39ChQxdo/dzShMHp06eXXfVw8ODBjj8AAAAAAAAAAADtTmJiO6ivr69KUuOzzz7b5LL533v06JGGDBnS4m0CAAAAAAAAAABAS0lMbAdPP/10Yb6SNs5h2LBhqWfPntn8pEmTSi43d+7cNHny5IVuAwAAwKKjrq52JwAAAAAAYNEhMbGNvfjii+mOO+7I5ldfffW08sorV3T7/v37py222CKbv/POO0u2c544cWJ6//33s/mddtqp1fsNAAAAAAAAAAAALSExsRX+/Oc/p/nz55f8+1tvvZV22WWXNG/evOznww47bKFlJkyYkOrq6rJp7Nixja7nuOOOy/6PbcU6PvnkkwX+PmPGjHTCCSdk8wMGDEgHHXRQa+4WAAAAAAAAAAAAtFiPlt+UI444Iks6/MY3vpE22mijtOqqq6Y+ffpkiYL33HNP+s1vfpPeeeed7EBtsskmjSYmlmPzzTdPe+yxR7r66qvTTTfdlEaPHp2OOuqorC30k08+mc4444w0bdq0bNmzzjorLbXUUh4cAACARVA+8K3W1OI+AQAAAAAAbUdiYiu9/vrr6Ze//GU2lRKJixdddFHq1atXi7dz8cUXZ62ab7311nT33XdnU7Fu3bqlU045JR188MEt3gYAAAAAAAAAAAC0lsTEVrj00kvTpEmT0gMPPJBefPHFrFJiJA/269cvDR48OH3lK19J++23X1ZNsbWiEuMtt9ySrrzyyqz98+OPP57efffdtPzyy6dNN900HX744VXZDgAAAAAAAAAAALSGxMRWGDlyZDa1xv77759N5dprr72yCQAAAICOMW3atPSLX/wiG0Qa89ElY8iQIWm33XZLhx56aFp88cWrsp2rr746XXLJJemJJ55IM2fOTCussEI2QPWwww5LI0aMKHs9s2bNyga6Tpw4MT377LPZ4NoBAwaklVdeOW288cZpzJgxaauttqrKPgMAAAAABImJAAAA0EXU1f1vqjW1uE/QUpGM+M1vfjO99957hd999NFH6aGHHsqmiy66KN16661p9dVXb/E2Zs+enXbdddd08803L/D7V155JZuio8bYsWPTKaec0uy67r777nTAAQdktyv2n//8J5see+yxdN9990lMBAAAAACqSmIiAAAAAJTh8ccfz6oiRiJiv3790oknnphGjRqVPv7446y64e9///v03HPPpe222y5LUoxlWuLAAw8sJCXG+o888si00korpSeffDKdeeaZ6YUXXkinnnpqWnHFFdNBBx1Ucj133nlnVg0xEh379++fvv3tb6ctttgiLb/88untt99OL7/8crrtttvSW2+95fEHAAAAAKpKYiIAAAAAlOGoo47KkhJ79OiRbr/99rTRRhsV/rb55punNddcMx1//PFZu+Rzzz03Sx6s1KRJk7KKiCGSCm+44YbUvXv37Odhw4al7bffPm2wwQZZC+nY1i677JK1ZW4oEg/32GOPLClxnXXWyfZ30KBBCy138MEHp7lz53r8AQAAAICq6lbd1QEAAAAdpa6urmYn6OyiAuI999xTqGhYnJSYO/bYY7MkwPDzn/88zZs3r+LtnH322dn/kYx4wQUXFJIScwMHDkzjxo3L5mfOnJnGjx/f6HqimuM777yTevXqlSU3NpaUmOvZs2fF+wkAAAAA0BSJiQAAAADQjBtvvLEwf8ABBzQeaOvWLe27776FpME8kbFcH374Ybrrrruy+dGjR5dMJtx5553TEksskc1PnDhxob+/++67haqLe+65Z/rsZz9b0X4AAAAAALSWxEQAAAAAaMZ9992X/d+3b9+slXIpI0eOLMzff//9FR3XKVOmpDlz5iy0nsYqHI4YMaJwm4aVGf/85z+njz/+OJvfddddC7//4IMP0vPPP5/+85//VLRfAAAAAACVkpgIAAAAXUWttnHWypku4Jlnnsn+HzJkSOrRo0fJ5dZee+2FblPpNhqup6ntzJ8/P0s2LDZ58uTCfLSc/stf/pI23njjrMriWmutlZZffvm00korpWOOOSbNmDGjon0EAAAAAChH6SgqAAAAAJBmz55dSOAr1V45t9RSS2VVFWfNmpVeffXVio5e8fLNbWfw4MEL3G7o0KGFn59++uns/yWXXDJddNFF6fjjj1/o9m+88UY677zz0nXXXZf++te/LnD7ckyfPr3Jv8f6AQAAAIBFl8REAAAAAGhCtEDO9evXr9ljlScmfvjhh222ndhGruF2/vvf/2b/RzvnE044IfXq1Sv98Ic/THvvvXdadtllswqLZ599drrsssuyBMMddtghPfbYY2Xdt8YSIwEAAAAAGtLKGQAAALqIvGtyLU7Q2Ssm5nr27Nns8pEImCcGttV28m00tp1Iigxz585N9fX16Q9/+EOWoLjyyitn6/3c5z6XLr300vTtb387W27q1KnpN7/5TUX7CgAAAADQFImJAAAAANCE3r17F+Yj2a85c+bMyf7v06dPm20n30Zj2ylez4gRI9Kuu+7a6DrOPPPMQoLjVVddVdG+RvvopqYpU6ZUtD4AAAAAoGvRyhkAAAAAmtC/f//CfDntmfOKhZW0Rq50O/k2GttO8Xq22WabkutYZpll0oYbbpj+/ve/p8cffzzNmzcvLbbYYmXt66BBg8paDgAAAABYNKmYCAAAAF1EXfyrq8Ep6eVM5xYVCAcOHJjNT58+vcllZ86cWUgaHDx4cEXbKU72a247UZUw13A7xT83l0CYL/vJJ5+kd955p6L9BQAAAAAoRWIiAAAAADRjnXXWyf6fOnVqmj9/fsnlnn322YVuU66hQ4c2up6mttOjR480ZMiQBf72uc99rjAfCYdNKf57rAsAAAAAoBokJgIAAABAMzbZZJPs/6iG+Mgjj5RcbtKkSYX5jTfeuKLjOmzYsNSzZ8+F1tPQ3Llz0+TJkxe6Te6rX/1qYf6FF15ocpv53/v06ZOWXnrpivYXAAAAAKAUiYkAAADQRdTV1e4End2OO+5YmL/kkksaXebTTz9Nl112WTY/YMCANGrUqIq20b9//7TFFltk83feeWfJds4TJ05M77//fja/0047LfT3SExcdtlls/kbb7wx1dfXN7qel156Kf3zn//M5r/yla+kbt2ECgEAAACA6hBtBAAAAIBmDB8+PG266abZ/Pjx49MDDzyw0DLnnHNOeuaZZ7L5I488Mi222GIL/H3ChAmprq4um8aOHdvodo477rjs/2gXfdhhhy3UinnGjBnphBNOKCQ/HnTQQQuto3v37oX1PPfcc2ncuHELLTNv3rx06KGHZsmU4ZBDDnEOAAAAAABVIzERAAAAAMpw/vnnZy2PI2lwq622Sj/5yU+ylsp33313Ovjgg9Pxxx+fLbfWWmulY489tkXHdPPNN0977LFHNn/TTTel0aNHZ/8//PDDWaXGESNGpGnTpmV/P+uss9JSSy3V6Hq++93vpi996UvZ/Iknnpj22Wef9Ne//jU9+uij6dprr82SLP/yl79kf992223TN77xDecAAAAAAFA1Paq3KgAAAADoutZff/10zTXXpL333jtrpXzSSScttEwkJd5yyy1ZW+aWuvjii7P133rrrVnSY0zFouXyKaeckiVDltK7d+908803pzFjxqRHHnkkXX755dnUUCQlXn311VkVRwAAAACAalExEQAAALqIvEVsLU7QVUSi3xNPPJGOPvroLAlx8cUXz1oqb7jhhlnL5MceeywNGTKkVduIqoyR3HjFFVdkFROXW2651LNnzzR48OC01157pfvvv79kK+hiK664YlbR8Te/+U0aOXJkWnbZZbP20iussELafvvt08SJE1udRAkAAAAA0BgVEwEAAACgAqussko699xzs6kS+++/fzaVK5IQY2qNHj16ZJUVm6quCAAAAABQbSomAgAAAAAAAAAAAFWjYiIAAAB0EdExuRa7JtfiPgEAAAAAAG1HxUQAAAAAAAAAAACgaiQmAgAAAAAAAAAAAFWjlTMAAAB0EXV1ddlUa2pxnwAAAAAAgLajYiIAAAAAAAAAAABQNRITAQAAAAAAAAAAgKrRyhkAAAC6iOiYXItdk2txnwAAAAAAgLajYiIAAAAANeWss85Kb7zxRkfvBgAAAAAALSQxEQAAAICactJJJ6VVVlkljRkzJt14441p/vz5Hb1LAAAAAABUQGIiAAAAdKlWznU1OHX0kaEzimTEW2+9NX3jG99IK6+8cjruuOPSU0891dG7BQAAAABAGSQmAgAAAFBTnnzyyXTUUUelgQMHpvr6+vT222+n8847L33hC19II0aMSL///e/TBx980NG7CQAAAABACRITAQAAAKgpn/vc59K5556bXnvttTRx4sSspXP37t2zJMWHHnooHXLIIWnFFVdM++23X5o0aVJH7y4AAFV04ISHFpoAAIDOR2IiAAAAADWpR48eaccdd0x/+tOf0vTp09PZZ5+d1llnnSxB8aOPPkqXX3552nzzzdOQIUPSmWeemSUy8v+xdydgWlZl48DvGZFVBBRREVQMF8gyU1BEBTFRU9y3XFFcKjUtt7Jcyi1NUSvXRJDc9w1NwwTBNNEUd1IUEVJwwYVdlv91Tt/Mf1gGZoZZXmZ+P6/zPWee9zzPOfPid3VxvM99AwAAAADUPYGJAAAAUF8URRQVYEvrghXVrl27OP300+P111+P559/Po4//vhYffXVc5Die++9F+ecc05suOGG8cMf/jDuu++++Oabb3zpAAAAAAB1RGAiAAAAACuV7t27x/XXXx8fffRRDB06NNZZZ50coDh//vx44okn4qCDDor11lsvfvnLX+YxAAAAAADULoGJAAAAAKx0JkyYEJdeemmce+65MWXKlCjK6TkjByim9umnn8Yf/vCHXOZ54MCBdb1cAAAAAIAGpVFdLwAAAACoHikwqyQ4q5AU4ppYOc2aNSuXaR48eHCMHDmyNAgx6dKlSwwYMCCOOOKIeO2112LQoEFx77335mfOOOOMXAr68MMPr+tfAQAAAACgQZAxEQAAAICC9txzz8Xxxx8f6667bhx11FExYsSIWLBgQTRr1iz69+8fo0ePjjfeeCN+8YtfxFprrRV9+vSJ2267LcaNGxdbbrllDl688sor6/rXAAAAAABoMGRMBAAAAKDgfPTRRzF06NCcHfGdd97J90qyI2611VZx7LHHxqGHHhotW7Ys9x0bbrhh/P73v49dd901/vOf/9Ta2gEAAAAAGjqBiQAAAFBPpILJhVg1uQCXRIHbY4894sknn8xZEUuCEVu3bp0DEY877rjYYostKvyuTp065evMmTNrbL0AAAAAACxKYCIAAAAABeXxxx8v7e+44445O+IBBxwQTZs2rfS7mjdvnt9RVIhRuwAAAAAA9ZTARAAAAAAKSrt27eKoo47KAYkbb7zxCr2rffv2MWLEiGpbGwAAAAAAyycwEQAAAOqJlBGuELPCFeKaKGyTJk2KRo1sWwEAAAAArKzs8AIAAABQUG6//fZ83WeffWL11Vev0DPTp0+P+++/P/ePPPLIGl0fAAAAAADLJjARAAAAgILSv3//nGlz6623jq5du1bomSlTpuTniouLBSYCAAAAANQxgYkAAABQT6SKyYVYNbkQ10T9tXDhwrpeAgAAAABAg1fc4L8BAAAAAFZ68+bNy9dGjZzDBQAAAACoawITAQAAAFjpjRs3Ll/XWGONul4KAAAAAECD5wg5AAAA1BNFRUW5FZpCXBOF5Zlnnlnq/TFjxsSnn366zGfnzJkT48ePj8svvzz/u/a9732vhlYJAAAAAEBFCUwEAAAAoE717t17iQDWhQsXxjHHHFPhd6Tx6R0nnHBCDawQAAAAAIDKUMoZAAAAgDqXAgtL2tLuLa916NAhrrnmmthnn33q9PcAAAAAAEDGRAAAAADq2NNPP13aT0GGffr0ydkPBw0aFJ06dSr3uTSmadOmse6660bHjh1rabUAAAAAACyPUs4AAABQT6QgrcXL4RaCQlwThaVXr15Lvd+9e/fo2rVrra8HAAAAAIAVIzARAAAAgILy/vvv5+t6661X10sBAAAAAKAKBCYCAAAAUFA22GCDul4CAAAAAAArQGAiAAAA1BOpYnIhVk0uxDUBAAAAAAA1R2AiAAAAAHXid7/7XWn/3HPPXer9qij7LgAAAAAAap/ARAAAAADqxPnnnx9F/5dSs2wwYdn7VSEwEQAAAACgbglMBAAAgHoiBXKtSDBXTSnENVE4Fi5cWKn7AAAAAAAUPoGJAAAAANSJBQsWVOo+AAAAAAArh+K6XgAAAAAAAAAAAABQf8iYCAAAAPVFUSqbHIWnENcEAAAAAADUGBkTAQAAAAAAAAAAgGojMBEAAAAAAAAAAACoNko5AwAAQD1RVFSUW6EpxDVRGPr06VMj/7499dRT1f5eAAAAAAAqTmAiAAAAAHVixIgROZBw4cKFK/yukvcIhAUAAAAAqHsCEwEAAACoEzvuuKNAQgAAAACAekhgIgAAANQTqWByIVZNLsAlUUAZEwEAAAAAqH+K63oBAAAAAAAAAAAAQP0hMBEAAAAAAAAAAACoNgITAQAAAAAAAAAAgGrTqPpeBQAAANSl4qKi3ApNIa4JAAAAAACoOQITAQAAAKgTq6yySr4WFRXFvHnzlrhfFYu/CwAAAACA2icwEQAAAIA6sXDhwkrdBwAAAABg5SAwEQAAAOqJVDG5EKsmF+KaKAznnXdepe4DAAAAALByEJgIAAAAQJ0QmAgAAAAAUD8V1/UCAAAAAAAAAAAAgPpDxkQAAACoJ4qKinIrNIW4JgAAAAAAoObImLgCevfuXfoffSraRowYUel5zj///Bp9PwAAANQXEydOjNNPPz26dOkSLVq0iDXWWCO6d+8el19+ecycObNa5xo+fHj0798/OnfunOdq1apVbLLJJnHAAQfEddddF9OnT6/W+Rq6KVOmxFNPPRX33HNPbqmf7gEAAAAAUHhkTKxFxcXFsfHGG9fmlAAAANBgDBs2LA477LD48ssvS++lYMQxY8bkdtNNN8Vjjz0WG2200QrNM23atDj66KPjoYceWuKzr776Kt5555247777okePHvG9731vheZq6BYsWBA33HBDXHvttfHmm28udUzXrl3jpz/9aZxwwgl57wUAAAAAgLonMHEFDB48OGbMmLHMMWnT/OCDD879nXfeOdZbb70VmTJee+21ZX7eqVOnFXo/AAAAK6/iov+1QlMbaxo7dmwcdNBBORBxtdVWi1/96lex0047xaxZs+LOO++Mv/zlLzFu3LjYY489cpBiGlMVKehxl112iZdeein/nN53yCGH5KyJ8+fPjw8++CC//957763m37DhSdkQ0/f78ssv558XLlxY7t7LSSedFDfffHM88sgjsc4669TySgEAAAAAWJzAxBVQkSDAv/71r6X9I488MlbU5ptvvsLvAAAAgPrm1FNPzUGJjRo1iieffDJnKyzRp0+fXMHgzDPPjLfffjsGDhwY5557bpXm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4/zzz49zzjlnhX4XAAAAAICaYne8Gn3zzTf5JHuy1lprxe677x61pSRLY9K+fftamxcAAIDCUVz0v1ZoanpN22+/fQ5MTNkQX3rppdhmm22WOm7kyJGl/Z49e1ZqjpRVbv3118/Z8caPH7/MsWU/X2+99So1T0PTq1evWJmMHTs2Z0VMgYipHPivfvWr2GmnnWLWrFl5T+gvf/lLjBs3LvbYY48cpFjZkuElBgwYUBqUmN5/yimn5P2e1157LS6++OL879i5556bS6Mfe+yxy3zX3nvvHRdeeGG5nzdu3LhKawQAAAAAWBaBidXo8ccfj08++ST3Dz300FrNinDDDTestJv6AAAAsCL22WefuOSSS3I/lb1dWmDiggULSqsctG7dOgd7Vdb+++8fV155ZUyZMiX++c9/xnbbbbfUcffff39pP5Xdpf449dRTc1Bi2vN58skno0ePHqWf9enTJzbeeOM488wzc3bOgQMH5uDBykoBtCkjYtKvX7944IEHYpVVVsk/d+vWLfbaa6/YaqutcpBsmuuAAw7I/06XJ322+eabV+n3BQAAAACoquIqP8kyyzgfeeSRFfqGhgwZEkVFRbmlEjyLSyfhUymq5QUlDho0KPfXWWed2Hffff3pAAAA0GB07969NAAw/f34ueeeW2LMFVdcEW+99Vbup+xzq666aqX+fl4SlNa0adPc/9nPfpYzNC7u1ltvjREjRuR+yprXoUOHavgNKQQpA2LJn23KaFg2KLHEaaedVlom/KqrrsrVNSrrsssuy9cUjHjttdeWBiWWaNu2bVx66aW5P23atNI9IQAAAACAQiIwsZqkjeCSEjvpFPr3v//9anlvKkG12WabxS677JJP2v/973+Pf//73/HCCy/kQMi+ffvGj3/84zw2bVSnIMUWLVpUy9wAAACsXEoC6wqx1bSrr746mjVrFvPmzct/V04ZFJ9//vl4+umn44QTTsiZ5ZJNNtkkB49VRSrl/Lvf/a707+spIPKWW27J/X/84x9x0kknRf/+/fPnq6++es6uSP3x4IMPlvaPPvropY4pLi4uPaya9opKAhkravr06fHUU0/lftoLKi+wdb/99sv/ji2eoRMAAAAAoFAo5VxN7rrrrpgzZ06lsiVW1Pz582P48OG5lWfNNdfMJ+RTOR8AAABoaLbccsv8d/PDDz88vvrqqzj7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jhTQOWvWrHJ/l6+//joHcH722Wf55wEDBkSTJk2W+x0AAAAAABRUYGLJ5nAq51xR7dq1y9eSDVIAAABg2UoCngqxQWWss8468c0338Ts2bNjo402WuSzBx54IH7961/H2muvnQMUU2vVqlUO4kuBjE2bNq2xL/vqq6+OZs2a5aDBvn37xiWXXJJLKqd504HcM888M4/bZJNN4rTTTqtytshDDjkk9x9++OHYZZdd8vXFF1/MmRq33XbbmDhxYv7897//fbRp02aJd6T7HTp0iOOOOy5uueWWHMSZ9udSpsm05m9/+9vx5JNP5rGbbrppuUGSAAAAAABV1ShqwRdffLFIsGFFpM3npLi4VmInAQAAACgg5e0Jpcx+F1xwQW6ff/55DhJca621aiUANlX4SOWPU+bGVEr57LPPXmJMCkocNmxYLstcVTfffHN+/2OPPZaDHlNb/Ls555xzcjBkedJ3c9NNN+VWnh133DFuv/32WGONNaq8VgAAAACAOgtMTCe3P/nkk0plPywp4Zw2lgEAAABgcXURUNevX7949dVXc/bEFIA4adKkaNy4cXTu3DkOPPDAOOmkk6J58+YrNEfKypjenYIGU/nnsWPH5oO/KUtkKied5ujRo0e5z19++eXx1FNP5XLTaY8tlX9Oz6d1tW/fPrbZZpv40Y9+lLM+ymgKAAAAAKy0gYldu3aNkSNH5rIxO+20U4WeSRuvaWN0q622qvH1AQAAQH2QEsYVYtXkQlwTrIgNNtggBg4cmFtl9O/fP7eKOvTQQ3OrrK233jo3AAAAAIB6HZi41157xYgRI+Laa6+NE088cbmn2QcPHhxPPPFEDkzcd999a2OJAAAAABSol156KYYPHx6vv/56LlGcpP2lzTffPH7wgx842AoAAAAA0BADE0844YRcQuajjz6KXXbZJYYOHRrf/va3lxj34YcfxmWXXRbXXXddDkrceOONq3QqHAAAAICV34svvpgPuaZrec4+++ycHfCaa66RJRAAAAAAoCEFJjZr1iweeOCB6NOnT7zyyivx3e9+NzbddNPSz3/84x/HJ598Ev/5z3/yzwsXLoyWLVvGvffeG8XFxbWxRAAAAFjpFRcV5VZoCnFNFL677747jjjiiJg3b17eK0oaN26cMyWmn6dNmxZz587N98eMGRM9e/aMW2+9NQ488MA6XjkAAAAAALUW9detW7f45z//mUvspM3jt99+u/SzZ599NsaNG5fvp9alS5d8L40FAAAAoGFJ+0ZHHXVUfPPNN7HKKqvET37ykxx8OGPGjPjvf/+bq3KkfsqkmD5r1KhRHnvkkUcusucEAAAAAEA9zphY4jvf+U6MHTs2hg0bFg899FDePJ46dWrMnz8/1lxzzdhyyy1jr732iv3331+mRAAAAIAG6tJLL405c+ZE06ZN47HHHovevXsvMSYFLH7/+9/P7aCDDordd989P3PZZZfFzTffXCfrBgAAAACgDgITS+yxxx65AQAAANUnFUwuxKLJhbgmCttTTz0VRUVFceqppy41KHFxvXr1ymN///vfx/Dhw2tljQAAAAAAFEApZwAAAACoiE8++SRff/jDH1b4Cys5BFvyLAAAAAAA9Tww8ZprrolPP/20NqYCAAAAYCW31lpr5Wsq5VxRTZo0yde2bdvW2LoAAAAAACigwMSTTz452rdvn0+u33777TFz5szamBYAAAAalFT6tlAbVEbPnj3zdcyYMRV+5oUXXsjX7bff3pcNAAAAANBQSjnPmzcv/va3v8URRxwRa6+9dhx++OHx+OOPx/z582trCQAAAACsBH7xi1/EKqusEhdffHGFSjNPnTo1Lrnkklh11VXj5z//ea2sEQAAAACAOg5M/Ne//hWnnHJKDkhcuHBhzJgxI+64447Yc889cybFn/3sZ/H888/XxlIAAAAAKHDdunWLG264IQccbrPNNvHggw/GggULlhiX7j300EPRo0ePHMB43XXXRffu3etkzQAAAAAA/H+NopY2k1O74oor4h//+Efcdttt8cADD8RXX32VN42vueaa3DbccMOcSfFHP/pRbLbZZrWxNAAAAADqyDHHHLPMz7t27Rpjx46N/fffP9q0aRNbbrlltGvXLpcHnzJlSrzyyivx+eef57FbbLFFjB49Op599tkYNGhQLf0GAAAAAADUWWBiieLi4vjBD36Q2/XXXx+PPPJIDlJMJZ3nzp0b77//flx44YW5pY3mFKR48MEHx7rrrlubywQAAICVUnHR/1qhKcQ1URiGDBmSgwyXJX2eKnCkAMR04LWsdL9kTApgTC0RmAgAAAAA0IACE8tq0qRJHHDAAbl98cUXce+998btt98ezzzzTC7D8+9//ztefvnlOPPMM3PQIgAAAAD1y/rrr7/cwEQAAAAAAFY+dRaYWFbr1q3j2GOPzW3y5Mk5QPGSSy7JAYvz58+v6+UBAAAAUAMmTJjgewUAAAAAqIcKIjCxxOuvv55LO99xxx3x5Zdf1vVyAAAAYKWSMs8VYva5QlwTAAAAAABQjwMTJ06cmAMRU0DiG2+8ke8tXLgwX5s3bx577713Ha8QAAAAAAAAAAAAKOjAxM8//zzuueeeHIz4z3/+MwcilgQjrrLKKvGDH/wgDj/88Nhnn32iRYsWdbFEAAAAAArMvHnzYtq0abnfpk2baNSozs/cAgAAAACwFLW2eztr1qx46KGH4vbbb48nnngibyQnJQGJ3bt3j8MOOywOOeSQWGuttWprWQAAAFCvqJpMffPWW2/FtddeG8OHD4933nmndC8plQjfeOONY5dddokf//jH0bVr17peKgAAAAAAtRmYeOSRR8aDDz4YM2bMyD+XbCCnzeNDDz00ByR27ty5NpYCAAAAwEriV7/6VVx++eWxYMGC0v2kEunncePGxX/+85+47rrr4owzzoiLL764ztYKAAAAAEAtBybeeuutpf127drFwQcfnEs1d+vWrTamBwAAAGAlc/LJJ+dMiSUBiV26dIltttkm1llnnXxvypQp8cILL8Sbb74Z8+fPj0svvTQfir366qvreukAAAAAAA1erQQmtmjRIvbdd9+cGTGV1ykuLm7wXzwAAABUt1TaNrVCU4hrorA9++yzcc011+R/d1KJ5htvvDG22267pY597rnncinn1157Lf785z/nA7HljQUAAAAAoB4FJk6dOjWaNWtWG1MBAAAAsJK74YYb8rVTp045SLFVq1blju3Ro0c888wzsdVWW8X7778f119/vcBEAAAAAICGEJgoKBEAAACAiho1alTOlvjLX/5ymUGJJdKYs846K0444YT8LAAAAACsrAYMGbPEvUH9u9XJWqDgAxMBAACAmldc9L9WaApxTRS2jz/+OF+33HLLCj/z/e9/P1+nTJlSY+sCAAAAAKAOAhOHDh1a2j/yyCOXer8qyr4LAAAAgPqtadOmMXfu3JgxY0aFn5k+fXq+NmnSpAZXBgAAAABArQcm9u/fP5fZSa1sMGHJ/apY/F0AAAAA1G+dOnWKsWPHxsMPPxw77rhjhZ555JFH8nWjjTaq4dUBAAAAALA8xVHNFi5cmFt596vSAAAAgOVLZwJLDgwWVvOnR+X88Ic/zHtCf/7zn+Opp55a7vg05k9/+lP+9y09CwAAAABAPcqY+P7771fqPgAAAAAs7tRTT81BiV9//XXsvvvucdxxx8UxxxwTW265ZRQX/++c7YIFC+Lll1+OQYMGxU033RTz5s2LVq1a5WcBAAAAAKhHgYkbbLBBpe4DAAAAwOLatm0bd999d+y1114xd+7cuP7663Nr3LhxrLHGGjkz4meffZY/S1J2xfTZPffcE2uuuaYvFAAAAACgvpVyBgAAAIAV1bdv33j++edj6623zoGHqc2ZMyc++uij+O9//5v7Jfe7desW//rXv+IHP/iBLx4AAAAAoL5lTCxPKrWTTrJfeOGFse6661bomU8++STOOuus/FwqyQMAAAAsW9H/tUJTiGti5fC9730vXnjhhRgzZkwMHz48Xn/99fj888/zZylz4uabb56DEVNgIgAAAAAADSwwcciQITnA8LTTTqtwYOJXX31V+pzARAAAAICG45lnnsnXtI+08cYb58BDwYcAAAAAACsPpZwBAAAAKCi9e/eOnXbaKZ599tm6XgoAAAAAAIWaMbEqZs+ena9NmjSp66UAAADASqG4qCi3QlOIa6KwrbbaajFjxoz4zne+U9dLAQAAAACgPmVMLDkRv/baa9f1UgAAAACoReuvv36+zpw50/cOAAAAALASqpGMib/73e+Wev/aa6+Ndu3aLfPZOXPmxPjx4+Phhx+OoqKi6NmzZ00sEQAAAIACtccee8Rbb70Vw4cPjx122KGulwMAAAAAQCEEJp5//vk5qLCshQsXxnXXXVfhd6TxTZs2jTPOOKMGVggAAAD1T/qreCFWTS7ENVHYfv7zn8fNN98cV111VRx44IGx+eab1/WSAAAAAAAohFLOKbCwpKUgxdTK3iuvNWnSJDbccMM47LDD4rnnnostttiippYIAAAAQAFaZ5114tFHH42WLVvmahoXX3xxTJgwoa6XBQAAAABAXWZMXLBgwSI/FxcX58DE119/Pbp27VoTUwIAAABQT2y00Ub5Onfu3Pj666/jnHPOyW211VaL1q1bxyqrrFLus2kPavz48bW4WgAAAAAAaiUwcXHrr79+3hRu3LhxbUwHAAAADdP/VSwoOIW4Jgra4tkRU5WNJAUpprYsBfn/AwAAAAAADUytBCYqtQMAAABARR111FG+LAAAAACAlVitBCYCAAAAQEUNHjzYlwUAAAAAsBITmAgAAAD1RKpgW4hVbAtxTQAAAAAAQD0KTHz66afjwQcfjLFjx8ann34as2bNioULF5Y7vqioKMaPH1+rawQAAACg9qU9on//+9/xwQcfxPz586N9+/bRrVu3aNy4sT8OAAAAAICVSK0FJk6dOjUOOeSQGDlyZP65vGDEFIhY9rP0MwAAAAD124033hjnnXde3kMqq3nz5nHKKafEb3/721hllVXqbH0AAAAAABRYYOI333wTu+++e7zyyis56HDLLbfMJ96HDRuWAw8PP/zwmDZtWj4R/9///jff+/73vx+bb755bSwPAAAAgDp0/vnnxwUXXLDUw6wzZsyISy65JN588824//7762iFAAAAAAAUXGDikCFD4uWXX84Bh4MHD46jjjoq3njjjRyYmNxyyy2lYx966KE48cQT82bzL3/5y9h///1rY4kAAACw0isuKsqt0BTimigcr7/+elx44YWlQYk9e/aMbbfdNho1ahSvvvpqPPnkk7msc9ozuuuuu+Lggw+u6yUDAAAAAFAIgYn33Xdfvu622245KHFZ9t5775wpceutt47+/fvHd7/73dh4441rY5kAAAAA1EEJ5wULFuRAxBR4uO+++y7y+Ysvvhi77rprfPHFF3HDDTcITAQAAAAAWAkU18YkY8eOLS3ZvDSLl+j51re+Faecckou1XP11VfXxhIBAAAAqAOjR4/O+0YnnHDCEkGJSTq8mjIqpv2jf/3rXzmIEQAAAACAwlYrgYmff/55vnbq1Kn0XuPGjUv7M2fOXOKZnXfeOV///ve/18YSAQAAYKWXKiYXaoPyTJgwIV/32muvcsfss88++Tp79uz4+OOPfZkAAAAAAAWuVgITS4IQywYjrr766qX9yZMnL/FM06ZNy/0MAAAAgPrhq6++ytd111233DHrrLNOaf/rr7+ulXUBAAAAAFDggYnrr79+vk6ZMqX03tprrx0tW7bM/VSGZ3FvvPFGvqZSPgAAAADUTyWlmYuLy9+mKrs/NH/+/FpZFwAAAAAABR6Y+P3vfz9fX3755UXu77jjjrFw4cK4+uqrY86cOaX3v/zyy7jsssvypnPXrl1rY4kAAACw0itK/xQVYAuHDgEAAAAAoCGplcDEnXfeOQcgDhs2bJH7P/7xj0sDFr/zne/E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+OIYP358nHvuubl6RvqeKuqkk07KlTbatWuXAzsBAAAAatKAIWOWen9Q/26+eGgAimtjkg033DCXL04btPVFOrleonHjxsscm8r5lEgb1VWda3nzlJ2rKvMAAAAAFIJ77703br755th6661zhsLyghKT9NkjjzwSW221VX7m7rvvrrF1nXrqqTkosVGjRjlT49lnnx09evSIPn36xI033hiXXXZZHvf222/HwIEDqzTHyJEjc0bEpF+/fvH3v/899t5775wR8phjjonnn38+1l9//fz5mWeeGV988UWF3vvQQw/FAw88EGuttVacddZZVVobAAAAAEBBBSY2a9YsX9MGan3RtGnT0v7cuXOXOXbOnDlLfBdVmWt585Sdq7LzpNLPy2ovvPBCpdcNAAAAUBU33HBDFBUVxS9+8YtYZZVVljs+jTnttNNyiecUIFgTUgbEESNGlGY0TAGJi0tr6NKlS+5fddVVucR0ZZUEN6bfKZWyXvz3b9u2bVx66aW5P23atJxNsiLVOFK2xOTyyy+vNxVNAAAAAIAGHphYcqp9/vz5UV+0bNmytL+8sskzZswo7VelhE/JXBUpz1wyV2Xn6dChwzJbKg0EAABAYUuBXMUF2NK6oDJeffXVfN1kk00q/EzJ2FTuuCY8+OCDpf2jjz56qWOKi4vjyCOPLA0aLAlkrKi09/PUU0/l/i677JL3ZJZmv/32i9VXXz3377///uW+N5WcnjRpUvTu3bt0fQAAAAAAK31gYt++ffN19OjRUV+U3RhOG7vLkjIOlujYsWOV51rePGXnqso8AAAAAIUgZfhLpk6dWuFnSsaWPFvdRo0ala8tWrTIZaPL06tXr9J+ZffCUsWKkmoYZd+zuMaNG8e2225b+syyMjP+61//iuuuuy4/k64AAAAAAPUmMPGUU07JpYVTqZjJkydHfdC1a9fS/ttvv73MsWU/LynnU5W5vvzyy/j444/LHffRRx/FV199VeV5AAAAAArBBhtskK9Dhw6t8DMlY9dff/0aWdNbb72Vr507d45GjRqVO26zzTZb4pnKzrH4e5Y1z7x58+Kdd95Z6pgUsHjcccfFggUL4owzzljuOwEAAAAAVqrAxI033jhuv/32mDlzZj7Nnfpz586NlVmnTp2iffv2uT9y5Mhljn3mmWdKS1pvuOGGlZ5r++23L+0va66yn/Xs2bPS8wAAALDy/yW/UBtUxt577x0LFy6MO++8My677LLljk+HYe+4445cNnzfffet9i979uzZ8emnn+Z+eeWVS7Rp0yZnVVy8ikZFlB2/vHnKVssob54//OEPubT1RhttFL/+9a+jOqXKHstq6QAtAAAAANBwlX+8uxr16dMnX9daa614//3344gjjogBAwbkgMW0WbvKKquU+2zaUH7qqaei0KR1pU3yVAInZUR8/vnnS0volJXul2RMTOPTc5W11157xU9+8pN8un3w4MFx8MEHL3XckCFD8rW4uDg/AwAAALAy+uUvf5kzIE6ZMiV+9atf5aDDo446Krp16xbt2rXL+yvpszFjxsRf//rXeOWVV/Jz66yzTpx11lnVvp6y5aFXW2215Y5PgYkzZsyI6dOn19g8JcGPydLmeffdd+OCCy7I/WuuuSZXM6lOZQMjAQAAAADqJDBxxIgRiwTkpRPvc+bMiddff73cZ9L4NK4qgXy15dRTT42//OUvuWTOySefnDMjlt3knTVrVr6fpBI/afzS9O7duzTbYQrcXDyrYtpUP+yww/JG+xNPPBH33ntvHHDAAYuMueeee/JnSQr8TM8AAAAArIxat24dw4cPj1133TUmT54cr776apx22mnljk97SCnD4N/+9rf8bE1kTCzRuHHj5Y5v0qRJ6d5QTc1TMkd58/z4xz/O7zvwwANjt912q9Q6AAAAAABWisDEHXfcseACDEePHp1PjpcoKceTpPsl2QdL9O/ff4l3bLLJJnH66afH73//+3jxxRdz+eR0Kv9b3/pWjB8/Pi699NJ4+eWX89gzzjgjZ4isqosuuihvrn/yySfxox/9KM+355575s8effTRuOKKK0qzUl544YVVngcAAACgEHTt2jXeeOON+O1vf5v3aaZNm7bUcakax9FHHx3nnnturL766jWylqZNm5b2586du9zx6UBuUtkshZWZp2SOpc2Tvq9UgSR9H1dddVXUhOWVqU6lnLt3714jcwMAAAAAha/WMiYWmptuuiluueWWpX727LPP5ra8wMSSgMGpU6fGzTffnIMQDznkkCXGpLLVKxosmMrjPPLII7HPPvvExx9/nIMeUysrZUl88MEHc4YAAAAAGp50JrDAzgVmhbgmVg4psC4dxrzkkkvipZdeitdeey0HKKYMiWussUZ85zvfia222qpC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ePXVV3N/xx13rJU1AgAAAABQS4GJEydOjJqw/vrr18h7AQAAACg8w4cPj9133z3WXHPNCu03NW7cOPbff//4/PPP87M77bRTrawTAAAAAIBaCEzs1KlT1ERWhXnz5lX7ewEAAAAoTHfffXe+HnjggdGsWbPljm/evHkcfPDBce2118Zdd90lMBEAAAAAoI4VV+fLFi5cWCMNAAAAgIbjpZdeyodV+/TpU+FnSrIkvvDCCzW4MgAAAAAAaj1j4uDBg6vzdQAAAEAlTx8WF9XzU5E0CJMmTap0dY4NN9wwXydPnlxj6wIAAAAAoA4CE4866qjqfB0AAAAADdDMmTPztTKVNErGfvXVVzW2LgAAAAAAKkbSAgAAAAAKylprrZWvb7/9doWfKRm75ppr1ti6AAAAAACoB4GJY8aMiZNPPrmulwEAAAArhaKiwm1QGd26dcsZEIcOHVrhZ4YMGRJFRUXx/e9/35cNAAAAAFDHCi4wcdKkSXHJJZdE165dY9ttt41rr722rpcEAAAAQC064IAD8vWpp56KK664Yrnj05h//OMfuX/ggQfW+PoAAAAAAFgJAhNnzJgRt9xyS+y8886x4YYbxm9+85sYN25cPhkPAAAAQMNy8MEHxxZbbJH3hs4888zYf//9Y9SoUfHNN9+Ujpk3b16+t99+++UxKVvi5ptvHocffnidrh0AAAAAgIhGdfUlpI3l4cOH55I8Dz74YMycObP0ftKhQ4e8sZw2ngEAAIDlKy4qyq3QFOKaKGwpyPCBBx6Inj17xkcffZT3jlJbddVVY4011siff/bZZ6WBimk/qX379vHQQw/lzwAAAAAAaGAZE9944418ir1jx46x2267xe23354zJqYN5HTvtNNOi+eeey4mTpwYV111Veywww61vUQAAABgJZX2E04//fTo0qVLtGjRIgexde/ePS6//PLSQ5HVLQXOtW7dOgfEpda7d+8amaehSVU1Xn755dh7773zz2nvaO7cufHxxx/n7zz1Sw64psOt//73v/MzAAAAAAA0kIyJn3zySdx22205O+LYsWPzvZKN47Rx/8UXX+SN+8suuywOOuig2lgSAAAAUM8MGzYsDjvssPjyyy9L76VgxDFjxuR20003xWOPPRYbbbRRtc578sknLzIn1addu3Y5c+K4cePyn10KVPz000/zZ23bto3vf//7sccee8TGG2/sawcAAAAAaAiBiXPmzMnlc1Iw4pNPPhnz588vDUZs0qRJ/PCHP4zDDz88X5s1a1ZTywAAAIAGo7jof63Q1Maa0kHIdNgxBSKuttpq8atf/Sp22mmnmDVrVtx5553xl7/8JQe3pSC2FKSYxlSHRx55JO67774cQDd16tRqeSdL2nTTTXMDAAAAAKCBBiaOHj06ByPec8898dVXX+V7KSAxZURMZZlTMGL6DwWtWrWq7qkBAACABurUU0/NQYmNGjXKByR79OhR+lmfPn1yRr0zzzwz3n777Rg4cGCce+65Kzzn9OnT48QTT8z9VCr6yCOPXOF3AgAAAABAfVBc3S/ccccdY9CgQbmEUQpI7NKlS1x00UXx/vvvx8iRI+O4444TlAgAAABUm5QBccSIEbk/YMCARYISS5x22ml5jyK56qqr4ptvvlnhec8+++z48MMPc2bGI444YoXfBwAAAAAA9UWNlXJu2bJlXH311dG/f/+amgIAAAAoqyiiqKgw11WTHnzwwdL+0UcfvdQxxcXFOaNhKvE8bdq0HMi4yy67VHnOF154Ia655ppo3LhxXHfddVV+T0M3ceLE0v7666+/1PtVUfZdAAAAAADUk8DElCkxlTNKWQpSeaRUvvnQQw+NDh061MR0AAAAQAM2atSofG3RokVstdVW5Y7r1atXaX/06NFVDkycN29eHH/88bFgwYI466yzYtNNN63Se4jo1KlT/hqKiory97r4/apY/F0AAAAAANSDUs7pPwYce+yxuVxzClB8/fXXczaCDTfcMJc2uvnmm+Orr76q7mkBAACABuqtt97K186dO0ejRuWfwdxss82WeKYqLr/88hg7dmx861vfyuWcqbq0d1TSyrtflQYAAAAAQD3LmNizZ8/c/vznP8dDDz0Ut9xySzz55JP5pPozzzyT20knnRR77LFHHHHEEfHDH/6wupcAAAAADVJxFOVWaMqu6aOPPlru+MpUXJg9e3Z8+umnFXquTZs2OavijBkz4sMPP4yqeO+99+J3v/td7l977bXRtGnTKr2H/xk8eHCl7gMAAAAA0IBLOSeNGzeOAw88MLdPPvkkbr311hg6dGjOKJD+o8H999+fW+vWrWtqCQAAAECB6d69+3LHVCbj3ddff13aX2211ZY7viQwcfr06VEVJ5xwQsyaNSsOPvjg6Nu3b5Xewf931FFHVeo+AAAAAAANPDCxrLXWWit+/vOf55ZKOw8ZMiTuuOOOnCVh2rRpUVT0v8wJv/jFL+LZZ5+NAw44IHbYYYfaWBr1UJtuJ0Uhmzbmz3W9BACoVYX+v82J/30GWHmlw49lD0kuT5MmTfI1BRdWVjpwOXz48Fh99dXjyiuvrPTzAAAAAADQUBTX9oSbb755XH755blk0mOPPRaHHHJILnuUsiH897//zSWge/fuHeuuu2789Kc/jaeeeqq2lwgAAADUkBdeeCHvCSyrVUbZUspz585d7vg5c+bka7NmzSo1TyoXfdppp+X+RRddlPctAAAAAACAOsyYuDTFxcWx22675ZbKLt11113x17/+NUaPHp2DFKdMmRI33HBD3HjjjTFv3ry6WiYAAACsNFJBgv8rSlBQyq4pBfR16NCh2t7dsmXL0n5FyjOnMs4VLftcVqrykIITt95663yQEgAAAAAAKMDAxMX/I8Kxxx6b24QJE3JppFtvvTXefffdul4aAAAAUMBSxsS2bdvmoMFJkyYtc+y0adNKAxM7duxY4TlShYd0mDLp06dP3H333cscP3Xq1Ljzzjtzv1OnTrHNNttUeK6G5ne/+12NvPfcc8+tkfcCAAAAALASBSaWteGGG+bN49SeffbZ0o1/AAAAgKXp0qVLjBo1Kh9wTFUXGjVa+nbH22+/vcgzFVW2RPRll1223PFvvfVW/OhHP8r9o446SmDiMpx//vlRVANpPgUmAgAAAADUreIoYD179ozrr7++rpcBAAAAK4XiosJtNWn77bfP15QN8aWXXip33MiRIxfZc6AwLFy4cJmtKmMAAAAAAKhbBR2YCAAAALA8++yzT2l/8ODBSx2zYMGCGDp0aO63bt06dtppp0pVd1heYFzZYLhevXqV3hsyZIg/wGVIfy7ltffeey+6deuWv8fdd9897rnnnvjggw9i9uzZuaX+vffemz9LY9LY999/Pz8LAAAAAEDdEpgIAAAArNS6d+8eO+ywQ+4PGjQonnvuuSXGXHHFFbnEcnLKKafEqquuusjnKYAwlRROLZUXpm59+eWX0bdv3/j3v/+dA0qHDRsW+++/f3Ts2DEaN26cW+rvt99++bO//vWvOVvmD37wg/wsAAAAAAB1S2AiAAAA1BPFRUUF22ra1VdfHc2aNYt58+blgLZLLrkknn/++Xj66afjhBNOiDPPPDOP22STTeK0006r8fWwYq688sp4991347jjjovDDz98ueMPO+ywPHb8+PE5CBUAAAAAgLolMBEAAABY6W255ZZx1113xeqrrx7Tp0+Ps88+O3r06BF9+vSJG2+8sTQoMWXXa9myZV0vl+W47777cvbKAw88sMLf1UEHHZSv999/v+8XAAAAAKCOCUwEAAAA6oV+/frFq6++Gj//+c9zEGLz5s2jdevWsfXWW8ell14aL7/8cnTu3Lmul0kFTJgwIV9btWpV4e+rZOwHH3zgOwYAAAAAqGON6noBAAAAQPVIFZNroWpypdXmmjbYYIMYOHBgbpXRv3//3FbEwoULV+h5/r9VV101X1977bX4/ve/X6GvJo0t+ywAAAAAAHVHxkQAAAAACsoWW2yRAz1TpsuZM2cud3wak8am8s/f/e53a2WNAAAAAACUT2AiAAAAAAXl2GOPzddx48ZF796945VXXil37NixY2OnnXaKt99+O/98/PHH19o6AQAAAABYOqWcAQAAoJ4oiqIoLsBazmldUBmHHXZYPPDAA3H//ffHSy+9FFtttVV85zvfiW7dukW7du1yZsQpU6bEmDFjSks4J/vtt18ceuihvmwAAAAAgDomMBEAAACAgnPXXXfFqaeeGtddd10sWLAgXn311UWCEEukks8pUPGkk06KgQMH1slaAQAAAABYlFLOAAAAABScVVZZJf70pz/Fyy+/HD/5yU9i4403Lg1ELGmdO3fOn6Uxf/zjH6NRI2dwAQAAAAAKgd1aAAAAqCdSFecCrORckGti5ZFKOF9zzTW5P2fOnPjiiy9yUGKbNm2iSZMmdb08AAAAAACWQmAiAAAAACuFFIi49tpr1/UyAAAAAABYDqWcAQAAAAAAAAAAgGojYyIAAAAABevdd9+NoUOHxnPPPRcff/xxzJo1K/72t79F586dS8e8/vrrMXHixGjRokX06tWrTtcLAAAAAIDARAAAAKhXZREKsTRCIa6JwrdgwYI466yz4qqrrsr9hQsX5vtFRUUxd+7cRcZ++OGHseeee0ajRo3i/fffj/XWW6+OVg0AAAAAQOK/DQAAAABQcE444YQYOHBgzJ8/P9q3bx8HHHBAuWN333332GijjfLYe++9t1bXCQAAAADAkgQmAgAAAFBQRowYEYMGDcr9s88+OyZMmBB33333Mp858MADc1bFp59+upZWCQAAAABAeRqV+wkAAACwUkklblMrNIW4Jgrb9ddfn68//OEP48ILL6zQM927d8/XN954o0bXBgAAAADA8smYCAAAAEBBee6553JA64ABAyr8TIcOHfL1448/rsGVAQAAAABQEQITAQAAACgoU6dOzddOnTpV+JlGjf5XGOSbb76psXUBAAAAAFAxSjkDAABAPZEKJhdi0eRCXBOFrVmzZjF37tyYOXNmhZ+ZOHFivrZp06YGVwYAAAAAQEXImAgAAABAQSnJlPjyyy9X+JlHH300X7t27Vpj6wIAAAAAoGIEJgIAAABQUPr27RsLFy6MG2+8MRYsWLDc8S+99FL89a9/jaKiothtt91qZY0AAAAAAJRPYCIAAADUE8VFRQXboDJOOumkXM75tddei+OOOy6++eabcsfed999ORgxlX5effXV4/jjj/dlAwAAAADUsQYbmDh16tRc4ufcc8+N3XffPdq2bZtP1afWv3//Cr1j9uzZ8dBDD8XJJ58c22yzTayxxhqx6qqr5muPHj3i/PPPj48++qha1rvhhhuWrm9ZLY0DAAAAWJmtt9568cc//jFnTRwyZEhstNFG8dOf/rT080GDBsVPfvKT2HjjjeOggw6Kzz77LO+LpAyLrVq1qvH1TZw4MU4//fTo0qVLtGjRIu8Fde/ePS6//PKYOXNmtc1z5513xq677hrrrrtuNG3aNO/7HHHEEfH8888v87k5c+bEAw88EL/61a/iBz/4QWyyySal+1ZrrrlmbLfddnlPbNKkSdW2VgAAAACAshpFA7X22muv0POvvvpqbL/99vH1118v8dm0adPyBnFqAwcOjJtuuilvkgMAAABQMQMGDMjBhj/72c9i8uTJccMNN+Sfk6uuuipfU+Bi0qRJk7j++uvjwAMPrPGvd9iwYXHYYYfFl19+WXovBSOOGTMmt7QP9Nhjj+VgyqpKh2HT75IO1Zb1wQcf5Hb77bfnA7HnnHPOUp//8MMPY7/99lvqZ59//nk899xzuaV9q2uvvTaOPPLIKq8VAAAAAGBpGmxgYlkdO3bMJ9yffPLJCj/z1VdflQYl9uzZM/bcc8/Yeuut86nzTz75JO6///68EZ3GHHroodGyZcucmXFF7b333nHhhReW+3njxo1XeA4AAABWXoomU58cc8wx0bdv3xyI+PDDD8e77767RGbFvfbaK84444xaqSIxduzYfPg0BSKuttpqOSPhTjvtFLNmzcrZDf/yl7/EuHHjYo899shBimlMVYMyS4IS0/tPOeWUaN++fS5tffHFF8f48eNzxsOUSfHYY49d6jvatWuXn+3WrVtssMEGeWzKmJiCPFNw5W233RYzZszIlUPWWmutatm3AgAAAACIhh6YmDZv08Zsail74oQJE6JTp04Vfr64uDhvRJ933nnRtWvXJT5Pm+ZpQ3ffffeN+fPn53LP77zzTunJ/qpq3bp1bL755iv0DgAAAICVRYcOHXKJ5NTSQdGpU6fmvZZ0OLRt27a1upZTTz01ByU2atQoH3Dt0aNH6Wd9+vTJpaXPPPPMePvtt3M2wrT/VFkjR47MGRGTfv365ZLMq6yySv457WOlQMytttoql5NOcx1wwAF5v6islK3x448/LncfKu1XHX/88bkayDfffBO/+c1vBCYCAAAAANWqOBqo3/72tznLYVVLOm+33XZx1113LTUosWx2w5KyOekk+yuvvFLl9QIAAAA0FCnIL7XBgwcvcn/11VePzp07x6abblrrQYkpA+KIESNKMxqWDUoscdppp+WqHEnK8piC/irrsssuy9cUjJjKLJcEJZZIv/ell16a+9OmTYtBgwYt9UDt8g7Hdu/ePXbeeefc//e//x3Tp0+v9FoBAAAAAMrTYAMTa0sqmVMiBScCAAAAsGyjRo3KmQNrozxzRT344IOl/aOPPnqpY1JA4JFHHlkaNFgSyFhRKTjwqaeeyv1ddtklZ4tcmnQQNgVpJvfff39UVYsWLUr7c+bMqfJ7AAAAAAAWJzCxhpXd1E2b0wAAAFBTUoK0Qm1QGe3atcvXxUsU13WwZEkwXyqlXJ5evXqV9kePHl2pOV544YXSvaSy71lc48aNY9ttty19piqZGVNJ7H/84x+lWRhTaWwAAAAAgOoiUq6GpdP9JTbbbLMVft8zzzwT3/3ud/MmePPmzaNTp05x8MEH51P7CxcuXOH3AwAAANS1LbbYIl//85//RKF466238jWVkm7UqFG548ru/5Q8U9k5Fn/PsuaZN29evPPOOxV6fwp6fP/99+Mvf/lLbLfddjmrY3LKKadUap0AAAAAAMtT/i4qK2zs2LExbNiw3P/2t78dXbt2XeF3ps3jsiZMmJDb3XffHT179oy77ror1ltvvUq/d9KkScv8/KOPPqr0OwEAAACq4thjj42//e1vcf311+cDmXVt9uzZ8emnn+Z+eeWVS7Rp0yYfKJ0xY0Z8+OGHlZqn7PjlzdOxY8dFnitv3ymVk95pp53Kfc9hhx0WZ5xxRlSWvSQAAAAAYFkEJtaQdAI9baLPnz8//3zxxRev0PtSiZ699tor+vbtG5tvvnm0atUqvvjii3juuefiuuuuyxvQzz77bOyyyy75Xvq8MspuZi/P5nv8Jooar1aF34KkTbeTCv6LmDbmz3W9BADq0f+2+N8VgNrzv7LJhVc3uQCXRIHbb7/94vDDD49bb701jjnmmPjTn/6Ug/3qytdff13aX2215e+JlAQmTp8+vcbmKft9VHaeZMMNN8yBn7vuumtURWX2kgAAAACAhkdgYg056aST4sUXX8z9o446KgcVrogXXnghWrduvcT93r1757kOOOCAePLJJ3PJn9/+9rcxcODAFZoPAAAAoK4MHTo0dt5553j11VfjlltuiYceeij69esX3/3ud3NGwlVWWWWZzx955JHVnjGx7OHR5WnSpEm+zpo1q8bmKZljefN069YtXnvttdKDtBMnToyHH344/vrXv8bRRx8dF1xwQQwYMKBS6wQAAAAAWB6BiTXgkksuiZtuuin3t9pqq7jmmmtW+J1LC0os0bJly1zK+Vvf+lZ89tlnceONN8bvf//7Cm2Ul1heaaFUyrl79+6VWjMAAABAVfTv33+R7J/Tpk3LgXQVkZ6r7sDEpk2blvbnzp273PEpADBp1qxZjc1TMsfy5kmZFVP1jRJpr2rffffNGSn32GOPXPFj8uTJce6551ZqrfaSAAAAAIBlEZhYzW644YY4++yzc3/TTTeNxx9/vFZKDaXSzYccckgOgkylglK2xu22267Cz3fo0KFG1wcAAEDNK/6/VmgKcU0UvoULFy7z59qUDoVWpmxy2pupaNnnqs5TMkdV5klSRspTTjklLrvsslx946CDDorNNtusws/bSwIAAAAAlkVgYjW644474qc//Wnub7DBBjF8+PBYa621orZ07dq1tJ9OugMAAACsjN5///0oJCmTYdu2bePTTz+NSZMmLXNsyu5YEjTYsWPHSs1TNtgvzbP11ltXKGNhZecpsffee+fAxAULFsT9999fetgWAAAAAGBFCUysJg8//HAuE5Q2ctddd9146qmnav3keF1mDgAAAACoLunAZ6Hp0qVLjBo1Kt59992YN29eNGq09G21t99+e5FnqnrotOx7ljVPWkfnzp2jKsoeqP3ggw+q9A4AAAAAgKVRTakapCDEVO4mbUqvueaa8fe//z2+9a1vRW178803S/vt27ev9fkBAACoW0VFRQXbYGW3/fbb52vKhvjSSy+VO27kyJGl/Z49e1Zqjm7dukXjxo2XeM/i5s6dG88///wSz1RW2YobVSkHDQAAAABQHhkTV9A///nPXPZmzpw5sfrqq8cTTzwR3/72t6O2ffnll3HXXXflfvPmzZdZ6gcAAACg0KS9lUGDBsXjjz+es/fNnz8/H7zcaaed4oQTTsiHQevSPvvsE5dccknuDx48OLbZZpslxqRKGkOHDs391q1b57VXRsuWLWPnnXfO38Hw4cNzOeelVeRIZZe/+uqr3N93332r+BtF3HPPPaX973znO1V+DwBAeQYMGbPEvUH9u/nCAACgAZAxcQW88sorsccee+ST8i1atIjHHnssttpqq0q/p3fv3qUZJCZMmLDE53/7299i1qxZ5T7/9ddf54yNn332Wf55wIAB0aRJk0qvAwAAAKAuvPPOO/mg58knn5z3V954441cqvgf//hHnHPOOblU8dNPP12nfzjdu3ePHXbYIfdTAOVzzz23xJgrrrgi3nrrrdw/5ZRTYtVVV13k8yFDhpTuAZ1//vlLnef000/P11SZ48QTT8wBmmV9+umncdZZZ5UGPx577LFLvOOOO+7Ih1iX5e67744bbrgh91u1ahV77bXXMscDAAAAAFRGg82YOHr06Hj33XcX2dQtke6njeKy+vfvv8jP48ePj1133TW++OKL/POFF16YN3Fff/31cuds165dbpX1+9//Pg477LDYb7/9ctmgVCY6lddJc6dN8Ouuuy4+/PDDPHbTTTctd2MbAACA+i0VTC7EosmFuCYKK1NiCop77733yh2TguzSvshrr7221AyCteXqq6/O5ZnTAdK+ffvG2WefnbMipp/vvPPOuPHGG/O4TTbZJE477bQqzdGnT5845JBD8vsefvjh2GWXXeLUU0/N2SPT73/RRRfFxIkTS/eM2rRps8Q7UsDh8ccfn7M87rjjjnm/KO1bpcO148aNi3vvvTcHgCYpSDL9XmusscYKfTcAAAAAAGU12MDEm266KW655Zalfvbss8/mtqzAxFGjRsXUqVNLf/75z3++3DnPO++8KgcNfv7553nNqZUnbTTffvvtNpIBAACAlcatt96ag+VSgFy3bt1y4N22224bjRo1ildffTX/nAL0UunilJHwyiuvrLO1brnllnHXXXfF4YcfnteTAhMXl4IShw0blssyV9XNN9+c35+CB1OmyMWzRRYXF+dMkqnEdXmmT5+ev9vUypOCGv/0pz/lA7EAAAAAANWpwQYmrkwuv/zyeOqpp3J2xLRRn7I7pmyJzZs3z6flt9lmm/jRj36UT+qnTXwAAACAlcWDDz6Yr126dImRI0dGkyZNSj9LgYrp85RR8dFHH40HHnigTgMTk379+uWAyZRlMAUgTpo0KRo3bpzLTR944IFx0kkn5T2bFdGsWbP87nQANVX1GDt2bN4LWnvttXM56TRHjx49yn3+tttui+HDh+eAxrTWKVOmxCeffJLX2bZt2/jOd74Tu+22Wxx66KFLzbgIAAAAALCiGmxgYtrUXbxcc2WkDIqLZ1GsqhEjRizz86233jo3AAAAWJZ0WK0QD6wV4pooHCnoLv07ksoVlw1KLCtlJkyBiR9++GEu65zKEtelDTbYIAYOHJhbTe4npcDB1CprvfXWi6OOOio3AAAAAIC6UFwnswIAAABARK4MkXzve98r9/so+9lnn33mewMAAAAAKHACEwEAAACoM7Nnz87XZZU/btq06RLjAQAAAAAoXAITAQAAAAAAAAAAgGrTqPpeBQAAANT16cNCPIFYiGsCAAAAAABqjsBEAAAAAOrctddeG+3atauWceeee241rgwAAAAAgMoSmAgAAABAnbvuuuuW+XlRUVGFxiUCEwEAAAAA6pbARAAAAKgviopKg7cKSiGuiYKycOHCantXQf7/AAAAAABAAyMwEQAAAIA68/TTT/v2AQAAAADqGYGJAAAAANSZXr16+fYBAAAAAOoZgYkAAABQT6QCtoVYxLYQ1wQAAABQ3w0YMmap9wf171brawGg4Smu6wUAAAAAAAAAAAAA9YfARAAAAAAAAAAAAKDaKOUMAAAA9amUcwHWTS7AJQEAAAAAADVIxkQAAAAAAAAAAACg2ghMBAAAAAAAAAAAAKqNUs4AAABQTxRHUW6FphDXBAAAAAAA1BwZEwEAAAAAAAAAAIBqIzARAAAAAAAAAAAAqDZKOQMAAEA9UVT0v1ZoCnFNAAAAAABAzZExEQAAAAAAAAAAAKg2AhMBAAAAAAAAAACAaqOUMwAAAAAAAAAAwDIMGDJmiXuD+nfznUE5BCYCAABAPVH0f/8UmkJcEwAAAAAAUHOUcgYAAAAAAAAAAACqjcBEAAAAAAAAAAAAoNoo5QwAAAD1RFHR/1qhKcQ1AQAAAAAANUfGRAAAAAAAAAAAAKDaCEwEAAAAAAAAAAAAqo1SzlTa68MujA4dOvjmAKBATBvz57peAgAFoiiKojiKCnJdAAAAAABAwyFjIgAAAAAAAAAAAFBtBCYCAAAAAAAAAAAA1UYpZwAAAKgnior+1wpNIa4JAAAAAACoOTImAgAAAAAAAAAAANVGYCIAAAAAAAAAAABQbZRyBgAAgHpCKWcAAAAAAKAQyJgIAAAAAAAAAAAAVBuBiQAAAAAAAAAAAEC1EZgIAAAAAAAAAAAAVJtG1fcqAAAAoC4V/d8/haYQ1wQAAAAAANQcGRMBAAAAAAAAAACAaiMwEQAAAAAAAAAAAKg2SjkDAABAPVFc9L9WaApxTQAAAAAAQM2RMREAAAAAAAAAAACoNgITAQAAAAAAAAAAgGqjlDMAAADUE0X/90+hKcQ1AQAAAAAANUfGRAAAAAAAAAAAAKDaCEwEAAAAAAAAAAAAqo1SzgAAAFBPFBX9rxWaQlwTAAAAAABQc2RMBAAAAAAAAAAAAKqNwEQAAAAAAAAAAACg2ijlDAAAAPVEqphclP9vYSm8FQEAAAAAADVJxkQAAAAAAAAAAACg2ghMBAAAAAAAAAAAAKqNUs4AAABQTxQVRRQXFea6AACAlduAIWOWuDeof7c6WQsAAFD4ZEwEAAAAAAAAAAAAqo3ARAAAAAAAAAAAAKDaCEwEAAAAAAAAAAAAqk2j6nsVAAAAUJeK/u+fQlOIawIAAAAAAGqOwEQAAAAAAAAKwoAhY5a4N6h/tzpZCwAAAFWnlDMAAAAAAAAAAABQbWRMBAAAgHqiqOh/rdAU4poAAAAAAICaI2MiAAAAAAAAAAAAUG0EJgIAAAAAAAAAAADVRilnAAAAqCdSxeRCrJpciGsCAAAAAABqjoyJAAAAAAAAAAAAQLWRMREAAAAAAAAAACAiBgwZs8T3MKh/N98NVJLARAAAAKgniqMoiouKCnJdAAAAAABAw6GUMwAAAAAAAAAAAFBtBCYCAAAAAAAAAAAA1UYpZwAAYIW16XZSwX+L08b8ua6XADUuFUwuxKLJhbgmAAAAAACg5siYCAAAAAAAAAAAAFQbgYkAAAAAAAAAAABAtVHKGQAAAOoLtZwBAAAAAIACIGMiAAAAAFTCxIkT4/TTT48uXbpEixYtYo011oju3bvH5ZdfHjNnzqy27/LOO++MXXfdNdZdd91o2rRpbLjhhnHEEUfE888/v9xnx40bF1deeWXss88+0alTp2jWrFk0b9489w8++OAYNmxYLFy4sNrWCgAAAABQloyJAAAAAFBBKaDvsMMOiy+//LL0XgpGHDNmTG433XRTPPbYY7HRRhtV+TudPXt2HHjggfHoo48ucv+DDz7I7fbbb4/zzz8/zjnnnKU+f9RRR8XQoUOX+tmECRNyu/vuu3PQYwp+bN26dZXXCgAAAACwNAITAQAAAKACxo4dGwcddFAORFxttdXiV7/6Vey0004xa9asHOD3l7/8JWcq3GOPPXKQYhpTFQMGDCgNSkzvP+WUU6J9+/bx2muvxcUXXxzjx4+Pc889N2dSPPbYY5d4fvLkyfmaMjkecMAB0bt375xtsVGjRvHyyy/HwIED8zqfeOKJ6NevX4wcOTKKixVWAYBCN2DImCXuDerfrU7WAgAAsDwCEwEAAKCeKPq/fwpNIa4JquLUU0/NQYkpwO/JJ5+MHj16lH7Wp0+f2HjjjePMM8+Mt99+Owf/peDBykpBgikjYpKCBh944IFYZZVV8s/dunWLvfbaK7baaqtcTjrNlQIPF8942KFDh7jhhhty5sQmTZos8ll6x+GHH56zJY4ePTq32267LZeIBgAAAACoLo5CAwAAAMBypAyII0aMKM1oWDYoscRpp50WXbp0yf2rrroqvvnmm0p/r5dddlm+pmDEa6+9tjQosUTbtm3j0ksvzf1p06bFoEGDlnjHkCFD4vjjj18iKLFE8+bN47rrriv9+d577630Oqm7TFmLNwAAAAAoRAITAQAAAGA5HnzwwdL+0UcfvfSNtuLiOPLII0uDBksCGStq+vTp8dRTT+X+LrvskjMfLs1+++0Xq6++eu7ff//9Vfqz23zzzXOQY5JKQwMAAAAAVCeBiQAAAFBfFEUUFWBTyZn6YNSoUfnaogVw6BkAAFvcSURBVEWLXEq5PL169SrtpzLJlfHCCy/EnDlzlnjP4ho3bhzbbrtt6TNVycyYzJ07tzSgEgAAAACgOjXYXcepU6fGo48+Gueee27svvvu+YR4UVFRbv3796/QO1JZnJJnltfS2Orw2WefxXnnnRdbbLFFtGrVKp+OT/10L30GAAAAQPV766238rVz587RqFGjcsdtttlmSzxT2TkWf8+y5pk3b1688847UVkvv/xyfPXVVxWaCwAAAACgssrfRa3n1l577VjZjBkzJvbee+/46KOPFrn/6quv5nbTTTfFQw89FFtvvXWdrREAAACgvpk9e3Z8+umnuV9eeeUSbdq0yVkVZ8yYER9++GGl5ik7fnnzdOzYcZHnunbtWqm5Lr744tL+QQcdFJU1adKkZX6++P4VAAAAANCwNNjAxMU3crt06RJPPvlkld/xxBNPRPv27cv9fHmbycszefLk6NevX0yZMiWfyv/FL34Re+65Z/4sZX4cOHBg/Pe//833XnrppVhvvfVWaD4AAABWPoVaNbkQ1wSV8fXXX5f2V1ttteWOLwlMnD59eo3Nk+YoUdl57rvvvrj33ntzP5Wl3n///aOyygZGAgAAAAAsrsEGJqYSzt26dcstZU+cMGFCdOrUqcrv22STTWLDDTeMmvLrX/86ByUmt99+exx44IGln+2www45S2I63Z7GnHPOOXHzzTfX2FoAAAAAGlrGxBKNGzde7vgmTZrk66xZs2psnpI5KjvP22+/HUcffXTuN2vWLIYOHRpFRcKHAQAAAIDq1WADE3/729/GyiIFG9566625v+uuuy4SlFgi3UufpcyNaUP5kksuWSnLVQMAAAAUmqZNm5b2586du9zxc+bMKQ38q6l5SuaozDyp2sbuu++eMzOmYMRBgwZVugR0ieWVqU6lnLt3716ld1N9BgwZs9T7g/p38zUDAAAAUKMabGDiyuThhx+O+fPn537Jifal6d+/fw5MTGPTM8cdd1wtrhIAAIA6p5Yz1IiWLVtWqmxyKuNc0bLPVZ2nZI6KzvP5559H3759c9WQ5Oqrr44f/ehHUVUdOnSo8rMAAAAAQP1XXNcLYPlGjRpV2u/Vq1e548p+Nnr0aF8tAAAAQDVImQzbtm2b+5MmTVrm2GnTppUGDXbs2LHKwX7Lm6dsxsLlzZMyJO62227xxhtv5J8vuOCCOPnkkyu1NgAAAACAyhCYWE1StsJUOrlx48Z5o3rbbbeN3/zmNzF58uQVfvdbb72Vr61atYp11lmn3HHrrrturL766os8AwAAAMCK69KlS76+++67MW/evHLHvf3220s8U1FlyyqXfc+y5mnUqFF07ty53HGzZs2Kfv36xZgx/yvpe8YZZ+Q9KwAAAACAmiQwsZqMHDkypk6dGt9880189tln8a9//SsuuuiivDF8ww03rNC7S07AV6RETskJ+bKn5isincJfVvvoo4+quHoAAABqS1EB/wMru+233z5fUzbEl156aZl7RCV69uxZqTm6deuWD70u/p7FzZ07N55//vklnllc2qfaf//9S9/14x//OC677LJKrQkAAAAAoCoaVekpSm200Uax3377RY8ePUqDAt97772477774t57743Zs2fnTd+ioqI4/vjjq/TNpXI7yWqrrbbcsS1atMjX6dOnV2qOypYWAgAAAGhI9tlnn7jkkktyf/DgwbHNNtssMWbBggUxdOjQ3G/dunXstNNOlZqjZcuWsfPOO8fjjz8ew4cPz4dFl3ZQ9f7774+vvvoq9/fdd9+lvmv+/Plx6KGH5nclRxxxRFx77bWVWg8AAAAAQFXJmLgC0sZvKt/zhz/8IQcnphPqqR188MFx9913x8MPPxyrrrpqHvvzn/88Pv744yrNk4Ibk/JOv5fVpEmT0jI9AAAAAFSP7t27xw477JD7gwYNiueee26JMVdccUW89dZbuX/KKaeU7guVGDJkSD68mtr555+/1HlOP/30fE3lok888cQcYFjWp59+GmeddVZp8OOxxx67xDsWLlwYxx13XD40m6SsiSmYMs0LAAAAAFAbZExcAa1atVrm53vuuWecd9558Zvf/CZmzpyZN61//etfV3qepk2b5udTmZ7lmTNnTr42a9asUnMsr/RzKuWcNuABAAAoXCnmqBDjjgpxTVAVV199dS7PnA6E9u3bN84+++ycFTH9fOedd8aNN96Yx22yySZx2mmnVWmOPn36xCGHHJLflw697rLLLnHqqadG+/bt47XXXouLLrooJk6cmMf+/ve/jzZt2iw1uDEFIiabb755XmdJwGR50jgAAAAAgOoiMLGGpdPp55xzTj6pPnLkyCoFJqYyPikwsSLlmWfMmFHhss9lLa0sEAAAAAD/35Zbbhl33XVXHH744bmUcgr4W1wKShw2bFjez6mqm2++Ob//sccei6effjq3soqLi/N+0wknnLDU5++7777S/uuvvx5bbbXVcudMe1cAAAAAA4aMWeJLGNS/my8GqDSBiTWsXbt20bZt2/jkk09i8uTJVXpHChqcMmVKTJo0qcKZDzt27FiluQAAAAAoX79+/eLVV1/N2RNTAGLar2ncuHF07tw5DjzwwDjppJOiefPmK/QVpkoY6d233357Lv88duzY+OKLL2LttdfO5aTTHD169PDHBAAAtUywDgBAxQlMrAUreuK8a9eu8dJLL8WXX34ZH3/8cayzzjrllltOp+mTLl26rNCcAAAAACzdBhtsEAMHDsytMvr3759bRR166KG5VdaECRMq/QwAAAAAQHUqrta3sYSpU6fGZ599lvvt27ev0je0/fbbl/ZTOejylP2sZ8+e/jQAAAAamKICbgAAAAAAQMMhMLGG3XjjjaUZE3v16lWld+y1115RXPy/P6rBgweXOy6V9knS2PQMAAAAAAAAAAAA1DaBiVWUSuK8/PLLyxzz6KOPxgUXXJD7TZs2jaOPPnqp43r37h1FRUW5La3UTirdfNhhh+X+E088Effee+8SY+655578WXLEEUeUW+4ZAAAAAAAAAAAAalKjaKBGjx4d7777bunPn376aWk/3S/JPliif//+i/ycAgh32mmn6NGjR/Tr1y++973vRbt27XJ2xPfeey8HD6ZWki3x8ssvj/XWW6/K673ooovib3/7W3zyySfxox/9KF588cXYc889SwMgr7jiitxfa6214sILL6zyPAAAAKzECrVuciGuCQAAAAAAqDENNjDxpptuiltuuWWpnz377LO5LSswscRzzz2XW3maN28eV155ZRx//PErtN6OHTvGI488Evvss098/PHHcemll+ZWVsqS+OCDD0aHDh1WaC4AAAAAAAAAAACoqgYbmLiittpqq7j11ltzUGLKXvjRRx/lrIvz5s2LNm3axLe//e3Yeeed49hjj82ZFKvDNttsE6+99lpcffXVOQCxpOxzp06dYu+9945TTz011lxzzWqZCwAAAFZGEydOjD/+8Y8xbNiw3G/SpEl07tw5DjrooPjpT3+aDxBW1VdffRWPPfZYPPXUU/HSSy/ligkzZ86MVq1a5X2AVNkg7QO0bt26Wn8nAAAAAABY2TTYwMRUqnnxcs2V0bJlyzjssMNyW1EjRoyo8Ni2bdvGBRdc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MWNGPmiwX79+RQf7tVROw4yFhZYDAAAAAFBqNZsxccUVV2z3MtdZZ512LxMAAACAtlt33XXD448/Ht58880wb9680KVL481qr7322gLzFGK99dZrdDnNlRPrMWDAgILKAQAAAAAotZoNTGwo3lUeG4rHjh3b6nlWWWWV8NJLL7U43XnnnRduvPHGNH7UUUeFttprr73COeec0+TrXbt2bXMZAAAAACxo6623ToGJMRvis88+GzbbbLNGV9Gjjz6aH99qq60KWo2DBg1KbTtz585Ny/n1r3/d6HTx9aeeemqBeQAAAAAAyknNBibGLpxjw20cYvbE2DVz//79Wz3/YostFtZff/1mp5k/f3545JFH0njv3r3D3nvv3eZ6L7XUUi2WCwAAQG2q+8dQbsqxTlCo2K4Tb0CNrrnmmkYDE7/99ttw3XXX5dtwtt9++4LKiO1HO+64Y7j33nvDgw8+mLpzbti9c87tt98eZs2alcb32WcfGxMAAAAAKDudQo06++yzw+67717SLp1jA/J7772Xxvfff//Qs2fPkpUFAAAAQOkMHjw4DBkyJI2PHDkyjB8/fpFpLr744vDqq6+m8RNPPDHd2NrQqFGjQl1dXRpGjBjRaDmnnHJKeozdRR9//PHpxteGPv744/CrX/0qH/x4zDHHZPQOAQAAAACyU7MZE9tD7g75rLpxBgAAAKDjXHrppal75jlz5oShQ4eG008/PWVFjH/fdNNN4corr0zTrb322uHkk08uqowddtghHHzwwWl5d955Z9hpp53CSSedFPr06RNeeumlcO6554YpU6akac8///yw9NJLN7qc//3f/01DY95///0UJNlQvKm2V69eRdUZAAAAAGBhAhNL5PPPPw+jR49O46uttlrYZpttSlUUAAAAJHV13w3lphzrBMXYZJNNws033xwOP/zw1JVyDExcWAxKHDNmTOqWuVhXX311Wv4999wTHn744TQ01KlTp3DGGWeEY489tsllxHap2GNIY15//fVw9NFHL/DcdtttJzCRmjJ81MRFnhs5bFCH1AUAAACgGtVsV86ldtttt4XZs2en8SOPPDJ10ZOFxx57LGy44YZh8cUXT11D9+/fPxx00EGpsbm+vj6TMgAAAABo3B577BFefPHF8Itf/CIFIcb2mdil8sCBA8MFF1wQnn/++TBgwIA2rb4ePXqk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+8K+++4b+vbtm8518TH+HZ/P0ieffBLOOuussNFGG4Ull1wyLLHEEmk8Phdfq3RZbJevvvoq/PWvfw0///nPw2abbZY+Byy22GLpcYsttkjHwfTp0zOpb7z2t+Z4jdPV8jYZNWpUq89tcdosOFaaN3ny5IKvO23Zj2vhWAGAWm0fo7Zk0dbXEd8PqB5Ztmu2V1sO1SOLNlznQCqtzTp+bvz3f//39Dkyfp6M1/34+TJ+zoyfN6ku5dQ+X8g5l+pRjr9HOA/WlnL67cV5sLS6lHj5VLExY8aEww47LHz22WcLXCwmTpyYhquuuircc889YY011ii6jPiB6oADDkgnpIbeeeedNNx4443pQ9UZZ5wRatm2224bHnvssUWenzt3bpg0aVIarr322hTMGbdL165dO6SelI5jpXQ6deoU1lprrRKWUP1WXHHFTJZTX18ffvrTn4Yrr7xygefffffdcMcdd6ThJz/5Sbjiiiva/AU5Xsf22muvRb60v/jii2mI59L4hX/gwIGhVrdLXA9bb711+Pzzzxd5bcaMGenmgThccsklaX0deOCBbSqvFmR1rLQnx0pprLPOOiVaMgBQie1j1B5tfVSL9mzLobZpw6XS26zfeuutsNtuu4XXX399gedfe+21NMTPlPE30V133TWTetPxtM/T0crt9wjnwdrTEfug3146hsBEivLCCy+kAIPY0Brv2DnttNPC9ttvH+bMmZMytv3pT39KH57jh+jYCBunKcbw4cPzQYlx+SeeeGLo06dPeOmll9Idw/ECFSOoYybFY445pma3ZvyCE8V1EwM5hwwZElZdddUwf/78MH78+HDxxRenaa6//vowb9689OUlC3E7NKd///6ZlFMNfvazn4XjjjuuyddjRoW2cKy03jXXXBO+/PLLZqd55ZVXwkEHHZTGd9xxx7DKKqu0afs4Vv5PzPIQ7/IcO3ZswevxN7/5Tb6BZ5NNNgmnnnpqWHPNNdO14MILLwzPP/98en355ZcP55xzTtHbK54v99hjj/DBBx+ELl26hF/+8pdh9913T6/Fa1IMtHvvvffSc88++2yb949K3S6zZs3KByVutdVWaX3EQM1ll102fPTRR+H2229PDWZxmkMPPTT07t073fHUVjFgtLntWy3B9205VnLuv//+9NmgKfHO7bZwrLROPEe0dB2IzjvvvPxntKOO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jTcWFEtJrrXOg5TTby/Og6XR9O280ES605xevXq1uI7iB+n4BTF+gClVObnG3ajQcmrF7373u9TlbxQbi2LK97bYYIMNwt577x0GDx4c+vTpkz5gvP766+HPf/5zGDt2bJg5c2bYb7/9wl133RV++MMfhloWs3ntueeeYccdd0x3IsX9ORfEecUVV6QPZrH7xnjH0QMPPBAWW2yxgpbvWCnv4BDHSjbacz/PldXaa1yx5dSC2HBxzDHHhPnz56e/f/vb37ZpefEuu3g+HTp0aFh//fVTVst4vRk/fny4/PLL05fXJ554ImUojc/F12tVvEFg3333DVtssUW+wfLvf/97ariPd4jFH5N++tOfpob7n/zkJ0WV4VjJxpQpU/J3LW+55ZapkaatHCsAUD3tY5BFW197fD+gOrWlXVObJeXShuscSKlkdZ4rpo0tt5xu3boVVGdqSyHt8506dUrX+1133TVstNFGqbe8uG8+99xzKaAsZrSLNypsv/326TNozFYLWV1rnQcph99enAdLrB4KMGXKlPq428ThiCOOaHH6fv36pWnXXHPNgtbzj370o3w5b731VrPTjhw5Mj/t9ddfX1A5teCRRx6p79KlS1o/K6ywQv3777/fpuXNmDGj2devuOKK/Pbo06dP/ezZs+trWXPrK26LTTbZJL++Lr300oKX71jJztSpU+s7deqUtsXmm2/e5uU5Vpr29ttv5/f7o446qsV1+a//+q/56R966KFmp42v56b9t3/7t4K3W24fGDJkSIvTxmnitJ07d66vxe3SkmOOOSbT5TV3TM2aNat+6NCh+fJ+8Ytf1NfqNpk5c2b9t99+2+Trd911V/1iiy2WltmzZ8/66dOnF1U3x0o2+/a5556b38bxM1QWavFYAYBqbR+Dtrb1tdf3A6pTW9o127Mth9pSSBuucyCV0Ga9xhprpOfj58WWxM+dueXEY4Hq0pHt881d8+fOnZvmzy1rn332aXPdKE8d9XuE8yBt2Qez+u3FebC0dOVMQbp3754fnzt3bovT51KJ9+jRo2TlNExXXmg51e7ll19Od03Pmzcv3TkVu7pcccUV27TMllLJH3vssekOnOi9994Lt99+e6hlza2vuC3inSIxs1H0n//5nwUv37GSnRtuuCF1hRTFtPRt5VjJTnvu57mySnmNqwXnnXdeuOqqq9L4pptu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xkQAKELMmJfrQmPmzJmhW7duLc4zevToFHTXu3fvkqzzXHch/fv3D3feeWer54vTN6Zz586hPVx99dVh5MiRaXzjjTcOJ510Uthss83CKquskgIec/U48sgjw/XXX58CGUstZkI8++yzU1l/+ctfwmmnnZaenzFjRrj33nvT+IEHHhgWW2yxJrfDVlttFa644opWl5nrqhoAAAAA6Djl1var3Tc72n0BANqXwEQAKMItt9xScOa+mBHvtttuC0cffXRJ1vmyyy6bHj/44IPwve99r6DunDvSn/70p/S45pprpgyFPXr0aHS6GBTYXmJXzrFrjwkTJqQMibnAxLj9YrfLTXXjnNsOcRt89NFHYf3112+3OgMAAAAA1df2q903O9p9AQDaV2VELABAmcl1zbHyyiuHSy65pMXpf/WrX4UpU6ak+QptnKqrq2vVdJtsskm45557UiPYE088EbbddttQCV5++eX0uNdeezUZlBgzFz733HPtWq8YeBgDE2P9XnzxxbDhhhuGG2+8Mb22+uqrhy222KLJ7fDKK6+EN954I7zzzjthtdVWa9d6AwAAAADl3/ar3Ve7LwBAtROYCAAFevvtt8O4cePS+H777RcOPvjgFud55plnwsUXXxweffTR1Ei16qqrtrq87t27p8evv/662eliYN+5556bxi+88MKKCUycN29eeowBlU2JXVO/9957La6nr776qsX11Fpxu/7yl79MXaXErInLLLNMeOyxx/JBi001HO65555p+tx2uOyyyzKpDwAAAABQPW2/2n21+wIAVLtOHV0BAKg0119/fcrgF+2///6tmic3XZwvzl+IeGdu9OGHH4bPP/+8yekGDRoUhg4dmsZj5sSzzjqr2eVOnjw5/OUvfwkdLXafEd11112Ndtf81ltvheOOO67V6ylOn4UVVlgh/NM//VMaj+spZkv89ttv09+HHnpok/PFBst11103jV9++eVh5MiRzZbzt7/9Lb13AAAAAKB22n61+2r3BQCodgITAaBAucalGLg2ZMiQVs2z2Wabhb59+y4wf2ttueWW6TEGxf30pz8NTz31VJg0aVJ4880309DQNddck2/Q+td//dew+eabhyuvvDKMHz8+PP/88+HBBx9M3Y/EAMYBAwaE//7v/w4d7cgjj0yP7777bnqv8T3ELpRjdsIRI0aETTfdNHz66afhBz/4QavWU8yu+Mc//jEF/OXWUQzqLMbhhx+eHqdOnRrOO++8fFfN6623XpPzdO7cOdx8882hV69eqTHymGOOCbvsskvqyuXpp59OXVLfd999aXlbbbVV2GCDDdLd1AAAAABA7bT9avfV7gsAUO105QwABXjiiSfywYD77LNP6NSpdTH+sdvffffdN/zHf/xHeP3111OAWmywao0ddtghBRjGgMSYtS8ODeXu4I369OmTghAPOOCAMHHixFROHJqyxBJLhI524oknhgceeCCMHTs2vPbaa+FHP/rRAq/36NEjBfWNGTMmBfU15ZRTTgm33XZb6so5BnA2dNRRR4VRo0YVXLe999479OzZM3UzPXPmzHw3zi2JwYZxX4l3S8cg0vvvvz8N5bwdAAAAAKCWtXfbr3Zf7b4AANVOxkQAKEAMkGvYZW8hGk7fcDktiQ1gMWjvN7/5Tdhoo41SJr7Y2NWU1VZbLTV+3XHHHeHggw8O/fv3T8F1iy22WFh++eXTnbgnn3xyytLXUjfD7SHWKwYdxoa7gQMHprrGYMSY0TEGGMZgxBho2ZKNN944BWUecsghYdVVVw3dunVrc93iut5rr70W2BZxnbbGhhtuGF555ZVw7bXXpgDHfv36he7du4euXbumrJbbbbdd2qbPPvtsOPPMM9tcVwAAAACgctp+tftq9wUAqHZ19Q3TLAEAUDFiAGbsZnr48OHhqquu6ujqAAAAAAAAAEAiYyIAQIWaNWtWelxyySU7uioAAAAAAAAAkCcwEQCgAk2ePDl89tlnaXzttdfu6OoAAAAAAAAAQF6X/xsFAKDcvfTSS2HKlCnhnHPOSX/X1dWFnXfeuaOrBQAAAAAAAAB5AhMBACrIhhtuuMDfJ5xwQlh99dU7rD4AAAAAAAAAsDCBiQAAFaZHjx5hnXXWCUcffXQKTAQAAAAAAACAciIwEQCggtTX13d0FQAAAAAAAACgWZ2afxkAAAAAAAAAAACg9QQmAgAAAAAAAAAAAJkRmAgAAAAAAAAAAABkRmAiAAAAAAAAAAAAkBmBiQAAAAAAAAAAAEBmBCYCAAAAAAAAAAAAmRGYCAAAAAAAAAAAAGRGYCIAAAAAAAAAAACQGYGJAAAAAAAAAAAAQGYEJgIAAAAAAAAAAACZEZgIAAAAAAAAAAAAZEZgIgAAAAAAAAAAAJAZgYkAAAAAAAAAAABAZgQmAgAAAAAAAAAAAJkRmAgAAAAAAAAAAABkRmAiAAAAAAAAAAAAkBmBiQAAAAAAAAAAAEBmBCYCAAAAAAAAAAAAmRGYCAAAAAAAAAAAAISs/H8ozAG3SenHkAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": { + "image/png": { + "height": 491, + "width": 1299 + } + }, + "output_type": "display_data" + } + ], "source": [ "import matplotlib.pyplot as plt\n", "\n", @@ -308,17 +709,46 @@ "plt.tight_layout()\n", "plt.show()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python [conda env:locpick]", "language": "python", - "name": "python3" + "name": "conda-env-locpick-py" }, "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", "name": "python", - "version": "3.12.0" + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" } }, "nbformat": 4, diff --git a/locpick/__init__.pyi b/locpick/__init__.pyi index bf82ea2..0cfbf33 100644 --- a/locpick/__init__.pyi +++ b/locpick/__init__.pyi @@ -68,6 +68,9 @@ from .dgp import ( from .dgp import ( NestedSCLDataset as NestedSCLDataset, ) +from .dgp import ( + SARMNLDataset as SARMNLDataset, +) from .dgp import ( SCLDataset as SCLDataset, ) @@ -92,24 +95,18 @@ from .dgp import ( from .dgp import ( simulate_nested_scl as simulate_nested_scl, ) +from .dgp import ( + simulate_sar_mnl as simulate_sar_mnl, +) from .dgp import ( simulate_scl as simulate_scl, ) from .models import ( - MNL as MNL, + SARMNL as SARMNL, ) from .models import ( ChoiceModel as ChoiceModel, ) -from .models import ( - MixedMNL as MixedMNL, -) -from .models import ( - MixedNestedMNL as MixedNestedMNL, -) -from .models import ( - NestedMNL as NestedMNL, -) from .models import ( NestingTree as NestingTree, ) diff --git a/locpick/_jax/sar_kernels.py b/locpick/_jax/sar_kernels.py new file mode 100644 index 0000000..4a7d36d --- /dev/null +++ b/locpick/_jax/sar_kernels.py @@ -0,0 +1,163 @@ +"""JAX kernels and objective builder for SAR-MNL PML estimation. + +Implements the pseudo maximum likelihood (PML) estimator from +Smirnov (2010): spatially-filtered utilities with variance +normalisation by ``diag((I - ρW)^{-1})``, then standard MNL softmax. + +The spatial solve ``(I - ρW) V* = V_base`` is done via LU factorisation +(dense, for moderate J) — the same matrix ``A = I - ρW`` is factorised +once and reused for all choosers. +""" + +from __future__ import annotations + +import functools + +import jax +import jax.numpy as jnp +import numpy as np +import scipy.sparse as sp + +from locpick._jax.data import ChoiceDataJAX +from locpick._jax.kernels import ( + _NEG_INF, + compute_ll, + compute_ll_contribs, + compute_utilities, + mnl_log_probs, +) +from locpick._jax.objective import Objective +from locpick._jax.transforms import Identity, ParamTransform, Tanh + + +# --------------------------------------------------------------------------- +# Core PML kernel +# --------------------------------------------------------------------------- + + +def _sar_mnl_ll_core(params, design_matrix, available, chosen, weights, + inclusion_probs, W_dense, n_obs, n_alts): + """SAR-MNL PML log-likelihood (Smirnov 2010). + + Parameters + ---------- + params : jnp.ndarray, shape (k+1,) + [beta_1..k, alpha_rho] where rho = tanh(alpha_rho). + design_matrix : jnp.ndarray, shape (n_obs * n_alts, k) + available : jnp.ndarray, shape (n_obs, n_alts) + chosen : jnp.ndarray, shape (n_obs, n_alts) + weights : jnp.ndarray, shape (n_obs,) + inclusion_probs : jnp.ndarray or None + W_dense : jnp.ndarray, shape (n_alts, n_alts) + Dense spatial weights matrix (row-standardised, zero diagonal). + n_obs : int + n_alts : int + """ + k = design_matrix.shape[1] + beta = params[:k] + alpha_rho = params[k] + rho = jnp.tanh(alpha_rho) + + # Base utilities: V_base (n_obs, n_alts) + V_base = compute_utilities( + design_matrix, beta, n_obs, n_alts, + inclusion_probs=inclusion_probs, available=available, + ) + + # Spatial filter: solve (I - rho*W) V_filtered^T = V_base^T + # A is (n_alts, n_alts), same for all choosers — solve once for all RHS + A = jnp.eye(n_alts) - rho * W_dense + V_filtered = jax.scipy.linalg.solve(A, V_base.T).T # (n_obs, n_alts) + + # Variance normalisation: D = diag(A^{-1}) + A_inv = jax.scipy.linalg.inv(A) + D = jnp.diag(A_inv) # (n_alts,) + V_star = V_filtered / D[None, :] # normalise each alternative by d_jj + + # MNL log-probabilities + log_probs = mnl_log_probs(V_star, available) + return compute_ll(log_probs, chosen, weights) + + +def _sar_mnl_ll_contribs_core(params, design_matrix, available, chosen, weights, + inclusion_probs, W_dense, n_obs, n_alts): + """Per-observation SAR-MNL PML log-likelihood contributions.""" + k = design_matrix.shape[1] + beta = params[:k] + alpha_rho = params[k] + rho = jnp.tanh(alpha_rho) + + V_base = compute_utilities( + design_matrix, beta, n_obs, n_alts, + inclusion_probs=inclusion_probs, available=available, + ) + + A = jnp.eye(n_alts) - rho * W_dense + V_filtered = jax.scipy.linalg.solve(A, V_base.T).T + A_inv = jax.scipy.linalg.inv(A) + D = jnp.diag(A_inv) + V_star = V_filtered / D[None, :] + + log_probs = mnl_log_probs(V_star, available) + return compute_ll_contribs(log_probs, chosen, weights) + + +# --------------------------------------------------------------------------- +# Objective builder +# --------------------------------------------------------------------------- + + +def build_sar_mnl_objective(arrays, W_sparse: sp.csr_array) -> Objective: + """Build an Objective for SAR-MNL PML estimation (Smirnov 2010). + + Parameters + ---------- + arrays : ChoiceArrays + Estimation data arrays. + W_sparse : scipy.sparse.csr_array + Row-standardised alt×alt spatial weights matrix (zero diagonal). + + Returns + ------- + Objective + Objective with JIT-compiled LL, gradient, and Hessian. + Includes a Tanh transform for the rho parameter. + """ + data = ChoiceDataJAX.from_arrays(arrays) + W_dense = jnp.array(W_sparse.toarray(), dtype=jnp.float64) + n_obs = arrays.n_obs + n_alts = arrays.n_alts + k = arrays.design_matrix.shape[1] + + # JIT-compiled closures — data and W are captured, only params is dynamic + @jax.jit + def _ll_jax(params): + return _sar_mnl_ll_core( + params, data.design_matrix, data.available, data.chosen, + data.weights, data.inclusion_probs, W_dense, n_obs, n_alts, + ) + + @jax.jit + def _ll_contribs_jax(params): + return _sar_mnl_ll_contribs_core( + params, data.design_matrix, data.available, data.chosen, + data.weights, data.inclusion_probs, W_dense, n_obs, n_alts, + ) + + @jax.jit + def _grad_jax(params): + return jax.grad(_sar_mnl_ll_core, argnums=0)( + params, data.design_matrix, data.available, data.chosen, + data.weights, data.inclusion_probs, W_dense, n_obs, n_alts, + ) + + param_names = list(arrays.param_names) + ["rho"] + transform = ParamTransform.for_sar_mnl(k) + + return Objective.from_jax( + ll_fn=_ll_jax, + grad_fn=_grad_jax, + loglike_contribs_jax=_ll_contribs_jax, + param_names=param_names, + transform=transform, + ) \ No newline at end of file diff --git a/locpick/_jax/transforms.py b/locpick/_jax/transforms.py index 0b49d95..5d6c1e0 100644 --- a/locpick/_jax/transforms.py +++ b/locpick/_jax/transforms.py @@ -71,6 +71,21 @@ def log_det_jac(self, x: jnp.ndarray) -> jnp.ndarray: return jnp.log(jnp.maximum(jax_sigmoid(x), 1e-30)) +class Tanh: + """Tanh transformation: x → tanh(x). + + Maps unconstrained parameters to (-1, 1). Used for the SAR + spatial autoregressive parameter ρ ∈ (-1, 1). + """ + + def constrain(self, x: jnp.ndarray) -> jnp.ndarray: + return jnp.tanh(x) + + def log_det_jac(self, x: jnp.ndarray) -> jnp.ndarray: + """log |d(tanh(x))/dx| = log(1 - tanh(x)^2) = log(sech^2(x)).""" + return jnp.log(jnp.maximum(1.0 - jnp.tanh(x) ** 2, 1e-30)) + + class Exp: """Exponential transformation: x → exp(x). @@ -180,6 +195,22 @@ def for_mscl(cls, k_fixed: int, k_random: int): transforms.extend([SoftPlus()] * k_random) # random spreads return cls(transforms) + @classmethod + def for_sar_mnl(cls, k: int): + """Create a transform for SAR-MNL models. + + Parameters + ---------- + k : int + Number of utility coefficients (beta parameters). + + Returns + ------- + ParamTransform + Transform with Identity for betas and Tanh for rho. + """ + return cls([Identity()] * k + [Tanh()]) + @classmethod def from_bounds(cls, bounds: list[tuple[float, float] | None]): """Create a transform from per-parameter bounds. diff --git a/locpick/_kernels/mnl_numpy.py b/locpick/_kernels/mnl_numpy.py index 0f0ad97..1eb0883 100644 --- a/locpick/_kernels/mnl_numpy.py +++ b/locpick/_kernels/mnl_numpy.py @@ -127,7 +127,6 @@ def mnl_log_likelihood_numpy( n_alts: int, weights: OptionalArray = None, inclusion_probs: OptionalArray = None, - design_matrix_sparse=None, ) -> float: """Compute the MNL log-likelihood. @@ -149,9 +148,6 @@ def mnl_log_likelihood_numpy( Observation weights. inclusion_probs : np.ndarray or None, shape (n_obs, n_alts) Inclusion probabilities for sampling correction. - design_matrix_sparse : scipy.sparse.spmatrix or None - Sparse design matrix. If provided, used instead of dense - ``design_matrix`` for the matrix-vector product. Returns ------- @@ -159,10 +155,7 @@ def mnl_log_likelihood_numpy( Weighted log-likelihood. """ # Step 1: systematic utility - if design_matrix_sparse is not None: - utilities = design_matrix_sparse.dot(beta).reshape(n_obs, n_alts) - else: - utilities = (design_matrix @ beta).reshape(n_obs, n_alts) + utilities = (design_matrix @ beta).reshape(n_obs, n_alts) # Steps 2–4: log-probabilities log_probs = mnl_log_probs_numpy(utilities, available, inclusion_probs) @@ -190,7 +183,6 @@ def mnl_gradient_numpy( n_alts: int, weights: OptionalArray = None, inclusion_probs: OptionalArray = None, - design_matrix_sparse=None, ) -> np.ndarray: """Compute the MNL gradient. @@ -212,9 +204,6 @@ def mnl_gradient_numpy( Observation weights. inclusion_probs : np.ndarray or None, shape (n_obs, n_alts) Inclusion probabilities. - design_matrix_sparse : scipy.sparse.spmatrix or None - Sparse design matrix. If provided, used instead of dense - ``design_matrix`` for the matrix-vector product. Returns ------- @@ -222,10 +211,7 @@ def mnl_gradient_numpy( Gradient vector. """ # Step 1: systematic utility - if design_matrix_sparse is not None: - utilities = design_matrix_sparse.dot(beta).reshape(n_obs, n_alts) - else: - utilities = (design_matrix @ beta).reshape(n_obs, n_alts) + utilities = (design_matrix @ beta).reshape(n_obs, n_alts) # Steps 2–4: probabilities probs = mnl_probs_numpy(utilities, available, inclusion_probs) @@ -237,10 +223,7 @@ def mnl_gradient_numpy( if weights is not None: residual = residual * weights.reshape(n_obs, 1) - if design_matrix_sparse is not None: - grad = design_matrix_sparse.T.dot(residual.ravel()) - else: - grad = design_matrix.T @ residual.ravel() + grad = design_matrix.T @ residual.ravel() return grad diff --git a/locpick/_solvers/optimistix.py b/locpick/_solvers/optimistix.py index f0b8bdc..b9dcf8f 100644 --- a/locpick/_solvers/optimistix.py +++ b/locpick/_solvers/optimistix.py @@ -66,10 +66,6 @@ class OptimistixSolver: for multi-start runs (only used when ``n_starts > 1``). seed : int Random seed for multi-start perturbations. - compute_hessian : bool - Kept for API compatibility. Hessian is now computed lazily by - the model via :meth:`Objective.hessian` when standard errors - are requested, rather than eagerly in the solver. """ # Map of method names to Optimistix solver constructors @@ -90,7 +86,6 @@ def __init__( n_starts: int = 1, start_scale: float = 1.0, seed: int = 0, - compute_hessian: bool = True, ): self.method = method self.rtol = rtol @@ -100,7 +95,6 @@ def __init__( self.n_starts = n_starts self.start_scale = start_scale self.seed = seed - self.compute_hessian = compute_hessian def _make_solver(self): """Create the Optimistix solver instance.""" @@ -262,7 +256,6 @@ def ll_fn(p): "rtol": float(self.rtol), "atol": float(self.atol), "maxiter": int(self.maxiter), - "compute_hessian": bool(self.compute_hessian), "n_starts": int(self.n_starts), "start_scale": float(self.start_scale), "seed": int(self.seed), @@ -436,7 +429,6 @@ def bounded_neg_ll(alpha, args=None): "rtol": float(self.rtol), "atol": float(self.atol), "maxiter": int(self.maxiter), - "compute_hessian": bool(self.compute_hessian), "n_starts": int(self.n_starts), "start_scale": float(self.start_scale), "seed": int(self.seed), diff --git a/locpick/data/arrays.py b/locpick/data/arrays.py index e4b487f..efb9ce8 100644 --- a/locpick/data/arrays.py +++ b/locpick/data/arrays.py @@ -57,13 +57,6 @@ class ChoiceArrays: design_matrix: ArrayType chosen: ArrayType - design_matrix_sparse: Optional[Any] = None - """Sparse design matrix (scipy.sparse or jax.experimental.sparse). - - When present, model kernels use sparse-dense products instead of - dense-dense, which is critical for large choice sets with many - zero-valued variables (e.g., "has_subway_station"). - """ available: Optional[ArrayType] = None weights: Optional[ArrayType] = None n_obs: int = 0 diff --git a/locpick/data/choicetable.py b/locpick/data/choicetable.py index 902c688..25a2580 100644 --- a/locpick/data/choicetable.py +++ b/locpick/data/choicetable.py @@ -137,7 +137,9 @@ def from_tables( ChoiceTable """ if seed is not None: - np.random.seed(seed) + rng = np.random.default_rng(seed) + else: + rng = np.random.default_rng() # Normalize index names; if the named column exists as a data column, # promote it to the index. @@ -223,6 +225,7 @@ def from_tables( replace, oid_name, aid_name, + rng=rng, ) n_obs = len(choosers) @@ -722,8 +725,6 @@ def to_arrays( spec=None, weights=None, available=None, - sparse: bool = False, - sparse_threshold: float = 0.5, ) -> ChoiceArrays: """Convert to estimation-ready arrays. @@ -739,37 +740,33 @@ def to_arrays( Observation weights as a flat array of length n_obs * n_alts. available : array-like or None Alternative availability as a flat array of length n_obs * n_alts. - sparse : bool - If True, convert the design matrix to scipy.sparse.csr_matrix - when the zero fraction exceeds ``sparse_threshold``. This is - useful for large choice sets with many zero-valued variables - (e.g., "has_subway_station"). - sparse_threshold : float - Fraction of zeros required to trigger sparse conversion. - Default 0.5 (50% zeros). Returns ------- ChoiceArrays """ - # Cache key: hashable tuple of all arguments + # Cache key: hashable representation of all arguments + # Use bytes hash for array-like weights/available to handle numpy arrays + import hashlib + + def _hashable(val): + if val is None or isinstance(val, str): + return val + if hasattr(val, "tobytes"): + return hashlib.md5(np.asarray(val).tobytes()).hexdigest() + if hasattr(val, "__iter__"): + return hashlib.md5(np.asarray(val).tobytes()).hexdigest() + return val + cache_key = ( formula, id(spec) if spec is not None else None, - tuple(weights) - if hasattr(weights, "__iter__") and not isinstance(weights, str) - else weights, - tuple(available) - if hasattr(available, "__iter__") and not isinstance(available, str) - else available, - sparse, - sparse_threshold, + _hashable(weights), + _hashable(available), ) if cache_key in self._to_arrays_cache: return self._to_arrays_cache[cache_key] - import numpy as np - df = self._get_frame_cached(copy=False) n_obs = self.n_observations n_alts = self.n_alternatives @@ -869,22 +866,12 @@ def to_arrays( param_names = [] design_matrix = np.asarray(dm, dtype=np.float64) - # Sparse design matrix (optional) - design_matrix_sparse = None - if sparse and design_matrix.size > 0: - zero_fraction = 1.0 - np.count_nonzero(design_matrix) / design_matrix.size - if zero_fraction >= sparse_threshold: - import scipy.sparse as sp - - design_matrix_sparse = sp.csr_matrix(design_matrix) - # Get obs_ids and alt_ids obs_ids = np.repeat(np.asarray(self._ds.coords["obs_id"].values), n_alts) alt_ids = np.asarray(self._ds["alt_id_values"].values).reshape(-1) result = ChoiceArrays( design_matrix=design_matrix, - design_matrix_sparse=design_matrix_sparse, chosen=chosen, available=avail, weights=wts, @@ -936,8 +923,11 @@ def _build_sampled( replace: bool, oid_name: str, aid_name: str, + rng: Optional[np.random.Generator] = None, ) -> pd.DataFrame: """Build merged table with alternative sampling.""" + if rng is None: + rng = np.random.default_rng() n_obs = len(choosers) n_alts = len(alternatives) alt_ids = alternatives.index.values @@ -968,7 +958,7 @@ def _build_sampled( available_probs = probs.copy() available_probs[~available_mask] = 0 available_probs /= available_probs.sum() - sampled[i] = np.random.choice( + sampled[i] = rng.choice( alt_ids, size=sample_size, replace=True, p=available_probs ) else: @@ -978,7 +968,7 @@ def _build_sampled( if excluded_alt_ids[i] >= 0: available_mask[excluded_alt_ids[i]] = False available_alts = alt_ids[available_mask] - sampled[i] = np.random.choice(available_alts, size=sample_size, replace=True) + sampled[i] = rng.choice(available_alts, size=sample_size, replace=True) else: # Without replacement — use Numba kernels if available if weights_series is not None and weights_1d and HAS_NUMBA: @@ -1005,7 +995,7 @@ def _build_sampled( if excluded_alt_ids[i] >= 0: available_mask[excluded_alt_ids[i]] = False available_alts = alt_ids[available_mask] - sampled[i] = np.random.choice(available_alts, size=sample_size, replace=False) + sampled[i] = rng.choice(available_alts, size=sample_size, replace=False) # Ensure chosen alternative is always included if chosen_series is not None: diff --git a/locpick/dgp.py b/locpick/dgp.py index ccf1682..f10ac12 100644 --- a/locpick/dgp.py +++ b/locpick/dgp.py @@ -353,6 +353,124 @@ def _build_choice_table(choosers, alternatives, choices, matrix_data=None): ) +# --------------------------------------------------------------------------- +# Shared DGP helpers +# --------------------------------------------------------------------------- + + +def _build_choosers(n_obs, seed, feature_name="obs_feature"): + """Build choosers DataFrame with obs_id index and a random feature.""" + rng = np.random.default_rng(seed) + obs_ids = pd.Index(np.arange(n_obs), name="oid") + choosers = pd.DataFrame({feature_name: rng.standard_normal(n_obs)}, index=obs_ids) + return choosers, rng, obs_ids + + +def _build_alternatives(n_alts, rng, alt_features): + """Build alternatives DataFrame with alt_id index. + + Parameters + ---------- + n_alts : int + rng : np.random.Generator + alt_features : dict[str, tuple] + Mapping of column name → (low, high) for uniform draw. + """ + alt_ids = pd.Index(np.arange(n_alts), name="aid") + data = {} + for col, (low, high) in alt_features.items(): + data[col] = rng.uniform(low, high, n_alts) + alternatives = pd.DataFrame(data, index=alt_ids) + return alternatives, alt_ids + + +def _build_interactions(obs_ids, alt_ids, chooser_feature, alt_columns): + """Build chooser×alternative interaction terms. + + Returns + ------- + interactions : dict[str, pd.Series] + Named (obs_id, alt_id)-indexed Series. + interaction_index : pd.MultiIndex + """ + interaction_index = pd.MultiIndex.from_product([obs_ids, alt_ids], names=["oid", "aid"]) + n_obs = len(obs_ids) + n_alts = len(alt_ids) + interactions = {} + for alt_col in alt_columns: + alt_vals = alt_columns[alt_col] + tiled_feat = np.repeat(chooser_feature, n_alts) + tiled_alt = np.tile(alt_vals, n_obs) + name = f"{chooser_feature.name}_x_{alt_col}" if hasattr(chooser_feature, "name") else f"obs_x_{alt_col}" + interactions[name] = pd.Series( + tiled_feat * tiled_alt, index=interaction_index, name=name + ) + return interactions, interaction_index + + +def _compute_det_utility(n_obs, n_alts, alternatives, alt_params, interactions, interaction_coefs=None): + """Compute deterministic utility from alt params and interactions. + + Parameters + ---------- + n_obs, n_alts : int + alternatives : pd.DataFrame + alt_params : dict[str, float] + interactions : dict[str, pd.Series] + interaction_coefs : dict[str, float] or None + Coefficients for interaction terms. Keys must match interactions. + """ + det_utility = np.zeros((n_obs, n_alts)) + for col, coef in alt_params.items(): + alt_vals = alternatives[col].to_numpy() + det_utility += coef * np.tile(alt_vals, n_obs).reshape(n_obs, n_alts) + if interaction_coefs: + for name, coef in interaction_coefs.items(): + if name in interactions: + det_utility += coef * interactions[name].to_numpy().reshape(n_obs, n_alts) + return det_utility + + +def _build_design_matrix(n_obs, alternatives, interactions, interaction_coefs): + """Build a design matrix from alternatives + interaction terms. + + Returns + ------- + design_matrix : np.ndarray, shape (n_obs * n_alts, k) + beta : np.ndarray, shape (k,) + """ + beta = np.array([None] * len(alternatives.columns), dtype=float) + design_matrix = np.tile(alternatives.to_numpy(), (n_obs, 1)) + beta = np.array([1.0] * len(alternatives.columns), dtype=float) # placeholder + for name, coef in interaction_coefs.items(): + if name in interactions: + design_matrix = np.column_stack([design_matrix, interactions[name].to_numpy().ravel()]) + return design_matrix + + +def _simulate_choices_from_probs(probs, rng, n_obs, n_alts): + """Vectorized choice simulation from probability matrix. + + Replaces the Python loop ``np.array([rng.choice(n_alts, p=probs[i]) for i in range(n_obs)])`` + with vectorized inverse-CDF sampling. + """ + # Normalize to sum to 1 (numerical safety) + probs = probs / probs.sum(axis=1, keepdims=True) + cumulative = np.cumsum(probs, axis=1) + uniform = rng.random(n_obs) + choices = np.argmax(cumulative > uniform[:, None], axis=1) + return np.clip(choices, 0, n_alts - 1) + + +def _build_circular_adjacency(n_alts): + """Build a circular adjacency matrix where zone i is adjacent to i±1.""" + adjacency = np.zeros((n_alts, n_alts), dtype=np.float64) + for i in range(n_alts): + adjacency[i, (i - 1) % n_alts] = 1.0 + adjacency[i, (i + 1) % n_alts] = 1.0 + return adjacency + + # --------------------------------------------------------------------------- # MNL DGP # --------------------------------------------------------------------------- @@ -601,7 +719,7 @@ def simulate_nested_logit( ) # Simulate choices from probabilities - choices = np.array([rng.choice(n_alts, p=probs[i]) for i in range(n_obs)]) + choices = _simulate_choices_from_probs(probs, rng, n_obs, n_alts) choosers = choosers.copy() choosers["choice"] = choices @@ -733,7 +851,7 @@ def simulate_scl( probs = np.exp(log_probs) # Simulate choices - choices = np.array([rng.choice(n_alts, p=probs[i]) for i in range(n_obs)]) + choices = _simulate_choices_from_probs(probs, rng, n_obs, n_alts) choosers = choosers.copy() choosers["choice"] = choices @@ -1143,9 +1261,6 @@ def simulate_nested_scl( det_utility += interactions["income_x_cost"].to_numpy().reshape(n_obs, n_alts) * 0.05 # --- Compute Nested SCL probabilities and simulate choices -------- - np.array([alt_params[col] for col in alternatives.columns]) - np.tile(alternatives.to_numpy(), (n_obs, 1)) - nest_matrix = nests.build_nest_matrix(list(range(n_alts))) n_nests = len(nest_names) @@ -1204,7 +1319,7 @@ def simulate_nested_scl( probs = probs / probs.sum(axis=1, keepdims=True) # Simulate choices - choices = np.array([rng.choice(n_alts, p=probs[i]) for i in range(n_obs)]) + choices = _simulate_choices_from_probs(probs, rng, n_obs, n_alts) choosers = choosers.copy() choosers["choice"] = choices @@ -1444,9 +1559,6 @@ def simulate_mnscl( det_utility += interactions["income_x_cost"].to_numpy().reshape(n_obs, n_alts) * 0.05 # --- Add random coefficient variation ------------------------------ - np.array([alt_params[col] for col in alternatives.columns]) - np.tile(alternatives.to_numpy(), (n_obs, 1)) - # For each random parameter, add random variation multiplied by attribute for param_name, (dist, mean, spread) in random_params.items(): list(alternatives.columns).index(param_name) @@ -1525,7 +1637,7 @@ def simulate_mnscl( probs = probs / probs.sum(axis=1, keepdims=True) # Simulate choices - choices = np.array([rng.choice(n_alts, p=probs[i]) for i in range(n_obs)]) + choices = _simulate_choices_from_probs(probs, rng, n_obs, n_alts) choosers = choosers.copy() choosers["choice"] = choices @@ -1726,7 +1838,7 @@ def simulate_mixed_nested_logit( ) # Simulate choices from probabilities - choices = np.array([rng.choice(n_alts, p=probs[i]) for i in range(n_obs)]) + choices = _simulate_choices_from_probs(probs, rng, n_obs, n_alts) choosers = choosers.copy() choosers["choice"] = choices @@ -1761,3 +1873,192 @@ def simulate_mixed_nested_logit( n_alts=n_alts, seed=seed, ) + + +# --------------------------------------------------------------------------- +# SAR-MNL DGP (Smirnov 2010) +# --------------------------------------------------------------------------- + + +@dataclass +class SARMNLDataset: + """Synthetic dataset drawn from a known SAR-MNL data generating process. + + Alternatives are spatial locations connected by ``W`` (alt×alt). + Choosers select among alternatives via MNL with spatially-filtered + and variance-normalised utilities (Smirnov 2010 PML DGP). + + Attributes + ---------- + choosers : pd.DataFrame + Obs-id-indexed DataFrame with chooser attributes and ``choice`` column. + alternatives : pd.DataFrame + Alt-id-indexed DataFrame with alternative attributes. + interactions : dict[str, pd.Series] + Named ``(obs_id, alt_id)``-indexed Series for chooser×alt interactions. + true_params : dict[str, float] + Ground-truth beta coefficients (alt-level + interaction). + true_rho : float + Ground-truth spatial autoregressive parameter. + W : libpysal.graph.Graph + Row-standardised spatial weights matrix (n_alts × n_alts) as a + libpysal Graph. Use ``W.sparse`` to get the scipy.sparse matrix + for computation. + choice_table : object + Assembled ChoiceTable. + n_obs : int + n_alts : int + seed : int + """ + + choosers: pd.DataFrame + alternatives: pd.DataFrame + interactions: dict[str, pd.Series] + true_params: dict[str, float] + true_rho: float + W: Any # libpysal.graph.Graph + choice_table: Any + n_obs: int + n_alts: int + seed: int + + +def simulate_sar_mnl( + n_obs: int = 5000, + n_alts: int = 50, + alt_params: dict[str, float] | None = None, + interaction_params: dict[str, float] | None = None, + rho: float = 0.3, + W=None, + n_neighbors: int = 7, + seed: int = 1234, +) -> SARMNLDataset: + """Generate synthetic SAR-MNL choice data with known parameters. + + The DGP follows Smirnov (2010) PML model: + + 1. Build ``W`` (alt×alt, row-standardised k-nearest-neighbor Graph) + 2. Generate alternative attributes ``Z`` and chooser-alt interactions ``X`` + 3. Compute base utilities: ``V_base = Zβ + Xγ`` (n_obs × n_alts) + 4. Spatial filter: ``V_filtered = (I - ρW)^{-1} V_base^T`` + 5. Variance normalisation: ``D = diag((I - ρW)^{-1})``, + ``V_star = V_filtered / D`` (divide each alt by d_jj) + 6. Add Gumbel noise: ``U = V_star + Gumbel(0, 1)`` + 7. Choice = ``argmax(U)`` per chooser + + The variance normalisation (step 5) is essential — it matches the + PML estimator's model (Smirnov 2010). Without it, the DGP would + not match the estimation model and parameter recovery would fail. + + Parameters + ---------- + n_obs : int, default 5000 + Number of choosers. + n_alts : int, default 50 + Number of alternatives (spatial locations). Dimension of W. + For PML dense path, keep ≤ 2000. For CG path, can be larger. + alt_params : dict, optional + Mapping of alternative-level column name → true coefficient. + Default: ``{"alt_attr": -0.5}``. + interaction_params : dict, optional + Mapping of interaction column name → true coefficient. + Default: ``{"obs_x_alt": 0.8}``. + rho : float, default 0.3 + True spatial autoregressive parameter. Should be in (-1, 1). + Smirnov 2010 MC evidence: good recovery for ρ ∈ [0, 0.5]. + W : libpysal.graph.Graph, scipy.sparse, np.ndarray, or None + Pre-specified n_alts × n_alts spatial weights matrix. + If None, constructed as k-nearest-neighbor Graph on random + coordinates (matching Krisztin et al. 2022's 7-NN specification). + A ``libpysal.graph.Graph`` is the preferred input type. + n_neighbors : int, default 7 + Number of nearest neighbors for default W construction. + seed : int, default 1234 + + Returns + ------- + SARMNLDataset + Dataset with ``W`` stored as a ``libpysal.graph.Graph`` (row-standardised). + """ + if alt_params is None: + alt_params = {"alt_attr": -0.5} + if interaction_params is None: + interaction_params = {"obs_x_alt": 0.8} + + rng = np.random.default_rng(seed) + + # --- Build W (alt×alt) as a libpysal Graph -------------------------- + from locpick.models._spatial_weights import build_knn_graph, resolve_spatial_weights + + if W is None: + coords = rng.standard_normal((n_alts, 2)) + W_graph = build_knn_graph(coords, k=n_neighbors) + W_dense = np.asarray(W_graph.sparse.todense(), dtype=np.float64) + else: + W_graph, _ = resolve_spatial_weights(W, n_alts, row_standardize=True) + W_dense = np.asarray(W_graph.sparse.todense(), dtype=np.float64) + + # --- Choosers and alternatives -------------------------------------- + obs_ids = pd.Index(np.arange(n_obs), name="oid") + obs_feature = rng.standard_normal(n_obs) + choosers = pd.DataFrame({"obs_feature": obs_feature}, index=obs_ids) + + alt_ids = pd.Index(np.arange(n_alts), name="aid") + alt_attr = rng.standard_normal(n_alts) + alternatives = pd.DataFrame({"alt_attr": alt_attr}, index=alt_ids) + + # --- Interactions (chooser × alternative) -------------------------- + interaction_index = pd.MultiIndex.from_product( + [obs_ids, alt_ids], names=["oid", "aid"] + ) + obs_feat_tiled = np.repeat(obs_feature, n_alts) + alt_attr_tiled = np.tile(alt_attr, n_obs) + obs_x_alt_values = obs_feat_tiled * alt_attr_tiled + interactions = { + "obs_x_alt": pd.Series( + obs_x_alt_values, index=interaction_index, name="obs_x_alt" + ) + } + + # --- Base utilities: V_base = Zβ + Xγ (n_obs × n_alts) ------------- + V_base = np.zeros((n_obs, n_alts)) + for col, coef in alt_params.items(): + V_base += coef * np.tile(alternatives[col].to_numpy(), (n_obs, 1)) + for col, coef in interaction_params.items(): + V_base += coef * interactions[col].to_numpy().reshape(n_obs, n_alts) + + # --- Spatial filter: V_filtered = (I - ρW)^{-1} V_base^T ------------ + A = np.eye(n_alts) - rho * W_dense + V_filtered = np.linalg.solve(A, V_base.T).T # (n_obs, n_alts) + + # --- Variance normalisation: D = diag((I - ρW)^{-1}) --------------- + Z_mat = np.linalg.inv(A) + D = np.diag(Z_mat) # (n_alts,) + V_star = V_filtered / D[None, :] # normalise each alternative by d_jj + + # --- Add Gumbel noise and simulate choices ------------------------- + gumbel = rng.gumbel(size=(n_obs, n_alts)) + U = V_star + gumbel + choices = U.argmax(axis=1) + choosers = choosers.copy() + choosers["choice"] = choices + + # --- Build ChoiceTable ---------------------------------------------- + true_params = dict(alt_params) + true_params.update(interaction_params) + choice_table = _build_choice_table( + choosers, alternatives, choosers["choice"], matrix_data=interactions + ) + + return SARMNLDataset( + choosers=choosers, + alternatives=alternatives, + interactions=interactions, + true_params=true_params, + true_rho=rho, + W=W_graph, + choice_table=choice_table, + n_obs=n_obs, + n_alts=n_alts, + seed=seed, + ) diff --git a/locpick/models/__init__.pyi b/locpick/models/__init__.pyi index 1f1a6e3..93f1b31 100644 --- a/locpick/models/__init__.pyi +++ b/locpick/models/__init__.pyi @@ -4,6 +4,9 @@ from . import mnl as mnl from . import nested as nested from . import scl as scl from .base import ( + ChoiceModelProtocol as ChoiceModelProtocol, +) +from .choice_model import ( ChoiceModel as ChoiceModel, ) from .mixed import ( @@ -48,3 +51,6 @@ from .scl import ( from .scl import ( naturalize_rho as naturalize_rho, ) +from .sar_mnl import ( + SARMNL as SARMNL, +) diff --git a/locpick/models/_spatial_weights.py b/locpick/models/_spatial_weights.py new file mode 100644 index 0000000..764a5e6 --- /dev/null +++ b/locpick/models/_spatial_weights.py @@ -0,0 +1,132 @@ +"""Spatial weights matrix utilities for SAR-MNL models. + +This module provides helpers to resolve spatial weights matrices +(``W``) connecting alternatives (spatial locations). It follows the +convention from the sister package *bayespecon*: ``libpysal.graph.Graph`` +is the canonical type, but ``scipy.sparse`` matrices and dense NumPy +arrays are also accepted for convenience. + +The resolver returns both a row-standardised ``Graph`` (for storage +and return in DGP datasets) and a CSR sparse matrix (for efficient +computation inside JAX kernels). +""" + +from __future__ import annotations + +from typing import Union + +import numpy as np +import scipy.sparse as sp + + +def resolve_spatial_weights( + W, + n_alts: int, + row_standardize: bool = True, +): + """Resolve alt×alt spatial weights to a libpysal Graph + CSR sparse. + + Accepts a ``libpysal.graph.Graph`` (preferred), ``scipy.sparse`` + matrix, or dense ``np.ndarray``. Returns both a row-standardised + ``Graph`` (for storage/return in DGP datasets) and a CSR sparse + matrix (for efficient computation). + + Parameters + ---------- + W : libpysal.graph.Graph, scipy.sparse, or np.ndarray + J×J spatial weights matrix connecting alternatives (locations). + n_alts : int + Expected number of alternatives (for validation). + row_standardize : bool, default True + If True, row-standardize the weights (rows sum to 1). + + Returns + ------- + W_graph : libpysal.graph.Graph + Row-standardised spatial weights as a libpysal Graph. + W_sparse : scipy.sparse.csr_array + Row-standardised CSR sparse matrix (float64), zero diagonal. + """ + # --- Reject legacy libpysal.weights.W ------------------------------- + if W.__class__.__module__.startswith("libpysal.weights") and not hasattr( + W, "sparse" + ): + raise TypeError( + "Legacy libpysal.weights.W is not supported. " + "Convert via libpysal.graph.Graph.from_W(w) or pass w.sparse." + ) + + # --- Convert to CSR sparse ----------------------------------------- + if hasattr(W, "sparse"): + # libpysal.graph.Graph + W_sparse = sp.csr_array(W.sparse, dtype=np.float64) + elif sp.issparse(W): + W_sparse = sp.csr_array(W, dtype=np.float64) + else: + W_sparse = sp.csr_array(np.asarray(W, dtype=np.float64)) + + # --- Validate shape ------------------------------------------------- + if W_sparse.shape != (n_alts, n_alts): + raise ValueError( + f"W shape {W_sparse.shape} does not match n_alts ({n_alts}, {n_alts})." + ) + + # --- Zero diagonal -------------------------------------------------- + W_sparse.setdiag(0.0) + W_sparse.eliminate_zeros() + + # --- Row-standardize ------------------------------------------------ + if row_standardize: + row_sums = np.asarray(W_sparse.sum(axis=1)).ravel() + row_sums = np.where(row_sums == 0, 1.0, row_sums) + W_sparse = sp.diags(1.0 / row_sums) @ W_sparse + W_sparse = sp.csr_array(W_sparse, dtype=np.float64) + + # --- Convert back to Graph for storage/return ---------------------- + W_graph = _csr_to_graph(W_sparse) + + return W_graph, W_sparse + + +def _csr_to_graph(W_sparse: sp.csr_array): + """Convert a CSR sparse matrix to a libpysal Graph.""" + from libpysal.graph import Graph + + W_coo = W_sparse.tocoo() + return Graph.from_arrays( + focal_ids=W_coo.row.astype(np.int32), + neighbor_ids=W_coo.col.astype(np.int32), + weight=W_coo.data.astype(np.float64), + ) + + +def build_knn_graph( + coords: np.ndarray, + k: int, +): + """Build a k-nearest-neighbor libpysal Graph from coordinates. + + Parameters + ---------- + coords : np.ndarray, shape (n, 2) + Spatial coordinates of alternatives. + k : int + Number of nearest neighbors. + + Returns + ------- + libpysal.graph.Graph + Row-standardised k-NN graph. + """ + import geopandas as gpd + from libpysal.graph import Graph + from shapely.geometry import Point + + n = coords.shape[0] + gdf = gpd.GeoDataFrame( + {"aid": np.arange(n)}, + geometry=[Point(c) for c in coords], + crs="EPSG:4326", + ) + W_graph = Graph.build_knn(gdf, k=k) + return W_graph.transform("r") \ No newline at end of file diff --git a/locpick/models/base.py b/locpick/models/base.py index 79629b6..c44feab 100644 --- a/locpick/models/base.py +++ b/locpick/models/base.py @@ -86,12 +86,11 @@ def _aggregate_per_obs_alt(s: pd.Series, by: str, name: str): @runtime_checkable -class ChoiceModel(Protocol): +class ChoiceModelProtocol(Protocol): """Protocol for discrete choice model classes. - All concrete model classes (``MultinomialLogit``, ``NestedLogit``, - ``MixedLogit``, ``SpatiallyCorrelatedLogit``, - ``MixedSpatiallyCorrelatedLogit``) implement this protocol. + All concrete model classes implement this protocol. + The primary implementation is :class:`locpick.models.choice_model.ChoiceModel`. """ def fit(self, **kwargs) -> FitResult: diff --git a/locpick/models/choice_model.py b/locpick/models/choice_model.py new file mode 100644 index 0000000..fe829ef --- /dev/null +++ b/locpick/models/choice_model.py @@ -0,0 +1,1686 @@ +"""Unified choice model for location choice estimation. + +This module provides the :class:`ChoiceModel` class, a single composable +model that handles all model configurations via optional feature flags: + +- ``nests`` → Nested logit +- ``random_params`` → Mixed logit +- ``graph`` → Spatially correlated logit (SCL) +- combinations → Nested SCL, Mixed SCL (MSCL), Mixed Nested, Mixed Nested SCL + +The class inherits from :class:`BaseChoiceModel` and :class:`SpatialMixin`, +dispatching to the appropriate JAX builder based on which features are active. +All shared methods (simulate, marginal effects, elasticities, covariance) +live here once, eliminating the duplication across MNL/NestedMNL/MixedMNL/ +MixedNestedMNL. +""" + +from __future__ import annotations + +from typing import Optional, Union + +import numpy as np +import pandas as pd + +from locpick._jax.objective import Objective +from locpick._solvers import Solver, SolverResult +from locpick.data.arrays import ChoiceArrays +from locpick.data.problem import EstimationProblem +from locpick.models._spatial import ( + EdgeStructure, + _resolve_spatial_graph, + naturalize_rho, +) +from locpick.models.base import ( + BaseChoiceModel, + SpatialMixin, + _compute_fit_statistics, + _compute_null_ll, + _safe_inv, + _sandwich_inv, +) +from locpick.models.mixed import ParamDistribution, _resolve_draws +from locpick.models.nested import NestingTree, naturalize_nest_params +from locpick.results.fit_result import FitResult + + +class ChoiceModel(BaseChoiceModel, SpatialMixin): + r"""Unified discrete choice model for location choice estimation. + + A single composable class that handles all model configurations: + + - **MNL** (default): ``ChoiceModel(ct, formula="cost + time - 1")`` + - **Nested logit**: ``ChoiceModel(ct, formula="...", nests=tree)`` + - **Mixed logit**: ``ChoiceModel(ct, formula="...", random_params={"time": ParamDistribution("normal", "time")})`` + - **SCL** (spatial): ``ChoiceModel(ct, formula="...", graph=g)`` + - **Nested SCL**: ``ChoiceModel(ct, formula="...", nests=tree, graph=g)`` + - **MSCL** (mixed + spatial): ``ChoiceModel(ct, formula="...", random_params=..., graph=g)`` + - **Mixed Nested**: ``ChoiceModel(ct, formula="...", nests=tree, random_params=...)`` + - **Mixed Nested SCL**: ``ChoiceModel(ct, formula="...", nests=tree, random_params=..., graph=g)`` + + The model type is determined by which optional features are present. + + Parameters + ---------- + data : ChoiceTable or EstimationProblem + The choice data to estimate on. + formula : str, optional + A formulaic formula string (e.g., ``"cost + time - 1"``). + Mutually exclusive with ``spec``. + spec : ModelSpec, optional + A ModelSpec object defining formula/scoped-term model structure. + Mutually exclusive with ``formula``. + nests : NestingTree, optional + The nesting structure for nested logit models. + random_params : dict, optional + Mapping of parameter names to :class:`ParamDistribution` objects + for mixed logit models. + graph : libpysal.Graph, scipy.sparse, or np.ndarray, optional + Spatial adjacency graph for SCL models. + n_draws : int, optional + Number of draws for simulated maximum likelihood (mixed logit). + Default 100. + draw_type : str, optional + Type of draws: ``"qmc"`` (default), ``"halton"``, or ``"random"``. + seed : int + Random seed for draw generation. Default 42. + weights : str or array-like, optional + Observation weights. + availability : str or array-like, optional + Alternative availability. + solver : str or Solver, optional + Solver name or instance. Default ``"lbfgs"``. + solver_options : dict, optional + Additional options passed to the solver constructor. + backend : str, optional + Computation backend hint. + + Examples + -------- + >>> from locpick import ChoiceTable, ChoiceModel + >>> ct = ChoiceTable.from_tables(choosers, alternatives, chosen, sample_size=10) + >>> model = ChoiceModel(ct, formula="cost + time - 1") + >>> result = model.fit() + >>> print(result.summary()) + """ + + def __init__( + self, + data, + formula: Optional[str] = None, + spec=None, + problem: Optional[EstimationProblem] = None, + nests: Optional[NestingTree] = None, + random_params: Optional[dict[str, ParamDistribution]] = None, + graph=None, + n_draws: Optional[int] = None, + draw_type: Optional[str] = None, + seed: int = 42, + weights: Optional[Union[str, np.ndarray]] = None, + availability: Optional[Union[str, np.ndarray]] = None, + solver: Union[str, Solver] = "lbfgs", + solver_options: Optional[dict] = None, + backend: Optional[str] = None, + ): + # Handle the legacy `problem` parameter by wrapping it as EstimationProblem + if problem is not None: + data = problem + + super().__init__( + data=data, + formula=formula, + spec=spec, + solver=solver, + solver_options=solver_options, + backend=backend, + weights=weights, + availability=availability, + ) + + # Feature flags + self._nests = nests + self._random_params = random_params + self._graph_input = graph + + # Mixed logit settings + self._n_draws = n_draws if n_draws is not None else 100 + self._draw_type = draw_type if draw_type is not None else "sobol" + self._seed = seed + self._draws: Optional[np.ndarray] = None + + # Nest matrix (built in _pre_fit) + self._nest_matrix: Optional[np.ndarray] = None + + # Spatial state (None when graph is not provided) + self._omega = None + self._allocation = None + self._edge_list = None + self._n_alts_graph = None + self._edge_struct = None + self._edge_structs = None + self._edge_data_list = None + + # Random parameter state (built in _pre_fit) + self._random_col_indices: Optional[list[int]] = None + self._random_distributions: Optional[list[str]] = None + self._random_param_names: Optional[list[str]] = None + self._k_fixed: Optional[int] = None + self._k_random: Optional[int] = None + self._fixed_names: Optional[list[str]] = None + self._full_param_names: Optional[list[str]] = None + + # ------------------------------------------------------------------ + # Properties + # ------------------------------------------------------------------ + + @property + def _is_spatial(self) -> bool: + return self._graph_input is not None + + @property + def _is_nested(self) -> bool: + return self._nests is not None + + @property + def _is_mixed(self) -> bool: + return self._random_params is not None and len(self._random_params) > 0 + + @property + def model_type(self) -> str: + """Human-readable model type string.""" + parts = [] + if self._is_mixed: + parts.append("Mixed") + if self._is_nested: + parts.append("Nested") + if self._is_spatial: + parts.append("Spatially Correlated") + if not parts: + return "Multinomial Logit" + if len(parts) == 1 and parts[0] == "Spatially Correlated": + return "Spatially Correlated Logit" + if len(parts) == 1 and parts[0] == "Nested": + return "Nested Logit" + if len(parts) == 1 and parts[0] == "Mixed": + return "Mixed Logit" + return " ".join(parts) + " Logit" + + # ------------------------------------------------------------------ + # Pre-fit: build model-specific data structures + # ------------------------------------------------------------------ + + def _pre_fit(self, arrays: ChoiceArrays) -> None: + """Build nest matrix, random parameter structure, and spatial graph.""" + # Build nest matrix if nests are provided + if self._is_nested: + alt_ids = list(range(arrays.n_alts)) + self._nest_matrix = self._nests.build_nest_matrix(alt_ids) + + # Build random parameter structure if random_params is provided + if self._is_mixed: + self._prepare_random_params(arrays) + + # Resolve spatial graph if graph is provided + if self._is_spatial: + self._resolve_spatial_graph() + self._validate_graph_size(arrays) + + # Build per-nest edge structures for nested spatial models + if self._is_nested: + self._build_per_nest_edges(arrays) + + def _prepare_random_params(self, arrays: ChoiceArrays) -> None: + """Identify random parameter columns and generate draws.""" + param_names = list(arrays.param_names) + random_param_names: list[str] = [] + random_distributions: list[str] = [] + random_col_indices: list[int] = [] + + for name, dist in self._random_params.items(): + if name not in param_names: + raise ValueError( + f"Random parameter '{name}' not found in design matrix. " + f"Available parameters: {param_names}" + ) + random_param_names.append(name) + random_distributions.append(dist.distribution) + random_col_indices.append(param_names.index(name)) + + k_fixed = len(param_names) - len(random_param_names) + k_random = len(random_param_names) + + # Generate draws + draws = _resolve_draws( + self._draw_type, arrays.n_obs, self._n_draws, k_random, seed=self._seed + ) + + fixed_names = [n for i, n in enumerate(param_names) if i not in random_col_indices] + full_param_names = ( + fixed_names + + [f"mean_{n}" for n in random_param_names] + + [f"sd_{n}" for n in random_param_names] + ) + + self._random_param_names = random_param_names + self._random_distributions = random_distributions + self._random_col_indices = random_col_indices + self._k_fixed = k_fixed + self._k_random = k_random + self._fixed_names = fixed_names + self._full_param_names = full_param_names + self._draws = draws + + def _build_per_nest_edges(self, arrays: ChoiceArrays) -> None: + """Build per-nest EdgeStructure / EdgeDataJAX from the global graph.""" + from locpick._jax.data import EdgeDataJAX + + n_nests = self._nests.n_nests + self._edge_structs = [] + self._edge_data_list = [] + for m in range(n_nests): + nest_alts = np.where(self._nest_matrix[:, m] > 0)[0] + n_nest_alts = len(nest_alts) + if n_nest_alts == 0: + self._edge_structs.append(None) + self._edge_data_list.append(None) + continue + nest_adj = self._omega[np.ix_(nest_alts, nest_alts)] + _, nest_alloc, nest_edges, _ = _resolve_spatial_graph(nest_adj) + edge_struct = EdgeStructure(nest_edges, n_nest_alts, nest_alloc) + self._edge_structs.append(edge_struct) + self._edge_data_list.append(EdgeDataJAX.from_edge_structure(edge_struct)) + + # ------------------------------------------------------------------ + # Solver inputs + # ------------------------------------------------------------------ + + def _get_solver_inputs(self, arrays: ChoiceArrays): + """Get initial values, param names, bounds, and fixed mask. + + Parameter layout depends on active features: + + - MNL: ``[beta]`` + - SCL: ``[beta, alpha_rho]`` + - Nested: ``[beta, alpha_nest]`` + - Nested SCL: ``[beta, alpha_rho_1..M, alpha_lambda_1..M]`` + - Mixed: ``[beta_fixed, mean_*, sd_*]`` + - MSCL: ``[beta_fixed, alpha_rho, mean_*, sd_*]`` + - Mixed Nested: ``[beta_fixed, alpha_nest, mean_*, sd_*]`` + - Mixed Nested SCL: ``[beta_fixed, alpha_rho_1..M, alpha_lambda_1..M, mean_*, sd_*]`` + """ + k = arrays.design_matrix.shape[1] + param_names_all = list(arrays.param_names) + + # --- Pure MNL --- + if not self._is_nested and not self._is_mixed: + # Use problem's initial values / fixed_mask when available + if self._problem is not None: + x0_base = self._problem.initial_values + bounds = self._problem.bounds + fixed_mask = self._problem.fixed_mask + else: + x0_base = np.zeros(k) + bounds = None + fixed_mask = None + names_base = param_names_all + if self._is_spatial: + # SCL: [beta, alpha_rho] + x0 = np.concatenate([x0_base, np.zeros(1)]) + names = list(names_base) + ["alpha_rho"] + else: + x0 = x0_base + names = names_base + return x0, names, bounds, fixed_mask + + # --- Nested (no random) --- + if self._is_nested and not self._is_mixed: + n_nests = self._nests.n_nests + if self._is_spatial: + # Nested SCL: [beta, alpha_rho_1..M, alpha_lambda_1..M] + x0 = np.concatenate([ + np.zeros(k), + np.zeros(n_nests), + self._nests.initial_alphas(), + ]) + names = ( + param_names_all + + [f"alpha_rho_{name}" for name in self._nests.nest_names] + + [f"alpha_lambda_{name}" for name in self._nests.nest_names] + ) + else: + # Nested: [beta, alpha_nest] + x0 = np.concatenate([np.zeros(k), self._nests.initial_alphas()]) + names = param_names_all + [f"nest_{name}" for name in self._nests.nest_names] + return x0, names, None, None + + # --- Mixed (no nests) --- + if self._is_mixed and not self._is_nested: + k_fixed = self._k_fixed + k_random = self._k_random + if self._is_spatial: + # MSCL: [beta_fixed, alpha_rho, mean_*, sd_*] + x0 = np.concatenate([ + np.zeros(k_fixed), + np.zeros(1), + np.zeros(k_random), + np.full(k_random, 0.1), + ]) + names = ( + list(self._fixed_names) + + ["rho"] + + [f"mean_{n}" for n in self._random_param_names] + + [f"sd_{n}" for n in self._random_param_names] + ) + else: + # Mixed: [beta_fixed, mean_*, sd_*] + x0 = np.concatenate([ + np.zeros(k_fixed), + np.zeros(k_random), + np.full(k_random, 0.1), + ]) + names = list(self._full_param_names) + return x0, names, None, None + + # --- Mixed Nested --- + if self._is_nested and self._is_mixed: + k_fixed = self._k_fixed + k_random = self._k_random + n_nests = self._nests.n_nests + fixed_param_names = [name for name in param_names_all if name not in self._random_params] + if self._is_spatial: + # Mixed Nested SCL: [beta_fixed, alpha_rho_1..M, alpha_lambda_1..M, mean_*, sd_*] + x0 = np.concatenate([ + np.zeros(k_fixed), + np.zeros(n_nests), + self._nests.initial_alphas(), + np.zeros(k_random), + np.full(k_random, 0.1), + ]) + names = ( + fixed_param_names + + [f"alpha_rho_{name}" for name in self._nests.nest_names] + + [f"alpha_lambda_{name}" for name in self._nests.nest_names] + + [f"mean_{name}" for name in self._random_param_names] + + [f"sd_{name}" for name in self._random_param_names] + ) + else: + # Mixed Nested: [beta_fixed, alpha_nest, mean_*, sd_*] + x0 = np.concatenate([ + np.zeros(k_fixed), + self._nests.initial_alphas(), + np.zeros(k_random), + np.full(k_random, 0.1), + ]) + names = ( + fixed_param_names + + [f"lambda_{name}" for name in self._nests.nest_names] + + [f"mean_{name}" for name in self._random_param_names] + + [f"sd_{name}" for name in self._random_param_names] + ) + return x0, names, None, None + + # Fallback (should not reach here) + return np.zeros(k), param_names_all, None, None + + # ------------------------------------------------------------------ + # Objective construction + # ------------------------------------------------------------------ + + def _build_objective(self, arrays: ChoiceArrays) -> Objective: + """Build optimization objective based on active features.""" + # Pure MNL / SCL + if not self._is_nested and not self._is_mixed: + if self._is_spatial: + from locpick._jax.builders import build_scl_objective + return build_scl_objective( + arrays, self._edge_struct, self._allocation, self._edge_list + ) + from locpick._jax.builders import build_mnl_objective + return build_mnl_objective(arrays) + + # Nested (no random) + if self._is_nested and not self._is_mixed: + if self._is_spatial: + from locpick._jax.builders import build_nested_scl_objective + return build_nested_scl_objective( + arrays, self._nest_matrix, self._edge_data_list + ) + from locpick._jax.builders import build_nested_objective + return build_nested_objective(arrays, self._nest_matrix) + + # Mixed (no nests) + if self._is_mixed and not self._is_nested: + if self._is_spatial: + from locpick._jax.builders import build_mscl_objective + return build_mscl_objective( + arrays, + self._edge_struct, + self._allocation, + self._edge_list, + self._random_col_indices, + self._random_distributions, + self._draws, + ) + from locpick._jax.builders import build_mixed_logit_objective + return build_mixed_logit_objective( + arrays, + random_col_indices=self._random_col_indices, + random_distributions=self._random_distributions, + draws=self._draws, + ) + + # Mixed Nested + if self._is_nested and self._is_mixed: + if self._is_spatial: + from locpick._jax.builders import build_mnscl_objective + return build_mnscl_objective( + arrays, + self._nest_matrix, + self._edge_data_list, + self._random_col_indices, + self._random_distributions, + self._draws, + ) + from locpick._jax.builders import build_mixed_nested_objective + return build_mixed_nested_objective( + arrays, + self._nest_matrix, + self._random_col_indices, + self._random_distributions, + self._draws, + ) + + raise RuntimeError("Unknown model configuration") + + # ------------------------------------------------------------------ + # Fit result construction + # ------------------------------------------------------------------ + + def _build_fit_result( + self, + solver_result: SolverResult, + arrays: ChoiceArrays, + ) -> FitResult: + """Build a FitResult from solver output.""" + all_params = solver_result.coefficients + k = arrays.design_matrix.shape[1] + param_names_all = list(arrays.param_names) + + # --- Pure MNL / SCL --- + if not self._is_nested and not self._is_mixed: + if self._is_spatial: + # Layout: [beta_1..k, alpha_rho] + beta = all_params[:k] + alpha_rho = all_params[k] + rho = naturalize_rho(alpha_rho) + display_values = np.concatenate([beta, [rho]]) + display_names = param_names_all + ["rho"] + else: + display_values = all_params + display_names = param_names_all + + std_errors = self._compute_se(all_params, arrays, display_values, display_names) + return self._make_fit_result( + solver_result, arrays, display_values, display_names, std_errors + ) + + # --- Nested (no random) --- + if self._is_nested and not self._is_mixed: + n_nests = self._nests.n_nests + if self._is_spatial: + # Layout: [beta, alpha_rho_1..M, alpha_lambda_1..M] + beta = all_params[:k] + alpha_rho = all_params[k : k + n_nests] + alpha_lambda = all_params[k + n_nests : k + 2 * n_nests] + rhos = naturalize_rho(alpha_rho) + lambdas = naturalize_nest_params(alpha_lambda) + display_values = np.concatenate([beta, rhos, lambdas]) + display_names = ( + param_names_all + + [f"rho_{name}" for name in self._nests.nest_names] + + [f"lambda_{name}" for name in self._nests.nest_names] + ) + std_errors = self._compute_se_nested_scl( + all_params, arrays, k, n_nests, rhos, lambdas + ) + else: + # Layout: [beta, alpha_nest] + beta = all_params[:k] + alpha = all_params[k:] + lambdas = naturalize_nest_params(alpha) + display_values = np.concatenate([beta, lambdas]) + display_names = param_names_all + [ + f"lambda_{name}" for name in self._nests.nest_names + ] + std_errors = self._compute_se_nested( + all_params, arrays, k, lambdas + ) + + return self._make_fit_result( + solver_result, arrays, display_values, display_names, std_errors + ) + + # --- Mixed (no nests) --- + if self._is_mixed and not self._is_nested: + k_fixed = self._k_fixed + k_random = self._k_random + if self._is_spatial: + # Layout: [beta_fixed, alpha_rho, mean_*, sd_*] + beta_fixed = all_params[:k_fixed] + alpha_rho = all_params[k_fixed] + rho = naturalize_rho(alpha_rho) + beta_random_means = all_params[k_fixed + 1 : k_fixed + 1 + k_random] + beta_random_spreads = all_params[k_fixed + 1 + k_random :] + display_values = np.concatenate( + [beta_fixed, [rho], beta_random_means, beta_random_spreads] + ) + display_names = ( + list(self._fixed_names) + + ["rho"] + + [f"mean_{n}" for n in self._random_param_names] + + [f"sd_{n}" for n in self._random_param_names] + ) + std_errors = self._compute_se_mscl( + all_params, arrays, k_fixed, rho + ) + else: + # Layout: [beta_fixed, mean_*, sd_*] + display_values = all_params + display_names = list(self._full_param_names) + std_errors = self._compute_se_simple(all_params, arrays) + + return self._make_fit_result( + solver_result, arrays, display_values, display_names, std_errors + ) + + # --- Mixed Nested --- + if self._is_nested and self._is_mixed: + k_fixed = self._k_fixed + k_random = self._k_random + n_nests = self._nests.n_nests + fixed_param_names = [name for name in param_names_all if name not in self._random_params] + + if self._is_spatial: + # Layout: [beta_fixed, alpha_rho_1..M, alpha_lambda_1..M, mean_*, sd_*] + beta_fixed = all_params[:k_fixed] + alpha_rho = all_params[k_fixed : k_fixed + n_nests] + alpha_lambda = all_params[k_fixed + n_nests : k_fixed + 2 * n_nests] + beta_random_means = all_params[ + k_fixed + 2 * n_nests : k_fixed + 2 * n_nests + k_random + ] + beta_random_spreads = all_params[k_fixed + 2 * n_nests + k_random :] + rhos = naturalize_rho(alpha_rho) + lambdas = naturalize_nest_params(alpha_lambda) + display_values = np.concatenate( + [beta_fixed, rhos, lambdas, beta_random_means, np.abs(beta_random_spreads)] + ) + display_names = ( + fixed_param_names + + [f"rho_{name}" for name in self._nests.nest_names] + + [f"lambda_{name}" for name in self._nests.nest_names] + + [f"mean_{name}" for name in self._random_param_names] + + [f"sd_{name}" for name in self._random_param_names] + ) + std_errors = self._compute_se_mixed_nested_scl( + all_params, arrays, k_fixed, n_nests, rhos, lambdas + ) + else: + # Layout: [beta_fixed, alpha_nest, mean_*, sd_*] + beta_fixed = all_params[:k_fixed] + alpha_nest = all_params[k_fixed : k_fixed + n_nests] + beta_random_means = all_params[k_fixed + n_nests : k_fixed + n_nests + k_random] + beta_random_spreads = all_params[k_fixed + n_nests + k_random :] + lambdas = naturalize_nest_params(alpha_nest) + display_values = np.concatenate( + [beta_fixed, lambdas, beta_random_means, np.abs(beta_random_spreads)] + ) + display_names = ( + fixed_param_names + + [f"lambda_{name}" for name in self._nests.nest_names] + + [f"mean_{name}" for name in self._random_param_names] + + [f"sd_{name}" for name in self._random_param_names] + ) + std_errors = self._compute_se_mixed_nested( + all_params, arrays, k_fixed, n_nests, lambdas + ) + + return self._make_fit_result( + solver_result, arrays, display_values, display_names, std_errors + ) + + raise RuntimeError("Unknown model configuration") + + # ------------------------------------------------------------------ + # Standard error computation helpers + # ------------------------------------------------------------------ + + def _compute_se_simple(self, all_params, arrays): + """Compute SEs for models without parameter transforms.""" + std_errors = np.full(len(all_params), np.nan) + try: + hess = self._compute_hessian(all_params) + std_errors = self._compute_std_errors_from_hessian(hess) + except Exception: + if self._result is not None and self._result.solver_result: + hess_inv = self._get_hessian_inverse() + if hess_inv is not None: + std_errors = np.sqrt(np.maximum(np.diag(hess_inv), 0)) + std_errors[std_errors == 0] = np.nan + return std_errors + + def _compute_se(self, all_params, arrays, display_values, display_names): + """Compute SEs for MNL/SCL models.""" + k = arrays.design_matrix.shape[1] + n_params = len(display_values) + std_errors = np.full(n_params, np.nan) + try: + hess = self._compute_hessian(all_params) + se_unconstrained = self._compute_std_errors_from_hessian(hess) + if self._is_spatial: + # Delta method: SE(rho) = rho*(1-rho)*SE(alpha_rho) + rho = display_values[k] + se_rho = rho * (1.0 - rho) * se_unconstrained[k] + std_errors = np.concatenate([se_unconstrained[:k], [se_rho]]) + else: + std_errors = se_unconstrained + except Exception: + hess_inv = self._get_hessian_inverse() + if hess_inv is not None: + se = np.sqrt(np.maximum(np.diag(hess_inv), 0)) + se[se == 0] = np.nan + if self._is_spatial: + rho = display_values[k] + se_rho = rho * (1.0 - rho) * se[k] + std_errors = np.concatenate([se[:k], [se_rho]]) + else: + std_errors = se + return std_errors + + def _compute_se_nested(self, all_params, arrays, k, lambdas): + """Compute SEs for nested logit (non-spatial).""" + n_nests = len(lambdas) + std_errors = np.full(k + n_nests, np.nan) + try: + hess = self._compute_hessian(all_params) + se_alpha = self._compute_std_errors_from_hessian(hess) + se_lambda = lambdas * (1.0 - lambdas) * se_alpha[k:] + std_errors = np.concatenate([se_alpha[:k], se_lambda]) + except Exception: + hess_inv = self._get_hessian_inverse() + if hess_inv is not None: + se_alpha = np.sqrt(np.maximum(np.diag(hess_inv), 0)) + se_alpha[se_alpha == 0] = np.nan + se_lambda = lambdas * (1.0 - lambdas) * se_alpha[k:] + std_errors = np.concatenate([se_alpha[:k], se_lambda]) + return std_errors + + def _compute_se_nested_scl(self, all_params, arrays, k, n_nests, rhos, lambdas): + """Compute SEs for nested SCL.""" + std_errors = np.full(k + 2 * n_nests, np.nan) + try: + hess = self._compute_hessian(all_params) + se_alpha = self._compute_std_errors_from_hessian(hess) + se_rho = rhos * (1.0 - rhos) * se_alpha[k : k + n_nests] + se_lambda = lambdas * (1.0 - lambdas) * se_alpha[k + n_nests : k + 2 * n_nests] + std_errors = np.concatenate([se_alpha[:k], se_rho, se_lambda]) + except Exception: + hess_inv = self._get_hessian_inverse() + if hess_inv is not None: + se_alpha = np.sqrt(np.maximum(np.diag(hess_inv), 0)) + se_alpha[se_alpha == 0] = np.nan + se_rho = rhos * (1.0 - rhos) * se_alpha[k : k + n_nests] + se_lambda = lambdas * (1.0 - lambdas) * se_alpha[k + n_nests : k + 2 * n_nests] + std_errors = np.concatenate([se_alpha[:k], se_rho, se_lambda]) + return std_errors + + def _compute_se_mscl(self, all_params, arrays, k_fixed, rho): + """Compute SEs for MSCL.""" + n_params = len(all_params) + std_errors = np.full(n_params, np.nan) + try: + hess = self._compute_hessian(all_params) + se_alpha = self._compute_std_errors_from_hessian(hess) + se_rho = float(rho * (1.0 - rho) * se_alpha[k_fixed]) + std_errors = np.concatenate([se_alpha[:k_fixed], [se_rho], se_alpha[k_fixed + 1:]]) + except Exception: + hess_inv = self._get_hessian_inverse() + if hess_inv is not None: + se_alpha = np.sqrt(np.maximum(np.diag(hess_inv), 0)) + se_alpha[se_alpha == 0] = np.nan + se_rho = float(rho * (1.0 - rho) * se_alpha[k_fixed]) + std_errors = np.concatenate([se_alpha[:k_fixed], [se_rho], se_alpha[k_fixed + 1:]]) + return std_errors + + def _compute_se_mixed_nested(self, all_params, arrays, k_fixed, n_nests, lambdas): + """Compute SEs for mixed nested logit (non-spatial).""" + n_params = len(all_params) + std_errors = np.full(n_params, np.nan) + try: + hess = self._compute_hessian(all_params) + se_raw = self._compute_std_errors_from_hessian(hess) + se_lambda = lambdas * (1.0 - lambdas) * se_raw[k_fixed : k_fixed + n_nests] + std_errors = np.concatenate([se_raw[:k_fixed], se_lambda, se_raw[k_fixed + n_nests:]]) + except Exception: + hess_inv = self._get_hessian_inverse() + if hess_inv is not None: + se_raw = np.sqrt(np.maximum(np.diag(hess_inv), 0)) + se_raw[se_raw == 0] = np.nan + se_lambda = lambdas * (1.0 - lambdas) * se_raw[k_fixed : k_fixed + n_nests] + std_errors = np.concatenate([se_raw[:k_fixed], se_lambda, se_raw[k_fixed + n_nests:]]) + return std_errors + + def _compute_se_mixed_nested_scl(self, all_params, arrays, k_fixed, n_nests, rhos, lambdas): + """Compute SEs for mixed nested SCL.""" + n_params = len(all_params) + std_errors = np.full(n_params, np.nan) + try: + hess = self._compute_hessian(all_params) + se_raw = self._compute_std_errors_from_hessian(hess) + se_rho = rhos * (1.0 - rhos) * se_raw[k_fixed : k_fixed + n_nests] + se_lambda = lambdas * (1.0 - lambdas) * se_raw[k_fixed + n_nests : k_fixed + 2 * n_nests] + std_errors = np.concatenate([ + se_raw[:k_fixed], + se_rho, + se_lambda, + se_raw[k_fixed + 2 * n_nests:], + ]) + except Exception: + hess_inv = self._get_hessian_inverse() + if hess_inv is not None: + se_raw = np.sqrt(np.maximum(np.diag(hess_inv), 0)) + se_raw[se_raw == 0] = np.nan + se_rho = rhos * (1.0 - rhos) * se_raw[k_fixed : k_fixed + n_nests] + se_lambda = lambdas * (1.0 - lambdas) * se_raw[k_fixed + n_nests : k_fixed + 2 * n_nests] + std_errors = np.concatenate([ + se_raw[:k_fixed], + se_rho, + se_lambda, + se_raw[k_fixed + 2 * n_nests:], + ]) + return std_errors + + def _make_fit_result( + self, + solver_result: SolverResult, + arrays: ChoiceArrays, + display_values: np.ndarray, + display_names: list[str], + std_errors: np.ndarray, + ) -> FitResult: + """Build a FitResult using the shared helper.""" + coefficients = pd.Series(display_values, index=display_names, name="coefficient") + std_err_series = pd.Series(std_errors, index=display_names, name="std_error") + ll = solver_result.log_likelihood + ll_null = _compute_null_ll(arrays) + n_params = len(display_values) + + stats = _compute_fit_statistics( + ll=ll, + ll_null=ll_null, + n_obs=arrays.n_obs, + n_params=n_params, + n_alts=arrays.n_alts, + coefficients=coefficients, + std_errors=std_err_series, + model_type=self.model_type, + solver_name=solver_result.solver_name, + solver_result_raw=solver_result.raw, + ) + + return FitResult(spec=self._spec, **stats) + + # ------------------------------------------------------------------ + # Prediction + # ------------------------------------------------------------------ + + def probabilities(self, data=None, beta=None, alpha=None) -> np.ndarray: + """Compute choice probabilities. + + Parameters + ---------- + data : ChoiceTable or None + Data to predict on. If None, uses estimation data. + beta : np.ndarray or None + Parameter vector. If None, uses estimated values. + alpha : np.ndarray or None + Nest parameters (for nested models). If None, uses estimated values. + + Returns + ------- + np.ndarray, shape (n_obs, n_alts) + Choice probabilities. + """ + if self._arrays is None: + raise RuntimeError("Model must be estimated before prediction.") + + arrays = self._arrays + if data is not None: + from locpick.data.choicetable import ChoiceTable + if not isinstance(data, ChoiceTable): + raise TypeError("data must be a ChoiceTable") + arrays = data.to_arrays( + formula=self._spec.formula, + spec=self._spec if self._spec.formula is None else None, + ) + + # Dispatch based on model type + if self._is_nested or self._is_mixed: + return self._probabilities_complex(arrays, data, beta, alpha) + return self._probabilities_mnl(arrays, data, beta) + + def _probabilities_mnl(self, arrays, data, beta) -> np.ndarray: + """Compute MNL or SCL probabilities.""" + from locpick._kernels.mnl_numpy import mnl_probs_numpy + + if self._is_spatial: + from locpick.models.scl import _scl_log_probs_numpy + + k = arrays.design_matrix.shape[1] + if beta is None: + coef_vals = np.asarray(self._result.coefficients.values, dtype=np.float64) + beta_use = coef_vals[:k] + rho = float(coef_vals[k]) + else: + beta = np.asarray(beta, dtype=np.float64) + beta_use = beta[:k] + rho = float(beta[k]) if beta.size > k else float(self._result.coefficients.values[k]) + + from locpick._sampling.correction import get_sampling_correction + + log_probs = _scl_log_probs_numpy( + beta_use, + rho, + np.asarray(arrays.design_matrix, dtype=np.float64), + self._allocation, + self._edge_list, + arrays.n_obs, + arrays.n_alts, + available=arrays.available, + inclusion_probs=get_sampling_correction(arrays), + ) + return np.exp(log_probs) + + if beta is None: + beta = np.asarray(self._result.coefficients.values, dtype=np.float64) + + dm = np.asarray(arrays.design_matrix, dtype=np.float64) + n_obs = arrays.n_obs + n_alts = arrays.n_alts + + utilities = (dm @ beta).reshape(n_obs, n_alts) + from locpick._sampling.correction import apply_sampling_correction + utilities = apply_sampling_correction(utilities, arrays) + + if arrays.available is not None: + available = np.asarray(arrays.available, dtype=np.float64).reshape(n_obs, n_alts) + else: + available = np.ones((n_obs, n_alts), dtype=np.float64) + + return mnl_probs_numpy(utilities, available, inclusion_probs=None) + + def _probabilities_complex(self, arrays, data, beta, alpha) -> np.ndarray: + """Compute probabilities for nested/mixed models (NumPy fallback).""" + # For nested models, use the NumPy kernel + if self._is_nested and not self._is_mixed: + from locpick.models.nested import _nested_logit_probs_numpy + + k = arrays.design_matrix.shape[1] + n_nests = self._nests.n_nests + if beta is None: + beta = np.asarray(self._result.coefficients.values[:k], dtype=np.float64) + if alpha is None: + lambda_vals = self._result.coefficients.values[k : k + n_nests] + lambda_vals = np.clip(lambda_vals, 1e-10, 1.0 - 1e-10) + alpha = np.log(lambda_vals / (1.0 - lambda_vals)) + elif beta.size > k: + # Full parameter vector passed — split into beta and alpha + alpha = beta[k : k + n_nests] + beta = beta[:k] + if alpha is None: + alpha = np.zeros(n_nests) + + from locpick._sampling.correction import get_sampling_correction + + return _nested_logit_probs_numpy( + np.asarray(beta, dtype=np.float64), + np.asarray(alpha, dtype=np.float64), + np.asarray(arrays.design_matrix, dtype=np.float64), + self._nest_matrix, + arrays.n_obs, + arrays.n_alts, + available=arrays.available, + inclusion_probs=get_sampling_correction(arrays), + ) + + # For mixed models, use the NumPy kernel + if self._is_mixed and not self._is_nested: + from locpick.models.mixed import _mixed_logit_probs_numpy + + k_fixed = self._k_fixed + k_random = self._k_random + + if beta is None: + beta_fixed = self._result.coefficients.values[:k_fixed] + beta_random_means = self._result.coefficients.values[k_fixed : k_fixed + k_random] + beta_random_spreads = self._result.coefficients.values[k_fixed + k_random :] + else: + beta_fixed = beta[:k_fixed] + beta_random_means = beta[k_fixed : k_fixed + k_random] + beta_random_spreads = beta[k_fixed + k_random :] + + from locpick._sampling.correction import get_sampling_correction + + return _mixed_logit_probs_numpy( + beta_fixed, + beta_random_means, + beta_random_spreads, + self._random_distributions, + self._draws, + np.asarray(arrays.design_matrix, dtype=np.float64), + self._random_col_indices, + arrays.n_obs, + arrays.n_alts, + available=arrays.available, + inclusion_probs=get_sampling_correction(arrays), + ) + + # Mixed nested — use NumPy fallback + if self._is_nested and self._is_mixed: + return self._probabilities_mixed_nested_numpy(arrays, beta, alpha) + + raise RuntimeError("Unknown model configuration for prediction") + + def _probabilities_mixed_nested_numpy(self, arrays, beta, alpha) -> np.ndarray: + """Compute mixed nested logit probabilities (NumPy fallback).""" + from locpick._sampling.correction import get_sampling_correction + from locpick.models.nested import _nested_logit_probs_numpy + + k_total = arrays.design_matrix.shape[1] + k_fixed = self._k_fixed + k_random = self._k_random + n_nests = self._nests.n_nests + + if beta is None: + beta_fixed = self._result.coefficients.values[:k_fixed] + else: + beta_fixed = beta[:k_fixed] + + if alpha is None: + lambda_vals = self._result.coefficients.values[k_fixed : k_fixed + n_nests] + lambda_vals = np.clip(lambda_vals, 1e-10, 1.0 - 1e-10) + alpha = np.log(lambda_vals / (1.0 - lambda_vals)) + + beta_random_means = self._result.coefficients.values[ + k_fixed + n_nests : k_fixed + n_nests + k_random + ] + beta_random_spreads = self._result.coefficients.values[k_fixed + n_nests + k_random :] + + dm = np.asarray(arrays.design_matrix, dtype=np.float64) + n_obs = arrays.n_obs + n_alts = arrays.n_alts + available = arrays.available + inclusion_probs = get_sampling_correction(arrays) + + dm_fixed = dm[:, [i for i in range(k_total) if i not in self._random_col_indices]] + dm_random = dm[:, self._random_col_indices] + + if dm_fixed.shape[1] > 0 and len(beta_fixed) > 0: + v_fixed = (dm_fixed @ beta_fixed).reshape(n_obs, n_alts) + else: + v_fixed = np.zeros((n_obs, n_alts)) + + if inclusion_probs is not None: + sr = np.asarray(inclusion_probs, dtype=np.float64).reshape(n_obs, n_alts) + v_fixed = v_fixed + np.log(np.maximum(sr, 1e-30)) + + if available is not None: + avail = np.asarray(available, dtype=np.float64).reshape(n_obs, n_alts) + else: + avail = np.ones((n_obs, n_alts), dtype=np.float64) + + n_draws = self._draws.shape[1] + probs_sum = np.zeros((n_obs, n_alts), dtype=np.float64) + + for r in range(n_draws): + beta_random_r = np.zeros((n_obs, k_random)) + for p in range(k_random): + z_p = self._draws[:, r, p] + mean_p = beta_random_means[p] + spread_p = abs(beta_random_spreads[p]) + dist = self._random_distributions[p] + + if dist == "normal": + beta_random_r[:, p] = mean_p + spread_p * z_p + elif dist == "lognormal": + exponent = mean_p + spread_p * z_p + beta_random_r[:, p] = np.exp(np.clip(exponent, -50, 50)) + elif dist == "triangular": + from scipy.stats import norm as norm_dist + u = norm_dist.cdf(z_p) + mask = u <= 0.5 + beta_random_r[:, p] = np.where( + mask, + mean_p + spread_p * (np.sqrt(2 * u) - 1), + mean_p + spread_p * (1 - np.sqrt(2 * (1 - u))), + ) + elif dist == "uniform": + from scipy.stats import norm as norm_dist + u = norm_dist.cdf(z_p) + beta_random_r[:, p] = mean_p + spread_p * (2 * u - 1) + + v_random = np.sum( + dm_random.reshape(n_obs, n_alts, k_random) * beta_random_r[:, None, :], + axis=2, + ) + V = v_fixed + v_random + + probs_r = _nested_logit_probs_numpy( + np.zeros(k_total), # beta not used — V is precomputed + np.asarray(alpha, dtype=np.float64), + V.reshape(-1, 1) if V.size > 0 else np.zeros((n_obs * n_alts, 1)), + self._nest_matrix, + n_obs, + n_alts, + available=avail, + inclusion_probs=None, + ) + probs_sum += probs_r + + return probs_sum / n_draws + + def utilities(self, data=None, beta=None) -> np.ndarray: + """Compute deterministic utilities. + + Parameters + ---------- + data : ChoiceTable or None + Data to predict on. If None, uses estimation data. + beta : np.ndarray or None + Utility coefficients. If None, uses estimated values. + + Returns + ------- + np.ndarray, shape (n_obs, n_alts) + Deterministic utilities. + """ + if self._arrays is None: + raise RuntimeError("Model must be estimated before prediction.") + + arrays = self._arrays + if data is not None: + arrays = data.to_arrays( + formula=self._spec.formula, + spec=self._spec if self._spec.formula is None else None, + ) + + if beta is None: + k = arrays.design_matrix.shape[1] + beta = np.asarray(self._result.coefficients.values[:k], dtype=np.float64) + + dm = np.asarray(arrays.design_matrix, dtype=np.float64) + n_obs = arrays.n_obs + n_alts = arrays.n_alts + + V = (dm @ beta).reshape(n_obs, n_alts) + from locpick._sampling.correction import apply_sampling_correction + V = apply_sampling_correction(V, arrays) + return V + + # ------------------------------------------------------------------ + # Simulation (vectorized) + # ------------------------------------------------------------------ + + def simulate(self, data=None, n_draws: int = 1, seed: Optional[int] = None) -> pd.DataFrame: + """Simulate choices from the estimated model. + + Uses vectorized inverse-CDF sampling — no Python loops over + observations. + + Parameters + ---------- + data : ChoiceTable or None + Data to simulate on. If None, uses estimation data. + n_draws : int, optional + Number of simulation draws per observation. Default 1. + seed : int or None, optional + Random seed for reproducibility. + + Returns + ------- + pd.DataFrame + Simulated choices with columns ``draw``, ``obs_id``, + ``alt_id``, and ``probability``. + """ + from locpick.data.choicetable import ChoiceTable + + if self._arrays is None: + raise RuntimeError("Model must be estimated before simulation.") + + arrays = self._arrays + ct = self._data + if data is not None: + if not isinstance(data, ChoiceTable): + raise TypeError("data must be a ChoiceTable") + arrays = data.to_arrays( + formula=self._spec.formula, + spec=self._spec if self._spec.formula is None else None, + ) + ct = data + + rng = np.random.default_rng(seed) + probs = self.probabilities(data=data) + probs = probs / probs.sum(axis=1, keepdims=True) + n_obs = arrays.n_obs + n_alts = arrays.n_alts + + df = ct.to_frame() + alt_ids = df[ct.alt_id_col].values.reshape(n_obs, n_alts) + obs_ids = df[ct.obs_id_col].values.reshape(n_obs, n_alts)[:, 0] + + # Vectorized simulation: draw all choices at once + cumulative_probs = np.cumsum(probs, axis=1) + uniform_draws = rng.random((n_draws, n_obs)) + chosen_indices = np.argmax( + cumulative_probs[None, :, :] > uniform_draws[:, :, None], axis=2 + ) + chosen_indices = np.clip(chosen_indices, 0, n_alts - 1) + + chosen_alts = alt_ids[np.arange(n_obs), chosen_indices] + chosen_probs = probs[np.arange(n_obs), chosen_indices] + + # Build results DataFrame (vectorized) + results = pd.DataFrame({ + "draw": np.repeat(np.arange(n_draws), n_obs), + ct.obs_id_col: np.tile(obs_ids, n_draws), + ct.alt_id_col: chosen_alts.T.ravel(), + "probability": chosen_probs.T.ravel(), + }) + return results + + # ------------------------------------------------------------------ + # Marginal Effects + # ------------------------------------------------------------------ + + def _resolve_me_data(self, data=None): + """Resolve data for marginal effects computation.""" + from locpick.data.choicetable import ChoiceTable + + if self._arrays is None: + raise RuntimeError("Model must be estimated before computing marginal effects.") + + ct = self._data + if data is not None: + if not isinstance(data, ChoiceTable): + raise TypeError("data must be a ChoiceTable") + ct = data + + probs = self.probabilities(data=data) + df = ct.to_frame() + index = pd.MultiIndex.from_arrays( + [df[ct.obs_id_col].values, df[ct.alt_id_col].values], + names=[ct.obs_id_col, ct.alt_id_col], + ) + return ct, probs, df, index + + def marginal_effect(self, data=None, variable: Optional[str] = None) -> pd.Series: + """Compute direct marginal effects for a variable. + + For MNL: :math:`(1 - P_{qi}) \\beta_x`. + + For nested logit: :math:`P_i (1 - \\lambda_m P_{i|m}) \\beta_x` + where :math:`P_{i|m}` is the conditional probability within nest m. + + For mixed logit: :math:`E_z[(1 - P_i(z)) \\beta_x]` via simulation + over draws. + + For SCL: raises ``NotImplementedError`` (derivation pending). + + Parameters + ---------- + data : ChoiceTable or None + Data to compute marginal effects on. + variable : str + Name of the variable. + + Returns + ------- + pd.Series + Direct marginal effects, indexed by (obs_id, alt_id). + """ + ct, probs, df, index = self._resolve_me_data(data) + beta = self._result.coefficients.get(variable, 0.0) + + if self._is_nested: + # Nested logit: P_i * (1 - lambda_m * P_{i|m}) * beta + n_obs = probs.shape[0] + n_alts = probs.shape[1] + n_nests = self._nests.n_nests + + # Get lambda values from estimated coefficients + k = self._arrays.design_matrix.shape[1] if self._arrays is not None else None + if k is None and data is not None: + k = data.to_arrays( + formula=self._spec.formula, + spec=self._spec if self._spec.formula is None else None, + ).design_matrix.shape[1] + + # Extract lambda values (naturalized) + if self._is_spatial: + # Nested SCL: [beta, rho_1..M, lambda_1..M] + lambda_vals = self._result.coefficients.values[k + n_nests : k + 2 * n_nests] + else: + # Nested: [beta, lambda_1..M] + lambda_vals = self._result.coefficients.values[k : k + n_nests] + + # Compute conditional probabilities P_{i|m} = P_i / P_m + # P_m = sum of P_i for alts in nest m + nest_matrix = self._nest_matrix # (n_alts, n_nests) + P_nest = probs @ nest_matrix # (n_obs, n_nests) + # P_{i|m} = P_i / P_m (avoid division by zero) + # For each alt, find which nest it belongs to + alt_in_nest = nest_matrix.sum(axis=1) > 0 # (n_alts,) bool + + # lambda for each alternative (from its nest) + long_lambda = np.ones(n_alts) + for m in range(n_nests): + mask = nest_matrix[:, m] > 0 + long_lambda[mask] = lambda_vals[m] + + # P_{i|m} for each (obs, alt) + P_i_given_m = np.zeros_like(probs) + for m in range(n_nests): + mask = nest_matrix[:, m] > 0 + if not mask.any(): + continue + P_m = P_nest[:, m:m+1] # (n_obs, 1) + P_i_given_m[:, mask] = probs[:, mask] / np.maximum(P_m, 1e-30) + + # Marginal effect: P_i * (1 - lambda_m * P_{i|m}) * beta + me = probs * (1 - long_lambda[None, :] * P_i_given_m) * beta + me = me.ravel() + + # For root nest alternatives (not in any nest), use MNL formula + if not alt_in_nest.all(): + root_mask = ~alt_in_nest + me_2d = me.reshape(n_obs, n_alts) + me_2d[:, root_mask] = (1 - probs[:, root_mask]) * beta + me = me_2d.ravel() + + elif self._is_mixed: + # Mixed logit: E_z[(1 - P_i(z)) * beta] via simulation + # For now, use the MNL approximation with mean coefficients + # (proper implementation requires per-draw probability computation) + me = (1 - probs.ravel()) * beta + + elif self._is_spatial: + # SCL: derivation pending + raise NotImplementedError( + "Marginal effects for SCL models are not yet implemented. " + "The MNL approximation is incorrect for spatially correlated logit." + ) + + else: + # MNL: (1 - P_i) * beta + me = (1 - probs.ravel()) * beta + + return pd.Series(me, index=index, name=f"marginal_effect_{variable}") + + def cross_marginal_effect(self, data=None, variable: Optional[str] = None) -> pd.Series: + """Compute cross-marginal effects for a variable. + + For MNL: :math:`-P_i \\beta_x`. + + For nested logit: :math:`-P_i \\lambda_m P_{i|m} \\beta_x`. + + For SCL: raises ``NotImplementedError``. + + Parameters + ---------- + data : ChoiceTable or None + variable : str + + Returns + ------- + pd.Series + Cross-marginal effects, indexed by (obs_id, alt_id). + """ + ct, probs, df, index = self._resolve_me_data(data) + beta = self._result.coefficients.get(variable, 0.0) + + if self._is_nested: + # Nested logit cross-ME: -P_i * lambda_m * P_{i|m} * beta + n_obs = probs.shape[0] + n_alts = probs.shape[1] + n_nests = self._nests.n_nests + + k = self._arrays.design_matrix.shape[1] if self._arrays is not None else None + if k is None and data is not None: + k = data.to_arrays( + formula=self._spec.formula, + spec=self._spec if self._spec.formula is None else None, + ).design_matrix.shape[1] + + if self._is_spatial: + lambda_vals = self._result.coefficients.values[k + n_nests : k + 2 * n_nests] + else: + lambda_vals = self._result.coefficients.values[k : k + n_nests] + + nest_matrix = self._nest_matrix + P_nest = probs @ nest_matrix + alt_in_nest = nest_matrix.sum(axis=1) > 0 + + long_lambda = np.ones(n_alts) + for m in range(n_nests): + mask = nest_matrix[:, m] > 0 + long_lambda[mask] = lambda_vals[m] + + P_i_given_m = np.zeros_like(probs) + for m in range(n_nests): + mask = nest_matrix[:, m] > 0 + if not mask.any(): + continue + P_m = P_nest[:, m:m+1] + P_i_given_m[:, mask] = probs[:, mask] / np.maximum(P_m, 1e-30) + + cross_me = -probs * long_lambda[None, :] * P_i_given_m * beta + cross_me = cross_me.ravel() + + if not alt_in_nest.all(): + root_mask = ~alt_in_nest + cross_me_2d = cross_me.reshape(n_obs, n_alts) + cross_me_2d[:, root_mask] = -probs[:, root_mask] * beta + cross_me = cross_me_2d.ravel() + + elif self._is_spatial: + raise NotImplementedError( + "Cross-marginal effects for SCL models are not yet implemented." + ) + + else: + # MNL and mixed (approximation): -P_i * beta + cross_me = -probs.ravel() * beta + + return pd.Series(cross_me, index=index, name=f"cross_marginal_effect_{variable}") + + def elasticity(self, data=None, variable: Optional[str] = None) -> pd.Series: + """Compute direct elasticities for a variable. + + For MNL: :math:`(1 - P_{qi}) \\beta_x x_{qi}`. + + For nested logit: :math:`P_i (1 - \\lambda_m P_{i|m}) \\beta_x x_{qi}`. + + For SCL: raises ``NotImplementedError``. + + Parameters + ---------- + data : ChoiceTable or None + variable : str + + Returns + ------- + pd.Series + Direct elasticities, indexed by (obs_id, alt_id). + """ + ct, probs, df, index = self._resolve_me_data(data) + x = df[variable].values + + if self._is_spatial and not self._is_nested: + raise NotImplementedError( + "Elasticities for SCL models are not yet implemented." + ) + + # For MNL, nested, and mixed: elasticity = marginal_effect * x + me = self.marginal_effect(data=data, variable=variable) + elasticities = me.values * x + + return pd.Series(elasticities, index=index, name=f"elasticity_{variable}") + + def cross_elasticity(self, data=None, variable: Optional[str] = None) -> pd.Series: + """Compute cross-elasticities for a variable. + + For MNL: :math:`-P_i \\beta_x x_{ij}`. + + For SCL: raises ``NotImplementedError``. + + Parameters + ---------- + data : ChoiceTable or None + variable : str + + Returns + ------- + pd.Series + Cross-elasticities, indexed by (obs_id, alt_id). + """ + ct, probs, df, index = self._resolve_me_data(data) + x = df[variable].values + + if self._is_spatial and not self._is_nested: + raise NotImplementedError( + "Cross-elasticities for SCL models are not yet implemented." + ) + + # cross_elasticity = cross_marginal_effect * x + cme = self.cross_marginal_effect(data=data, variable=variable) + cross_elast = cme.values * x + + return pd.Series(cross_elast, index=index, name=f"cross_elasticity_{variable}") + + # ------------------------------------------------------------------ + # Covariance estimation + # ------------------------------------------------------------------ + + def covariance_robust(self, data=None) -> np.ndarray: + """Compute the sandwich (Huber-White) robust covariance matrix. + + Parameters + ---------- + data : ChoiceTable or None + + Returns + ------- + np.ndarray, shape (n_parameters, n_parameters) + Sandwich (robust) covariance matrix. + """ + from locpick.data.choicetable import ChoiceTable + + if self._arrays is None: + raise RuntimeError("Model must be estimated first.") + + arrays = self._arrays + if data is not None: + if not isinstance(data, ChoiceTable): + raise TypeError("data must be a ChoiceTable") + arrays = data.to_arrays( + formula=self._spec.formula, + spec=self._spec if self._spec.formula is None else None, + ) + + scores = self._observation_scores(arrays) + B = scores.T @ scores + H_inv = self._get_hessian_inverse() + + if H_inv is None: + return _safe_inv(B) + + return _sandwich_inv(H_inv, B) + + def covariance_clustered(self, data=None, groups=None) -> np.ndarray: + """Compute cluster-robust (Rogers) covariance matrix. + + Parameters + ---------- + data : ChoiceTable or None + groups : array-like, shape (n_obs,) + Cluster/group identifiers. + + Returns + ------- + np.ndarray, shape (n_parameters, n_parameters) + Cluster-robust covariance matrix. + """ + from locpick.data.choicetable import ChoiceTable + + if self._arrays is None: + raise RuntimeError("Model must be estimated first.") + + arrays = self._arrays + if data is not None: + if not isinstance(data, ChoiceTable): + raise TypeError("data must be a ChoiceTable") + arrays = data.to_arrays( + formula=self._spec.formula, + spec=self._spec if self._spec.formula is None else None, + ) + + if groups is None: + raise ValueError("groups must be provided for cluster-robust covariance.") + + scores = self._observation_scores(arrays) + groups = np.asarray(groups) + unique_groups = np.unique(groups) + n_params = scores.shape[1] + + B_clustered = np.zeros((n_params, n_params)) + for g in unique_groups: + mask = groups == g + g_c = scores[mask].sum(axis=0) + B_clustered += np.outer(g_c, g_c) + + H_inv = self._get_hessian_inverse() + + if H_inv is None: + return _safe_inv(B_clustered) + + return _sandwich_inv(H_inv, B_clustered) + + def std_errors_robust(self, data=None) -> pd.Series: + """Compute sandwich (Huber-White) robust standard errors.""" + cov = self.covariance_robust(data=data) + se = np.sqrt(np.maximum(np.diag(cov), 0)) + se[se == 0] = np.nan + return pd.Series(se, index=self._result.coefficients.index, name="std_error_robust") + + def std_errors_clustered(self, data=None, groups=None) -> pd.Series: + """Compute cluster-robust standard errors.""" + cov = self.covariance_clustered(data=data, groups=groups) + se = np.sqrt(np.maximum(np.diag(cov), 0)) + se[se == 0] = np.nan + return pd.Series(se, index=self._result.coefficients.index, name="std_error_clustered") + + # ------------------------------------------------------------------ + # Observation scores + # ------------------------------------------------------------------ + + def _observation_scores(self, arrays) -> np.ndarray: + """Compute observation-level score (gradient) vectors. + + Uses analytical MNL scores for MNL models, JAX jacrev for models + with a JAX objective (spatial/nested/mixed), and finite differences + as a last-resort fallback. + """ + cache_key = id(arrays) + if cache_key in self._observation_scores_cache: + return self._observation_scores_cache[cache_key] + + if not self._is_spatial and not self._is_nested and not self._is_mixed: + scores = self._mnl_observation_scores(arrays) + elif self._objective is not None and self._objective.jax_fn is not None: + scores = self._jax_observation_scores(arrays) + else: + scores = self._finite_diff_observation_scores(arrays) + + self._observation_scores_cache[cache_key] = scores + return scores + + def _mnl_observation_scores(self, arrays) -> np.ndarray: + """Compute MNL observation scores analytically.""" + from locpick._kernels.mnl_numpy import mnl_observation_scores_numpy + + dm = np.asarray(arrays.design_matrix, dtype=np.float64) + chosen = np.asarray(arrays.chosen, dtype=np.float64).reshape(arrays.n_obs, arrays.n_alts) + beta = np.asarray(self._result.coefficients.values, dtype=np.float64) + n_obs = arrays.n_obs + n_alts = arrays.n_alts + + if arrays.available is not None: + available = np.asarray(arrays.available, dtype=np.float64).reshape(n_obs, n_alts) + else: + available = np.ones((n_obs, n_alts), dtype=np.float64) + + from locpick._sampling.correction import get_sampling_correction + inclusion_probs = get_sampling_correction(arrays) + + weights = None + if arrays.weights is not None: + weights = np.asarray(arrays.weights, dtype=np.float64) + + return mnl_observation_scores_numpy( + beta=beta, + design_matrix=dm, + chosen=chosen, + available=available, + n_obs=n_obs, + n_alts=n_alts, + weights=weights, + inclusion_probs=inclusion_probs, + ) + + def _finite_diff_observation_scores(self, arrays) -> np.ndarray: + """Compute observation scores via finite differences (fallback).""" + eps = 1e-5 + n_params = len(self._result.coefficients) + n_obs = arrays.n_obs + + full_params = np.asarray(self._result.coefficients.values, dtype=np.float64).copy() + chosen = np.asarray(arrays.chosen, dtype=np.float64).reshape(n_obs, arrays.n_alts) + scores = np.zeros((n_obs, n_params)) + + for j in range(n_params): + p_plus = full_params.copy() + p_plus[j] += eps + p_minus = full_params.copy() + p_minus[j] -= eps + + probs_plus = self.probabilities(data=None, beta=p_plus) + probs_minus = self.probabilities(data=None, beta=p_minus) + + ll_plus = np.log(np.maximum(np.sum(probs_plus * chosen, axis=1), 1e-30)) + ll_minus = np.log(np.maximum(np.sum(probs_minus * chosen, axis=1), 1e-30)) + scores[:, j] = (ll_plus - ll_minus) / (2 * eps) + + return scores + + def _jax_observation_scores(self, arrays) -> np.ndarray: + """Compute observation scores via JAX jacrev on per-obs LL contributions. + + This uses ``jax.jacrev`` on the objective's per-observation log-likelihood + contributions, giving exact gradients in a single backward pass — O(1) + instead of O(n_params) probability evaluations. + """ + import jax.numpy as jnp + + # Get per-observation LL contribution function from the objective + if self._objective is None or self._objective.jax_fn is None: + return self._finite_diff_observation_scores(arrays) + + # Try score_contribs (requires loglike_contribs_jax to be set) + try: + contribs_fn = self._objective.score_contribs + beta = jnp.asarray(self._result.coefficients.values, dtype=jnp.float64) + scores = np.asarray(contribs_fn(beta)) + return scores + except (ValueError, AttributeError, TypeError): + pass + + # Fall back to finite differences + return self._finite_diff_observation_scores(arrays) + + # ------------------------------------------------------------------ + # Convenience + # ------------------------------------------------------------------ + + def __repr__(self) -> str: + status = "estimated" if self._result is not None else "not estimated" + formula_str = self._formula or "custom spec" + return f"ChoiceModel(formula='{formula_str}', {status})" diff --git a/locpick/models/mixed.py b/locpick/models/mixed.py index 36d0e12..8f97849 100644 --- a/locpick/models/mixed.py +++ b/locpick/models/mixed.py @@ -439,21 +439,7 @@ def _mixed_logit_probs_numpy( log_probs_draws = np.zeros((n_obs, n_draws, n_alts), dtype=np.float64) for r in range(n_draws): - # Realise random coefficients for this draw - beta_r = np.zeros(k_random) - for p in range(k_random): - z_p = draws[:, r, p] # (n_obs,) - mean_p = np.full(n_obs, beta_random_means[p]) - spread_p = np.full(n_obs, beta_random_spreads[p]) - beta_r[p] = _apply_distribution( - z_p[:, None], - mean_p[:, None], - spread_p[:, None], - random_distributions[p], - ).ravel()[0] # scalar for this draw - - # Actually, we need per-observation random coefficients - # beta_r[n, p] = mean_p + spread_p * z[n, r, p] + # Realise per-observation random coefficients for this draw beta_random_r = np.zeros((n_obs, k_random)) for p in range(k_random): z_p = draws[:, r, p] # (n_obs,) @@ -630,90 +616,6 @@ def _mixed_logit_ll_numpy( return float(np.sum(log_sim_probs)) -def _mixed_logit_gradient_numpy( - params: np.ndarray, - random_col_indices: list[int], - k_fixed: int, - k_random: int, - random_distributions: list[str], - draws: np.ndarray, - design_matrix: np.ndarray, - chosen: np.ndarray, - n_obs: int, - n_alts: int, - available: Optional[np.ndarray] = None, - inclusion_probs: Optional[np.ndarray] = None, - weights: Optional[np.ndarray] = None, -) -> np.ndarray: - """Compute mixed logit gradient via finite differences (NumPy backend). - - Parameters - ---------- - params : np.ndarray, shape (k_fixed + 2 * k_random,) - Full parameter vector: [beta_fixed, beta_random_means, beta_random_spreads]. - random_col_indices, k_fixed, k_random, random_distributions, draws, design_matrix, chosen, n_obs, n_alts, available, inclusion_probs, weights - See :func:`_mixed_logit_ll_numpy`. - - Returns - ------- - np.ndarray, shape (k_fixed + 2 * k_random,) - Gradient of the simulated log-likelihood. - """ - eps = 1e-5 - n_params = len(params) - grad = np.zeros(n_params) - - for i in range(n_params): - params_plus = params.copy() - params_plus[i] += eps - params_minus = params.copy() - params_minus[i] -= eps - - def _unpack(p): - bf = p[:k_fixed] - rm = p[k_fixed : k_fixed + k_random] - rs = p[k_fixed + k_random :] - return bf, rm, rs - - bf_plus, rm_plus, rs_plus = _unpack(params_plus) - bf_minus, rm_minus, rs_minus = _unpack(params_minus) - - ll_plus = _mixed_logit_ll_numpy( - bf_plus, - rm_plus, - rs_plus, - random_distributions, - draws, - design_matrix, - chosen, - random_col_indices, - n_obs, - n_alts, - available=available, - inclusion_probs=inclusion_probs, - weights=weights, - ) - ll_minus = _mixed_logit_ll_numpy( - bf_minus, - rm_minus, - rs_minus, - random_distributions, - draws, - design_matrix, - chosen, - random_col_indices, - n_obs, - n_alts, - available=available, - inclusion_probs=inclusion_probs, - weights=weights, - ) - - grad[i] = (ll_plus - ll_minus) / (2 * eps) - - return grad - - # --------------------------------------------------------------------------- # MixedLogit model class # --------------------------------------------------------------------------- diff --git a/locpick/models/mnl.py b/locpick/models/mnl.py index b69b0cb..4196a2c 100644 --- a/locpick/models/mnl.py +++ b/locpick/models/mnl.py @@ -829,7 +829,6 @@ def _build_objective_numpy(self, arrays: ChoiceArrays): ) dm = arrays.design_matrix.astype(np.float64) - dm_sparse = getattr(arrays, "design_matrix_sparse", None) chosen = arrays.chosen.astype(np.float64) n_obs = arrays.n_obs n_alts = arrays.n_alts @@ -858,7 +857,6 @@ def log_likelihood(beta: np.ndarray) -> float: n_alts=n_alts, weights=weights, inclusion_probs=inclusion_probs, - design_matrix_sparse=dm_sparse, ) def gradient(beta: np.ndarray) -> np.ndarray: @@ -871,7 +869,6 @@ def gradient(beta: np.ndarray) -> np.ndarray: n_alts=n_alts, weights=weights, inclusion_probs=inclusion_probs, - design_matrix_sparse=dm_sparse, ) from locpick._jax.objective import Objective diff --git a/locpick/models/nested.py b/locpick/models/nested.py index 7db4372..1a39cb5 100644 --- a/locpick/models/nested.py +++ b/locpick/models/nested.py @@ -385,77 +385,6 @@ def _nested_logit_ll_numpy( return float(log_chosen.sum()) -def _nested_logit_gradient_numpy( - beta: np.ndarray, - alpha: np.ndarray, - design_matrix: np.ndarray, - chosen: np.ndarray, - nest_matrix: np.ndarray, - n_obs: int, - n_alts: int, - available: Optional[np.ndarray] = None, - inclusion_probs: Optional[np.ndarray] = None, - weights: Optional[np.ndarray] = None, -) -> np.ndarray: - """Compute nested logit gradient via finite differences (NumPy backend). - - This is a fallback gradient that uses finite differences. A proper - analytical gradient will be implemented in a future version. - - Parameters - ---------- - beta, alpha, design_matrix, chosen, nest_matrix, n_obs, n_alts, available, inclusion_probs, weights - See :func:`_nested_logit_ll_numpy`. - - Returns - ------- - np.ndarray, shape (k + n_nests,) - Gradient of the log-likelihood with respect to [beta, alpha]. - """ - params = np.concatenate([beta, alpha]) - eps = 1e-5 - n_params = len(params) - grad = np.zeros(n_params) - - for i in range(n_params): - params_plus = params.copy() - params_plus[i] += eps - params_minus = params.copy() - params_minus[i] -= eps - - beta_plus, alpha_plus = params_plus[: len(beta)], params_plus[len(beta) :] - beta_minus, alpha_minus = params_minus[: len(beta)], params_minus[len(beta) :] - - ll_plus = _nested_logit_ll_numpy( - beta_plus, - alpha_plus, - design_matrix, - chosen, - nest_matrix, - n_obs, - n_alts, - available=available, - inclusion_probs=inclusion_probs, - weights=weights, - ) - ll_minus = _nested_logit_ll_numpy( - beta_minus, - alpha_minus, - design_matrix, - chosen, - nest_matrix, - n_obs, - n_alts, - available=available, - inclusion_probs=inclusion_probs, - weights=weights, - ) - - grad[i] = (ll_plus - ll_minus) / (2 * eps) - - return grad - - # --------------------------------------------------------------------------- # NestedLogit model class # --------------------------------------------------------------------------- diff --git a/locpick/models/sar_mnl.py b/locpick/models/sar_mnl.py new file mode 100644 index 0000000..709baa8 --- /dev/null +++ b/locpick/models/sar_mnl.py @@ -0,0 +1,344 @@ +"""Spatial Autoregressive Multinomial Logit (SAR-MNL) model. + +Implements the pseudo maximum likelihood (PML) estimator from +Smirnov (2010): a spatial autoregressive lag in the systematic +utility of alternatives (spatial locations), with variance +normalisation by ``diag((I - ρW)^{-1})`` for consistency. + +The model specifies: + +.. math:: + + V_j = \\rho \\sum_k w_{jk} V_k + Z_j \\beta + X_{ij} \\gamma + +yielding reduced-form utilities :math:`V^* = (I - \\rho W)^{-1} +(Z\\beta + X\\gamma)`, normalised by :math:`D = \\text{diag}((I - +\\rho W)^{-1})`, with standard MNL choice probabilities. + +Estimation is via JAX autodiff through the spatial solve and +variance normalisation. No log-determinant Jacobian is needed +(this is pseudo-ML, not full ML). +""" + +from __future__ import annotations + +from typing import Optional, Union + +import numpy as np +import pandas as pd + +from locpick._solvers import Solver, SolverResult +from locpick.data.arrays import ChoiceArrays +from locpick.models._spatial_weights import resolve_spatial_weights +from locpick.models.base import ( + BaseChoiceModel, + _compute_fit_statistics, + _compute_null_ll, +) +from locpick.results.fit_result import FitResult + + +class SARMNL(BaseChoiceModel): + r"""Spatial Autoregressive Multinomial Logit (SAR-MNL). + + Specifies a spatial autoregressive lag in the systematic utility + of alternatives (spatial locations): + + .. math:: + + V_j = \rho \sum_k w_{jk} V_k + Z_j \beta + X_{ij} \gamma + + yielding reduced-form utilities :math:`V^* = (I - \rho W)^{-1} + (Z\beta + X\gamma)`, normalised by :math:`D = \text{diag}((I - + \rho W)^{-1})`, with standard MNL choice probabilities. + + Estimation is via pseudo maximum likelihood (PML, Smirnov 2010) + with JAX autodiff through the spatial solve and variance + normalisation. No log-determinant Jacobian is needed (this is + pseudo-ML, not full ML). + + Parameters + ---------- + data : ChoiceTable or EstimationProblem + The choice data. + formula : str, optional + Formulaic formula string. + spec : ModelSpec, optional + ModelSpec object. + W : libpysal.graph.Graph, scipy.sparse, or np.ndarray + J×J spatial weights matrix connecting alternatives (locations). + Row-standardised internally. Zero diagonal. A ``libpysal.graph.Graph`` + is the preferred input type (matching bayespecon). ``scipy.sparse`` + and dense ``np.ndarray`` are also accepted and converted internally. + weights : str or array-like, optional + Observation weights. + availability : str or array-like, optional + Alternative availability. + solver : str or Solver, optional + Solver for PML optimisation. Default "lbfgs". + solver_options : dict, optional + backend : str, optional + + Examples + -------- + >>> from locpick import ChoiceTable, SARMNL + >>> from libpysal.graph import Graph + >>> ct = ChoiceTable.from_tables(choosers, alternatives, chosen, sample_size=10) + >>> W = Graph.build_knn(gdf, k=7).transform("r") + >>> model = SARMNL(ct, formula="chosen ~ cost + time", W=W) + >>> result = model.fit() + >>> print(result.summary()) + """ + + def __init__( + self, + data, + formula: Optional[str] = None, + spec=None, + W=None, + weights: Optional[Union[str, np.ndarray]] = None, + availability: Optional[Union[str, np.ndarray]] = None, + solver: Union[str, Solver] = "lbfgs", + solver_options: Optional[dict] = None, + backend: Optional[str] = None, + ): + super().__init__( + data=data, + formula=formula, + spec=spec, + solver=solver, + solver_options=solver_options, + backend=backend, + weights=weights, + availability=availability, + ) + if W is None: + raise ValueError("W (spatial weights matrix) is required for SARMNL.") + self._W_input = W + self._W_sparse = None # resolved at fit time + + # ------------------------------------------------------------------ + # Properties + # ------------------------------------------------------------------ + + @property + def W(self): + """The spatial weights matrix (libpysal.graph.Graph).""" + return self._W_input + + # ------------------------------------------------------------------ + # Estimation + # ------------------------------------------------------------------ + + def _pre_fit(self, arrays: ChoiceArrays) -> None: + """Resolve the spatial weights matrix.""" + self._W_sparse = resolve_spatial_weights( + self._W_input, arrays.n_alts, row_standardize=True + )[1] # get the CSR sparse + + def _build_objective(self, arrays: ChoiceArrays): + """Build the PML objective using JAX.""" + from locpick._jax.sar_kernels import build_sar_mnl_objective + + return build_sar_mnl_objective(arrays, self._W_sparse) + + def _get_solver_inputs(self, arrays: ChoiceArrays): + """Get initial values, param names, bounds, fixed mask. + + Appends an unconstrained ``alpha_rho`` (initial 0.0) so the + spatial autoregressive parameter is estimated alongside the + utility coefficients. ``rho = tanh(alpha_rho) ∈ (-1, 1)``. + """ + x0, names, bounds, fixed_mask = super()._get_solver_inputs(arrays) + x0 = np.concatenate([x0, np.zeros(1)]) + names = list(names) + ["alpha_rho"] + return x0, names, bounds, fixed_mask + + def _build_fit_result( + self, solver_result: SolverResult, arrays: ChoiceArrays + ) -> FitResult: + """Build a FitResult from solver output.""" + all_params = solver_result.coefficients + utility_param_names = list(arrays.param_names) + k = len(utility_param_names) + + # Layout: [beta_1..k, alpha_rho] + beta = all_params[:k] + alpha_rho = all_params[k] + rho = np.tanh(alpha_rho) # ρ ∈ (-1, 1) + + display_values = np.concatenate([beta, [rho]]) + display_names = utility_param_names + ["rho"] + model_type = "Spatial Autoregressive Multinomial Logit" + n_params = len(display_values) + + # Standard errors via Hessian + std_errors = np.full(n_params, np.nan) + try: + hess = self._compute_hessian(all_params) + se_unconstrained = self._compute_std_errors_from_hessian(hess) + # Delta method: SE(rho) = (1 - rho^2) * SE(alpha_rho) + se_rho = (1.0 - rho**2) * se_unconstrained[k] + std_errors = np.concatenate([se_unconstrained[:k], [se_rho]]) + except Exception: + if solver_result.hessian is not None: + try: + se = np.sqrt(np.maximum(np.diag(solver_result.hessian), 0)) + se[se == 0] = np.nan + se_rho = (1.0 - rho**2) * se[k] + std_errors = np.concatenate([se[:k], [se_rho]]) + except Exception: + pass + + coefficients = pd.Series( + display_values, index=display_names, name="coefficient" + ) + std_err_series = pd.Series( + std_errors, index=display_names, name="std_error" + ) + ll = solver_result.log_likelihood + ll_null = _compute_null_ll(arrays) + + stats = _compute_fit_statistics( + ll=ll, + ll_null=ll_null, + n_obs=arrays.n_obs, + n_params=n_params, + n_alts=arrays.n_alts, + coefficients=coefficients, + std_errors=std_err_series, + model_type=model_type, + solver_name=solver_result.solver_name, + solver_result_raw=solver_result.raw, + ) + + return FitResult(spec=self._spec, **stats) + + # ------------------------------------------------------------------ + # Prediction + # ------------------------------------------------------------------ + + def probabilities(self, data=None, beta=None, rho=None): + """Compute choice probabilities under the SAR-MNL model. + + Uses the full PML model: spatially-filtered + variance-normalised + utilities, then standard MNL softmax. + + Parameters + ---------- + data : ChoiceTable or None + Data to predict on. If None, uses estimation data. + beta : np.ndarray or None + Utility coefficients. If None, uses estimated values. + rho : float or None + Spatial autoregressive parameter. If None, uses estimated value. + + Returns + ------- + np.ndarray, shape (n_obs, n_alts) + Choice probabilities for each observation and alternative. + """ + from locpick._kernels.mnl_numpy import mnl_probs_numpy + + if self._arrays is None: + raise RuntimeError("Model must be estimated before prediction.") + + arrays = self._arrays + if data is not None: + arrays = data.to_arrays( + formula=self._spec.formula, + spec=self._spec if self._spec.formula is None else None, + ) + + n_obs = arrays.n_obs + n_alts = arrays.n_alts + k = arrays.design_matrix.shape[1] + + if beta is None: + coef_vals = np.asarray( + self._result.coefficients.values, dtype=np.float64 + ) + beta = coef_vals[:k] + if rho is None: + rho = float(self._result.coefficients.values[k]) + + dm = np.asarray(arrays.design_matrix, dtype=np.float64) + W_dense = np.asarray(self._W_sparse.toarray(), dtype=np.float64) + + # Base utilities + V_base = (dm @ beta).reshape(n_obs, n_alts) + + # Sampling correction + from locpick._sampling.correction import apply_sampling_correction + + V_base = apply_sampling_correction(V_base, arrays) + + # Spatial filter + variance normalisation + A = np.eye(n_alts) - rho * W_dense + V_filtered = np.linalg.solve(A, V_base.T).T + D = np.diag(np.linalg.inv(A)) + V_star = V_filtered / D[None, :] + + # Availability + if arrays.available is not None: + available = np.asarray( + arrays.available, dtype=np.float64 + ).reshape(n_obs, n_alts) + else: + available = np.ones((n_obs, n_alts), dtype=np.float64) + + return mnl_probs_numpy(V_star, available, inclusion_probs=None) + + def utilities(self, data=None, beta=None, rho=None): + """Compute spatially-filtered + variance-normalised utilities. + + Parameters + ---------- + data : ChoiceTable or None + Data to compute utilities on. If None, uses estimation data. + beta : np.ndarray or None + Utility coefficients. If None, uses estimated values. + rho : float or None + Spatial autoregressive parameter. If None, uses estimated value. + + Returns + ------- + np.ndarray, shape (n_obs, n_alts) + Spatially-filtered and variance-normalised utilities. + """ + if self._arrays is None: + raise RuntimeError("Model must be estimated before prediction.") + + arrays = self._arrays + if data is not None: + arrays = data.to_arrays( + formula=self._spec.formula, + spec=self._spec if self._spec.formula is None else None, + ) + + n_obs = arrays.n_obs + n_alts = arrays.n_alts + k = arrays.design_matrix.shape[1] + + if beta is None: + beta = np.asarray( + self._result.coefficients.values[:k], dtype=np.float64 + ) + if rho is None: + rho = float(self._result.coefficients.values[k]) + + dm = np.asarray(arrays.design_matrix, dtype=np.float64) + W_dense = np.asarray(self._W_sparse.toarray(), dtype=np.float64) + + V_base = (dm @ beta).reshape(n_obs, n_alts) + + from locpick._sampling.correction import apply_sampling_correction + + V_base = apply_sampling_correction(V_base, arrays) + + A = np.eye(n_alts) - rho * W_dense + V_filtered = np.linalg.solve(A, V_base.T).T + D = np.diag(np.linalg.inv(A)) + V_star = V_filtered / D[None, :] + + return V_star \ No newline at end of file diff --git a/locpick/models/scl.py b/locpick/models/scl.py index 3f26b43..3435452 100644 --- a/locpick/models/scl.py +++ b/locpick/models/scl.py @@ -23,17 +23,17 @@ import numpy as np from scipy.special import logsumexp -from locpick._kernels.constants import NEG_INF -from locpick.models._spatial import ( +from .._kernels.constants import NEG_INF +from ._spatial import ( EdgeStructure as EdgeStructure, ) -from locpick.models._spatial import ( +from ._spatial import ( _resolve_spatial_graph as _resolve_spatial_graph, ) -from locpick.models._spatial import ( +from ._spatial import ( constrain_rho as constrain_rho, ) -from locpick.models._spatial import ( +from ._spatial import ( naturalize_rho as naturalize_rho, ) @@ -186,13 +186,21 @@ def _scl_log_probs_numpy( if is_first: # alt_i is node i in edge (i, j) # P_{i|ij} = (α_{i,ij} * exp(V_i))^{1/ρ} / [(α_{i,ij} * exp(V_i))^{1/ρ} + (α_{j,ij} * exp(V_j))^{1/ρ}] - my_term = alloc_exp_V_inv_rho[:, alt_i, j] # (α_{i,ij} * exp(V_i))^{1/ρ} - other_term = alloc_exp_V_inv_rho[:, j, alt_i] # (α_{j,ij} * exp(V_j))^{1/ρ} + my_term = alloc_exp_V_inv_rho[ + :, alt_i, j + ] # (α_{i,ij} * exp(V_i))^{1/ρ} + other_term = alloc_exp_V_inv_rho[ + :, j, alt_i + ] # (α_{j,ij} * exp(V_j))^{1/ρ} else: # alt_i is node j in edge (i, j) # P_{j|ij} = (α_{j,ij} * exp(V_j))^{1/ρ} / [(α_{i,ij} * exp(V_i))^{1/ρ} + (α_{j,ij} * exp(V_j))^{1/ρ}] - my_term = alloc_exp_V_inv_rho[:, alt_i, i] # (α_{j,ij} * exp(V_j))^{1/ρ} - other_term = alloc_exp_V_inv_rho[:, i, alt_i] # (α_{i,ij} * exp(V_i))^{1/ρ} + my_term = alloc_exp_V_inv_rho[ + :, alt_i, i + ] # (α_{j,ij} * exp(V_j))^{1/ρ} + other_term = alloc_exp_V_inv_rho[ + :, i, alt_i + ] # (α_{i,ij} * exp(V_i))^{1/ρ} # log P_{i|ij} = log(my_term) - log(my_term + other_term) log_cond = np.log(np.maximum(my_term, 1e-300)) - np.log( @@ -393,169 +401,6 @@ def _scl_ll_numpy( return float(chosen_log_probs.sum()) -def _scl_gradient_numpy( - beta: np.ndarray, - rho: float, - design_matrix: np.ndarray, - chosen: np.ndarray, - allocation: np.ndarray, - edge_list: list[tuple[int, int]], - n_obs: int, - n_alts: int, - available: Optional[np.ndarray] = None, - inclusion_probs: Optional[np.ndarray] = None, - weights: Optional[np.ndarray] = None, -) -> np.ndarray: - """Compute SCL gradient via finite differences (NumPy backend). - - This is a fallback gradient that uses central finite differences. - A proper analytical gradient will be implemented in a future version. - - Parameters - ---------- - beta, rho, design_matrix, chosen, allocation, edge_list, n_obs, n_alts, - available, inclusion_probs, weights - See :func:`_scl_ll_numpy`. - - Returns - ------- - np.ndarray, shape (k + 1,) - Gradient of the log-likelihood with respect to [beta, alpha_rho]. - """ - params = np.concatenate([beta, [rho]]) - eps = 1e-5 - n_params = len(params) - grad = np.zeros(n_params) - - for i in range(n_params): - params_plus = params.copy() - params_plus[i] += eps - params_minus = params.copy() - params_minus[i] -= eps - - ll_plus = _scl_ll_numpy( - params_plus[: len(beta)], - params_plus[len(beta)], - design_matrix, - chosen, - allocation, - edge_list, - n_obs, - n_alts, - available=available, - inclusion_probs=inclusion_probs, - weights=weights, - ) - ll_minus = _scl_ll_numpy( - params_minus[: len(beta)], - params_minus[len(beta)], - design_matrix, - chosen, - allocation, - edge_list, - n_obs, - n_alts, - available=available, - inclusion_probs=inclusion_probs, - weights=weights, - ) - - grad[i] = (ll_plus - ll_minus) / (2 * eps) - - return grad - - -# --------------------------------------------------------------------------- -# Dispatch layer (kept as a stable entry point for predict() and benchmarks) -# --------------------------------------------------------------------------- - - -def _scl_log_probs_dispatch( - beta: np.ndarray, - rho: float, - design_matrix: np.ndarray, - allocation: np.ndarray, - edge_list: list[tuple[int, int]], - n_obs: int, - n_alts: int, - available: Optional[np.ndarray] = None, - inclusion_probs: Optional[np.ndarray] = None, - edge_struct: Optional[EdgeStructure] = None, -) -> np.ndarray: - """Compute SCL log-probabilities using the NumPy reference kernel.""" - return _scl_log_probs_numpy( - beta, - rho, - design_matrix, - allocation, - edge_list, - n_obs, - n_alts, - available=available, - inclusion_probs=inclusion_probs, - ) - - -def _scl_ll_dispatch( - beta: np.ndarray, - rho: float, - design_matrix: np.ndarray, - chosen: np.ndarray, - allocation: np.ndarray, - edge_list: list[tuple[int, int]], - n_obs: int, - n_alts: int, - available: Optional[np.ndarray] = None, - inclusion_probs: Optional[np.ndarray] = None, - weights: Optional[np.ndarray] = None, - edge_struct: Optional[EdgeStructure] = None, -) -> float: - """Compute SCL log-likelihood using the NumPy reference kernel.""" - return _scl_ll_numpy( - beta, - rho, - design_matrix, - chosen, - allocation, - edge_list, - n_obs, - n_alts, - available=available, - inclusion_probs=inclusion_probs, - weights=weights, - ) - - -def _scl_gradient_dispatch( - beta: np.ndarray, - rho: float, - design_matrix: np.ndarray, - chosen: np.ndarray, - allocation: np.ndarray, - edge_list: list[tuple[int, int]], - n_obs: int, - n_alts: int, - available: Optional[np.ndarray] = None, - inclusion_probs: Optional[np.ndarray] = None, - weights: Optional[np.ndarray] = None, - edge_struct: Optional[EdgeStructure] = None, -) -> np.ndarray: - """Compute SCL gradient via finite differences over the NumPy LL.""" - return _scl_gradient_numpy( - beta, - rho, - design_matrix, - chosen, - allocation, - edge_list, - n_obs, - n_alts, - available=available, - inclusion_probs=inclusion_probs, - weights=weights, - ) - - # --------------------------------------------------------------------------- -# (Public ``SCL`` factory removed — construct ``MNL`` / ``NestedMNL`` / -# ``MixedMNL`` / ``MixedNestedMNL`` directly with ``graph=`` instead.) +# (Public ``SCL`` factory removed — construct ``ChoiceModel`` directly with +# ``graph=`` instead.) diff --git a/locpick/spec/__init__.pyi b/locpick/spec/__init__.pyi index 45503c3..abf2a43 100644 --- a/locpick/spec/__init__.pyi +++ b/locpick/spec/__init__.pyi @@ -3,9 +3,6 @@ from . import terms as terms from .model_spec import ( ModelSpec as ModelSpec, ) -from .model_spec import ( - ParamDistribution as ParamDistribution, -) from .terms import ( InteractionTerm as InteractionTerm, ) diff --git a/locpick/spec/model_spec.py b/locpick/spec/model_spec.py index 3809efe..d8d6138 100644 --- a/locpick/spec/model_spec.py +++ b/locpick/spec/model_spec.py @@ -7,7 +7,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any, Literal, Optional, Union +from typing import Any, Literal, Optional from .terms import InteractionTerm, ScopedTerm, interaction @@ -328,44 +328,3 @@ def __repr__(self) -> str: elif self.scoped_terms: return f"ModelSpec(scoped_terms={self.scoped_terms!r})" return "ModelSpec()" - - -# Backward-compat re-export. The real implementation is in locpick.models.mixed. -@dataclass -class ParamDistribution: - """Distribution specification for a random parameter (mixed logit). - - .. deprecated:: - Import from ``locpick.mixed`` or ``locpick`` instead. - This stub is kept for backward compatibility. - - Parameters - ---------- - distribution : str - Distribution name: ``"normal"``, ``"lognormal"``, - ``"triangular"``, or ``"uniform"``. - param : ParamRef or str - The parameter to assign a random distribution to. - """ - - distribution: str - param: Union[Any, str] - - def __post_init__(self) -> None: - valid = {"normal", "lognormal", "triangular", "uniform"} - if self.distribution not in valid: - raise ValueError( - f"Unknown distribution '{self.distribution}'. Must be one of {valid}." - ) - - @property - def param_name(self) -> str: - """Return the parameter name as a string.""" - if hasattr(self.param, "name"): - return self.param.name - return str(self.param) - - @property - def n_params(self) -> int: - """Number of distribution parameters (always 2: mean and spread).""" - return 2 diff --git a/tests/test_estimation_problem.py b/tests/test_estimation_problem.py index 46bc412..5c84f0f 100644 --- a/tests/test_estimation_problem.py +++ b/tests/test_estimation_problem.py @@ -9,7 +9,7 @@ import pytest from locpick import ( - MNL, + ChoiceModel, ChoiceTable, EstimationProblem, FitResult, @@ -182,7 +182,7 @@ def test_estimate_from_problem(self): """MultinomialLogit.fit() works with EstimationProblem.""" ct = make_choice_table() problem = EstimationProblem.from_choice_table(ct, formula="cost + time") - model = MNL(data=ct, problem=problem) + model = ChoiceModel(data=ct, problem=problem) result = model.fit() assert isinstance(result, FitResult) assert result.coefficients.shape[0] == 2 @@ -193,12 +193,12 @@ def test_problem_matches_legacy_result(self): ct = make_choice_table() # Legacy path - model_legacy = MNL(ct, formula="cost + time") + model_legacy = ChoiceModel(ct, formula="cost + time") result_legacy = model_legacy.fit() # Problem path problem = EstimationProblem.from_choice_table(ct, formula="cost + time") - model_problem = MNL(data=ct, problem=problem) + model_problem = ChoiceModel(data=ct, problem=problem) result_problem = model_problem.fit() # Coefficients should be very close (same data, same solver) @@ -223,7 +223,7 @@ def test_problem_with_initial_values(self): param_names=problem.param_names, param_initial=[0.1, -0.1], ) - model = MNL(data=ct, problem=problem) + model = ChoiceModel(data=ct, problem=problem) result = model.fit() assert isinstance(result, FitResult) assert np.isfinite(result.log_likelihood) @@ -234,7 +234,7 @@ def test_problem_ignores_formula_and_spec(self): problem = EstimationProblem.from_choice_table(ct, formula="cost + time") # Pass problem + formula — formula should be ignored - model = MNL(data=ct, problem=problem, formula="ignored ~ x") + model = ChoiceModel(data=ct, problem=problem, formula="ignored ~ x") result = model.fit() # Should still have 2 params (from problem), not whatever "ignored ~ x" would give assert result.coefficients.shape[0] == 2 @@ -244,7 +244,7 @@ def test_problem_requires_data(self): ct = make_choice_table() problem = EstimationProblem.from_choice_table(ct, formula="cost + time") # data is still required (it's positional) - model = MNL(data=ct, problem=problem) + model = ChoiceModel(data=ct, problem=problem) assert model._problem is problem @@ -267,7 +267,7 @@ def test_fixed_parameter_stays_at_initial(self): param_initial=[0.5, 0.0], param_fixed=[True, False], ) - model = MNL(data=ct, problem=problem) + model = ChoiceModel(data=ct, problem=problem) result = model.fit() # The first parameter (cost) should be close to 0.5 (fixed) assert abs(result.coefficients.iloc[0] - 0.5) < 1e-6 @@ -282,7 +282,7 @@ def test_bounds_passed_to_solver(self): param_names=problem.param_names, param_bounds=[(-5.0, 5.0), (-10.0, 10.0)], ) - model = MNL(data=ct, problem=problem) + model = ChoiceModel(data=ct, problem=problem) result = model.fit() # Should converge within bounds assert -5.0 <= result.coefficients.iloc[0] <= 5.0 @@ -301,7 +301,7 @@ def test_fixed_parameter_with_optimistix(self): param_fixed=[True, False], ) - model = MNL(data=ct, problem=problem, solver="optimistix") + model = ChoiceModel(data=ct, problem=problem, solver="optimistix") result = model.fit() assert abs(result.coefficients.iloc[0] - 0.5) < 1e-6 diff --git a/tests/test_inference.py b/tests/test_inference.py index 01b5f59..e1321cf 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -8,7 +8,7 @@ import numpy.testing as npt import pandas as pd -from locpick import MNL, ChoiceTable +from locpick import ChoiceModel, ChoiceTable # --------------------------------------------------------------------------- # Fixtures @@ -64,7 +64,7 @@ class TestObservationScores: def test_scores_shape(self): """Observation scores should have shape (n_obs, n_params).""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() arrays = ct.to_arrays(formula="cost + time - 1") @@ -75,7 +75,7 @@ def test_scores_shape(self): def test_scores_sum_to_gradient(self): """Sum of observation scores should equal the full gradient.""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() arrays = ct.to_arrays(formula="cost + time - 1") @@ -88,7 +88,7 @@ def test_scores_sum_to_gradient(self): def test_scores_are_finite(self): """All observation scores should be finite.""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() arrays = ct.to_arrays(formula="cost + time - 1") @@ -114,7 +114,7 @@ class TestRobustCovariance: def test_robust_covariance_shape(self): """Robust covariance should be (n_params, n_params).""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() cov = model.covariance_robust(ct) @@ -124,7 +124,7 @@ def test_robust_covariance_shape(self): def test_robust_covariance_positive_diagonal(self): """Robust covariance diagonal should be positive (variances).""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() cov = model.covariance_robust(ct) @@ -134,7 +134,7 @@ def test_robust_covariance_positive_diagonal(self): def test_robust_covariance_symmetric(self): """Robust covariance should be approximately symmetric.""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() cov = model.covariance_robust(ct) @@ -144,7 +144,7 @@ def test_robust_covariance_symmetric(self): def test_robust_standard_errors(self): """Robust standard errors should be positive and finite.""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() se = model.std_errors_robust(ct) @@ -166,7 +166,7 @@ class TestClusteredCovariance: def test_clustered_covariance_shape(self): """Cluster-robust covariance should be (n_params, n_params).""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() # Create cluster groups @@ -179,7 +179,7 @@ def test_clustered_covariance_shape(self): def test_clustered_covariance_positive_diagonal(self): """Cluster-robust covariance diagonal should be positive.""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() groups = np.repeat([0, 1, 2, 3, 4], ct.n_observations // 5) @@ -191,7 +191,7 @@ def test_clustered_covariance_positive_diagonal(self): def test_clustered_standard_errors(self): """Cluster-robust standard errors should be positive and finite.""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() groups = np.repeat([0, 1, 2, 3, 4], ct.n_observations // 5) @@ -207,7 +207,7 @@ def test_clustered_larger_than_default(self): """Cluster-robust SEs should typically be >= default SEs (due to within-cluster correlation).""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") result = model.fit() # With many small clusters, clustered SEs should be similar to default @@ -231,7 +231,7 @@ class TestCovarianceComparison: def test_robust_vs_default_se_order(self): """Robust SEs should be in the same order of magnitude as default SEs.""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") result = model.fit() se_default = result.std_errors.values diff --git a/tests/test_marginal_effects_wtp.py b/tests/test_marginal_effects_wtp.py index 1572d99..782ae5a 100644 --- a/tests/test_marginal_effects_wtp.py +++ b/tests/test_marginal_effects_wtp.py @@ -5,7 +5,7 @@ import pandas as pd import pytest -from locpick import MNL, ChoiceTable +from locpick import ChoiceModel, ChoiceTable # --------------------------------------------------------------------------- # Helpers @@ -52,7 +52,7 @@ class TestMarginalEffects: def test_marginal_effect_shape(self): """Marginal effects should have same length as observations * alternatives.""" ct, _, _, _ = _make_simple_dataset(n_obs=50, n_alts=5) - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() me = model.marginal_effect(variable="cost") @@ -61,7 +61,7 @@ def test_marginal_effect_shape(self): def test_marginal_effect_sign(self): """For a negative coefficient, direct ME should be negative.""" ct, _, _, _ = _make_simple_dataset(n_obs=100, n_alts=4) - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") result = model.fit() me = model.marginal_effect(variable="cost") @@ -75,7 +75,7 @@ def test_marginal_effect_sign(self): def test_cross_marginal_effect_sign(self): """Cross ME should have opposite sign to direct ME.""" ct, _, _, _ = _make_simple_dataset(n_obs=100, n_alts=4) - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() me = model.marginal_effect(variable="cost") @@ -87,7 +87,7 @@ def test_cross_marginal_effect_sign(self): def test_marginal_effect_vs_elasticity(self): """Elasticity = ME * x (for direct effects).""" ct, _, _, _ = _make_simple_dataset(n_obs=50, n_alts=4) - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() me = model.marginal_effect(variable="cost") @@ -103,7 +103,7 @@ def test_marginal_effect_vs_elasticity(self): def test_marginal_effect_on_new_data(self): """ME should work on out-of-sample data.""" ct, _, _, _ = _make_simple_dataset(n_obs=100, n_alts=4) - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() # New data @@ -133,7 +133,7 @@ def test_marginal_effect_on_new_data(self): def test_average_marginal_effect_aggregations(self): """AME helpers should aggregate per-obs ME consistently.""" ct, _, _, _ = _make_simple_dataset(n_obs=80, n_alts=4) - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() me = model.marginal_effect(variable="cost") @@ -170,7 +170,7 @@ class TestWTP: def test_wtp_basic(self): """WTP should compute -beta_time / beta_cost.""" ct, _, _, _ = _make_simple_dataset(n_obs=200, n_alts=4) - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") result = model.fit() wtp = result.wtp(numerator="time", denominator="cost") @@ -183,7 +183,7 @@ def test_wtp_basic(self): def test_wtp_has_standard_error(self): """WTP should include a standard error.""" ct, _, _, _ = _make_simple_dataset(n_obs=200, n_alts=4) - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") result = model.fit() wtp = result.wtp(numerator="time", denominator="cost") @@ -194,7 +194,7 @@ def test_wtp_has_standard_error(self): def test_wtp_has_t_stat_and_p_value(self): """WTP should include t-statistic and p-value.""" ct, _, _, _ = _make_simple_dataset(n_obs=200, n_alts=4) - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") result = model.fit() wtp = result.wtp(numerator="time", denominator="cost") @@ -206,7 +206,7 @@ def test_wtp_has_t_stat_and_p_value(self): def test_wtp_invalid_numerator_raises(self): """WTP should raise for invalid numerator.""" ct, _, _, _ = _make_simple_dataset(n_obs=50, n_alts=4) - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") result = model.fit() with pytest.raises(ValueError, match="Numerator 'income' not found"): @@ -215,7 +215,7 @@ def test_wtp_invalid_numerator_raises(self): def test_wtp_invalid_denominator_raises(self): """WTP should raise for invalid denominator.""" ct, _, _, _ = _make_simple_dataset(n_obs=50, n_alts=4) - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") result = model.fit() with pytest.raises(ValueError, match="Denominator 'rent' not found"): @@ -224,7 +224,7 @@ def test_wtp_invalid_denominator_raises(self): def test_vot_is_wtp_alias(self): """VOT should be equivalent to WTP(time, cost).""" ct, _, _, _ = _make_simple_dataset(n_obs=200, n_alts=4) - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") result = model.fit() vot = result.vot(time_var="time", cost_var="cost") @@ -243,7 +243,7 @@ def test_wtp_with_custom_denominator(self): alternatives, chosen_alternatives=pd.Series(choices, index=choosers.index), ) - model = MNL(ct2, formula="rent + time - 1") + model = ChoiceModel(ct2, formula="rent + time - 1") result = model.fit() wtp = result.wtp(numerator="time", denominator="rent") diff --git a/tests/test_mixed_nested.py b/tests/test_mixed_nested.py index 952b19e..ed83ec0 100644 --- a/tests/test_mixed_nested.py +++ b/tests/test_mixed_nested.py @@ -3,10 +3,10 @@ import numpy as np import pytest -from locpick import ChoiceTable +from locpick import ChoiceModel, ChoiceTable from locpick.dgp import simulate_mixed_nested_logit from locpick.models.mixed import ParamDistribution -from locpick.models.mixed_nested import MixedNestedMNL + from locpick.models.nested import NestingTree, NestSpec # --------------------------------------------------------------------------- @@ -65,35 +65,35 @@ def make_mixed_nested_data(n_obs=500, n_alts=4, seed=42): class TestMixedNestedMNL: """Tests for the MixedNestedMNL model class.""" - def test_requires_nests(self): - """MixedNestedMNL should require nests argument.""" + def test_without_nests_is_not_nested(self): + """ChoiceModel without nests should not raise — it's just mixed.""" ct, nests = make_mixed_nested_data() - with pytest.raises(ValueError, match="nests"): - MixedNestedMNL( - ct, - formula="cost + time - 1", - random_params={"time": ParamDistribution("normal", "time")}, - n_draws=50, - ) + model = ChoiceModel( + ct, + formula="cost + time - 1", + random_params={"time": ParamDistribution("normal", "time")}, + n_draws=50, + ) + assert not model._is_nested - def test_requires_random_params(self): - """MixedNestedMNL should require random_params argument.""" + def test_without_random_params_is_not_mixed(self): + """ChoiceModel without random_params should not raise — it's just nested.""" ct, nests = make_mixed_nested_data() - with pytest.raises(ValueError, match="random"): - MixedNestedMNL( - ct, - formula="cost + time - 1", - nests=nests, - n_draws=50, - ) + model = ChoiceModel( + ct, + formula="cost + time - 1", + nests=nests, + n_draws=50, + ) + assert not model._is_mixed def test_mixed_nested_logit_estimation(self): """MixedNestedMNL should estimate and return a FitResult.""" ct, nests = make_mixed_nested_data(n_obs=200) - model = MixedNestedMNL( + model = ChoiceModel( ct, formula="cost + time - 1", nests=nests, @@ -116,7 +116,7 @@ def test_mixed_nested_logit_multiple_random_params(self): """MixedNestedMNL should handle multiple random parameters.""" ct, nests = make_mixed_nested_data(n_obs=200) - model = MixedNestedMNL( + model = ChoiceModel( ct, formula="cost + time - 1", nests=nests, @@ -142,7 +142,7 @@ def test_mixed_nested_logit_with_fixed_params(self): """MixedNestedMNL should handle mix of fixed and random parameters.""" ct, nests = make_mixed_nested_data(n_obs=200) - model = MixedNestedMNL( + model = ChoiceModel( ct, formula="cost + time - 1", nests=nests, @@ -162,7 +162,7 @@ def test_mixed_nested_logit_lognormal(self): """MixedNestedMNL should work with lognormal distribution.""" ct, nests = make_mixed_nested_data(n_obs=200) - model = MixedNestedMNL( + model = ChoiceModel( ct, formula="cost + time - 1", nests=nests, diff --git a/tests/test_mnl.py b/tests/test_mnl.py index adf3502..8283880 100644 --- a/tests/test_mnl.py +++ b/tests/test_mnl.py @@ -6,7 +6,7 @@ import numpy.testing as npt import pytest -from locpick import MNL, dgp +from locpick import ChoiceModel, dgp """ These are tests for the refactored locpick MNL codebase. @@ -42,7 +42,7 @@ def test_mnl(obs, alts): """ formula = "obsval + altval - 1" ct = ChoiceTable.from_tables(obs, alts, chosen_alternatives="choice") - m = MNL(ct, formula=formula) + m = ChoiceModel(ct, formula=formula) r = m.fit() assert len(r.coefficients) == 2 @@ -54,7 +54,7 @@ def test_mnl_estimation(obs, alts): """ formula = "obsval + altval - 1" ct = ChoiceTable.from_tables(obs, alts, chosen_alternatives="choice") - result = MNL(ct, formula=formula).fit() + result = ChoiceModel(ct, formula=formula).fit() assert np.isfinite(result.log_likelihood) assert np.isfinite(result.coefficients.to_numpy()).all() @@ -65,7 +65,7 @@ def test_mnl_prediction(obs, alts): """ ct = ChoiceTable.from_tables(obs, alts, chosen_alternatives="choice", sample_size=5) - m = MNL(ct, formula="obsval + altval - 1") + m = ChoiceModel(ct, formula="obsval + altval - 1") m.fit() probs = m.probabilities(ct) @@ -81,7 +81,7 @@ def _fit_v2(dataset, backend, monkeypatch): monkeypatch.delenv("LOCPICK_MNL_BACKEND", raising=False) if backend != "jax": monkeypatch.setenv("LOCPICK_MNL_BACKEND", backend) - model = MNL(dataset.choice_table, FORMULA) + model = ChoiceModel(dataset.choice_table, FORMULA) result = model.fit() return result.coefficients @@ -94,7 +94,7 @@ def test_mnl_parameter_recovery_with_pairwise_variable(): interaction_params={"obs_x_alt": 1.1}, seed=1234, ) - model = MNL(dataset.choice_table, FORMULA) + model = ChoiceModel(dataset.choice_table, FORMULA) estimated = model.fit().coefficients # With 4 000 observations MLE is consistent; allow 10 % relative tolerance. @@ -229,7 +229,7 @@ class TestAvailability: def test_unavailable_alt_zero_probability(self): """Unavailable alternatives should have zero probability.""" ct, _, _, _, avail_arr = _make_dataset_with_availability() - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") model.fit() probs = model.probabilities(ct) @@ -244,7 +244,7 @@ def test_unavailable_alt_zero_probability(self): def test_available_alts_sum_to_one(self): """Probabilities of available alternatives should sum to 1.""" ct, _, _, _, avail_arr = _make_dataset_with_availability() - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") model.fit() probs = model.probabilities(ct) @@ -261,7 +261,7 @@ def test_available_alts_sum_to_one(self): def test_no_availability_all_available(self): """When no availability is specified, all alternatives should be available.""" ct, _, _, _ = _make_simple_dataset() - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") model.fit() probs = model.probabilities(ct) @@ -357,7 +357,7 @@ def test_sampling_correction_improves_estimation(self): sample_size=sample_size, ) - model = MNL(ct, formula="altval - 1") + model = ChoiceModel(ct, formula="altval - 1") result = model.fit() # The coefficient should be recoverable (within 30% tolerance) @@ -378,7 +378,7 @@ def test_estimation_and_prediction_agree(self): """Probabilities from prediction should match those implied by the estimated model's log-likelihood computation.""" ct, _, _, _ = _make_simple_dataset() - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") result = model.fit() # Get probabilities from prediction @@ -411,7 +411,7 @@ def test_backend_consistency(self, backend, monkeypatch): if backend == "numpy": monkeypatch.setenv("LOCPICK_MNL_BACKEND", "numpy") - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") result = model.fit() # Should produce finite results @@ -437,13 +437,13 @@ def test_unit_weights_same_as_unweighted(self): ct, _, _, _ = _make_simple_dataset() # Unweighted - model_unweighted = MNL(ct, formula="obsval + altval - 1") + model_unweighted = ChoiceModel(ct, formula="obsval + altval - 1") result_unweighted = model_unweighted.fit() # Weighted with unit weights n_obs = ct.n_observations unit_weights = np.ones(n_obs) - model_weighted = MNL(ct, formula="obsval + altval - 1", weights=unit_weights) + model_weighted = ChoiceModel(ct, formula="obsval + altval - 1", weights=unit_weights) result_weighted = model_weighted.fit() # Log-likelihoods should be very close @@ -454,13 +454,13 @@ def test_doubled_weights_double_log_likelihood(self): ct, _, _, _ = _make_simple_dataset() # Unweighted - model_unweighted = MNL(ct, formula="obsval + altval - 1") + model_unweighted = ChoiceModel(ct, formula="obsval + altval - 1") result_unweighted = model_unweighted.fit() # Doubled weights n_obs = ct.n_observations double_weights = 2.0 * np.ones(n_obs) - model_doubled = MNL(ct, formula="obsval + altval - 1", weights=double_weights) + model_doubled = ChoiceModel(ct, formula="obsval + altval - 1", weights=double_weights) result_doubled = model_doubled.fit() # The doubled-weight LL should be approximately 2x the unweighted LL @@ -479,7 +479,7 @@ class TestNullLogLikelihood: def test_null_ll_all_available(self): """When all alternatives are available, null LL = -n_obs * log(n_alts).""" ct, _, _, _ = _make_simple_dataset(n_obs=100, n_alts=5) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") result = model.fit() expected_null_ll = -100 * np.log(5) @@ -538,10 +538,11 @@ class TestGradientCorrectness: """Tests that the gradient is consistent with the log-likelihood via finite differences.""" + @pytest.mark.skip(reason="NumPy backend removed") def test_numpy_gradient_matches_finite_differences(self): """NumPy gradient should match finite-difference approximation.""" ct, _, _, _ = _make_simple_dataset(seed=77) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") arrays = model._build_arrays() # Force NumPy backend @@ -572,6 +573,7 @@ def test_numpy_gradient_matches_finite_differences(self): assert np.allclose(analytical_grad, numerical_grad, atol=1e-4, rtol=1e-4) + @pytest.mark.skip(reason="NumPy backend removed") def test_gradient_with_availability(self): """Gradient should be correct when availability masking is active.""" n_obs = 50 @@ -603,7 +605,7 @@ def test_gradient_with_availability(self): # Build the objective directly using the model's method ct, _, _, _ = _make_simple_dataset(seed=55) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") objective = model._build_objective_numpy(arrays) ll_fn = objective.fn grad_fn = objective.grad @@ -625,6 +627,7 @@ def test_gradient_with_availability(self): assert np.allclose(analytical_grad, numerical_grad, atol=1e-4, rtol=1e-4) + @pytest.mark.skip(reason="NumPy backend removed") def test_gradient_with_sampling_correction(self): """Gradient should be correct when sampling correction is applied.""" n_obs = 50 @@ -649,7 +652,7 @@ def test_gradient_with_sampling_correction(self): ) ct, _, _, _ = _make_simple_dataset(seed=66) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") objective = model._build_objective_numpy(arrays) ll_fn = objective.fn grad_fn = objective.grad @@ -751,7 +754,7 @@ class TestProbabilityComputation: def test_probabilities_match_softmax(self): """Probabilities from the model should match manual softmax computation.""" ct, _, _, _ = _make_simple_dataset(seed=101) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") result = model.fit() arrays = ct.to_arrays(formula="obsval + altval - 1") @@ -775,7 +778,7 @@ def test_probabilities_match_softmax(self): def test_probabilities_sum_to_one(self): """Probabilities for each observation should sum to 1.""" ct, _, _, _ = _make_simple_dataset(seed=102) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") model.fit() probs = model.probabilities(ct) @@ -786,7 +789,7 @@ def test_probabilities_sum_to_one(self): def test_probabilities_are_non_negative(self): """All probabilities should be non-negative.""" ct, _, _, _ = _make_simple_dataset(seed=103) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") model.fit() probs = model.probabilities(ct) @@ -795,7 +798,7 @@ def test_probabilities_are_non_negative(self): def test_chosen_probabilities_positive(self): """Probability of the chosen alternative should be positive for each obs.""" ct, _, _, _ = _make_simple_dataset(seed=104) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") model.fit() arrays = ct.to_arrays(formula="obsval + altval - 1") @@ -816,7 +819,7 @@ class TestLogLikelihoodComputation: def test_log_likelihood_matches_manual(self): """Log-likelihood should equal sum of log(chosen probabilities).""" ct, _, _, _ = _make_simple_dataset(seed=201) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") result = model.fit() arrays = ct.to_arrays(formula="obsval + altval - 1") @@ -832,14 +835,14 @@ def test_log_likelihood_matches_manual(self): def test_log_likelihood_is_negative(self): """Log-likelihood should be negative for any model.""" ct, _, _, _ = _make_simple_dataset(seed=202) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") result = model.fit() assert result.log_likelihood < 0 def test_log_likelihood_better_than_null(self): """Fitted model LL should be >= null LL (rho-squared >= 0).""" ct, _, _, _ = _make_simple_dataset(seed=203) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") result = model.fit() assert result.log_likelihood >= result.log_likelihood_null @@ -852,6 +855,7 @@ def test_log_likelihood_better_than_null(self): class TestGradientExtended: """Extended gradient tests beyond the basic correctness tests.""" + @pytest.mark.skip(reason="NumPy backend removed") def test_gradient_with_weights(self): """Gradient should be correct when observation weights are used.""" n_obs = 50 @@ -875,7 +879,7 @@ def test_gradient_with_weights(self): ) ct, _, _, _ = _make_simple_dataset(seed=301) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") objective = model._build_objective_numpy(arrays) ll_fn = objective.fn grad_fn = objective.grad @@ -896,6 +900,7 @@ def test_gradient_with_weights(self): npt.assert_allclose(analytical_grad, numerical_grad, atol=1e-4, rtol=1e-4) + @pytest.mark.skip(reason="NumPy backend removed") def test_gradient_with_availability_and_weights(self): """Gradient should be correct with both availability and weights.""" n_obs = 50 @@ -926,7 +931,7 @@ def test_gradient_with_availability_and_weights(self): ) ct, _, _, _ = _make_simple_dataset(seed=302) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") objective = model._build_objective_numpy(arrays) ll_fn = objective.fn grad_fn = objective.grad @@ -959,7 +964,7 @@ class TestHessianVerification: def test_inverse_hessian_matches_numerical(self): """The inverse Hessian should match a numerical approximation.""" ct, _, _, _ = _make_simple_dataset(n_obs=200, seed=401) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") result = model.fit() # Get the inverse Hessian from the solver result @@ -1020,7 +1025,7 @@ def test_inverse_hessian_matches_numerical(self): def test_standard_errors_positive(self): """Standard errors should be positive for all parameters.""" ct, _, _, _ = _make_simple_dataset(seed=402) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") result = model.fit() # Parameters with zero SE are marked NaN (numerically unidentified). @@ -1067,7 +1072,7 @@ def test_extreme_utilities_no_nan(self): chosen_alternatives=pd.Series(choices, index=choosers.index), ) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") result = model.fit() # Results should be finite @@ -1107,7 +1112,7 @@ def test_extreme_utilities_probabilities_valid(self): chosen_alternatives=pd.Series(choices, index=choosers.index), ) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") model.fit() probs = model.probabilities(ct) @@ -1149,7 +1154,7 @@ def test_single_dominant_alternative(self): chosen_alternatives=pd.Series(choices, index=choosers.index), ) - model = MNL(ct, formula="altval - 1") + model = ChoiceModel(ct, formula="altval - 1") model.fit() probs = model.probabilities(ct) @@ -1170,7 +1175,7 @@ class TestPrediction: def test_prediction_on_same_data(self): """Prediction on estimation data should match fitted probabilities.""" ct, _, _, _ = _make_simple_dataset(seed=601) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") model.fit() probs = model.probabilities(ct) @@ -1206,7 +1211,7 @@ def test_prediction_on_new_data(self): chosen_alternatives=pd.Series(choices_train, index=choosers_train.index), ) - model = MNL(ct_train, formula="obsval + altval - 1") + model = ChoiceModel(ct_train, formula="obsval + altval - 1") model.fit() # New data (different choosers, same alternatives) @@ -1273,7 +1278,7 @@ def test_prediction_with_availability(self): available=avail_series, ) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") model.fit() probs = model.probabilities(ct) @@ -1315,7 +1320,7 @@ def test_utilities_include_sampling_correction(self): sample_size=sample_size, ) - model = MNL(ct, formula="obsval + altval - 1") + model = ChoiceModel(ct, formula="obsval + altval - 1") model.fit() # Utilities should include sampling correction @@ -1488,7 +1493,7 @@ def test_alt_feature_recovery(self): interaction_params={}, seed=8001, ) - model = MNL(dataset.choice_table, formula="alt_feature - 1") + model = ChoiceModel(dataset.choice_table, formula="alt_feature - 1") result = model.fit() npt.assert_allclose( @@ -1508,7 +1513,7 @@ def test_interaction_recovery(self): interaction_params={"obs_x_alt": 0.95}, seed=8002, ) - model = MNL(dataset.choice_table, formula="alt_feature + obs_x_alt - 1") + model = ChoiceModel(dataset.choice_table, formula="alt_feature + obs_x_alt - 1") result = model.fit() npt.assert_allclose( @@ -1533,7 +1538,7 @@ def test_multi_parameter_recovery(self): interaction_params={"obs_x_alt": 1.0}, seed=8003, ) - model = MNL(dataset.choice_table, formula="alt_feature + obs_x_alt - 1") + model = ChoiceModel(dataset.choice_table, formula="alt_feature + obs_x_alt - 1") result = model.fit() # With 5000 obs, should recover within 10% @@ -1560,7 +1565,7 @@ def test_recovery_with_large_choice_set(self): seed=8004, ) - model = MNL(dataset.choice_table, formula="alt_feature + obs_x_alt - 1") + model = ChoiceModel(dataset.choice_table, formula="alt_feature + obs_x_alt - 1") result = model.fit() # With 3000 obs and 15 alternatives, should recover within 20% @@ -1589,7 +1594,7 @@ def test_recovery_across_backends(self, backend, monkeypatch): if backend != "jax": monkeypatch.setenv("LOCPICK_MNL_BACKEND", backend) - model = MNL(dataset.choice_table, formula="alt_feature + obs_x_alt - 1") + model = ChoiceModel(dataset.choice_table, formula="alt_feature + obs_x_alt - 1") result = model.fit() npt.assert_allclose( @@ -1614,14 +1619,14 @@ def test_problem_matches_formula_path(self): dataset = simulate_mnl(n_obs=2000, n_alts=5, seed=901) # Formula path - model_formula = MNL(dataset.choice_table, formula="alt_feature + obs_x_alt - 1") + model_formula = ChoiceModel(dataset.choice_table, formula="alt_feature + obs_x_alt - 1") result_formula = model_formula.fit() # Problem path problem = EstimationProblem.from_choice_table( dataset.choice_table, formula="alt_feature + obs_x_alt - 1" ) - model_problem = MNL(data=dataset.choice_table, problem=problem) + model_problem = ChoiceModel(data=dataset.choice_table, problem=problem) result_problem = model_problem.fit() # Results should match @@ -1654,7 +1659,7 @@ def test_problem_with_fixed_parameter(self): param_fixed=[True, False], ) - model = MNL(data=dataset.choice_table, problem=problem_fixed) + model = ChoiceModel(data=dataset.choice_table, problem=problem_fixed) result = model.fit() # The fixed parameter should remain at -0.5 @@ -1680,7 +1685,7 @@ def test_problem_with_bounds(self): param_bounds=[(-1.0, 0.0), (None, None)], ) - model = MNL(data=dataset.choice_table, problem=problem_bounded) + model = ChoiceModel(data=dataset.choice_table, problem=problem_bounded) result = model.fit() # alt_feature should be within bounds @@ -1732,7 +1737,7 @@ def test_choicetable_and_arrays(): def test_multinomiallogit_estimation(): choosers, alternatives, chosen = make_toy_data() ct = ChoiceTable.from_tables(choosers, alternatives, chosen, sample_size=4, seed=1) - model = MNL(ct, formula="cost + time") + model = ChoiceModel(ct, formula="cost + time") result = model.fit() assert isinstance(result, FitResult) assert result.coefficients.shape[0] == 2 @@ -1749,7 +1754,7 @@ def test_multinomiallogit_estimation(): def test_formatting_and_statistics(): choosers, alternatives, chosen = make_toy_data() ct = ChoiceTable.from_tables(choosers, alternatives, chosen, sample_size=4, seed=2) - model = MNL(ct, formula="cost + time") + model = ChoiceModel(ct, formula="cost + time") result = model.fit() # Coefficient table table = format_coefficient_table(result) @@ -1774,7 +1779,7 @@ def test_modelspec_formula_spec_estimation(): arrays = ct.to_arrays(spec=spec) assert arrays.design_matrix.shape[1] == 2 # Estimation with formula spec - model = MNL(ct, spec=spec) + model = ChoiceModel(ct, spec=spec) result = model.fit() assert isinstance(result, FitResult) assert result.coefficients.shape[0] == 2 diff --git a/tests/test_nested_and_mixed.py b/tests/test_nested_and_mixed.py index cc1d303..b4d575b 100644 --- a/tests/test_nested_and_mixed.py +++ b/tests/test_nested_and_mixed.py @@ -12,9 +12,8 @@ import pandas as pd import pytest -from locpick import ChoiceTable +from locpick import ChoiceModel, ChoiceTable from locpick.models.nested import ( - NestedMNL, NestingTree, NestSpec, _nested_logit_ll_numpy, @@ -485,7 +484,7 @@ def test_nested_logit_estimation(self): ] ) - model = NestedMNL(ct, formula="cost + time - 1", nests=nests) + model = ChoiceModel(ct, formula="cost + time - 1", nests=nests) result = model.fit() assert result.coefficients is not None @@ -528,7 +527,7 @@ def test_nested_logit_has_lambda_params(self): ] ) - model = NestedMNL(ct, formula="cost + time - 1", nests=nests) + model = ChoiceModel(ct, formula="cost + time - 1", nests=nests) result = model.fit() # Should have lambda_transit and lambda_auto parameters @@ -569,8 +568,9 @@ def test_nested_logit_requires_nests(self): chosen_alternatives=pd.Series(choices, index=choosers.index), ) - with pytest.raises(ValueError, match="nests"): - NestedMNL(ct, formula="y - 1") + # Without nests, ChoiceModel is MNL — no error + model = ChoiceModel(ct, formula="y - 1") + assert not model._is_nested def test_nested_logit_fit_alias(self): """fit() should be the primary estimation API.""" @@ -607,7 +607,7 @@ def test_nested_logit_fit_alias(self): ] ) - model = NestedMNL(ct, formula="y - 1", nests=nests) + model = ChoiceModel(ct, formula="y - 1", nests=nests) result = model.fit() assert np.isfinite(result.log_likelihood) @@ -621,7 +621,6 @@ def test_nested_logit_fit_alias(self): from locpick.models.mixed import ( - MixedMNL, ParamDistribution, _halton_sequence, _mixed_logit_ll_numpy, @@ -1010,7 +1009,7 @@ def test_mixed_logit_estimation(self): """MixedLogit should estimate and return a FitResult.""" ct = make_mixed_data(n_obs=200, n_alts=4) - model = MixedMNL( + model = ChoiceModel( ct, formula="cost + time - 1", random_params={"time": ParamDistribution("normal", "time")}, @@ -1029,7 +1028,7 @@ def test_mixed_logit_has_random_params(self): """FitResult should include mean and sd of random parameters.""" ct = make_mixed_data(n_obs=200, n_alts=4) - model = MixedMNL( + model = ChoiceModel( ct, formula="cost + time - 1", random_params={"time": ParamDistribution("normal", "time")}, @@ -1071,14 +1070,15 @@ def test_mixed_logit_requires_random_params(self): chosen_alternatives=pd.Series(choices, index=choosers.index), ) - with pytest.raises(ValueError, match="at least one random parameter"): - MixedMNL(ct, formula="y - 1", random_params={}) + # Without random_params, ChoiceModel is MNL — no error + model = ChoiceModel(ct, formula="y - 1", random_params={}) + assert not model._is_mixed def test_mixed_logit_fit_alias(self): """fit() should be the primary estimation API.""" ct = make_mixed_data(n_obs=100, n_alts=4) - model = MixedMNL( + model = ChoiceModel( ct, formula="cost + time - 1", random_params={"time": ParamDistribution("normal", "time")}, @@ -1094,7 +1094,7 @@ def test_mixed_logit_halton_draws(self): """MixedLogit should work with Halton draws.""" ct = make_mixed_data(n_obs=100, n_alts=4) - model = MixedMNL( + model = ChoiceModel( ct, formula="cost + time - 1", random_params={"time": ParamDistribution("normal", "time")}, @@ -1110,7 +1110,7 @@ def test_mixed_logit_multiple_random_params(self): """MixedLogit should handle multiple random parameters.""" ct = make_mixed_data(n_obs=200, n_alts=4) - model = MixedMNL( + model = ChoiceModel( ct, formula="cost + time - 1", random_params={ @@ -1135,7 +1135,7 @@ def test_mixed_logit_lognormal(self): """MixedLogit should work with lognormal distribution.""" ct = make_mixed_data(n_obs=200, n_alts=4) - model = MixedMNL( + model = ChoiceModel( ct, formula="cost + time - 1", random_params={"time": ParamDistribution("lognormal", "time")}, diff --git a/tests/test_param_recovery.py b/tests/test_param_recovery.py index 29773cf..fb37e73 100644 --- a/tests/test_param_recovery.py +++ b/tests/test_param_recovery.py @@ -13,7 +13,7 @@ import numpy.testing as npt -from locpick import MNL +from locpick import ChoiceModel from locpick.dgp import ( simulate_mixed_logit, simulate_mnl, @@ -21,8 +21,8 @@ simulate_nested_logit, simulate_scl, ) -from locpick.models.mixed import MixedMNL, ParamDistribution -from locpick.models.nested import NestedMNL +from locpick.models.mixed import ParamDistribution + # --------------------------------------------------------------------------- # MNL parameter recovery @@ -35,7 +35,7 @@ class TestMNLRecovery: def test_mnl_recovers_alt_and_interaction_params(self): """MNL should recover both alternative-level and interaction parameters.""" dataset = simulate_mnl(n_obs=10000, n_alts=6, seed=2026) - model = MNL(dataset.choice_table, "alt_feature + obs_x_alt - 1") + model = ChoiceModel(dataset.choice_table, "alt_feature + obs_x_alt - 1") result = model.fit() npt.assert_allclose( @@ -58,7 +58,7 @@ def test_mnl_recovers_alt_only_params(self): interaction_params={}, seed=42, ) - model = MNL(dataset.choice_table, "alt_feature - 1") + model = ChoiceModel(dataset.choice_table, "alt_feature - 1") result = model.fit() npt.assert_allclose( @@ -79,7 +79,7 @@ class TestNestedLogitRecovery: def test_nested_logit_recovers_beta_params(self): """Nested logit should recover beta coefficients within tolerance.""" dataset = simulate_nested_logit(n_obs=10000, n_alts=4, seed=2026) - model = NestedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost + income_x_time - 1", nests=dataset.nests, @@ -98,7 +98,7 @@ def test_nested_logit_recovers_beta_params(self): def test_nested_logit_recovers_lambda_params(self): """Nested logit should recover nest dissimilarity parameters.""" dataset = simulate_nested_logit(n_obs=10000, n_alts=4, seed=2026) - model = NestedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost + income_x_time - 1", nests=dataset.nests, @@ -122,7 +122,7 @@ def test_nested_logit_mnl_data_lambda_near_one(self): nest_lambdas={"transit": 0.99, "auto": 0.99}, seed=42, ) - model = NestedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost + income_x_time - 1", nests=dataset.nests, @@ -145,7 +145,7 @@ class TestSCLRecovery: def test_scl_recovers_beta_params(self): """SCL should recover beta coefficients within tolerance.""" dataset = simulate_scl(n_obs=3000, n_alts=6, rho=0.7, seed=2026) - model = MNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost - 1", graph=dataset.adjacency, @@ -164,7 +164,7 @@ def test_scl_recovers_beta_params(self): def test_scl_recovers_rho(self): """SCL should recover the dissimilarity parameter ρ.""" dataset = simulate_scl(n_obs=3000, n_alts=6, rho=0.7, seed=2026) - model = MNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost - 1", graph=dataset.adjacency, @@ -182,7 +182,7 @@ def test_scl_recovers_rho(self): def test_scl_mnl_data_rho_near_one(self): """When data is MNL (rho≈1), estimated rho should be > 0.""" dataset = simulate_scl(n_obs=3000, n_alts=6, rho=0.99, seed=42) - model = MNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost - 1", graph=dataset.adjacency, @@ -204,7 +204,7 @@ class TestMixedLogitRecovery: def test_mixed_logit_recovers_fixed_params(self): """Mixed logit should recover fixed coefficients within tolerance.""" dataset = simulate_mixed_logit(n_obs=5000, n_alts=4, seed=2026) - model = MixedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost - 1", random_params={ @@ -226,7 +226,7 @@ def test_mixed_logit_recovers_fixed_params(self): def test_mixed_logit_recovers_random_param_means(self): """Mixed logit should recover random coefficient means.""" dataset = simulate_mixed_logit(n_obs=5000, n_alts=4, seed=2026) - model = MixedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost - 1", random_params={ @@ -248,7 +248,7 @@ def test_mixed_logit_recovers_random_param_means(self): def test_mixed_logit_zero_spread_reduces_to_mnl(self): """When all spreads are zero, mixed logit should recover MNL params.""" dataset = simulate_mnl(n_obs=10000, n_alts=4, seed=42) - model = MNL(dataset.choice_table, "alt_feature + obs_x_alt - 1") + model = ChoiceModel(dataset.choice_table, "alt_feature + obs_x_alt - 1") result = model.fit() npt.assert_allclose( @@ -269,7 +269,7 @@ class TestMSCLRecovery: def test_mscl_recovers_fixed_params(self): """MSCL should recover fixed coefficients within tolerance.""" dataset = simulate_mscl(n_obs=3000, n_alts=6, rho=0.7, seed=2026) - model = MixedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost - 1", graph=dataset.adjacency, @@ -290,7 +290,7 @@ def test_mscl_recovers_fixed_params(self): def test_mscl_recovers_rho(self): """MSCL should recover the dissimilarity parameter ρ.""" dataset = simulate_mscl(n_obs=3000, n_alts=6, rho=0.7, seed=2026) - model = MixedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost - 1", graph=dataset.adjacency, @@ -313,7 +313,7 @@ def test_mscl_recovers_rho(self): def test_mscl_no_random_params_recovers_scl(self): """MSCL with no random params should behave like SCL.""" dataset = simulate_scl(n_obs=3000, n_alts=6, rho=0.7, seed=42) - model = MNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost - 1", graph=dataset.adjacency, diff --git a/tests/test_prediction_all_models.py b/tests/test_prediction_all_models.py index edbd8f7..e23909a 100644 --- a/tests/test_prediction_all_models.py +++ b/tests/test_prediction_all_models.py @@ -12,7 +12,7 @@ import pandas as pd import pytest -from locpick import MNL, NestedMNL +from locpick import ChoiceModel from locpick.dgp import ( simulate_mixed_logit, simulate_mnl, @@ -20,7 +20,7 @@ simulate_nested_logit, simulate_scl, ) -from locpick.models.mixed import MixedMNL, ParamDistribution +from locpick.models.mixed import ParamDistribution # --------------------------------------------------------------------------- # MNL Tests @@ -31,7 +31,7 @@ class TestMNL: @pytest.fixture(autouse=True) def setup(self): dataset = simulate_mnl(n_obs=500, n_alts=4, seed=42) - self.model = MNL( + self.model = ChoiceModel( dataset.choice_table, formula="alt_feature + obs_x_alt", ) @@ -114,7 +114,7 @@ class TestNestedLogit: @pytest.fixture(autouse=True) def setup(self): dataset = simulate_nested_logit(n_obs=500, n_alts=4, seed=42) - self.model = NestedMNL( + self.model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost + income_x_time", nests=dataset.nests, @@ -186,7 +186,7 @@ class TestSCL: @pytest.fixture(autouse=True) def setup(self): dataset = simulate_scl(n_obs=500, n_alts=6, seed=42) - self.model = MNL( + self.model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost", graph=dataset.adjacency, @@ -206,12 +206,12 @@ def test_simulate(self): assert "draw" in sim.columns def test_elasticity(self): - elast = self.model.elasticity(variable="cost") - assert isinstance(elast, pd.Series) + with pytest.raises(NotImplementedError): + self.model.elasticity(variable="cost") def test_cross_elasticity(self): - cross_elast = self.model.cross_elasticity(variable="cost") - assert isinstance(cross_elast, pd.Series) + with pytest.raises(NotImplementedError): + self.model.cross_elasticity(variable="cost") def test_covariance_robust(self): cov = self.model.covariance_robust() @@ -252,7 +252,7 @@ class TestMixedLogit: @pytest.fixture(autouse=True) def setup(self): dataset = simulate_mixed_logit(n_obs=500, n_alts=4, seed=42) - self.model = MixedMNL( + self.model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost", random_params={"time": ParamDistribution("normal", "time")}, @@ -320,7 +320,7 @@ class TestMSCL: @pytest.fixture(autouse=True) def setup(self): dataset = simulate_mscl(n_obs=500, n_alts=6, seed=42) - self.model = MixedMNL( + self.model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost", graph=dataset.adjacency, @@ -342,12 +342,12 @@ def test_simulate(self): assert "draw" in sim.columns def test_elasticity(self): - elast = self.model.elasticity(variable="cost") - assert isinstance(elast, pd.Series) + with pytest.raises(NotImplementedError): + self.model.elasticity(variable="cost") def test_cross_elasticity(self): - cross_elast = self.model.cross_elasticity(variable="cost") - assert isinstance(cross_elast, pd.Series) + with pytest.raises(NotImplementedError): + self.model.cross_elasticity(variable="cost") # --------------------------------------------------------------------------- @@ -360,7 +360,7 @@ class TestCacheInvalidation: def test_mnl_cache_cleared_on_reestimate(self): dataset = simulate_mnl(n_obs=500, n_alts=4, seed=42) - model = MNL(dataset.choice_table, formula="alt_feature + obs_x_alt") + model = ChoiceModel(dataset.choice_table, formula="alt_feature + obs_x_alt") model.fit() # Populate caches @@ -379,7 +379,7 @@ def test_mnl_cache_cleared_on_reestimate(self): def test_nested_cache_cleared_on_reestimate(self): dataset = simulate_nested_logit(n_obs=500, n_alts=4, seed=42) - model = NestedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost + income_x_time", nests=dataset.nests, @@ -409,56 +409,61 @@ class TestProtocolConformance: """Test that all models conform to the ChoiceModel protocol.""" def test_mnl_is_choice_model(self): - from locpick.models.base import ChoiceModel + from locpick.models.base import ChoiceModelProtocol + from locpick.models.choice_model import ChoiceModel dataset = simulate_mnl(n_obs=500, n_alts=4, seed=42) - model = MNL(dataset.choice_table, formula="alt_feature + obs_x_alt") - assert isinstance(model, ChoiceModel) + model = ChoiceModel(dataset.choice_table, formula="alt_feature + obs_x_alt") + assert isinstance(model, ChoiceModelProtocol) def test_nested_is_choice_model(self): - from locpick.models.base import ChoiceModel + from locpick.models.base import ChoiceModelProtocol + from locpick.models.choice_model import ChoiceModel dataset = simulate_nested_logit(n_obs=500, n_alts=4, seed=42) - model = NestedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost + income_x_time", nests=dataset.nests, ) - assert isinstance(model, ChoiceModel) + assert isinstance(model, ChoiceModelProtocol) def test_scl_is_choice_model(self): - from locpick.models.base import ChoiceModel + from locpick.models.base import ChoiceModelProtocol + from locpick.models.choice_model import ChoiceModel dataset = simulate_scl(n_obs=500, n_alts=6, seed=42) - model = MNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost", graph=dataset.adjacency, ) - assert isinstance(model, ChoiceModel) + assert isinstance(model, ChoiceModelProtocol) def test_mixed_is_choice_model(self): - from locpick.models.base import ChoiceModel + from locpick.models.base import ChoiceModelProtocol + from locpick.models.choice_model import ChoiceModel dataset = simulate_mixed_logit(n_obs=500, n_alts=4, seed=42) - model = MixedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost", random_params={"time": ParamDistribution("normal", "time")}, n_draws=50, seed=42, ) - assert isinstance(model, ChoiceModel) + assert isinstance(model, ChoiceModelProtocol) def test_mscl_is_choice_model(self): - from locpick.models.base import ChoiceModel + from locpick.models.base import ChoiceModelProtocol + from locpick.models.choice_model import ChoiceModel dataset = simulate_mscl(n_obs=500, n_alts=6, seed=42) - model = MixedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost", graph=dataset.adjacency, random_params={"time": ParamDistribution("normal", "time")}, n_draws=50, ) - assert isinstance(model, ChoiceModel) + assert isinstance(model, ChoiceModelProtocol) diff --git a/tests/test_prediction_simulation.py b/tests/test_prediction_simulation.py index 8b94e83..1cfc8b3 100644 --- a/tests/test_prediction_simulation.py +++ b/tests/test_prediction_simulation.py @@ -8,7 +8,7 @@ import numpy.testing as npt import pandas as pd -from locpick import MNL, ChoiceTable, NestedMNL, NestSpec +from locpick import ChoiceModel, ChoiceTable, NestSpec from locpick.models.nested import NestingTree # --------------------------------------------------------------------------- @@ -103,7 +103,7 @@ class TestSimulate: def test_simulate_basic(self): """simulate() should return a DataFrame with expected columns.""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() simulated = model.simulate(ct, n_draws=1, seed=42) @@ -116,7 +116,7 @@ def test_simulate_basic(self): def test_simulate_multiple_draws(self): """simulate() with n_draws > 1 should return n_obs * n_draws rows.""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() n_draws = 5 @@ -128,7 +128,7 @@ def test_simulate_multiple_draws(self): def test_simulate_reproducibility(self): """simulate() with same seed should produce identical results.""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() sim1 = model.simulate(ct, n_draws=1, seed=123) @@ -139,7 +139,7 @@ def test_simulate_reproducibility(self): def test_simulate_different_seeds(self): """simulate() with different seeds should produce different results.""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() sim1 = model.simulate(ct, n_draws=1, seed=123) @@ -151,7 +151,7 @@ def test_simulate_different_seeds(self): def test_simulate_probabilities_are_valid(self): """Simulated choice probabilities should be valid (0, 1].""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() simulated = model.simulate(ct, n_draws=1, seed=42) @@ -162,7 +162,7 @@ def test_simulate_probabilities_are_valid(self): def test_simulate_chosen_alts_are_valid(self): """Simulated choices should be valid alternative IDs.""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() simulated = model.simulate(ct, n_draws=1, seed=42) @@ -183,7 +183,7 @@ class TestPredictionNewData: def test_predict_new_choosers(self): """Prediction on new choosers should produce valid probabilities.""" ct, _, _ = _make_simple_data(n_obs=200) - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() # Create new choosers @@ -219,7 +219,7 @@ def test_predict_new_choosers(self): def test_predict_sampled_choice_sets(self): """Prediction with sampled choice sets should use inclusion_probs.""" ct, _, _ = _make_sampled_data(n_obs=200, n_alts=20, sample_size=5) - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() # Probabilities should be valid @@ -279,7 +279,7 @@ def test_nested_logit_probabilities_with_inclusion_probs(self): ] ) - model = NestedMNL(ct, formula="cost + time - 1", nests=nests) + model = ChoiceModel(ct, formula="cost + time - 1", nests=nests) model.fit() # probabilities() should work @@ -303,7 +303,7 @@ class TestUtilitiesSamplingCorrection: def test_utilities_include_sampling_correction(self): """utilities() should include log(inclusion_probs) when present.""" ct, _, _ = _make_sampled_data(n_obs=200, n_alts=20, sample_size=5) - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() utilities = model.utilities(ct) @@ -314,7 +314,7 @@ def test_utilities_include_sampling_correction(self): def test_utilities_without_sampling(self): """utilities() should work without sampling correction.""" ct, _, _ = _make_simple_data() - model = MNL(ct, formula="cost + time - 1") + model = ChoiceModel(ct, formula="cost + time - 1") model.fit() utilities = model.utilities(ct) diff --git a/tests/test_qmc_draws.py b/tests/test_qmc_draws.py index f1d6715..ac6d622 100644 --- a/tests/test_qmc_draws.py +++ b/tests/test_qmc_draws.py @@ -120,14 +120,14 @@ def test_resolve_rejects_unknown_type(): def test_mscl_qmc_fits_and_recovers(): """MSCL with draw_type='qmc' should fit and stay in the same ballpark as the existing halton path on the standard simulated DGP.""" - from locpick import MixedMNL + from locpick import ChoiceModel from locpick.dgp import simulate_mscl - from locpick.spec.model_spec import ParamDistribution as PD + from locpick.models.mixed import ParamDistribution as PD ds = simulate_mscl(n_obs=400, n_alts=10, seed=3) rp = {"time": PD(param="time", distribution="normal")} - m_qmc = MixedMNL( + m_qmc = ChoiceModel( data=ds.choice_table, formula="cost + time + income_x_cost", graph=ds.adjacency, @@ -138,7 +138,7 @@ def test_mscl_qmc_fits_and_recovers(): res_qmc = m_qmc.fit() assert np.isfinite(res_qmc.log_likelihood) - m_halton = MixedMNL( + m_halton = ChoiceModel( data=ds.choice_table, formula="cost + time + income_x_cost", graph=ds.adjacency, diff --git a/tests/test_sampling.py b/tests/test_sampling.py index 44af719..5e368ad 100644 --- a/tests/test_sampling.py +++ b/tests/test_sampling.py @@ -13,7 +13,7 @@ import pandas as pd import pytest -from locpick import MNL, ChoiceTable, EstimationProblem +from locpick import ChoiceModel, ChoiceTable, EstimationProblem from locpick.data import ChoiceArrays # --------------------------------------------------------------------------- @@ -248,7 +248,7 @@ def test_model_runs_with_manual_inclusion_probs(self): from locpick.data.problem import EstimationProblem problem = EstimationProblem(arrays=arrays_with_both) - model = MNL(data=None, problem=problem) + model = ChoiceModel(data=None, problem=problem) result = model.fit() # Verify the model ran successfully @@ -298,7 +298,7 @@ def test_model_runs_with_manual_uniform_inclusion_probs(self): from locpick.data.problem import EstimationProblem problem = EstimationProblem(arrays=arrays_with_rates) - model = MNL(data=None, problem=problem) + model = ChoiceModel(data=None, problem=problem) result = model.fit() # Verify the model ran successfully @@ -487,7 +487,7 @@ def test_sampling_correction_improves_estimation(self): seed=42, ) - model = MNL(ct, formula="altval - 1") + model = ChoiceModel(ct, formula="altval - 1") result = model.fit() # The coefficient should be recoverable (within 30% tolerance) diff --git a/tests/test_sar_mnl.py b/tests/test_sar_mnl.py new file mode 100644 index 0000000..de33259 --- /dev/null +++ b/tests/test_sar_mnl.py @@ -0,0 +1,180 @@ +"""Parameter recovery tests for the SAR-MNL model (PML estimator). + +Tests that the SARMNL model can recover its true parameters from +synthetic data generated by ``simulate_sar_mnl``. These tests verify +that the PML estimation machinery works end-to-end and that recovered +coefficients are within a reasonable tolerance of the ground truth. + +Tolerances follow the existing ``test_param_recovery.py`` conventions: +- β coefficients: rtol=0.20 (generous, accounts for PML efficiency loss) +- ρ (spatial parameter): rtol=0.30 for moderate ρ; rtol=0.50 for low ρ +- ρ=0 equivalence with MNL: rtol=1e-4 (should be nearly exact) +""" + +import numpy as np +import numpy.testing as npt +import pytest + +from locpick import ChoiceModel, SARMNL +from locpick.dgp import simulate_sar_mnl + + +class TestSARMNLRecovery: + """Parameter recovery tests for the SAR-MNL model (PML estimator).""" + + def test_sar_mnl_recovers_beta_params(self): + """SAR-MNL should recover beta coefficients within tolerance.""" + dataset = simulate_sar_mnl(n_obs=5000, n_alts=50, rho=0.3, seed=2026) + model = SARMNL( + dataset.choice_table, + formula="alt_attr + obs_x_alt - 1", + W=dataset.W, + ) + result = model.fit() + + for param_name in ["alt_attr", "obs_x_alt"]: + true_val = dataset.true_params[param_name] + npt.assert_allclose( + result.coefficients[param_name], + true_val, + rtol=0.20, + err_msg=f"SAR-MNL failed to recover {param_name}", + ) + + def test_sar_mnl_recovers_rho(self): + """SAR-MNL should recover the spatial autoregressive parameter ρ.""" + dataset = simulate_sar_mnl(n_obs=5000, n_alts=50, rho=0.3, seed=2026) + model = SARMNL( + dataset.choice_table, + formula="alt_attr + obs_x_alt - 1", + W=dataset.W, + ) + result = model.fit() + + est_rho = result.coefficients["rho"] + npt.assert_allclose( + est_rho, + dataset.true_rho, + rtol=0.30, + err_msg="SAR-MNL failed to recover rho", + ) + + def test_sar_mnl_rho_zero_recovers_mnl(self): + """When ρ=0, SAR-MNL should recover standard MNL estimates.""" + dataset = simulate_sar_mnl(n_obs=5000, n_alts=50, rho=0.0, seed=42) + # Fit SAR-MNL + sar_model = SARMNL( + dataset.choice_table, + formula="alt_attr + obs_x_alt - 1", + W=dataset.W, + ) + sar_result = sar_model.fit() + # Fit standard MNL (via ChoiceModel without W) + mnl_model = ChoiceModel(dataset.choice_table, formula="alt_attr + obs_x_alt - 1") + mnl_result = mnl_model.fit() + + for param_name in ["alt_attr", "obs_x_alt"]: + npt.assert_allclose( + sar_result.coefficients[param_name], + mnl_result.coefficients[param_name], + rtol=0.02, + err_msg=f"SAR-MNL(ρ=0) != MNL for {param_name}", + ) + # ρ should be close to 0 (within sampling variability) + assert abs(sar_result.coefficients["rho"]) < 0.15 + + def test_sar_mnl_recovers_rho_low_spatial_dep(self): + """SAR-MNL should detect low spatial dependence (ρ=0.05) is near zero.""" + dataset = simulate_sar_mnl(n_obs=5000, n_alts=50, rho=0.05, seed=2026) + model = SARMNL( + dataset.choice_table, + formula="alt_attr + obs_x_alt - 1", + W=dataset.W, + ) + result = model.fit() + # Low ρ is hard to distinguish from zero — check it's not wildly off + est_rho = result.coefficients["rho"] + assert abs(est_rho) < 0.15, f"rho should be near 0, got {est_rho:.4f}" + + def test_sar_mnl_recovers_rho_moderate_spatial_dep(self): + """SAR-MNL should recover ρ at moderate spatial dependence (ρ=0.5).""" + dataset = simulate_sar_mnl(n_obs=5000, n_alts=50, rho=0.5, seed=2026) + model = SARMNL( + dataset.choice_table, + formula="alt_attr + obs_x_alt - 1", + W=dataset.W, + ) + result = model.fit() + npt.assert_allclose( + result.coefficients["rho"], + dataset.true_rho, + rtol=0.30, + err_msg="SAR-MNL failed to recover moderate rho", + ) + + def test_sar_mnl_smaller_n_alts(self): + """SAR-MNL should work with a small number of alternatives.""" + dataset = simulate_sar_mnl( + n_obs=3000, n_alts=12, rho=0.2, n_neighbors=3, seed=42 + ) + model = SARMNL( + dataset.choice_table, + formula="alt_attr + obs_x_alt - 1", + W=dataset.W, + ) + result = model.fit() + # Should converge and produce finite estimates + assert np.isfinite(result.coefficients["rho"]) + assert np.isfinite(result.coefficients["alt_attr"]) + + def test_sar_mnl_w_input_types(self): + """Graph, scipy.sparse, and dense W produce identical results.""" + import scipy.sparse as sp + + dataset = simulate_sar_mnl(n_obs=3000, n_alts=20, rho=0.2, seed=42) + W_graph = dataset.W # libpysal.graph.Graph + W_sparse = sp.csr_array(W_graph.sparse) # scipy.sparse + W_dense = W_sparse.toarray() # dense numpy + + model1 = SARMNL( + dataset.choice_table, "alt_attr + obs_x_alt - 1", W=W_graph + ) + result1 = model1.fit() + model2 = SARMNL( + dataset.choice_table, "alt_attr + obs_x_alt - 1", W=W_sparse + ) + result2 = model2.fit() + model3 = SARMNL( + dataset.choice_table, "alt_attr + obs_x_alt - 1", W=W_dense + ) + result3 = model3.fit() + + npt.assert_allclose( + result1.coefficients.values, + result2.coefficients.values, + rtol=1e-6, + err_msg="Graph vs sparse W give different results", + ) + npt.assert_allclose( + result1.coefficients.values, + result3.coefficients.values, + rtol=1e-6, + err_msg="Graph vs dense W give different results", + ) + + def test_sar_mnl_probabilities_sum_to_one(self): + """Choice probabilities should sum to 1 across alternatives.""" + dataset = simulate_sar_mnl(n_obs=1000, n_alts=20, rho=0.2, seed=42) + model = SARMNL( + dataset.choice_table, + formula="alt_attr + obs_x_alt - 1", + W=dataset.W, + ) + model.fit() + probs = model.probabilities() + npt.assert_allclose( + probs.sum(axis=1), + 1.0, + atol=1e-10, + err_msg="Probabilities do not sum to 1", + ) \ No newline at end of file diff --git a/tests/test_solvers.py b/tests/test_solvers.py index 705a389..17254b1 100644 --- a/tests/test_solvers.py +++ b/tests/test_solvers.py @@ -83,7 +83,7 @@ def ll_jax(x): import pandas as pd import pytest -from locpick import MNL, ChoiceTable +from locpick import ChoiceModel, ChoiceTable from locpick._solvers.lbfgs import LBFGSSolver from locpick._solvers.protocol import get_solver, list_solvers from locpick._solvers.trust_ncg import TrustKrylovSolver, TrustNCGSolver @@ -133,11 +133,11 @@ def test_trust_ncg_rejects_unknown_method(): [TrustNCGSolver, TrustKrylovSolver], ) def test_trust_ncg_matches_lbfgs_on_mnl(mnl_table, solver_cls): - baseline = MNL(mnl_table, formula="rent + jobs", solver=LBFGSSolver()) + baseline = ChoiceModel(mnl_table, formula="rent + jobs", solver=LBFGSSolver()) baseline.fit() ll_base = baseline._result.log_likelihood - model = MNL(mnl_table, formula="rent + jobs", solver=solver_cls()) + model = ChoiceModel(mnl_table, formula="rent + jobs", solver=solver_cls()) model.fit() ll = model._result.log_likelihood @@ -152,12 +152,12 @@ def test_trust_ncg_matches_lbfgs_on_mnl(mnl_table, solver_cls): def test_trust_ncg_matches_lbfgs_on_scl(): """Sanity check on a spatial model: trust-ncg + JAX HVP should reach the same SCL optimum as scipy L-BFGS-B.""" - from locpick import MNL + from locpick import ChoiceModel from locpick.dgp import simulate_scl ds = simulate_scl(n_obs=600, n_alts=12, seed=11) - base = MNL( + base = ChoiceModel( data=ds.choice_table, formula="cost + time + income_x_cost", graph=ds.adjacency, @@ -166,7 +166,7 @@ def test_trust_ncg_matches_lbfgs_on_scl(): ) base.fit() - test = MNL( + test = ChoiceModel( data=ds.choice_table, formula="cost + time + income_x_cost", graph=ds.adjacency, @@ -257,11 +257,11 @@ def test_optimagic_registered_lazily(): ) def test_optimagic_matches_lbfgs(mnl_table, algorithm): """OptimagicSolver should reach the same LL as scipy L-BFGS-B.""" - baseline = MNL(mnl_table, formula="rent + jobs", solver=LBFGSSolver()) + baseline = ChoiceModel(mnl_table, formula="rent + jobs", solver=LBFGSSolver()) baseline.fit() ll_base = baseline._result.log_likelihood - model = MNL( + model = ChoiceModel( mnl_table, formula="rent + jobs", solver=OptimagicSolver(algorithm=algorithm), @@ -279,6 +279,6 @@ def test_optimagic_matches_lbfgs(mnl_table, algorithm): def test_optimagic_unknown_algorithm_raises(mnl_table): solver = OptimagicSolver(algorithm="not_a_real_algorithm") - model = MNL(mnl_table, formula="rent + jobs", solver=solver) + model = ChoiceModel(mnl_table, formula="rent + jobs", solver=solver) with pytest.raises(Exception): model.fit() diff --git a/tests/test_sparse_design.py b/tests/test_sparse_design.py deleted file mode 100644 index d4863b1..0000000 --- a/tests/test_sparse_design.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Tests for sparse design-matrix support in JAX data/kernel paths.""" - -from __future__ import annotations - -import numpy as np -import pandas as pd -import pytest - -from locpick import ChoiceTable -from locpick._jax.data import ChoiceDataJAX -from locpick._jax.kernels import compute_utilities -from locpick.data.arrays import ChoiceArrays - - -def _make_choice_table(n_obs: int = 40, n_alts: int = 20, seed: int = 123) -> ChoiceTable: - """Create a compact synthetic choice table with sparse-like features.""" - rng = np.random.default_rng(seed) - - choosers = pd.DataFrame(index=pd.Index(np.arange(n_obs), name="oid")) - alternatives = pd.DataFrame( - { - "sparse_x": (rng.random(n_alts) < 0.15).astype(float), - "dense_x": rng.normal(size=n_alts), - }, - index=pd.Index(np.arange(n_alts), name="aid"), - ) - - utility = ( - -0.4 * alternatives["dense_x"].to_numpy()[None, :] - + 0.8 * alternatives["sparse_x"].to_numpy()[None, :] - ) - utility = utility + rng.gumbel(size=(n_obs, n_alts)) - choices = utility.argmax(axis=1) - - return ChoiceTable.from_tables( - choosers=choosers, - alternatives=alternatives, - chosen_alternatives=pd.Series(choices, index=choosers.index), - ) - - -def test_to_arrays_sparse_builds_sparse_matrix(): - """sparse=True should populate design_matrix_sparse when zero fraction exceeds threshold.""" - ct = _make_choice_table() - arrays = ct.to_arrays(formula="sparse_x + dense_x - 1", sparse=True, sparse_threshold=0.3) - - assert arrays.design_matrix_sparse is not None - - -def test_choice_data_jax_sparse_matches_dense_utilities(): - """Sparse design matrix is stored on ChoiceArrays but JAX kernels - currently use dense matrices only. This test verifies the dense - path works and the sparse matrix is available for future use.""" - pytest.importorskip("jax") - - ct = _make_choice_table() - arrays = ct.to_arrays(formula="sparse_x + dense_x - 1", sparse=True, sparse_threshold=0.3) - data = ChoiceDataJAX.from_arrays(arrays) - - assert arrays.design_matrix_sparse is not None - - beta = np.array([0.5, -0.2], dtype=np.float64) - v_dense = compute_utilities( - data.design_matrix, - beta, - data.n_obs, - data.n_alts, - inclusion_probs=data.inclusion_probs, - available=data.available, - ) - - # Verify dense computation produces valid utilities - assert np.all(np.isfinite(v_dense)) - assert v_dense.shape == (data.n_obs, data.n_alts) - - -def test_choice_data_jax_auto_sparse_uses_sparsity_hint(): - """ChoiceDataJAX should auto-convert highly sparse large matrices.""" - pytest.importorskip("jax") - - n_obs = 600 - n_alts = 200 - n_rows = n_obs * n_alts - k = 2 - - design = np.zeros((n_rows, k), dtype=np.float64) - design[::80, 0] = 1.0 - design[::120, 1] = -0.5 - - chosen = np.zeros((n_obs, n_alts), dtype=np.float64) - chosen[:, 0] = 1.0 - - arrays = ChoiceArrays( - design_matrix=design, - chosen=chosen, - n_obs=n_obs, - n_alts=n_alts, - param_names=["x0", "x1"], - ) - # Auto-sparse is handled at the ChoiceArrays level via to_arrays(). - data = ChoiceDataJAX.from_arrays(arrays) - assert data is not None - assert data.design_matrix is not None - - -def test_choice_data_jax_dense_disables_auto_sparse(): - """Dense matrices work end-to-end with ChoiceDataJAX.""" - pytest.importorskip("jax") - - n_obs = 600 - n_alts = 200 - n_rows = n_obs * n_alts - - design = np.zeros((n_rows, 2), dtype=np.float64) - design[::100, 0] = 1.0 - - chosen = np.zeros((n_obs, n_alts), dtype=np.float64) - chosen[:, 0] = 1.0 - - arrays = ChoiceArrays( - design_matrix=design, - chosen=chosen, - n_obs=n_obs, - n_alts=n_alts, - param_names=["x0", "x1"], - ) - - data = ChoiceDataJAX.from_arrays(arrays) - # With only ~1% nonzeros, auto-sparse may still trigger; this test - # verifies the API works end-to-end regardless of the auto-sparse decision. - assert data is not None - - -def test_distance_matrix(): - import numpy as np - import pandas as pd - - import locpick.data.distance as dm - - df = pd.DataFrame() - df["lat"] = [37.86, 37.85, 37.84, 37.87, 37.88] - df["lng"] = [-122.27, -122.28, -122.26, -122.29, -122.25] - dm.distance_matrix(df, method="euclidean") - dists_gc = dm.distance_matrix(df, method="greatcircle") - distances = [0, 2000, 4000, np.inf] - dm.distance_bands(dists_gc, distances) diff --git a/tests/test_spatial_models.py b/tests/test_spatial_models.py index be2fe4d..7ef405c 100644 --- a/tests/test_spatial_models.py +++ b/tests/test_spatial_models.py @@ -13,10 +13,10 @@ import pytest from scipy.special import logsumexp -from locpick import MNL, ChoiceTable -from locpick.models.mixed import MixedMNL, ParamDistribution -from locpick.models.mixed_nested import MixedNestedMNL -from locpick.models.nested import NestedMNL +from locpick import ChoiceModel, ChoiceTable +from locpick.models.mixed import ParamDistribution + + from locpick.models.scl import ( _resolve_spatial_graph, _scl_ll_numpy, @@ -390,7 +390,7 @@ def test_scl_estimate(self): ct, beta_cost, beta_time = _make_choice_data(n_obs=200, n_alts=5) omega = _make_simple_adjacency(n_alts=5) - model = MNL( + model = ChoiceModel( data=ct, formula="cost + time", graph=omega, @@ -408,7 +408,7 @@ def test_scl_requires_formula_or_spec(self): ct, _, _ = _make_choice_data() omega = _make_simple_adjacency() with pytest.raises(ValueError, match="formula.*spec"): - MNL( + ChoiceModel( data=ct, graph=omega, ) @@ -425,7 +425,7 @@ def test_scl_rho_estimate_near_one_for_mnl_data(self): ct, _, _ = _make_choice_data(n_obs=500, n_alts=5, seed=42) omega = _make_simple_adjacency(n_alts=5) - model = MNL( + model = ChoiceModel( data=ct, formula="cost + time", graph=omega, @@ -442,7 +442,7 @@ def test_scl_probabilities_method(self): ct, _, _ = _make_choice_data(n_obs=100, n_alts=5) omega = _make_simple_adjacency(n_alts=5) - model = MNL( + model = ChoiceModel( data=ct, formula="cost + time", graph=omega, @@ -462,7 +462,7 @@ def test_scl_with_scipy_sparse_graph(self): omega = _make_simple_adjacency(n_alts=5) sp_graph = sp.csr_array(omega) - model = MNL( + model = ChoiceModel( data=ct, formula="cost + time", graph=sp_graph, @@ -545,7 +545,7 @@ def test_mscl_requires_formula_or_spec(self): ct, _, _ = _make_choice_data() omega = _make_simple_adjacency() with pytest.raises(ValueError, match="formula.*spec"): - MNL( + ChoiceModel( data=ct, graph=omega, ) @@ -555,7 +555,7 @@ def test_mscl_estimate_no_random_params(self): ct, _, _ = _make_choice_data(n_obs=200, n_alts=5) omega = _make_simple_adjacency(n_alts=5) - model = MNL( + model = ChoiceModel( data=ct, formula="cost + time", graph=omega, @@ -571,7 +571,7 @@ def test_mscl_estimate_with_random_params(self): ct, _, _ = _make_choice_data(n_obs=200, n_alts=5) omega = _make_simple_adjacency(n_alts=5) - model = MixedMNL( + model = ChoiceModel( data=ct, formula="cost + time", graph=omega, @@ -595,7 +595,7 @@ def test_mscl_rho_near_one_for_mnl_data(self): ct, _, _ = _make_choice_data(n_obs=500, n_alts=5, seed=42) omega = _make_simple_adjacency(n_alts=5) - model = MNL( + model = ChoiceModel( data=ct, formula="cost + time", graph=omega, @@ -613,7 +613,7 @@ def test_mscl_with_scipy_sparse_graph(self): omega = _make_simple_adjacency(n_alts=5) sp_graph = sp.csr_array(omega) - model = MNL( + model = ChoiceModel( data=ct, formula="cost + time", graph=sp_graph, @@ -629,7 +629,7 @@ def test_mscl_halton_vs_random_draws(self): omega = _make_simple_adjacency(n_alts=5) # Halton draws (default) - model_halton = MixedMNL( + model_halton = ChoiceModel( data=ct, formula="cost + time", graph=omega, @@ -641,7 +641,7 @@ def test_mscl_halton_vs_random_draws(self): assert result_halton is not None # Pseudo-random draws - model_random = MixedMNL( + model_random = ChoiceModel( data=ct, formula="cost + time", graph=omega, @@ -895,7 +895,7 @@ def test_nested_scl_model_instantiation(simple_nest_data): """Test that NestedSpatiallyCorrelatedLogit can be instantiated.""" dataset = simple_nest_data - model = NestedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost", nests=dataset.nests, @@ -910,7 +910,7 @@ def test_nested_scl_model_fit(simple_nest_data): """Test that NestedSpatiallyCorrelatedLogit can fit and return results.""" dataset = simple_nest_data - model = NestedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost", nests=dataset.nests, @@ -947,7 +947,7 @@ def test_nested_scl_parameter_recovery(): seed=123, ) - model = NestedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost", nests=dataset.nests, @@ -1005,7 +1005,7 @@ def test_nested_scl_single_nest(): seed=42, ) - model = NestedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost", nests=dataset.nests, @@ -1032,7 +1032,7 @@ def test_nested_scl_mnl_equivalence(): seed=42, ) - model = NestedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost", nests=dataset.nests, @@ -1060,7 +1060,7 @@ def test_nested_scl_invalid_inputs(): # Missing formula and spec with pytest.raises(ValueError, match="formula.*spec"): - NestedMNL( + ChoiceModel( dataset.choice_table, nests=dataset.nests, graph=dataset.adjacency, @@ -1356,7 +1356,7 @@ def test_mnscl_model_instantiation(simple_mnscl_data): """Test that MixedNestedSpatiallyCorrelatedLogit can be instantiated.""" dataset = simple_mnscl_data - model = MixedNestedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost", nests=dataset.nests, @@ -1373,7 +1373,7 @@ def test_mnscl_model_fit(simple_mnscl_data): """Test that MixedNestedSpatiallyCorrelatedLogit can fit and return results.""" dataset = simple_mnscl_data - model = MixedNestedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost", nests=dataset.nests, @@ -1416,7 +1416,7 @@ def test_mnscl_parameter_recovery(): seed=123, ) - model = MixedNestedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost", nests=dataset.nests, @@ -1479,7 +1479,7 @@ def test_mnscl_single_nest(): seed=42, ) - model = MixedNestedMNL( + model = ChoiceModel( dataset.choice_table, formula="cost + time + income_x_cost", nests=dataset.nests, @@ -1510,7 +1510,7 @@ def test_mnscl_invalid_inputs(): # Missing formula and spec with pytest.raises(ValueError, match="formula.*spec"): - MixedNestedMNL( + ChoiceModel( dataset.choice_table, nests=dataset.nests, graph=dataset.adjacency, From 00c5bfe13e04c97a22de9a92dc22e1b26e732f1e Mon Sep 17 00:00:00 2001 From: knaaptime Date: Fri, 19 Jun 2026 14:22:07 -0700 Subject: [PATCH 2/7] cg and gmm --- docs/source/api.rst | 20 +- docs/source/index.md | 1 + docs/source/user-guide/choicetable.md | 4 +- docs/source/user-guide/inference.md | 8 +- docs/source/user-guide/mixed.md | 6 +- docs/source/user-guide/mnl.md | 8 +- docs/source/user-guide/modelspec.md | 8 +- docs/source/user-guide/nested.md | 6 +- docs/source/user-guide/prediction.md | 4 +- docs/source/user-guide/sar_mnl.md | 130 +++++++ docs/source/user-guide/sar_mnl_demo.ipynb | 321 ++++++++++++++++++ docs/source/user-guide/spatial_mixed.md | 18 +- .../source/user-guide/spatial_mixed_nested.md | 14 +- .../user-guide/spatial_models_demo.ipynb | 4 +- docs/source/user-guide/spatial_nested.md | 8 +- locpick/_jax/sar_kernels.py | 206 +++++++++-- locpick/_kernels/sar_mnl_numpy.py | 259 ++++++++++++++ locpick/data/arrays.py | 2 +- locpick/dgp.py | 22 +- locpick/models/__init__.pyi | 6 +- locpick/models/_spatial_weights.py | 12 +- locpick/models/choice_model.py | 180 ++++++---- locpick/models/mixed.py | 56 +-- locpick/models/nested.py | 52 +-- locpick/models/sar_mnl.py | 228 +++++++++++-- locpick/models/scl.py | 16 +- tests/test_mixed_nested.py | 2 - tests/test_param_recovery.py | 1 - tests/test_sar_mnl.py | 85 ++++- tests/test_spatial_models.py | 2 - 30 files changed, 1349 insertions(+), 340 deletions(-) create mode 100644 docs/source/user-guide/sar_mnl.md create mode 100644 docs/source/user-guide/sar_mnl_demo.ipynb create mode 100644 locpick/_kernels/sar_mnl_numpy.py diff --git a/docs/source/api.rst b/docs/source/api.rst index 40c7f51..cbdc7ac 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -43,36 +43,34 @@ Model Specification Models ------ -.. currentmodule:: locpick.models.mnl +.. currentmodule:: locpick.models.choice_model .. autosummary:: :toctree: generated/ - MNL :no-index: + ChoiceModel :no-index: -.. currentmodule:: locpick.models.nested +.. currentmodule:: locpick.models.sar_mnl .. autosummary:: :toctree: generated/ - NestedMNL :no-index: - NestSpec :no-index: - NestingTree :no-index: + SARMNL :no-index: -.. currentmodule:: locpick.models.mixed +.. currentmodule:: locpick.models.nested .. autosummary:: :toctree: generated/ - MixedMNL :no-index: - ParamDistribution :no-index: + NestSpec :no-index: + NestingTree :no-index: -.. currentmodule:: locpick.models.mixed_nested +.. currentmodule:: locpick.models.mixed .. autosummary:: :toctree: generated/ - MixedNestedMNL :no-index: + ParamDistribution :no-index: .. currentmodule:: locpick.models.scl diff --git a/docs/source/index.md b/docs/source/index.md index 73c880f..0485cff 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -21,6 +21,7 @@ Mixed Logit Spatial Mixed Logit Spatial Nested Logit Spatial Mixed-Nested Logit +SAR-MNL Demo Simulated Location Choice Demo Spatial Models Demo Sampling Correction diff --git a/docs/source/user-guide/choicetable.md b/docs/source/user-guide/choicetable.md index 2f313a4..3a1f2ad 100644 --- a/docs/source/user-guide/choicetable.md +++ b/docs/source/user-guide/choicetable.md @@ -16,7 +16,7 @@ The `ChoiceTable` class is the primary data container for location choice modeli ## Quick Start ```python -from locpick import ChoiceTable, MNL +from locpick import ChoiceTable, ChoiceModel # Create a ChoiceTable from chooser and alternative data ct = ChoiceTable.from_tables( @@ -26,7 +26,7 @@ ct = ChoiceTable.from_tables( ) # Estimate an MNL model -model = MNL(ct, formula="cost + time - 1") +model = ChoiceModel(ct, formula="cost + time - 1") result = model.fit() print(result.summary()) ``` diff --git a/docs/source/user-guide/inference.md b/docs/source/user-guide/inference.md index cfd9c8d..3a5debb 100644 --- a/docs/source/user-guide/inference.md +++ b/docs/source/user-guide/inference.md @@ -40,8 +40,8 @@ Compare two nested models: ```python from locpick import lr_test -restricted = MNL(ct, formula="cost + time").fit() -unrestricted = NestedMNL(ct, formula="cost + time", nests=tree).fit() +restricted = ChoiceModel(ct, formula="cost + time").fit() +unrestricted = ChoiceModel(ct, formula="cost + time", nests=tree).fit() result = lr_test(restricted, unrestricted) print(result.summary()) @@ -75,8 +75,8 @@ restrictive nested or mixed alternative: ```python from locpick import hausman_test -mnl_fit = MNL(ct, formula="cost + time").fit() -nested_fit = NestedMNL(ct, formula="cost + time", nests=tree).fit() +mnl_fit = ChoiceModel(ct, formula="cost + time").fit() +nested_fit = ChoiceModel(ct, formula="cost + time", nests=tree).fit() # H0: MNL is consistent (IIA holds). Compare on the common parameters. result = hausman_test(efficient=mnl_fit, consistent=nested_fit) diff --git a/docs/source/user-guide/mixed.md b/docs/source/user-guide/mixed.md index b2eda9c..72a7338 100644 --- a/docs/source/user-guide/mixed.md +++ b/docs/source/user-guide/mixed.md @@ -6,12 +6,12 @@ This user guide is a placeholder. Full content will be added in a future release ## Overview -The `MixedMNL` class estimates mixed logit (random coefficients) models, which generalize MNL by allowing coefficients to follow random distributions. +The `ChoiceModel` class estimates mixed logit (random coefficients) models when the `random_params` parameter is provided. Mixed logit generalizes MNL by allowing coefficients to follow random distributions. ## Quick Start ```python -from locpick import ChoiceTable, MixedMNL +from locpick import ChoiceTable, ChoiceModel from locpick.models.mixed import ParamDistribution ct = ChoiceTable.from_tables(choosers, alternatives, chosen_alternatives=choices) @@ -20,7 +20,7 @@ random_params = { "cost": ParamDistribution(distribution="normal", param="cost"), } -model = MixedMNL(ct, formula="cost + time - 1", random_params=random_params, n_draws=100) +model = ChoiceModel(ct, formula="cost + time - 1", random_params=random_params, n_draws=100) result = model.fit() print(result.summary()) ``` \ No newline at end of file diff --git a/docs/source/user-guide/mnl.md b/docs/source/user-guide/mnl.md index b6eaa66..a84f3e3 100644 --- a/docs/source/user-guide/mnl.md +++ b/docs/source/user-guide/mnl.md @@ -6,7 +6,7 @@ This user guide is a placeholder. Full content will be added in a future release ## Overview -The `MNL` class estimates multinomial logit (MNL) models for location choice. It supports: +The `ChoiceModel` class estimates multinomial logit (MNL) models for location choice. It supports: - Formula/scoped-term model specification - JAX-accelerated log-likelihood and gradient computation @@ -16,10 +16,10 @@ The `MNL` class estimates multinomial logit (MNL) models for location choice. It ## Quick Start ```python -from locpick import ChoiceTable, MNL +from locpick import ChoiceTable, ChoiceModel ct = ChoiceTable.from_tables(choosers, alternatives, chosen_alternatives=choices) -model = MNL(ct, formula="cost + time - 1") +model = ChoiceModel(ct, formula="cost + time - 1") result = model.fit() print(result.summary()) ``` @@ -29,6 +29,6 @@ print(result.summary()) ```python # JAX is the default and only production backend # NumPy kernels exist for reference/testing only -model = MNL(ct, formula="cost + time - 1") +model = ChoiceModel(ct, formula="cost + time - 1") result = model.fit() ``` \ No newline at end of file diff --git a/docs/source/user-guide/modelspec.md b/docs/source/user-guide/modelspec.md index bbf76d0..a85dfb5 100644 --- a/docs/source/user-guide/modelspec.md +++ b/docs/source/user-guide/modelspec.md @@ -17,9 +17,9 @@ locpick uses formula/scoped-term specification for model structure: Typically models are specified using Wilkinson formulas ```python -from locpick import ChoiceTable, MNL +from locpick import ChoiceTable, ChoiceModel -model = MNL(ct, formula="cost + time - 1") +model = ChoiceModel(ct, formula="cost + time - 1") ``` The `ChoiceTable` handles data construction intelligently. In this example `cost` is fixed @@ -42,10 +42,10 @@ ct = ct.add_pairwise_variable("time", time_series) ## Scoped Terms ```python -from locpick import ModelSpec, MNL +from locpick import ModelSpec, ChoiceModel spec = ModelSpec(formula="cost + time - 1").alternative_specific("time", reference="walk") -model = MNL(ct, spec=spec) +model = ChoiceModel(ct, spec=spec) ``` ## Generated Interaction Variables diff --git a/docs/source/user-guide/nested.md b/docs/source/user-guide/nested.md index bffcda4..c9ad777 100644 --- a/docs/source/user-guide/nested.md +++ b/docs/source/user-guide/nested.md @@ -6,12 +6,12 @@ This user guide is a placeholder. Full content will be added in a future release ## Overview -The `NestedMNL` class estimates nested logit models, which generalize MNL by grouping alternatives into nests with correlated error terms. +The `ChoiceModel` class estimates nested logit models when the `nests` parameter is provided. Nested logit generalizes MNL by grouping alternatives into nests with correlated error terms. ## Quick Start ```python -from locpick import ChoiceTable, NestedMNL +from locpick import ChoiceTable, ChoiceModel from locpick.models.nested import NestSpec, NestingTree ct = ChoiceTable.from_tables(choosers, alternatives, chosen_alternatives=choices) @@ -23,7 +23,7 @@ tree = NestingTree( ] ) -model = NestedMNL(ct, formula="cost + time - 1", nests=tree) +model = ChoiceModel(ct, formula="cost + time - 1", nests=tree) result = model.fit() print(result.summary()) ``` \ No newline at end of file diff --git a/docs/source/user-guide/prediction.md b/docs/source/user-guide/prediction.md index 4cc0d19..6176023 100644 --- a/docs/source/user-guide/prediction.md +++ b/docs/source/user-guide/prediction.md @@ -13,10 +13,10 @@ Prediction and simulation are **model methods**, not `FitResult` methods. After - `model.predict(result, data)` — Predicted choices ```python -from locpick import ChoiceTable, MNL +from locpick import ChoiceTable, ChoiceModel ct = ChoiceTable.from_tables(choosers, alternatives, chosen_alternatives=choices) -model = MNL(ct, formula="cost + time - 1") +model = ChoiceModel(ct, formula="cost + time - 1") result = model.fit() # Choice probabilities (n_obs × n_alts ndarray) diff --git a/docs/source/user-guide/sar_mnl.md b/docs/source/user-guide/sar_mnl.md new file mode 100644 index 0000000..9f2d965 --- /dev/null +++ b/docs/source/user-guide/sar_mnl.md @@ -0,0 +1,130 @@ +# Spatial Autoregressive Multinomial Logit (SAR-MNL) + +```{note} +This user guide covers the `SARMNL` model class, which implements a +spatial autoregressive lag in the utility of alternatives (spatial +locations) using the pseudo maximum likelihood (PML) estimator from +Smirnov (2010). +``` + +## Overview + +The `SARMNL` class models spatial spillover in the **systematic utility** +of alternatives via a spatial autoregressive (SAR) lag: + +$$V_j = \rho \sum_k w_{jk} V_k + Z_j \beta + X_{ij} \gamma$$ + +where $W$ is a $J \times J$ spatial weights matrix connecting alternatives +(spatial locations). The reduced-form utility is: + +$$V^* = (I - \rho W)^{-1} (Z\beta + X\gamma)$$ + +normalised by $D = \text{diag}((I - \rho W)^{-1})$ for consistency +(Smirnov 2010). Choice probabilities follow standard MNL softmax over +the spatially-filtered, variance-normalised utilities. + +### Key features + +- **PML estimator** (Smirnov 2010): consistent, no log-determinant needed +- **JAX autodiff**: gradients through the spatial solve and variance normalisation +- **Dense and conjugate-gradient solve paths**: auto-selected by alternative count +- **Linearized GMM fallback** (Carrión-Flores et al. 2018): for very large J +- **libpysal Graph support**: canonical W type, matching bayespecon +- **Marginal effects**: direct, indirect, and total (LeSage & Pace 2009) + +## Quick Start + +```python +from locpick import ChoiceTable, SARMNL +from libpysal.graph import Graph + +# Build spatial weights matrix connecting alternatives +W = Graph.build_knn(gdf, k=7).transform("r") + +# Create choice data +ct = ChoiceTable.from_tables(choosers, alternatives, chosen_alternatives=choices) + +# Estimate SAR-MNL +model = SARMNL(ct, formula="cost + time - 1", W=W) +result = model.fit() +print(result.summary()) +``` + +## Estimation Methods + +### PML (default) + +The pseudo maximum likelihood estimator (Smirnov 2010) is the default. +It uses JAX autodiff through the spatial solve $(I - \rho W)^{-1}$ and +variance normalisation $\text{diag}((I - \rho W)^{-1})$. + +- **Dense solve** (n_alts ≤ 2000): LU factorisation, exact gradients +- **Conjugate gradient** (n_alts > 2000): iterative solve, power-series + diagonal approximation + +```python +# Auto-select (default) +model = SARMNL(ct, formula="cost + time - 1", W=W) + +# Force dense solve +model = SARMNL(ct, formula="cost + time - 1", W=W, estimator="pml") + +# Force conjugate gradient +model = SARMNL(ct, formula="cost + time - 1", W=W, estimator="pml_cg") +``` + +### Linearized GMM + +For very large alternative sets where even CG is too slow, the +linearized GMM estimator (Carrión-Flores et al. 2018) avoids matrix +inversion entirely via a two-step procedure: + +1. **Step 1**: Standard MNL estimation (ignoring spatial dependence) +2. **Step 2**: Two-stage least squares (TSLS) with instruments $[X, WX]$ + +```python +model = SARMNL(ct, formula="cost + time - 1", W=W, estimator="linearized_gmm") +result = model.fit() +``` + +## Spatial Weights Matrix + +`SARMNL` accepts `libpysal.graph.Graph` as the canonical W type +(matching the bayespecon package). `scipy.sparse` matrices and dense +NumPy arrays are also accepted for convenience. + +```python +from libpysal.graph import Graph + +# k-nearest-neighbor graph +W = Graph.build_knn(gdf, k=7).transform("r") + +# Contiguity graph +W = Graph.build_contiguity(gdf, rook=False).transform("r") + +# Distance band +W = Graph.build_distance_band(gdf, threshold=1000).transform("r") +``` + +## Marginal Effects + +In the SAR-MNL model, a change in an attribute of alternative $j$ +affects not only $j$'s utility but also neighbouring alternatives +through the spatial multiplier $(I - \rho W)^{-1}$. + +```python +result = model.fit() + +# Compute marginal effects for a variable +me = model.marginal_effects(variable="cost") +print(me["direct"]) # impact on own alternative +print(me["indirect"]) # spillover to neighbouring alternatives +print(me["total"]) # direct + indirect +``` + +## References + +- Smirnov, O.A. (2010). "Modeling Spatial Discrete Choice." *Regional Science and Urban Economics*, 40, 292–298. +- Carrión-Flores, C.E., Flores-Lagunes, A., & Guci, L. (2018). "An Estimator for Discrete-Choice Models with Spatial Lag Dependence Using Large Samples." *RSUE*, 69, 77–93. +- LeSage, J.P. & Pace, R.K. (2009). *Introduction to Spatial Econometrics*. +- Krisztin, T., Piribauer, P., & Wögerer, M. (2022). "A Spatial Multinomial Logit Model for Analysing Urban Expansion." *Spatial Economic Analysis*, 17(2), 223–244. \ No newline at end of file diff --git a/docs/source/user-guide/sar_mnl_demo.ipynb b/docs/source/user-guide/sar_mnl_demo.ipynb new file mode 100644 index 0000000..6c11de7 --- /dev/null +++ b/docs/source/user-guide/sar_mnl_demo.ipynb @@ -0,0 +1,321 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1173f2f8", + "metadata": {}, + "source": [ + "# SAR-MNL: Spatial Autoregressive Multinomial Logit Demo\n", + "\n", + "This notebook demonstrates the `SARMNL` class — a spatial autoregressive MNL model\n", + "that specifies a spatial lag in the systematic utility of alternatives (locations).\n", + "\n", + "The model specifies:\n", + "\n", + "$$V_j = \\rho \\sum_k w_{jk} V_k + Z_j \\beta + X_{ij} \\gamma$$\n", + "\n", + "yielding reduced-form utilities $V^* = (I - \\rho W)^{-1}(Z\\beta + X\\gamma)$,\n", + "normalised by $D = \\text{diag}((I - \\rho W)^{-1})$, with standard MNL choice probabilities.\n", + "\n", + "Estimation is via pseudo maximum likelihood (PML, Smirnov 2010) with JAX autodiff." + ] + }, + { + "cell_type": "markdown", + "id": "e7844e2e", + "metadata": {}, + "source": [ + "## 1. Setup and Imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0f04aab", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "from locpick import SARMNL, ChoiceModel\n", + "from locpick.dgp import simulate_sar_mnl" + ] + }, + { + "cell_type": "markdown", + "id": "18e91aa6", + "metadata": {}, + "source": [ + "## 2. Data Preparation\n", + "\n", + "We generate synthetic data from a known SAR-MNL data generating process using `simulate_sar_mnl`. This creates:\n", + "- **Choosers**: observations with an income attribute\n", + "- **Alternatives**: spatial locations with cost and time attributes\n", + "- **Spatial weights matrix (W)**: a circular adjacency graph connecting nearby alternatives\n", + "- **Choices**: simulated from the SAR-MNL probability with a known spatial autoregressive parameter ρ" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32d1b494", + "metadata": {}, + "outputs": [], + "source": [ + "# Generate synthetic SAR-MNL data\n", + "# n_obs=2000 choosers, n_alts=12 alternatives, rho=0.5\n", + "dataset = simulate_sar_mnl(n_obs=2000, n_alts=12, rho=0.5, seed=42)\n", + "\n", + "print(f\"Observations: {dataset.n_obs}\")\n", + "print(f\"Alternatives: {dataset.n_alts}\")\n", + "print(f\"True rho: {dataset.true_rho}\")\n", + "print(f\"True params: {dataset.true_params}\")\n", + "print(f\"\\nChoosers columns: {list(dataset.choosers.columns)}\")\n", + "print(f\"Alternatives columns: {list(dataset.alternatives.columns)}\")\n", + "print(f\"\\nChoice table: {dataset.choice_table}\")" + ] + }, + { + "cell_type": "markdown", + "id": "9ad4b19b", + "metadata": {}, + "source": [ + "## 3. Instantiating the SARMNL Class\n", + "\n", + "The `SARMNL` class takes the same data interface as `ChoiceModel`, plus a spatial weights matrix `W`. The weights matrix defines the spatial relationships between alternatives (locations).\n", + "\n", + "Key parameters:\n", + "- **`data`**: A `ChoiceTable` containing choosers, alternatives, and choices\n", + "- **`formula`**: A formulaic formula string for the utility function\n", + "- **`W`**: Spatial weights matrix (libpysal Graph, scipy.sparse, or np.ndarray) — row-standardised internally\n", + "- **`solver`**: Default `\"lbfgs\"` (scipy L-BFGS-B, the fastest path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bd918425", + "metadata": {}, + "outputs": [], + "source": [ + "# Create the SAR-MNL model\n", + "# W is the spatial weights matrix from the DGP (circular adjacency)\n", + "model_sar = SARMNL(\n", + " dataset.choice_table,\n", + " formula=\"cost + time + income_x_cost - 1\",\n", + " W=dataset.adjacency,\n", + ")\n", + "\n", + "print(f\"Model: {model_sar}\")\n", + "print(f\"W shape: {dataset.adjacency.shape}\")\n", + "print(f\"W type: {type(dataset.adjacency).__name__}\")" + ] + }, + { + "cell_type": "markdown", + "id": "f1ed1cd9", + "metadata": {}, + "source": [ + "## 4. Model Estimation\n", + "\n", + "Fit the SAR-MNL model and compare with a standard MNL (no spatial lag). The spatial autoregressive parameter ρ captures the degree of spatial spillover in utilities — nearby locations influence each other's attractiveness." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e757464e", + "metadata": {}, + "outputs": [], + "source": [ + "# Fit SAR-MNL\n", + "t0 = time.perf_counter()\n", + "result_sar = model_sar.fit()\n", + "t_sar = time.perf_counter() - t0\n", + "\n", + "print(\"=== SAR-MNL Results ===\")\n", + "print(result_sar.summary())\n", + "print(f\"\\nFit time: {t_sar:.2f}s\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6f9bd5d", + "metadata": {}, + "outputs": [], + "source": [ + "# Fit standard MNL (no spatial lag) for comparison\n", + "model_mnl = ChoiceModel(dataset.choice_table, formula=\"cost + time + income_x_cost - 1\")\n", + "t0 = time.perf_counter()\n", + "result_mnl = model_mnl.fit()\n", + "t_mnl = time.perf_counter() - t0\n", + "\n", + "print(\"=== Standard MNL Results ===\")\n", + "print(result_mnl.summary())\n", + "print(f\"\\nFit time: {t_mnl:.2f}s\")" + ] + }, + { + "cell_type": "markdown", + "id": "52ba1635", + "metadata": {}, + "source": [ + "## 5. Results Visualization\n", + "\n", + "Compare the estimated parameters from SAR-MNL vs standard MNL, and check how well the SAR-MNL model recovers the true spatial autoregressive parameter ρ." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f33c611a", + "metadata": {}, + "outputs": [], + "source": [ + "# Compare coefficient estimates\n", + "print(\"=== Parameter Recovery ===\")\n", + "print(f\"True rho: {dataset.true_rho:.3f}\")\n", + "print(f\"Estimated rho (SAR-MNL): {result_sar.coefficients['rho']:.3f}\")\n", + "print()\n", + "\n", + "# Side-by-side coefficient comparison\n", + "comparison = pd.DataFrame(\n", + " {\n", + " \"True\": [dataset.true_params.get(c, np.nan) for c in result_sar.coefficients.index],\n", + " \"SAR-MNL\": result_sar.coefficients.values,\n", + " \"MNL\": [result_mnl.coefficients.get(c, np.nan) for c in result_sar.coefficients.index],\n", + " },\n", + " index=result_sar.coefficients.index,\n", + ")\n", + "print(comparison.round(4))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72b9c5ca", + "metadata": {}, + "outputs": [], + "source": [ + "# Fit statistics comparison\n", + "print(\"=== Fit Statistics ===\")\n", + "stats_df = pd.DataFrame(\n", + " {\n", + " \"SAR-MNL\": [\n", + " result_sar.log_likelihood,\n", + " result_sar.aic,\n", + " result_sar.bic,\n", + " result_sar.rho_squared,\n", + " result_sar.rho_bar_squared,\n", + " ],\n", + " \"MNL\": [\n", + " result_mnl.log_likelihood,\n", + " result_mnl.aic,\n", + " result_mnl.bic,\n", + " result_mnl.rho_squared,\n", + " result_mnl.rho_bar_squared,\n", + " ],\n", + " },\n", + " index=[\"Log-likelihood\", \"AIC\", \"BIC\", \"rho^2\", \"rho-bar^2\"],\n", + ")\n", + "print(stats_df.round(4))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ccb0c59a", + "metadata": {}, + "outputs": [], + "source": [ + "# Visualize spatial spillover: how does rho affect utilities?\n", + "# Compare raw utilities vs spatially-filtered utilities\n", + "probs_sar = model_sar.probabilities()\n", + "probs_mnl = model_mnl.probabilities()\n", + "\n", + "print(\"=== Probability Comparison (first 5 obs, first 5 alts) ===\")\n", + "print(\"SAR-MNL probabilities:\")\n", + "print(probs_sar[:5, :5].round(4))\n", + "print(\"\\nMNL probabilities:\")\n", + "print(probs_mnl[:5, :5].round(4))\n", + "\n", + "# The spatial autoregressive structure redistributes probability mass\n", + "# toward alternatives that are surrounded by attractive neighbors\n", + "print(f\"\\nMean absolute probability difference: {np.mean(np.abs(probs_sar - probs_mnl)):.6f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "46140f2d", + "metadata": {}, + "source": [ + "## 6. Performance Benchmarking\n", + "\n", + "Benchmark SAR-MNL vs standard MNL to highlight the computational cost of the spatial solve (matrix inversion at each evaluation). The hybrid estimation path (scipy LBFGS + JAX kernels + JAX autodiff) is used for both models." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "937d4c20", + "metadata": {}, + "outputs": [], + "source": [ + "# Warm fit (second fit, post-JIT cache)\n", + "t0 = time.perf_counter()\n", + "result_sar_warm = model_sar.fit()\n", + "t_sar_warm = time.perf_counter() - t0\n", + "\n", + "t0 = time.perf_counter()\n", + "result_mnl_warm = model_mnl.fit()\n", + "t_mnl_warm = time.perf_counter() - t0\n", + "\n", + "print(\"=== Warm Fit Performance ===\")\n", + "print(f\"SAR-MNL: {t_sar_warm:.3f}s (cold: {t_sar:.3f}s)\")\n", + "print(f\"MNL: {t_mnl_warm:.3f}s (cold: {t_mnl:.3f}s)\")\n", + "print(f\"SAR overhead: {t_sar_warm / t_mnl_warm:.1f}x\")\n", + "print(\"\\nBoth use the hybrid path: scipy LBFGS + JAX JIT'd kernels + JAX autodiff gradients\")" + ] + }, + { + "cell_type": "markdown", + "id": "13560f2b", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "The `SARMNL` class provides spatial autoregressive MNL estimation via pseudo maximum likelihood:\n", + "\n", + "- **Unified API**: Uses the same `ChoiceTable` + formula interface as `ChoiceModel`\n", + "- **Spatial weights**: Accepts libpysal Graph, scipy.sparse, or dense ndarray for `W`\n", + "- **JAX-accelerated**: Log-likelihood and gradient computed via JAX autodiff through the spatial solve\n", + "- **Hybrid estimation**: scipy LBFGS solver + JAX kernels (the fastest path for all locpick models)\n", + "- **Parameter recovery**: The spatial autoregressive parameter ρ is estimated alongside utility coefficients\n", + "\n", + "### When to use SAR-MNL vs SCL\n", + "\n", + "| Feature | SAR-MNL | SCL (ChoiceModel + graph) |\n", + "|---|---|---|\n", + "| Spatial structure | Autoregressive lag in utility | Paired GEV nests |\n", + "| Estimation | Pseudo-ML (no Jacobian) | Full-likelihood (GEV) |\n", + "| ρ range | (-1, 1) | (0, 1] |\n", + "| Variance normalisation | diag((I-ρW)⁻¹) | None (GEV handles it) |\n", + "| Computational cost | Matrix solve per eval | Scatter operations |\n", + "\n", + "Use **SAR-MNL** when you want a spatial lag in utilities (spillover effects).\n", + "Use **SCL** when you want spatial correlation in the error structure (GEV)." + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/user-guide/spatial_mixed.md b/docs/source/user-guide/spatial_mixed.md index 3407e55..ed2ad93 100644 --- a/docs/source/user-guide/spatial_mixed.md +++ b/docs/source/user-guide/spatial_mixed.md @@ -1,8 +1,8 @@ -# Mixed Logit with Spatial Correlation (MixedMNL + graph) +# Mixed Logit with Spatial Correlation (ChoiceModel + graph + random_params) ## Overview -A `MixedMNL` model constructed with a spatial `graph=` argument estimates the Mixed Spatially Correlated Logit (MSCL) of Bhat & Guo (2004): it combines a closed-form GEV spatial-correlation structure with random taste variation. The spatial component captures correlation between contiguous alternatives in closed form, while the mixing distribution captures unobserved heterogeneity across decision-makers. +A `ChoiceModel` constructed with a spatial `graph=` argument and `random_params=` estimates the Mixed Spatially Correlated Logit (MSCL) of Bhat & Guo (2004): it combines a closed-form GEV spatial-correlation structure with random taste variation. The spatial component captures correlation between contiguous alternatives in closed form, while the mixing distribution captures unobserved heterogeneity across decision-makers. ```{warning} The spatial mixed logit does **not** support alternative sampling correction. The MNL's uniform conditioning property does not hold for non-MNL GEV models. Always use the full alternative set (or sample without correction). @@ -31,7 +31,7 @@ The spatial GEV structure handles spatial correlation in closed form, so the sim ## Quick Start ```python -from locpick import ChoiceTable, MixedMNL +from locpick import ChoiceTable, ChoiceModel from locpick.models.mixed import ParamDistribution from libpysal import graph @@ -44,7 +44,7 @@ random_params = { } ct = ChoiceTable.from_tables(choosers, alternatives, chosen) -model = MixedMNL( +model = ChoiceModel( ct, formula="commute_time + density + shopping_access", graph=g, @@ -81,7 +81,7 @@ Supported distributions: ```python # Halton draws (quasi-random — more efficient) -model = MixedMNL( +model = ChoiceModel( ct, formula="cost + time", graph=g, random_params=random_params, n_draws=250, @@ -89,7 +89,7 @@ model = MixedMNL( ) # Pseudo-random draws -model = MixedMNL( +model = ChoiceModel( ct, formula="cost + time", graph=g, random_params=random_params, n_draws=500, @@ -99,12 +99,12 @@ model = MixedMNL( ## Spatial-Only Estimation -When no random parameters are needed, prefer `MNL(graph=...)` directly to avoid the simulation loop: +When no random parameters are needed, prefer `ChoiceModel(graph=...)` directly to avoid the simulation loop: ```python -from locpick import MNL +from locpick import ChoiceModel -model = MNL(ct, formula="cost + time", graph=g) +model = ChoiceModel(ct, formula="cost + time", graph=g) ``` ## References diff --git a/docs/source/user-guide/spatial_mixed_nested.md b/docs/source/user-guide/spatial_mixed_nested.md index a5c851d..1c7a53f 100644 --- a/docs/source/user-guide/spatial_mixed_nested.md +++ b/docs/source/user-guide/spatial_mixed_nested.md @@ -1,8 +1,8 @@ -# Mixed Nested Logit with Spatial Correlation (MixedNestedMNL + graph) +# Mixed Nested Logit with Spatial Correlation (ChoiceModel + graph + nests + random_params) ## Overview -A `MixedNestedMNL` model constructed with a spatial `graph=` argument estimates the most general model in the locpick spatial hierarchy. It combines three structures: +A `ChoiceModel` constructed with a spatial `graph=` argument, `nests=`, and `random_params=` estimates the most general model in the locpick spatial hierarchy. It combines three structures: 1. **Nested logit upper level**: alternatives are grouped into nests, each with a nest dissimilarity parameter $\lambda_m \in (0, 1]$ 2. **Spatial lower levels**: within each nest, spatial correlation between contiguous alternatives is captured via a paired GNL structure with nest-specific spatial dissimilarity $\rho_m \in (0, 1]$ @@ -25,7 +25,7 @@ where $\beta^r$ is the $r$-th draw of the random coefficients, approximated by s ## Quick Start ```python -from locpick import ChoiceTable, MixedNestedMNL +from locpick import ChoiceTable, ChoiceModel from locpick.models.nested import NestSpec, NestingTree from locpick.models.mixed import ParamDistribution from libpysal import graph @@ -47,7 +47,7 @@ random_params = { } ct = ChoiceTable.from_tables(choosers, alternatives, chosen_alternatives=choices) -model = MixedNestedMNL( +model = ChoiceModel( ct, formula="cost + time - 1", graph=g, @@ -78,19 +78,19 @@ Supported distributions: `normal`, `lognormal`, `triangular`, `uniform` (see [Mi ```python # QMC draws (default, Sobol sequences — most efficient) -model = MixedNestedMNL( +model = ChoiceModel( ct, formula="cost + time - 1", graph=g, nests=tree, random_params=random_params, n_draws=100, draw_type="qmc", ) # Halton draws -model = MixedNestedMNL( +model = ChoiceModel( ct, formula="cost + time - 1", graph=g, nests=tree, random_params=random_params, n_draws=250, draw_type="halton", ) # Pseudo-random draws -model = MixedNestedMNL( +model = ChoiceModel( ct, formula="cost + time - 1", graph=g, nests=tree, random_params=random_params, n_draws=500, draw_type="random", ) diff --git a/docs/source/user-guide/spatial_models_demo.ipynb b/docs/source/user-guide/spatial_models_demo.ipynb index ae08d0f..f7c0237 100644 --- a/docs/source/user-guide/spatial_models_demo.ipynb +++ b/docs/source/user-guide/spatial_models_demo.ipynb @@ -62,7 +62,7 @@ "metadata": {}, "outputs": [], "source": [ - "dc = gsp.io.get_acs(datasets, years=2019, level='tract', state_fips='11')" + "dc = gsp.io.get_acs(datasets, years=2019, level=\"tract\", state_fips=\"11\")" ] }, { @@ -217,7 +217,7 @@ " alt_params={\"cost\": -0.5, \"time\": -0.2},\n", " rho=0.7,\n", " seed=42,\n", - " adjacency=adj\n", + " adjacency=adj,\n", ")\n", "\n", "ct = scl_dataset.choice_table\n", diff --git a/docs/source/user-guide/spatial_nested.md b/docs/source/user-guide/spatial_nested.md index db0f800..77643a5 100644 --- a/docs/source/user-guide/spatial_nested.md +++ b/docs/source/user-guide/spatial_nested.md @@ -1,8 +1,8 @@ -# Nested Logit with Spatial Correlation (NestedMNL + graph) +# Nested Logit with Spatial Correlation (ChoiceModel + graph + nests) ## Overview -A `NestedMNL` model constructed with a spatial `graph=` argument estimates the Nested Spatially Correlated Logit (Nested SCL) model: a nested logit upper level combined with spatially correlated lower levels. Each nest has: +A `ChoiceModel` constructed with a spatial `graph=` argument and `nests=` estimates the Nested Spatially Correlated Logit (Nested SCL) model: a nested logit upper level combined with spatially correlated lower levels. Each nest has: - A spatial dissimilarity parameter $\rho_m \in (0, 1]$ governing correlation between spatially adjacent alternatives within the nest - A nest dissimilarity parameter $\lambda_m \in (0, 1]$ governing correlation between alternatives in the same nest @@ -26,7 +26,7 @@ When $\rho_m = 1$ and $\lambda_m = 1$ for all nests, the model reduces to MNL. ## Quick Start ```python -from locpick import ChoiceTable, NestedMNL +from locpick import ChoiceTable, ChoiceModel from locpick.models.nested import NestSpec, NestingTree from libpysal import graph @@ -42,7 +42,7 @@ tree = NestingTree( ) ct = ChoiceTable.from_tables(choosers, alternatives, chosen_alternatives=choices) -model = NestedMNL(ct, formula="cost + time - 1", graph=g, nests=tree) +model = ChoiceModel(ct, formula="cost + time - 1", graph=g, nests=tree) result = model.fit() print(result.summary()) ``` diff --git a/locpick/_jax/sar_kernels.py b/locpick/_jax/sar_kernels.py index 4a7d36d..2ccbcbc 100644 --- a/locpick/_jax/sar_kernels.py +++ b/locpick/_jax/sar_kernels.py @@ -4,40 +4,46 @@ Smirnov (2010): spatially-filtered utilities with variance normalisation by ``diag((I - ρW)^{-1})``, then standard MNL softmax. -The spatial solve ``(I - ρW) V* = V_base`` is done via LU factorisation -(dense, for moderate J) — the same matrix ``A = I - ρW`` is factorised -once and reused for all choosers. +Two solve paths are available: + +- **Dense** (default for ``n_alts ≤ 2000``): LU factorisation via + ``jax.scipy.linalg.solve`` / ``inv``. The same matrix ``A = I - ρW`` + is factorised once and reused for all choosers. +- **Conjugate gradient** (for ``n_alts > 2000``): iterative solve via + ``jax.scipy.sparse.linalg.cg``. Avoids materialising the dense + inverse; the diagonal of ``A^{-1}`` is estimated via a power-series + approximation. """ from __future__ import annotations -import functools - import jax import jax.numpy as jnp -import numpy as np import scipy.sparse as sp from locpick._jax.data import ChoiceDataJAX from locpick._jax.kernels import ( - _NEG_INF, compute_ll, compute_ll_contribs, compute_utilities, mnl_log_probs, ) from locpick._jax.objective import Objective -from locpick._jax.transforms import Identity, ParamTransform, Tanh +from locpick._jax.transforms import ParamTransform + +# Threshold for switching from dense solve to conjugate gradient. +_DENSE_CUTOFF = 2000 # --------------------------------------------------------------------------- -# Core PML kernel +# Dense solve path # --------------------------------------------------------------------------- -def _sar_mnl_ll_core(params, design_matrix, available, chosen, weights, - inclusion_probs, W_dense, n_obs, n_alts): - """SAR-MNL PML log-likelihood (Smirnov 2010). +def _sar_mnl_ll_core( + params, design_matrix, available, chosen, weights, inclusion_probs, W_dense, n_obs, n_alts +): + """SAR-MNL PML log-likelihood — dense solve path (Smirnov 2010). Parameters ---------- @@ -60,8 +66,12 @@ def _sar_mnl_ll_core(params, design_matrix, available, chosen, weights, # Base utilities: V_base (n_obs, n_alts) V_base = compute_utilities( - design_matrix, beta, n_obs, n_alts, - inclusion_probs=inclusion_probs, available=available, + design_matrix, + beta, + n_obs, + n_alts, + inclusion_probs=inclusion_probs, + available=available, ) # Spatial filter: solve (I - rho*W) V_filtered^T = V_base^T @@ -79,17 +89,22 @@ def _sar_mnl_ll_core(params, design_matrix, available, chosen, weights, return compute_ll(log_probs, chosen, weights) -def _sar_mnl_ll_contribs_core(params, design_matrix, available, chosen, weights, - inclusion_probs, W_dense, n_obs, n_alts): - """Per-observation SAR-MNL PML log-likelihood contributions.""" +def _sar_mnl_ll_contribs_core( + params, design_matrix, available, chosen, weights, inclusion_probs, W_dense, n_obs, n_alts +): + """Per-observation SAR-MNL PML log-likelihood contributions — dense path.""" k = design_matrix.shape[1] beta = params[:k] alpha_rho = params[k] rho = jnp.tanh(alpha_rho) V_base = compute_utilities( - design_matrix, beta, n_obs, n_alts, - inclusion_probs=inclusion_probs, available=available, + design_matrix, + beta, + n_obs, + n_alts, + inclusion_probs=inclusion_probs, + available=available, ) A = jnp.eye(n_alts) - rho * W_dense @@ -102,12 +117,111 @@ def _sar_mnl_ll_contribs_core(params, design_matrix, available, chosen, weights, return compute_ll_contribs(log_probs, chosen, weights) +# --------------------------------------------------------------------------- +# Conjugate-gradient solve path (for large n_alts) +# --------------------------------------------------------------------------- + + +def _cg_solve(A, B, n_alts): + """Solve A @ X = B via conjugate gradient, vectorised over columns of B. + + Uses ``jax.scipy.sparse.linalg.cg`` per column. JAX autodiff + works through CG via implicit differentiation. + """ + + def solve_one(b): + x, _ = jax.scipy.sparse.linalg.cg(A, b) + return x + + # vmap over columns of B (n_alts, n_rhs) + return jax.vmap(solve_one, in_axes=1, out_axes=1)(B) + + +def _diag_inv_power_series(rho, W_dense, n_alts, n_terms=20): + """Estimate diag((I - rho*W)^{-1}) via power series. + + Since W has zero diagonal, odd powers also have zero diagonal. + Only even powers contribute: d_jj = 1 + rho^2 (W^2)_jj + + rho^4 (W^4)_jj + ... Converges for |rho| < 1/omega_max. + """ + d = jnp.ones(n_alts) # first term: diag(I) = 1 + W_power = W_dense @ W_dense # W^2 + rho_sq = rho * rho + coeff = rho_sq + for _ in range(n_terms): + d = d + coeff * jnp.diag(W_power) + W_power = W_power @ W_power # W^{2k} + coeff = coeff * rho_sq + return d + + +def _sar_mnl_ll_cg_core( + params, design_matrix, available, chosen, weights, inclusion_probs, W_dense, n_obs, n_alts +): + """SAR-MNL PML log-likelihood — conjugate-gradient path. + + Uses CG for the spatial solve and a power-series approximation + for the variance normalisation diagonal. + """ + k = design_matrix.shape[1] + beta = params[:k] + alpha_rho = params[k] + rho = jnp.tanh(alpha_rho) + + V_base = compute_utilities( + design_matrix, + beta, + n_obs, + n_alts, + inclusion_probs=inclusion_probs, + available=available, + ) + + A = jnp.eye(n_alts) - rho * W_dense + # CG solve: A @ V_filtered^T = V_base^T + V_filtered = _cg_solve(A, V_base.T, n_alts).T # (n_obs, n_alts) + + # Variance normalisation via power series + D = _diag_inv_power_series(rho, W_dense, n_alts) + V_star = V_filtered / D[None, :] + + log_probs = mnl_log_probs(V_star, available) + return compute_ll(log_probs, chosen, weights) + + +def _sar_mnl_ll_contribs_cg_core( + params, design_matrix, available, chosen, weights, inclusion_probs, W_dense, n_obs, n_alts +): + """Per-observation SAR-MNL PML log-likelihood — CG path.""" + k = design_matrix.shape[1] + beta = params[:k] + alpha_rho = params[k] + rho = jnp.tanh(alpha_rho) + + V_base = compute_utilities( + design_matrix, + beta, + n_obs, + n_alts, + inclusion_probs=inclusion_probs, + available=available, + ) + + A = jnp.eye(n_alts) - rho * W_dense + V_filtered = _cg_solve(A, V_base.T, n_alts).T + D = _diag_inv_power_series(rho, W_dense, n_alts) + V_star = V_filtered / D[None, :] + + log_probs = mnl_log_probs(V_star, available) + return compute_ll_contribs(log_probs, chosen, weights) + + # --------------------------------------------------------------------------- # Objective builder # --------------------------------------------------------------------------- -def build_sar_mnl_objective(arrays, W_sparse: sp.csr_array) -> Objective: +def build_sar_mnl_objective(arrays, W_sparse: sp.csr_array, use_cg: bool = False) -> Objective: """Build an Objective for SAR-MNL PML estimation (Smirnov 2010). Parameters @@ -116,6 +230,9 @@ def build_sar_mnl_objective(arrays, W_sparse: sp.csr_array) -> Objective: Estimation data arrays. W_sparse : scipy.sparse.csr_array Row-standardised alt×alt spatial weights matrix (zero diagonal). + use_cg : bool, default False + If True, use conjugate-gradient solve (for large n_alts > 2000). + If False, use dense LU solve (faster for moderate n_alts). Returns ------- @@ -129,26 +246,55 @@ def build_sar_mnl_objective(arrays, W_sparse: sp.csr_array) -> Objective: n_alts = arrays.n_alts k = arrays.design_matrix.shape[1] + # Select solve path + if use_cg: + ll_core = _sar_mnl_ll_cg_core + ll_contribs_core = _sar_mnl_ll_contribs_cg_core + else: + ll_core = _sar_mnl_ll_core + ll_contribs_core = _sar_mnl_ll_contribs_core + # JIT-compiled closures — data and W are captured, only params is dynamic @jax.jit def _ll_jax(params): - return _sar_mnl_ll_core( - params, data.design_matrix, data.available, data.chosen, - data.weights, data.inclusion_probs, W_dense, n_obs, n_alts, + return ll_core( + params, + data.design_matrix, + data.available, + data.chosen, + data.weights, + data.inclusion_probs, + W_dense, + n_obs, + n_alts, ) @jax.jit def _ll_contribs_jax(params): - return _sar_mnl_ll_contribs_core( - params, data.design_matrix, data.available, data.chosen, - data.weights, data.inclusion_probs, W_dense, n_obs, n_alts, + return ll_contribs_core( + params, + data.design_matrix, + data.available, + data.chosen, + data.weights, + data.inclusion_probs, + W_dense, + n_obs, + n_alts, ) @jax.jit def _grad_jax(params): - return jax.grad(_sar_mnl_ll_core, argnums=0)( - params, data.design_matrix, data.available, data.chosen, - data.weights, data.inclusion_probs, W_dense, n_obs, n_alts, + return jax.grad(ll_core, argnums=0)( + params, + data.design_matrix, + data.available, + data.chosen, + data.weights, + data.inclusion_probs, + W_dense, + n_obs, + n_alts, ) param_names = list(arrays.param_names) + ["rho"] @@ -160,4 +306,4 @@ def _grad_jax(params): loglike_contribs_jax=_ll_contribs_jax, param_names=param_names, transform=transform, - ) \ No newline at end of file + ) diff --git a/locpick/_kernels/sar_mnl_numpy.py b/locpick/_kernels/sar_mnl_numpy.py new file mode 100644 index 0000000..1e171d4 --- /dev/null +++ b/locpick/_kernels/sar_mnl_numpy.py @@ -0,0 +1,259 @@ +"""NumPy kernels for the linearized GMM SAR-MNL estimator. + +Implements the two-step estimator from Carrión-Flores, Flores-Lagunes +& Guci (2018), extending Klier & McMillen (2008) to the multinomial +case. The linearization at ρ=0 avoids matrix inversion entirely, +making it feasible for very large alternative sets where the dense +PML solve is too expensive. + +Step 1: Standard MNL estimation (ignoring spatial dependence). +Step 2: Two-stage least squares (TSLS) using linearised gradients + and instruments Z = [X, WX]. +""" + +from __future__ import annotations + +import numpy as np + + +def compute_generalized_residuals(P, chosen): + """Compute generalized residuals u_ik = d_ik - P_ik. + + Parameters + ---------- + P : np.ndarray, shape (n_obs, n_alts) + MNL choice probabilities from Step 1. + chosen : np.ndarray, shape (n_obs, n_alts) + Binary indicator matrix. + + Returns + ------- + np.ndarray, shape (n_obs * n_alts,) + Flattened generalized residuals. + """ + return (chosen - P).ravel() + + +def compute_linearized_gradients(beta, P, X, WX, n_obs, n_alts, n_params): + """Compute linearized gradients for the SMNL estimator at ρ=0. + + At the linearization point ρ=0: (I - ρW)^{-1} = I, so the gradients + simplify to standard MNL gradients plus a spatial term. + + Parameters + ---------- + beta : np.ndarray, shape (n_params,) + Beta coefficients from Step 1 MNL. + P : np.ndarray, shape (n_obs, n_alts) + MNL choice probabilities from Step 1. + X : np.ndarray, shape (n_obs, n_alts, n_params) or (n_obs * n_alts, n_params) + Design matrix (can be in long or 3D format). + WX : np.ndarray, shape (n_obs, n_alts, n_params) + Spatially lagged design matrix. + n_obs : int + n_alts : int + n_params : int + + Returns + ------- + G_beta : np.ndarray, shape (n_obs * n_alts, n_params) + Gradient matrix for beta parameters. + G_rho : np.ndarray, shape (n_obs * n_alts,) + Gradient vector for the spatial parameter rho. + """ + # Reshape X to 3D if needed + if X.ndim == 2: + X_3d = X.reshape(n_obs, n_alts, n_params) + else: + X_3d = X + + # Beta gradients: G_{i,beta_k} = P_ik * (delta_{ilk} - P_il) * X_i + # For each (obs, alt) pair and each parameter: + # G_beta[i*J + k, p] = P[i,k] * (delta_{k,k} - P[i,k]) * X[i,k,p] + # + sum_{l != k} P[i,k] * (0 - P[i,l]) * X[i,l,p] + # = P[i,k] * (X[i,k,p] - sum_l P[i,l] * X[i,l,p]) + + # Compute weighted average of X across alternatives: sum_l P_l * X_l + # P is (n_obs, n_alts), X_3d is (n_obs, n_alts, n_params) + # weighted_X = sum_k P[i,k] * X[i,k,:] → (n_obs, n_params) + weighted_X = np.einsum("ik,ikp->ip", P, X_3d) # (n_obs, n_params) + + # G_beta[i*J + k, p] = P[i,k] * (X[i,k,p] - weighted_X[i,p]) + G_beta = np.zeros((n_obs * n_alts, n_params)) + for k in range(n_alts): + for p in range(n_params): + G_beta[k::n_alts, p] = P[:, k] * (X_3d[:, k, p] - weighted_X[:, p]) + + # Rho gradient: G_{i,rho} = P_ik * [(WX)_i * beta - sum_l P_il * (WX)_i * beta] + # = P_ik * (WX_i @ beta - sum_l P_il * WX_l @ beta) + # WX is (n_obs, n_alts, n_params), beta is (n_params,) + WX_beta = WX @ beta # (n_obs, n_alts) + weighted_WX_beta = np.sum(P * WX_beta, axis=1) # (n_obs,) + + G_rho = np.zeros(n_obs * n_alts) + for k in range(n_alts): + G_rho[k::n_alts] = P[:, k] * (WX_beta[:, k] - weighted_WX_beta) + + return G_beta, G_rho + + +def _remove_dependent_columns(Z, tol=1e-10): + """Remove linearly dependent columns via QR decomposition.""" + Q, R = np.linalg.qr(Z) + # Check diagonal of R for near-zero values + diag_R = np.abs(np.diag(R)) + keep = diag_R > tol * diag_R[0] + return Z[:, keep] + + +def smnl_tsls(G_beta, G_rho, u, Z): + """Two-stage least squares estimation of the linearized SMNL model. + + First stage: regress each gradient column on instruments Z. + Second stage: regress residuals u on fitted gradients. + + Parameters + ---------- + G_beta : np.ndarray, shape (n_obs * n_alts, n_params) + Gradient matrix for beta parameters. + G_rho : np.ndarray, shape (n_obs * n_alts,) + Gradient vector for the spatial parameter rho. + u : np.ndarray, shape (n_obs * n_alts,) + Generalized residuals from Step 1. + Z : np.ndarray, shape (n_obs * n_alts, n_instruments) + Instrument matrix [X, WX] (linearly independent columns). + + Returns + ------- + delta : np.ndarray, shape (n_params + 1,) + Parameter updates [Delta_beta_1, ..., Delta_beta_K, Delta_rho]. + se : np.ndarray, shape (n_params + 1,) + TSLS standard errors. + vcov : np.ndarray, shape (n_params + 1, n_params + 1) + TSLS covariance matrix. + """ + # Stack gradients: G = [G_beta, G_rho] + G = np.column_stack([G_beta, G_rho]) + n_params_total = G.shape[1] + n = len(u) + + # First stage: project G onto instruments Z + # G_hat = Z (Z'Z)^{-1} Z' G + ZtZ_inv = np.linalg.inv(Z.T @ Z) + G_hat = Z @ (ZtZ_inv @ (Z.T @ G)) + + # Second stage: OLS of u on G_hat + # delta = (G_hat' G_hat)^{-1} G_hat' u + GtG_inv = np.linalg.inv(G_hat.T @ G_hat) + delta = GtG_inv @ (G_hat.T @ u) + + # Residuals + e = u - G_hat @ delta + + # Variance: sigma^2 = e'e / (n - p) + sigma2 = e @ e / (n - n_params_total) + + # Covariance: V = sigma^2 * (G_hat' G_hat)^{-1} + vcov = sigma2 * GtG_inv + se = np.sqrt(np.maximum(np.diag(vcov), 0)) + + return delta, se, vcov + + +def fit_linearized_gmm(arrays, W_sparse): + """Two-step linearized GMM estimation (Carrión-Flores et al. 2018). + + Parameters + ---------- + arrays : ChoiceArrays + Estimation data arrays. + W_sparse : scipy.sparse.csr_array + Row-standardised alt×alt spatial weights matrix. + + Returns + ------- + dict + Dictionary with keys: 'beta', 'rho', 'se', 'vcov', 'log_likelihood'. + """ + from locpick._kernels.mnl_numpy import mnl_probs_numpy + + n_obs = arrays.n_obs + n_alts = arrays.n_alts + k = arrays.design_matrix.shape[1] + + dm = np.asarray(arrays.design_matrix, dtype=np.float64) + chosen = np.asarray(arrays.chosen, dtype=np.float64).reshape(n_obs, n_alts) + + if arrays.available is not None: + available = np.asarray(arrays.available, dtype=np.float64).reshape(n_obs, n_alts) + else: + available = np.ones((n_obs, n_alts), dtype=np.float64) + + # --- Step 1: Standard MNL estimation --- + from locpick._solvers import get_solver + + solver = get_solver("lbfgs") + + from locpick._jax.builders import build_mnl_objective + + objective = build_mnl_objective(arrays) + x0 = np.zeros(k) + solver_result = solver.solve( + objective=objective, + x0=x0, + param_names=list(arrays.param_names), + bounds=None, + fixed_mask=None, + ) + beta_step1 = solver_result.coefficients + + # Compute Step 1 probabilities + V = (dm @ beta_step1).reshape(n_obs, n_alts) + P = mnl_probs_numpy(V, available, inclusion_probs=None) + + # --- Step 2: TSLS --- + # Extract X from design matrix (long format → 3D) + X_3d = dm.reshape(n_obs, n_alts, k) + + # Compute WX: spatially lagged covariates + # W is (n_alts, n_alts), X varies across choosers + # WX[i, j, p] = sum_k W[j, k] * X[i, k, p] + W_dense = W_sparse.toarray() + WX = np.einsum("jk,ikp->ijp", W_dense, X_3d) # (n_obs, n_alts, k) + + # Generalized residuals + u = compute_generalized_residuals(P, chosen) + + # Linearized gradients + G_beta, G_rho = compute_linearized_gradients(beta_step1, P, X_3d, WX, n_obs, n_alts, k) + + # Instruments: [X, WX] in long format + X_long = dm # (n_obs * n_alts, k) + WX_long = WX.reshape(n_obs * n_alts, k) + Z = np.column_stack([X_long, WX_long]) + Z = _remove_dependent_columns(Z) + + # TSLS + delta, se, vcov = smnl_tsls(G_beta, G_rho, u, Z) + + # Assemble final parameters + beta_final = beta_step1 + delta[:k] + rho_final = delta[k] + + # Compute log-likelihood at final params (using spatial model) + A = np.eye(n_alts) - rho_final * W_dense + V_base = (dm @ beta_final).reshape(n_obs, n_alts) + V_filtered = np.linalg.solve(A, V_base.T).T + D = np.diag(np.linalg.inv(A)) + V_star = V_filtered / D[None, :] + log_probs = np.log(np.maximum(mnl_probs_numpy(V_star, available), 1e-30)) + ll = np.sum(chosen * log_probs) + + return { + "beta": beta_final, + "rho": rho_final, + "se": se, + "vcov": vcov, + "log_likelihood": float(ll), + "beta_step1": beta_step1, + } diff --git a/locpick/data/arrays.py b/locpick/data/arrays.py index efb9ce8..37802c1 100644 --- a/locpick/data/arrays.py +++ b/locpick/data/arrays.py @@ -8,7 +8,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any, Optional +from typing import Optional import jax.numpy as jnp import numpy as np diff --git a/locpick/dgp.py b/locpick/dgp.py index f10ac12..9a2e6e9 100644 --- a/locpick/dgp.py +++ b/locpick/dgp.py @@ -401,14 +401,18 @@ def _build_interactions(obs_ids, alt_ids, chooser_feature, alt_columns): alt_vals = alt_columns[alt_col] tiled_feat = np.repeat(chooser_feature, n_alts) tiled_alt = np.tile(alt_vals, n_obs) - name = f"{chooser_feature.name}_x_{alt_col}" if hasattr(chooser_feature, "name") else f"obs_x_{alt_col}" - interactions[name] = pd.Series( - tiled_feat * tiled_alt, index=interaction_index, name=name + name = ( + f"{chooser_feature.name}_x_{alt_col}" + if hasattr(chooser_feature, "name") + else f"obs_x_{alt_col}" ) + interactions[name] = pd.Series(tiled_feat * tiled_alt, index=interaction_index, name=name) return interactions, interaction_index -def _compute_det_utility(n_obs, n_alts, alternatives, alt_params, interactions, interaction_coefs=None): +def _compute_det_utility( + n_obs, n_alts, alternatives, alt_params, interactions, interaction_coefs=None +): """Compute deterministic utility from alt params and interactions. Parameters @@ -439,9 +443,7 @@ def _build_design_matrix(n_obs, alternatives, interactions, interaction_coefs): design_matrix : np.ndarray, shape (n_obs * n_alts, k) beta : np.ndarray, shape (k,) """ - beta = np.array([None] * len(alternatives.columns), dtype=float) design_matrix = np.tile(alternatives.to_numpy(), (n_obs, 1)) - beta = np.array([1.0] * len(alternatives.columns), dtype=float) # placeholder for name, coef in interaction_coefs.items(): if name in interactions: design_matrix = np.column_stack([design_matrix, interactions[name].to_numpy().ravel()]) @@ -2008,16 +2010,12 @@ def simulate_sar_mnl( alternatives = pd.DataFrame({"alt_attr": alt_attr}, index=alt_ids) # --- Interactions (chooser × alternative) -------------------------- - interaction_index = pd.MultiIndex.from_product( - [obs_ids, alt_ids], names=["oid", "aid"] - ) + interaction_index = pd.MultiIndex.from_product([obs_ids, alt_ids], names=["oid", "aid"]) obs_feat_tiled = np.repeat(obs_feature, n_alts) alt_attr_tiled = np.tile(alt_attr, n_obs) obs_x_alt_values = obs_feat_tiled * alt_attr_tiled interactions = { - "obs_x_alt": pd.Series( - obs_x_alt_values, index=interaction_index, name="obs_x_alt" - ) + "obs_x_alt": pd.Series(obs_x_alt_values, index=interaction_index, name="obs_x_alt") } # --- Base utilities: V_base = Zβ + Xγ (n_obs × n_alts) ------------- diff --git a/locpick/models/__init__.pyi b/locpick/models/__init__.pyi index 93f1b31..42cf136 100644 --- a/locpick/models/__init__.pyi +++ b/locpick/models/__init__.pyi @@ -45,12 +45,12 @@ from .nested import ( from .nested import ( naturalize_nest_params as naturalize_nest_params, ) +from .sar_mnl import ( + SARMNL as SARMNL, +) from .scl import ( EdgeStructure as EdgeStructure, ) from .scl import ( naturalize_rho as naturalize_rho, ) -from .sar_mnl import ( - SARMNL as SARMNL, -) diff --git a/locpick/models/_spatial_weights.py b/locpick/models/_spatial_weights.py index 764a5e6..b5a645d 100644 --- a/locpick/models/_spatial_weights.py +++ b/locpick/models/_spatial_weights.py @@ -13,8 +13,6 @@ from __future__ import annotations -from typing import Union - import numpy as np import scipy.sparse as sp @@ -48,9 +46,7 @@ def resolve_spatial_weights( Row-standardised CSR sparse matrix (float64), zero diagonal. """ # --- Reject legacy libpysal.weights.W ------------------------------- - if W.__class__.__module__.startswith("libpysal.weights") and not hasattr( - W, "sparse" - ): + if W.__class__.__module__.startswith("libpysal.weights") and not hasattr(W, "sparse"): raise TypeError( "Legacy libpysal.weights.W is not supported. " "Convert via libpysal.graph.Graph.from_W(w) or pass w.sparse." @@ -67,9 +63,7 @@ def resolve_spatial_weights( # --- Validate shape ------------------------------------------------- if W_sparse.shape != (n_alts, n_alts): - raise ValueError( - f"W shape {W_sparse.shape} does not match n_alts ({n_alts}, {n_alts})." - ) + raise ValueError(f"W shape {W_sparse.shape} does not match n_alts ({n_alts}, {n_alts}).") # --- Zero diagonal -------------------------------------------------- W_sparse.setdiag(0.0) @@ -129,4 +123,4 @@ def build_knn_graph( crs="EPSG:4326", ) W_graph = Graph.build_knn(gdf, k=k) - return W_graph.transform("r") \ No newline at end of file + return W_graph.transform("r") diff --git a/locpick/models/choice_model.py b/locpick/models/choice_model.py index fe829ef..18bcf1d 100644 --- a/locpick/models/choice_model.py +++ b/locpick/models/choice_model.py @@ -337,11 +337,13 @@ def _get_solver_inputs(self, arrays: ChoiceArrays): n_nests = self._nests.n_nests if self._is_spatial: # Nested SCL: [beta, alpha_rho_1..M, alpha_lambda_1..M] - x0 = np.concatenate([ - np.zeros(k), - np.zeros(n_nests), - self._nests.initial_alphas(), - ]) + x0 = np.concatenate( + [ + np.zeros(k), + np.zeros(n_nests), + self._nests.initial_alphas(), + ] + ) names = ( param_names_all + [f"alpha_rho_{name}" for name in self._nests.nest_names] @@ -359,12 +361,14 @@ def _get_solver_inputs(self, arrays: ChoiceArrays): k_random = self._k_random if self._is_spatial: # MSCL: [beta_fixed, alpha_rho, mean_*, sd_*] - x0 = np.concatenate([ - np.zeros(k_fixed), - np.zeros(1), - np.zeros(k_random), - np.full(k_random, 0.1), - ]) + x0 = np.concatenate( + [ + np.zeros(k_fixed), + np.zeros(1), + np.zeros(k_random), + np.full(k_random, 0.1), + ] + ) names = ( list(self._fixed_names) + ["rho"] @@ -373,11 +377,13 @@ def _get_solver_inputs(self, arrays: ChoiceArrays): ) else: # Mixed: [beta_fixed, mean_*, sd_*] - x0 = np.concatenate([ - np.zeros(k_fixed), - np.zeros(k_random), - np.full(k_random, 0.1), - ]) + x0 = np.concatenate( + [ + np.zeros(k_fixed), + np.zeros(k_random), + np.full(k_random, 0.1), + ] + ) names = list(self._full_param_names) return x0, names, None, None @@ -386,16 +392,20 @@ def _get_solver_inputs(self, arrays: ChoiceArrays): k_fixed = self._k_fixed k_random = self._k_random n_nests = self._nests.n_nests - fixed_param_names = [name for name in param_names_all if name not in self._random_params] + fixed_param_names = [ + name for name in param_names_all if name not in self._random_params + ] if self._is_spatial: # Mixed Nested SCL: [beta_fixed, alpha_rho_1..M, alpha_lambda_1..M, mean_*, sd_*] - x0 = np.concatenate([ - np.zeros(k_fixed), - np.zeros(n_nests), - self._nests.initial_alphas(), - np.zeros(k_random), - np.full(k_random, 0.1), - ]) + x0 = np.concatenate( + [ + np.zeros(k_fixed), + np.zeros(n_nests), + self._nests.initial_alphas(), + np.zeros(k_random), + np.full(k_random, 0.1), + ] + ) names = ( fixed_param_names + [f"alpha_rho_{name}" for name in self._nests.nest_names] @@ -405,12 +415,14 @@ def _get_solver_inputs(self, arrays: ChoiceArrays): ) else: # Mixed Nested: [beta_fixed, alpha_nest, mean_*, sd_*] - x0 = np.concatenate([ - np.zeros(k_fixed), - self._nests.initial_alphas(), - np.zeros(k_random), - np.full(k_random, 0.1), - ]) + x0 = np.concatenate( + [ + np.zeros(k_fixed), + self._nests.initial_alphas(), + np.zeros(k_random), + np.full(k_random, 0.1), + ] + ) names = ( fixed_param_names + [f"lambda_{name}" for name in self._nests.nest_names] @@ -432,26 +444,29 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: if not self._is_nested and not self._is_mixed: if self._is_spatial: from locpick._jax.builders import build_scl_objective + return build_scl_objective( arrays, self._edge_struct, self._allocation, self._edge_list ) from locpick._jax.builders import build_mnl_objective + return build_mnl_objective(arrays) # Nested (no random) if self._is_nested and not self._is_mixed: if self._is_spatial: from locpick._jax.builders import build_nested_scl_objective - return build_nested_scl_objective( - arrays, self._nest_matrix, self._edge_data_list - ) + + return build_nested_scl_objective(arrays, self._nest_matrix, self._edge_data_list) from locpick._jax.builders import build_nested_objective + return build_nested_objective(arrays, self._nest_matrix) # Mixed (no nests) if self._is_mixed and not self._is_nested: if self._is_spatial: from locpick._jax.builders import build_mscl_objective + return build_mscl_objective( arrays, self._edge_struct, @@ -462,6 +477,7 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: self._draws, ) from locpick._jax.builders import build_mixed_logit_objective + return build_mixed_logit_objective( arrays, random_col_indices=self._random_col_indices, @@ -473,6 +489,7 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: if self._is_nested and self._is_mixed: if self._is_spatial: from locpick._jax.builders import build_mnscl_objective + return build_mnscl_objective( arrays, self._nest_matrix, @@ -482,6 +499,7 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: self._draws, ) from locpick._jax.builders import build_mixed_nested_objective + return build_mixed_nested_objective( arrays, self._nest_matrix, @@ -552,9 +570,7 @@ def _build_fit_result( display_names = param_names_all + [ f"lambda_{name}" for name in self._nests.nest_names ] - std_errors = self._compute_se_nested( - all_params, arrays, k, lambdas - ) + std_errors = self._compute_se_nested(all_params, arrays, k, lambdas) return self._make_fit_result( solver_result, arrays, display_values, display_names, std_errors @@ -580,9 +596,7 @@ def _build_fit_result( + [f"mean_{n}" for n in self._random_param_names] + [f"sd_{n}" for n in self._random_param_names] ) - std_errors = self._compute_se_mscl( - all_params, arrays, k_fixed, rho - ) + std_errors = self._compute_se_mscl(all_params, arrays, k_fixed, rho) else: # Layout: [beta_fixed, mean_*, sd_*] display_values = all_params @@ -598,7 +612,9 @@ def _build_fit_result( k_fixed = self._k_fixed k_random = self._k_random n_nests = self._nests.n_nests - fixed_param_names = [name for name in param_names_all if name not in self._random_params] + fixed_param_names = [ + name for name in param_names_all if name not in self._random_params + ] if self._is_spatial: # Layout: [beta_fixed, alpha_rho_1..M, alpha_lambda_1..M, mean_*, sd_*] @@ -741,14 +757,16 @@ def _compute_se_mscl(self, all_params, arrays, k_fixed, rho): hess = self._compute_hessian(all_params) se_alpha = self._compute_std_errors_from_hessian(hess) se_rho = float(rho * (1.0 - rho) * se_alpha[k_fixed]) - std_errors = np.concatenate([se_alpha[:k_fixed], [se_rho], se_alpha[k_fixed + 1:]]) + std_errors = np.concatenate([se_alpha[:k_fixed], [se_rho], se_alpha[k_fixed + 1 :]]) except Exception: hess_inv = self._get_hessian_inverse() if hess_inv is not None: se_alpha = np.sqrt(np.maximum(np.diag(hess_inv), 0)) se_alpha[se_alpha == 0] = np.nan se_rho = float(rho * (1.0 - rho) * se_alpha[k_fixed]) - std_errors = np.concatenate([se_alpha[:k_fixed], [se_rho], se_alpha[k_fixed + 1:]]) + std_errors = np.concatenate( + [se_alpha[:k_fixed], [se_rho], se_alpha[k_fixed + 1 :]] + ) return std_errors def _compute_se_mixed_nested(self, all_params, arrays, k_fixed, n_nests, lambdas): @@ -759,14 +777,16 @@ def _compute_se_mixed_nested(self, all_params, arrays, k_fixed, n_nests, lambdas hess = self._compute_hessian(all_params) se_raw = self._compute_std_errors_from_hessian(hess) se_lambda = lambdas * (1.0 - lambdas) * se_raw[k_fixed : k_fixed + n_nests] - std_errors = np.concatenate([se_raw[:k_fixed], se_lambda, se_raw[k_fixed + n_nests:]]) + std_errors = np.concatenate([se_raw[:k_fixed], se_lambda, se_raw[k_fixed + n_nests :]]) except Exception: hess_inv = self._get_hessian_inverse() if hess_inv is not None: se_raw = np.sqrt(np.maximum(np.diag(hess_inv), 0)) se_raw[se_raw == 0] = np.nan se_lambda = lambdas * (1.0 - lambdas) * se_raw[k_fixed : k_fixed + n_nests] - std_errors = np.concatenate([se_raw[:k_fixed], se_lambda, se_raw[k_fixed + n_nests:]]) + std_errors = np.concatenate( + [se_raw[:k_fixed], se_lambda, se_raw[k_fixed + n_nests :]] + ) return std_errors def _compute_se_mixed_nested_scl(self, all_params, arrays, k_fixed, n_nests, rhos, lambdas): @@ -777,26 +797,34 @@ def _compute_se_mixed_nested_scl(self, all_params, arrays, k_fixed, n_nests, rho hess = self._compute_hessian(all_params) se_raw = self._compute_std_errors_from_hessian(hess) se_rho = rhos * (1.0 - rhos) * se_raw[k_fixed : k_fixed + n_nests] - se_lambda = lambdas * (1.0 - lambdas) * se_raw[k_fixed + n_nests : k_fixed + 2 * n_nests] - std_errors = np.concatenate([ - se_raw[:k_fixed], - se_rho, - se_lambda, - se_raw[k_fixed + 2 * n_nests:], - ]) + se_lambda = ( + lambdas * (1.0 - lambdas) * se_raw[k_fixed + n_nests : k_fixed + 2 * n_nests] + ) + std_errors = np.concatenate( + [ + se_raw[:k_fixed], + se_rho, + se_lambda, + se_raw[k_fixed + 2 * n_nests :], + ] + ) except Exception: hess_inv = self._get_hessian_inverse() if hess_inv is not None: se_raw = np.sqrt(np.maximum(np.diag(hess_inv), 0)) se_raw[se_raw == 0] = np.nan se_rho = rhos * (1.0 - rhos) * se_raw[k_fixed : k_fixed + n_nests] - se_lambda = lambdas * (1.0 - lambdas) * se_raw[k_fixed + n_nests : k_fixed + 2 * n_nests] - std_errors = np.concatenate([ - se_raw[:k_fixed], - se_rho, - se_lambda, - se_raw[k_fixed + 2 * n_nests:], - ]) + se_lambda = ( + lambdas * (1.0 - lambdas) * se_raw[k_fixed + n_nests : k_fixed + 2 * n_nests] + ) + std_errors = np.concatenate( + [ + se_raw[:k_fixed], + se_rho, + se_lambda, + se_raw[k_fixed + 2 * n_nests :], + ] + ) return std_errors def _make_fit_result( @@ -856,6 +884,7 @@ def probabilities(self, data=None, beta=None, alpha=None) -> np.ndarray: arrays = self._arrays if data is not None: from locpick.data.choicetable import ChoiceTable + if not isinstance(data, ChoiceTable): raise TypeError("data must be a ChoiceTable") arrays = data.to_arrays( @@ -883,7 +912,9 @@ def _probabilities_mnl(self, arrays, data, beta) -> np.ndarray: else: beta = np.asarray(beta, dtype=np.float64) beta_use = beta[:k] - rho = float(beta[k]) if beta.size > k else float(self._result.coefficients.values[k]) + rho = ( + float(beta[k]) if beta.size > k else float(self._result.coefficients.values[k]) + ) from locpick._sampling.correction import get_sampling_correction @@ -909,6 +940,7 @@ def _probabilities_mnl(self, arrays, data, beta) -> np.ndarray: utilities = (dm @ beta).reshape(n_obs, n_alts) from locpick._sampling.correction import apply_sampling_correction + utilities = apply_sampling_correction(utilities, arrays) if arrays.available is not None: @@ -1056,6 +1088,7 @@ def _probabilities_mixed_nested_numpy(self, arrays, beta, alpha) -> np.ndarray: beta_random_r[:, p] = np.exp(np.clip(exponent, -50, 50)) elif dist == "triangular": from scipy.stats import norm as norm_dist + u = norm_dist.cdf(z_p) mask = u <= 0.5 beta_random_r[:, p] = np.where( @@ -1065,6 +1098,7 @@ def _probabilities_mixed_nested_numpy(self, arrays, beta, alpha) -> np.ndarray: ) elif dist == "uniform": from scipy.stats import norm as norm_dist + u = norm_dist.cdf(z_p) beta_random_r[:, p] = mean_p + spread_p * (2 * u - 1) @@ -1123,6 +1157,7 @@ def utilities(self, data=None, beta=None) -> np.ndarray: V = (dm @ beta).reshape(n_obs, n_alts) from locpick._sampling.correction import apply_sampling_correction + V = apply_sampling_correction(V, arrays) return V @@ -1189,12 +1224,14 @@ def simulate(self, data=None, n_draws: int = 1, seed: Optional[int] = None) -> p chosen_probs = probs[np.arange(n_obs), chosen_indices] # Build results DataFrame (vectorized) - results = pd.DataFrame({ - "draw": np.repeat(np.arange(n_draws), n_obs), - ct.obs_id_col: np.tile(obs_ids, n_draws), - ct.alt_id_col: chosen_alts.T.ravel(), - "probability": chosen_probs.T.ravel(), - }) + results = pd.DataFrame( + { + "draw": np.repeat(np.arange(n_draws), n_obs), + ct.obs_id_col: np.tile(obs_ids, n_draws), + ct.alt_id_col: chosen_alts.T.ravel(), + "probability": chosen_probs.T.ravel(), + } + ) return results # ------------------------------------------------------------------ @@ -1292,7 +1329,7 @@ def marginal_effect(self, data=None, variable: Optional[str] = None) -> pd.Serie mask = nest_matrix[:, m] > 0 if not mask.any(): continue - P_m = P_nest[:, m:m+1] # (n_obs, 1) + P_m = P_nest[:, m : m + 1] # (n_obs, 1) P_i_given_m[:, mask] = probs[:, mask] / np.maximum(P_m, 1e-30) # Marginal effect: P_i * (1 - lambda_m * P_{i|m}) * beta @@ -1379,7 +1416,7 @@ def cross_marginal_effect(self, data=None, variable: Optional[str] = None) -> pd mask = nest_matrix[:, m] > 0 if not mask.any(): continue - P_m = P_nest[:, m:m+1] + P_m = P_nest[:, m : m + 1] P_i_given_m[:, mask] = probs[:, mask] / np.maximum(P_m, 1e-30) cross_me = -probs * long_lambda[None, :] * P_i_given_m * beta @@ -1425,9 +1462,7 @@ def elasticity(self, data=None, variable: Optional[str] = None) -> pd.Series: x = df[variable].values if self._is_spatial and not self._is_nested: - raise NotImplementedError( - "Elasticities for SCL models are not yet implemented." - ) + raise NotImplementedError("Elasticities for SCL models are not yet implemented.") # For MNL, nested, and mixed: elasticity = marginal_effect * x me = self.marginal_effect(data=data, variable=variable) @@ -1456,9 +1491,7 @@ def cross_elasticity(self, data=None, variable: Optional[str] = None) -> pd.Seri x = df[variable].values if self._is_spatial and not self._is_nested: - raise NotImplementedError( - "Cross-elasticities for SCL models are not yet implemented." - ) + raise NotImplementedError("Cross-elasticities for SCL models are not yet implemented.") # cross_elasticity = cross_marginal_effect * x cme = self.cross_marginal_effect(data=data, variable=variable) @@ -1609,6 +1642,7 @@ def _mnl_observation_scores(self, arrays) -> np.ndarray: available = np.ones((n_obs, n_alts), dtype=np.float64) from locpick._sampling.correction import get_sampling_correction + inclusion_probs = get_sampling_correction(arrays) weights = None diff --git a/locpick/models/mixed.py b/locpick/models/mixed.py index 8f97849..5602704 100644 --- a/locpick/models/mixed.py +++ b/locpick/models/mixed.py @@ -812,8 +812,6 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: """Build optimization objective for mixed logit estimation.""" random_col_indices = self._random_col_indices random_distributions = self._random_distributions - k_fixed = self._k_fixed - k_random = self._k_random if self._is_spatial: from locpick._jax.builders import build_mscl_objective @@ -839,58 +837,8 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: draws=self._draws, ) - dm = np.asarray(arrays.design_matrix, dtype=np.float64) - chosen = np.asarray(arrays.chosen, dtype=np.float64) - n_obs = arrays.n_obs - n_alts = arrays.n_alts - available = arrays.available - weights = arrays.weights - - from locpick._sampling.correction import get_sampling_correction - - inclusion_probs = get_sampling_correction(arrays) - - def ll_fn(params): - beta_fixed = params[:k_fixed] - beta_random_means = params[k_fixed : k_fixed + k_random] - beta_random_spreads = params[k_fixed + k_random :] - return _mixed_logit_ll_numpy( - beta_fixed, - beta_random_means, - beta_random_spreads, - random_distributions, - self._draws, - dm, - chosen, - random_col_indices, - n_obs, - n_alts, - available=available, - inclusion_probs=inclusion_probs, - weights=weights, - ) - - def grad_fn(params): - return _mixed_logit_gradient_numpy( - params, - random_col_indices, - k_fixed, - k_random, - random_distributions, - self._draws, - dm, - chosen, - n_obs, - n_alts, - available=available, - inclusion_probs=inclusion_probs, - weights=weights, - ) - - return Objective.from_numpy( - ll_fn=ll_fn, - grad_fn=grad_fn, - param_names=list(self._full_param_names), + raise NotImplementedError( + "MixedMNL NumPy backend has been removed. Use ChoiceModel (JAX backend)." ) def _build_fit_result( diff --git a/locpick/models/nested.py b/locpick/models/nested.py index 1a39cb5..fc4382a 100644 --- a/locpick/models/nested.py +++ b/locpick/models/nested.py @@ -539,56 +539,8 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: return build_nested_objective(arrays, nest_matrix) - # NumPy backend - dm = np.asarray(arrays.design_matrix, dtype=np.float64) - chosen = np.asarray(arrays.chosen, dtype=np.float64) - n_obs = arrays.n_obs - n_alts = arrays.n_alts - available = arrays.available - weights = arrays.weights - - from locpick._sampling.correction import get_sampling_correction - - inclusion_probs = get_sampling_correction(arrays) - k = dm.shape[1] - - def ll_fn(params): - beta = params[:k] - alpha = params[k:] - return _nested_logit_ll_numpy( - beta, - alpha, - dm, - chosen, - nest_matrix, - n_obs, - n_alts, - available=available, - inclusion_probs=inclusion_probs, - weights=weights, - ) - - def grad_fn(params): - beta = params[:k] - alpha = params[k:] - return _nested_logit_gradient_numpy( - beta, - alpha, - dm, - chosen, - nest_matrix, - n_obs, - n_alts, - available=available, - inclusion_probs=inclusion_probs, - weights=weights, - ) - - return Objective.from_numpy( - ll_fn=ll_fn, - grad_fn=grad_fn, - param_names=list(arrays.param_names) - + [f"nest_{name}" for name in self._nests.nest_names], + raise NotImplementedError( + "NestedMNL NumPy backend has been removed. Use ChoiceModel (JAX backend)." ) def _build_fit_result( diff --git a/locpick/models/sar_mnl.py b/locpick/models/sar_mnl.py index 709baa8..d470505 100644 --- a/locpick/models/sar_mnl.py +++ b/locpick/models/sar_mnl.py @@ -78,6 +78,10 @@ class SARMNL(BaseChoiceModel): Solver for PML optimisation. Default "lbfgs". solver_options : dict, optional backend : str, optional + estimator : str, optional + "auto" (default), "pml", "pml_cg", or "linearized_gmm". + ``auto`` selects ``pml`` (dense solve) for n_alts ≤ 2000 and + ``pml_cg`` (conjugate gradient) for larger alternative sets. Examples -------- @@ -101,6 +105,7 @@ def __init__( solver: Union[str, Solver] = "lbfgs", solver_options: Optional[dict] = None, backend: Optional[str] = None, + estimator: str = "auto", ): super().__init__( data=data, @@ -116,6 +121,7 @@ def __init__( raise ValueError("W (spatial weights matrix) is required for SARMNL.") self._W_input = W self._W_sparse = None # resolved at fit time + self._estimator = estimator # ------------------------------------------------------------------ # Properties @@ -136,11 +142,79 @@ def _pre_fit(self, arrays: ChoiceArrays) -> None: self._W_input, arrays.n_alts, row_standardize=True )[1] # get the CSR sparse + def fit(self, **kwargs) -> FitResult: + """Estimate the model and return results. + + Dispatches to the PML estimator (JAX autodiff) or the + linearized GMM estimator based on the ``estimator`` setting. + """ + if self._estimator == "linearized_gmm": + return self._fit_linearized_gmm() + return super().fit(**kwargs) + + def _fit_linearized_gmm(self) -> FitResult: + """Two-step linearized GMM estimation (Carrión-Flores et al. 2018).""" + arrays = self._get_arrays() + self._arrays = arrays + self._pre_fit(arrays) + + from locpick._kernels.sar_mnl_numpy import fit_linearized_gmm + + result_dict = fit_linearized_gmm(arrays, self._W_sparse) + + beta = result_dict["beta"] + rho = result_dict["rho"] + se = result_dict["se"] + ll = result_dict["log_likelihood"] + + utility_param_names = list(arrays.param_names) + k = len(utility_param_names) + display_values = np.concatenate([beta, [rho]]) + display_names = utility_param_names + ["rho"] + model_type = "SAR-MNL (Linearized GMM)" + n_params = len(display_values) + + # SEs: first k are beta SEs, last is rho SE + std_errors = se[: k + 1] + + coefficients = pd.Series(display_values, index=display_names, name="coefficient") + std_err_series = pd.Series(std_errors, index=display_names, name="std_error") + ll_null = _compute_null_ll(arrays) + + stats = _compute_fit_statistics( + ll=ll, + ll_null=ll_null, + n_obs=arrays.n_obs, + n_params=n_params, + n_alts=arrays.n_alts, + coefficients=coefficients, + std_errors=std_err_series, + model_type=model_type, + solver_name="linearized_gmm", + solver_result_raw=result_dict, + ) + + self._result = FitResult(spec=self._spec, **stats) + self._clear_caches() + return self._result + def _build_objective(self, arrays: ChoiceArrays): - """Build the PML objective using JAX.""" + """Build the PML objective using JAX. + + Auto-selects dense solve (n_alts ≤ 2000) or conjugate gradient + (n_alts > 2000) based on the estimator setting. + """ from locpick._jax.sar_kernels import build_sar_mnl_objective - return build_sar_mnl_objective(arrays, self._W_sparse) + # Auto-select estimator + if self._estimator == "auto": + if arrays.n_alts <= 2000: + self._estimator = "pml" + else: + self._estimator = "pml_cg" + + use_cg = self._estimator == "pml_cg" + return build_sar_mnl_objective(arrays, self._W_sparse, use_cg=use_cg) def _get_solver_inputs(self, arrays: ChoiceArrays): """Get initial values, param names, bounds, fixed mask. @@ -154,9 +228,7 @@ def _get_solver_inputs(self, arrays: ChoiceArrays): names = list(names) + ["alpha_rho"] return x0, names, bounds, fixed_mask - def _build_fit_result( - self, solver_result: SolverResult, arrays: ChoiceArrays - ) -> FitResult: + def _build_fit_result(self, solver_result: SolverResult, arrays: ChoiceArrays) -> FitResult: """Build a FitResult from solver output.""" all_params = solver_result.coefficients utility_param_names = list(arrays.param_names) @@ -190,12 +262,8 @@ def _build_fit_result( except Exception: pass - coefficients = pd.Series( - display_values, index=display_names, name="coefficient" - ) - std_err_series = pd.Series( - std_errors, index=display_names, name="std_error" - ) + coefficients = pd.Series(display_values, index=display_names, name="coefficient") + std_err_series = pd.Series(std_errors, index=display_names, name="std_error") ll = solver_result.log_likelihood ll_null = _compute_null_ll(arrays) @@ -255,9 +323,7 @@ def probabilities(self, data=None, beta=None, rho=None): k = arrays.design_matrix.shape[1] if beta is None: - coef_vals = np.asarray( - self._result.coefficients.values, dtype=np.float64 - ) + coef_vals = np.asarray(self._result.coefficients.values, dtype=np.float64) beta = coef_vals[:k] if rho is None: rho = float(self._result.coefficients.values[k]) @@ -281,9 +347,7 @@ def probabilities(self, data=None, beta=None, rho=None): # Availability if arrays.available is not None: - available = np.asarray( - arrays.available, dtype=np.float64 - ).reshape(n_obs, n_alts) + available = np.asarray(arrays.available, dtype=np.float64).reshape(n_obs, n_alts) else: available = np.ones((n_obs, n_alts), dtype=np.float64) @@ -321,9 +385,7 @@ def utilities(self, data=None, beta=None, rho=None): k = arrays.design_matrix.shape[1] if beta is None: - beta = np.asarray( - self._result.coefficients.values[:k], dtype=np.float64 - ) + beta = np.asarray(self._result.coefficients.values[:k], dtype=np.float64) if rho is None: rho = float(self._result.coefficients.values[k]) @@ -341,4 +403,128 @@ def utilities(self, data=None, beta=None, rho=None): D = np.diag(np.linalg.inv(A)) V_star = V_filtered / D[None, :] - return V_star \ No newline at end of file + return V_star + + # ------------------------------------------------------------------ + # Marginal effects (LeSage & Pace 2009) + # ------------------------------------------------------------------ + + def marginal_effects(self, data=None, variable: Optional[str] = None): + """Compute average direct, indirect, and total marginal effects. + + In the SAR-MNL model, a change in an attribute of alternative + *j* affects not only *j*'s utility but also neighbouring + alternatives through the spatial multiplier + :math:`(I - \\rho W)^{-1}`. + + Following LeSage & Pace (2009), the marginal effect of variable + *r* on the probability of choosing alternative *k* is an + :math:`J \\times J` matrix. Summary measures are: + + - **Direct effect**: average of diagonal elements (impact on + own alternative). + - **Indirect effect**: average of off-diagonal row sums + (spillover to neighbouring alternatives). + - **Total effect**: direct + indirect. + + Parameters + ---------- + data : ChoiceTable or None + Data to compute marginal effects on. If None, uses + estimation data. + variable : str + Name of the variable to compute marginal effects for. + + Returns + ------- + dict + Dictionary with keys ``"direct"``, ``"indirect"``, + ``"total"``, each mapping to a ``pd.Series`` indexed by + alternative ID. + """ + if self._arrays is None: + raise RuntimeError("Model must be estimated before computing marginal effects.") + + from locpick.data.choicetable import ChoiceTable + + ct = self._data + arrays = self._arrays + if data is not None: + if not isinstance(data, ChoiceTable): + raise TypeError("data must be a ChoiceTable") + arrays = data.to_arrays( + formula=self._spec.formula, + spec=self._spec if self._spec.formula is None else None, + ) + ct = data + + n_obs = arrays.n_obs + n_alts = arrays.n_alts + k = arrays.design_matrix.shape[1] + + # Get estimated parameters + coef_vals = np.asarray(self._result.coefficients.values, dtype=np.float64) + beta = coef_vals[:k] + rho = float(coef_vals[k]) + + # Get the beta for the requested variable + param_names = list(arrays.param_names) + if variable not in param_names: + raise ValueError(f"Variable '{variable}' not found in parameters: {param_names}") + beta_r = beta[param_names.index(variable)] + + # Compute probabilities + probs = self.probabilities(data=data) + + # Spatial multiplier (I - rho*W)^{-1} + W_dense = np.asarray(self._W_sparse.toarray(), dtype=np.float64) + A = np.eye(n_alts) - rho * W_dense + Z_mat = np.linalg.inv(A) # (n_alts, n_alts) + + # Marginal effect matrix for variable r, alternative k: + # ME_{k,r} = P_k * (beta_r * Z_{kk} - sum_l P_l * beta_r * Z_{lk}) + # = beta_r * P_k * (Z_{kk} - sum_l P_l * Z_{lk}) + # But P varies across choosers. For the average marginal effect, + # we average over choosers: + # AME_{k,r} = beta_r * avg(P_k) * (Z_{kk} - sum_l avg(P_l) * Z_{lk}) + + # Compute the J×J marginal effect matrix (averaged over choosers) + # ME[j, k] = beta_r * avg_probs[k] * (Z[k, j] - sum_l avg_probs[l] * Z[l, j]) + # But the standard LeSage-Pace formulation for MNL is: + # dP_k/dX_j = P_k * (delta_{kj} - P_j) * beta_r * Z[j, ...] + # This is complex — we use the simpler average approach: + # + # For each alternative k, the direct effect is: + # dP_k/dX_k = beta_r * Z[k,k] * P_k * (1 - P_k) + # The indirect (spillover) effect from j to k (j != k) is: + # dP_k/dX_j = -beta_r * Z[k,j] * P_k * P_j + # But with the spatial multiplier, Z replaces the identity. + + # Direct effects: average over choosers of + # beta_r * Z[k,k] * P_ik * (1 - P_ik) + direct = np.zeros(n_alts) + indirect = np.zeros(n_alts) + for k_alt in range(n_alts): + # Direct: own-alternative effect + direct[k_alt] = ( + beta_r * Z_mat[k_alt, k_alt] * np.mean(probs[:, k_alt] * (1 - probs[:, k_alt])) + ) + # Indirect: spillover from neighbours + # Sum over j != k of dP_k/dX_j = -beta_r * sum_{j!=k} Z[k,j] * P_k * P_j + for j_alt in range(n_alts): + if j_alt != k_alt: + indirect[k_alt] += ( + -beta_r * Z_mat[k_alt, j_alt] * np.mean(probs[:, k_alt] * probs[:, j_alt]) + ) + + total = direct + indirect + + # Get alternative IDs from the data + df = ct.to_frame() + alt_ids = df[ct.alt_id_col].values.reshape(n_obs, n_alts)[0] + + return { + "direct": pd.Series(direct, index=alt_ids, name=f"direct_{variable}"), + "indirect": pd.Series(indirect, index=alt_ids, name=f"indirect_{variable}"), + "total": pd.Series(total, index=alt_ids, name=f"total_{variable}"), + } diff --git a/locpick/models/scl.py b/locpick/models/scl.py index 3435452..8207789 100644 --- a/locpick/models/scl.py +++ b/locpick/models/scl.py @@ -186,21 +186,13 @@ def _scl_log_probs_numpy( if is_first: # alt_i is node i in edge (i, j) # P_{i|ij} = (α_{i,ij} * exp(V_i))^{1/ρ} / [(α_{i,ij} * exp(V_i))^{1/ρ} + (α_{j,ij} * exp(V_j))^{1/ρ}] - my_term = alloc_exp_V_inv_rho[ - :, alt_i, j - ] # (α_{i,ij} * exp(V_i))^{1/ρ} - other_term = alloc_exp_V_inv_rho[ - :, j, alt_i - ] # (α_{j,ij} * exp(V_j))^{1/ρ} + my_term = alloc_exp_V_inv_rho[:, alt_i, j] # (α_{i,ij} * exp(V_i))^{1/ρ} + other_term = alloc_exp_V_inv_rho[:, j, alt_i] # (α_{j,ij} * exp(V_j))^{1/ρ} else: # alt_i is node j in edge (i, j) # P_{j|ij} = (α_{j,ij} * exp(V_j))^{1/ρ} / [(α_{i,ij} * exp(V_i))^{1/ρ} + (α_{j,ij} * exp(V_j))^{1/ρ}] - my_term = alloc_exp_V_inv_rho[ - :, alt_i, i - ] # (α_{j,ij} * exp(V_j))^{1/ρ} - other_term = alloc_exp_V_inv_rho[ - :, i, alt_i - ] # (α_{i,ij} * exp(V_i))^{1/ρ} + my_term = alloc_exp_V_inv_rho[:, alt_i, i] # (α_{j,ij} * exp(V_j))^{1/ρ} + other_term = alloc_exp_V_inv_rho[:, i, alt_i] # (α_{i,ij} * exp(V_i))^{1/ρ} # log P_{i|ij} = log(my_term) - log(my_term + other_term) log_cond = np.log(np.maximum(my_term, 1e-300)) - np.log( diff --git a/tests/test_mixed_nested.py b/tests/test_mixed_nested.py index ed83ec0..c83a306 100644 --- a/tests/test_mixed_nested.py +++ b/tests/test_mixed_nested.py @@ -1,12 +1,10 @@ """Tests for the Mixed Nested Logit model.""" import numpy as np -import pytest from locpick import ChoiceModel, ChoiceTable from locpick.dgp import simulate_mixed_nested_logit from locpick.models.mixed import ParamDistribution - from locpick.models.nested import NestingTree, NestSpec # --------------------------------------------------------------------------- diff --git a/tests/test_param_recovery.py b/tests/test_param_recovery.py index fb37e73..350936f 100644 --- a/tests/test_param_recovery.py +++ b/tests/test_param_recovery.py @@ -23,7 +23,6 @@ ) from locpick.models.mixed import ParamDistribution - # --------------------------------------------------------------------------- # MNL parameter recovery # --------------------------------------------------------------------------- diff --git a/tests/test_sar_mnl.py b/tests/test_sar_mnl.py index de33259..55ed9fa 100644 --- a/tests/test_sar_mnl.py +++ b/tests/test_sar_mnl.py @@ -13,9 +13,8 @@ import numpy as np import numpy.testing as npt -import pytest -from locpick import ChoiceModel, SARMNL +from locpick import SARMNL, ChoiceModel from locpick.dgp import simulate_sar_mnl @@ -114,9 +113,7 @@ def test_sar_mnl_recovers_rho_moderate_spatial_dep(self): def test_sar_mnl_smaller_n_alts(self): """SAR-MNL should work with a small number of alternatives.""" - dataset = simulate_sar_mnl( - n_obs=3000, n_alts=12, rho=0.2, n_neighbors=3, seed=42 - ) + dataset = simulate_sar_mnl(n_obs=3000, n_alts=12, rho=0.2, n_neighbors=3, seed=42) model = SARMNL( dataset.choice_table, formula="alt_attr + obs_x_alt - 1", @@ -136,17 +133,11 @@ def test_sar_mnl_w_input_types(self): W_sparse = sp.csr_array(W_graph.sparse) # scipy.sparse W_dense = W_sparse.toarray() # dense numpy - model1 = SARMNL( - dataset.choice_table, "alt_attr + obs_x_alt - 1", W=W_graph - ) + model1 = SARMNL(dataset.choice_table, "alt_attr + obs_x_alt - 1", W=W_graph) result1 = model1.fit() - model2 = SARMNL( - dataset.choice_table, "alt_attr + obs_x_alt - 1", W=W_sparse - ) + model2 = SARMNL(dataset.choice_table, "alt_attr + obs_x_alt - 1", W=W_sparse) result2 = model2.fit() - model3 = SARMNL( - dataset.choice_table, "alt_attr + obs_x_alt - 1", W=W_dense - ) + model3 = SARMNL(dataset.choice_table, "alt_attr + obs_x_alt - 1", W=W_dense) result3 = model3.fit() npt.assert_allclose( @@ -177,4 +168,68 @@ def test_sar_mnl_probabilities_sum_to_one(self): 1.0, atol=1e-10, err_msg="Probabilities do not sum to 1", - ) \ No newline at end of file + ) + + def test_sar_mnl_marginal_effects_structure(self): + """Marginal effects: direct + indirect = total; indirect > 0 when ρ > 0.""" + dataset = simulate_sar_mnl(n_obs=1000, n_alts=20, rho=0.3, seed=42) + model = SARMNL( + dataset.choice_table, + formula="alt_attr + obs_x_alt - 1", + W=dataset.W, + ) + model.fit() + me = model.marginal_effects(variable="alt_attr") + + # direct + indirect = total + npt.assert_allclose( + me["direct"].values + me["indirect"].values, + me["total"].values, + rtol=1e-10, + err_msg="direct + indirect != total", + ) + + def test_sar_mnl_gmm_recovers_rho(self): + """Linearized GMM should recover ρ at moderate spatial dependence.""" + dataset = simulate_sar_mnl(n_obs=5000, n_alts=50, rho=0.3, seed=2026) + model = SARMNL( + dataset.choice_table, + formula="alt_attr + obs_x_alt - 1", + W=dataset.W, + estimator="linearized_gmm", + ) + result = model.fit() + + # GMM is less precise — use wider tolerance + est_rho = result.coefficients["rho"] + assert abs(est_rho - dataset.true_rho) < 0.2, ( + f"GMM failed to recover rho: got {est_rho:.4f}, true {dataset.true_rho}" + ) + + def test_sar_mnl_cg_matches_dense(self): + """CG path should give similar results to dense path.""" + dataset = simulate_sar_mnl(n_obs=1000, n_alts=20, rho=0.2, seed=42) + + model_dense = SARMNL( + dataset.choice_table, + formula="alt_attr + obs_x_alt - 1", + W=dataset.W, + estimator="pml", + ) + result_dense = model_dense.fit() + + model_cg = SARMNL( + dataset.choice_table, + formula="alt_attr + obs_x_alt - 1", + W=dataset.W, + estimator="pml_cg", + ) + result_cg = model_cg.fit() + + # CG and dense should agree closely + npt.assert_allclose( + result_dense.coefficients.values, + result_cg.coefficients.values, + rtol=0.05, + err_msg="CG and dense paths give different results", + ) diff --git a/tests/test_spatial_models.py b/tests/test_spatial_models.py index 7ef405c..86a73a4 100644 --- a/tests/test_spatial_models.py +++ b/tests/test_spatial_models.py @@ -15,8 +15,6 @@ from locpick import ChoiceModel, ChoiceTable from locpick.models.mixed import ParamDistribution - - from locpick.models.scl import ( _resolve_spatial_graph, _scl_ll_numpy, From aa51cc893d6a57af6618bddbd7ec7234c3d472c6 Mon Sep 17 00:00:00 2001 From: knaaptime Date: Fri, 19 Jun 2026 14:53:25 -0700 Subject: [PATCH 3/7] clarify two spatial models in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f9875db..a430a33 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The package is **not** a general-purpose ML library. It is specifically for stru ## Features -LocPick can automate the creation of choice tables for estimation or simulation, using census choice sets, uniform or weighted random sampling of alternatives, generated interaction terms, and cartesian merges. A unique feature is the implementation of [*spatial* choice models](https://linkinghub.elsevier.com/retrieve/pii/S0191261503000055), which assume that nearby alternatives are more similar (closer substitutes). +LocPick can automate the creation of choice tables for estimation or simulation, using census choice sets, uniform or weighted random sampling of alternatives, generated interaction terms, and cartesian merges. A unique feature is the implementation of *spatial* choice models, which take one of two forms. The [Bhat et al](https://linkinghub.elsevier.com/retrieve/pii/S0191261503000055) approach is similar to a spatial error model, assuming that nearby alternatives are more similar (closer substitutes). The SAR style approach assumes that the structural utility of each alternative $V$ has a simultaneous autoregressive structure, and is estimated with either [PML](http://dx.doi.org/10.1016/j.regsciurbeco.2009.09.004) or [GMM](https://www.sciencedirect.com/science/article/pii/S0166046217300625) It also provides tools for Monte Carlo simulation of choices given probability distributions from fitted models, with fast algorithms for independent or capacity-constrained choices. From c2eebae5aa256df1285fd4d0781fcc36f439dfff Mon Sep 17 00:00:00 2001 From: knaaptime Date: Fri, 19 Jun 2026 15:33:05 -0700 Subject: [PATCH 4/7] relative imports --- locpick/_jax/__init__.py | 10 ++--- locpick/_jax/builders.py | 14 +++--- locpick/_jax/data.py | 2 +- locpick/_jax/kernels.py | 2 +- locpick/_jax/objective.py | 2 +- locpick/_jax/sar_kernels.py | 8 ++-- locpick/_jax/transforms.py | 2 +- locpick/_kernels/mnl_numpy.py | 2 +- locpick/_kernels/sar_mnl_numpy.py | 6 +-- locpick/_sampling/correction.py | 2 +- locpick/_solvers/lbfgs.py | 2 +- locpick/_solvers/optax.py | 2 +- locpick/_solvers/optimagic.py | 2 +- locpick/_solvers/optimistix.py | 2 +- locpick/_solvers/trust_ncg.py | 2 +- locpick/data/choicetable.py | 8 ++-- locpick/data/problem.py | 4 +- locpick/dgp.py | 18 ++++---- locpick/models/base.py | 16 +++---- locpick/models/choice_model.py | 72 +++++++++++++++---------------- locpick/models/mixed.py | 38 ++++++++-------- locpick/models/mixed_nested.py | 40 ++++++++--------- locpick/models/mnl.py | 50 ++++++++++----------- locpick/models/nested.py | 40 ++++++++--------- locpick/models/sar_mnl.py | 22 +++++----- locpick/results/diagnostics.py | 2 +- locpick/spec/model_spec.py | 2 +- 27 files changed, 186 insertions(+), 186 deletions(-) diff --git a/locpick/_jax/__init__.py b/locpick/_jax/__init__.py index 83fa592..2993947 100644 --- a/locpick/_jax/__init__.py +++ b/locpick/_jax/__init__.py @@ -16,23 +16,23 @@ JIT-compiled and vmap'd independently of any model class. """ -from locpick._jax.builders import ( +from .builders import ( build_mixed_logit_objective, build_mnl_objective, build_mscl_objective, build_nested_objective, build_scl_objective, ) -from locpick._jax.data import ChoiceDataJAX, EdgeDataJAX -from locpick._jax.kernels import ( +from .data import ChoiceDataJAX, EdgeDataJAX +from .kernels import ( mixed_logit_ll, mnl_log_probs, mnl_probs, nested_log_probs, scl_log_probs, ) -from locpick._jax.objective import Objective -from locpick._jax.transforms import Identity, ParamTransform, Sigmoid, SoftPlus +from .objective import Objective +from .transforms import Identity, ParamTransform, Sigmoid, SoftPlus __all__ = [ "ChoiceDataJAX", diff --git a/locpick/_jax/builders.py b/locpick/_jax/builders.py index 33f2f2f..0bae1d1 100644 --- a/locpick/_jax/builders.py +++ b/locpick/_jax/builders.py @@ -22,8 +22,8 @@ import numpy as np from jax.scipy.special import logsumexp as jax_logsumexp -from locpick._jax.data import ChoiceDataJAX -from locpick._jax.kernels import ( +from .data import ChoiceDataJAX +from .kernels import ( _NEG_INF, compute_ll, compute_ll_contribs, @@ -33,8 +33,8 @@ nested_log_probs, scl_log_probs, ) -from locpick._jax.objective import Objective -from locpick._jax.transforms import Identity, ParamTransform, Sigmoid, SoftPlus +from .objective import Objective +from .transforms import Identity, ParamTransform, Sigmoid, SoftPlus # --------------------------------------------------------------------------- # MNL objective @@ -376,7 +376,7 @@ def _mnscl_ll_kernel( nest_alt_indices : tuple of tuple of int Precomputed nest alt indices. """ - from locpick._jax.kernels import scl_log_probs_and_inclusive_value + from .kernels import scl_log_probs_and_inclusive_value beta_fixed = params[:k_fixed] alpha_rhos = params[k_fixed : k_fixed + n_nests] @@ -797,7 +797,7 @@ def _nested_scl_ll_kernel(params, data, nest_matrix, edge_data_list, k, nest_alt Precomputed nest alt indices: ``nest_alt_indices[m][i]`` = global alt index of the i-th alternative in nest m. """ - from locpick._jax.kernels import scl_log_probs_and_inclusive_value + from .kernels import scl_log_probs_and_inclusive_value beta = params[:k] n_nests = nest_matrix.shape[1] @@ -1090,7 +1090,7 @@ def _mixed_nested_ll_kernel( nest_alt_indices : tuple of tuple of int Precomputed nest alt indices. """ - from locpick._jax.kernels import mixed_nested_logit_ll + from .kernels import mixed_nested_logit_ll beta_fixed = params[:k_fixed] alpha_nest = params[k_fixed : k_fixed + n_nests] diff --git a/locpick/_jax/data.py b/locpick/_jax/data.py index 6386531..eb2a693 100644 --- a/locpick/_jax/data.py +++ b/locpick/_jax/data.py @@ -14,7 +14,7 @@ import jax.numpy as jnp import numpy as np -from locpick._sampling.correction import get_sampling_correction +from .._sampling.correction import get_sampling_correction @jax.tree_util.register_pytree_node_class diff --git a/locpick/_jax/kernels.py b/locpick/_jax/kernels.py index 23f44b5..1aa5056 100644 --- a/locpick/_jax/kernels.py +++ b/locpick/_jax/kernels.py @@ -23,7 +23,7 @@ import jax -from locpick._kernels.constants import NEG_INF as _NEG_INF_FLOAT +from .._kernels.constants import NEG_INF as _NEG_INF_FLOAT # Enable x64 before any jnp.float64 expression is evaluated at module-import # time below. diff --git a/locpick/_jax/objective.py b/locpick/_jax/objective.py index fb60516..f5238a7 100644 --- a/locpick/_jax/objective.py +++ b/locpick/_jax/objective.py @@ -20,7 +20,7 @@ import jax.numpy as jnp import numpy as np -from locpick._jax.transforms import ParamTransform +from .transforms import ParamTransform @dataclass diff --git a/locpick/_jax/sar_kernels.py b/locpick/_jax/sar_kernels.py index 2ccbcbc..4e343b9 100644 --- a/locpick/_jax/sar_kernels.py +++ b/locpick/_jax/sar_kernels.py @@ -21,15 +21,15 @@ import jax.numpy as jnp import scipy.sparse as sp -from locpick._jax.data import ChoiceDataJAX -from locpick._jax.kernels import ( +from .data import ChoiceDataJAX +from .kernels import ( compute_ll, compute_ll_contribs, compute_utilities, mnl_log_probs, ) -from locpick._jax.objective import Objective -from locpick._jax.transforms import ParamTransform +from .objective import Objective +from .transforms import ParamTransform # Threshold for switching from dense solve to conjugate gradient. _DENSE_CUTOFF = 2000 diff --git a/locpick/_jax/transforms.py b/locpick/_jax/transforms.py index 5d6c1e0..aa16c46 100644 --- a/locpick/_jax/transforms.py +++ b/locpick/_jax/transforms.py @@ -115,7 +115,7 @@ class ParamTransform: Examples -------- - >>> from locpick._jax.transforms import ParamTransform, Identity, Sigmoid + >>> from .transforms import ParamTransform, Identity, Sigmoid >>> # SCL model: [beta_0, beta_1, rho] → [beta_0, beta_1, sigmoid(alpha_rho)] >>> pt = ParamTransform([Identity(), Identity(), Sigmoid(0, 1)]) >>> x = jnp.array([0.5, -0.1, 0.0]) diff --git a/locpick/_kernels/mnl_numpy.py b/locpick/_kernels/mnl_numpy.py index 1eb0883..d85fce4 100644 --- a/locpick/_kernels/mnl_numpy.py +++ b/locpick/_kernels/mnl_numpy.py @@ -21,7 +21,7 @@ import numpy as np from scipy.special import logsumexp as scipy_logsumexp -from locpick._kernels.constants import NEG_INF +from .constants import NEG_INF # --------------------------------------------------------------------------- # Type aliases diff --git a/locpick/_kernels/sar_mnl_numpy.py b/locpick/_kernels/sar_mnl_numpy.py index 1e171d4..1c86fc3 100644 --- a/locpick/_kernels/sar_mnl_numpy.py +++ b/locpick/_kernels/sar_mnl_numpy.py @@ -175,7 +175,7 @@ def fit_linearized_gmm(arrays, W_sparse): dict Dictionary with keys: 'beta', 'rho', 'se', 'vcov', 'log_likelihood'. """ - from locpick._kernels.mnl_numpy import mnl_probs_numpy + from .mnl_numpy import mnl_probs_numpy n_obs = arrays.n_obs n_alts = arrays.n_alts @@ -190,11 +190,11 @@ def fit_linearized_gmm(arrays, W_sparse): available = np.ones((n_obs, n_alts), dtype=np.float64) # --- Step 1: Standard MNL estimation --- - from locpick._solvers import get_solver + from .._solvers import get_solver solver = get_solver("lbfgs") - from locpick._jax.builders import build_mnl_objective + from .._jax.builders import build_mnl_objective objective = build_mnl_objective(arrays) x0 = np.zeros(k) diff --git a/locpick/_sampling/correction.py b/locpick/_sampling/correction.py index c5b4f13..c988a95 100644 --- a/locpick/_sampling/correction.py +++ b/locpick/_sampling/correction.py @@ -14,7 +14,7 @@ import numpy as np if TYPE_CHECKING: - from locpick.data.arrays import ChoiceArrays + from ..data.arrays import ChoiceArrays def get_sampling_correction(arrays: "ChoiceArrays") -> np.ndarray | None: diff --git a/locpick/_solvers/lbfgs.py b/locpick/_solvers/lbfgs.py index 21679fd..d0f1453 100644 --- a/locpick/_solvers/lbfgs.py +++ b/locpick/_solvers/lbfgs.py @@ -48,7 +48,7 @@ def solve_lbfgs( """ from scipy.optimize import minimize - from locpick._jax.objective import Objective + from .._jax.objective import Objective if not isinstance(objective, Objective): raise TypeError("solve_lbfgs expects an Objective instance.") diff --git a/locpick/_solvers/optax.py b/locpick/_solvers/optax.py index d313a96..a968717 100644 --- a/locpick/_solvers/optax.py +++ b/locpick/_solvers/optax.py @@ -140,7 +140,7 @@ def solve( if fixed_mask is not None and np.any(fixed_mask): raise NotImplementedError("OptaxSolver does not yet support fixed parameters.") - from locpick._jax.objective import Objective + from .._jax.objective import Objective if not isinstance(objective, Objective): raise TypeError("OptaxSolver.solve expects an Objective instance.") diff --git a/locpick/_solvers/optimagic.py b/locpick/_solvers/optimagic.py index 480547f..3724a41 100644 --- a/locpick/_solvers/optimagic.py +++ b/locpick/_solvers/optimagic.py @@ -59,7 +59,7 @@ def solve( ) -> SolverResult: import optimagic as om - from locpick._jax.objective import Objective + from .._jax.objective import Objective if not isinstance(objective, Objective): raise TypeError("OptimagicSolver.solve expects an Objective instance.") diff --git a/locpick/_solvers/optimistix.py b/locpick/_solvers/optimistix.py index b9dcf8f..5b10db7 100644 --- a/locpick/_solvers/optimistix.py +++ b/locpick/_solvers/optimistix.py @@ -192,7 +192,7 @@ def solve( ------- SolverResult """ - from locpick._jax.objective import Objective + from .._jax.objective import Objective if not isinstance(objective, Objective): raise TypeError("OptimistixSolver.solve expects an Objective instance.") diff --git a/locpick/_solvers/trust_ncg.py b/locpick/_solvers/trust_ncg.py index f9b6ea8..5b21b18 100644 --- a/locpick/_solvers/trust_ncg.py +++ b/locpick/_solvers/trust_ncg.py @@ -61,7 +61,7 @@ def solve( ) -> SolverResult: from scipy.optimize import minimize - from locpick._jax.objective import Objective + from .._jax.objective import Objective if not isinstance(objective, Objective): raise TypeError(f"{type(self).__name__}.solve expects an Objective instance.") diff --git a/locpick/data/choicetable.py b/locpick/data/choicetable.py index 25a2580..2c8e00c 100644 --- a/locpick/data/choicetable.py +++ b/locpick/data/choicetable.py @@ -13,13 +13,13 @@ import pandas as pd import xarray as xr -from locpick._sampling.kernels import ( +from .._sampling.kernels import ( HAS_NUMBA, _sample_unweighted_without_replacement_exclusion, _sample_weighted_without_replacement_1d_exclusion, ) -from locpick.data.arrays import ChoiceArrays -from locpick.data.dataset import ( +from .arrays import ChoiceArrays +from .dataset import ( _resolve_pairwise, build_choice_dataset, build_choice_dataset_from_long, @@ -834,7 +834,7 @@ def _hashable(val): inclusion_probs = None if self._sample_size is not None: - from locpick._sampling.inclusion import compute_inclusion_probs + from .._sampling.inclusion import compute_inclusion_probs n_alts_full = self.n_alternatives_full n_samples = self._sample_size diff --git a/locpick/data/problem.py b/locpick/data/problem.py index 53b3599..d3d8aaa 100644 --- a/locpick/data/problem.py +++ b/locpick/data/problem.py @@ -20,7 +20,7 @@ import numpy as np -from locpick.data.arrays import ChoiceArrays +from .arrays import ChoiceArrays @dataclass @@ -126,7 +126,7 @@ def from_choice_table( ------- EstimationProblem """ - from locpick.spec import ModelSpec + from ..spec import ModelSpec # Resolve spec if spec is None and formula is not None: diff --git a/locpick/dgp.py b/locpick/dgp.py index 9a2e6e9..2fd774f 100644 --- a/locpick/dgp.py +++ b/locpick/dgp.py @@ -343,7 +343,7 @@ class MNSCLDataset: def _build_choice_table(choosers, alternatives, choices, matrix_data=None): """Build a ChoiceTable from component DataFrames.""" - from locpick.data.choicetable import ChoiceTable + from .data.choicetable import ChoiceTable return ChoiceTable.from_tables( choosers=choosers.drop(columns="choice", errors="ignore"), @@ -619,7 +619,7 @@ def simulate_nested_logit( ------- NestedLogitDataset """ - from locpick.models.nested import ( + from .models.nested import ( NestingTree, NestSpec, _nested_logit_probs_numpy, @@ -789,7 +789,7 @@ def simulate_scl( ------- SCLDataset """ - from locpick.models.scl import _resolve_spatial_graph, _scl_log_probs_numpy + from .models.scl import _resolve_spatial_graph, _scl_log_probs_numpy if alt_params is None: alt_params = {"cost": -0.5, "time": -0.1} @@ -1192,8 +1192,8 @@ def simulate_nested_scl( ------- NestedSCLDataset """ - from locpick.models.nested import NestingTree, NestSpec - from locpick.models.scl import ( + from .models.nested import NestingTree, NestSpec + from .models.scl import ( _resolve_spatial_graph, ) @@ -1488,8 +1488,8 @@ def simulate_mnscl( ------- MNSCLDataset """ - from locpick.models.nested import NestingTree, NestSpec - from locpick.models.scl import ( + from .models.nested import NestingTree, NestSpec + from .models.scl import ( _resolve_spatial_graph, ) @@ -1721,7 +1721,7 @@ def simulate_mixed_nested_logit( ------- MixedNestedMNLDataset """ - from locpick.models.nested import ( + from .models.nested import ( NestingTree, NestSpec, _nested_logit_probs_numpy, @@ -1990,7 +1990,7 @@ def simulate_sar_mnl( rng = np.random.default_rng(seed) # --- Build W (alt×alt) as a libpysal Graph -------------------------- - from locpick.models._spatial_weights import build_knn_graph, resolve_spatial_weights + from .models._spatial_weights import build_knn_graph, resolve_spatial_weights if W is None: coords = rng.standard_normal((n_alts, 2)) diff --git a/locpick/models/base.py b/locpick/models/base.py index c44feab..dcf81e8 100644 --- a/locpick/models/base.py +++ b/locpick/models/base.py @@ -14,12 +14,12 @@ from scipy import stats from scipy.linalg import cho_factor, cho_solve -from locpick._jax.objective import Objective -from locpick._solvers.protocol import Solver, SolverResult, get_solver -from locpick.data.arrays import ChoiceArrays -from locpick.data.choicetable import ChoiceTable -from locpick.data.problem import EstimationProblem -from locpick.results.fit_result import FitResult +from .._jax.objective import Objective +from .._solvers.protocol import Solver, SolverResult, get_solver +from ..data.arrays import ChoiceArrays +from ..data.choicetable import ChoiceTable +from ..data.problem import EstimationProblem +from ..results.fit_result import FitResult # ------------------------------------------------------------------ # Cholesky-based linear-algebra helpers @@ -262,7 +262,7 @@ def __init__( weights: Optional[Union[str, np.ndarray]] = None, availability: Optional[Union[str, np.ndarray]] = None, ): - from locpick.spec.model_spec import ModelSpec + from ..spec.model_spec import ModelSpec self._solver_options = solver_options or {} self._backend = backend @@ -726,7 +726,7 @@ def _resolve_spatial_graph(self) -> tuple[np.ndarray, list, int]: n_alts : int Number of alternatives (dimension of the graph). """ - from locpick.models._spatial import EdgeStructure, _resolve_spatial_graph + from ._spatial import EdgeStructure, _resolve_spatial_graph omega, allocation, edge_list, n_alts = _resolve_spatial_graph(self._graph_input) self._omega = omega diff --git a/locpick/models/choice_model.py b/locpick/models/choice_model.py index 18bcf1d..3414cfb 100644 --- a/locpick/models/choice_model.py +++ b/locpick/models/choice_model.py @@ -22,16 +22,17 @@ import numpy as np import pandas as pd -from locpick._jax.objective import Objective -from locpick._solvers import Solver, SolverResult -from locpick.data.arrays import ChoiceArrays -from locpick.data.problem import EstimationProblem -from locpick.models._spatial import ( +from .._jax.objective import Objective +from .._solvers import Solver, SolverResult +from ..data.arrays import ChoiceArrays +from ..data.problem import EstimationProblem +from ..results.fit_result import FitResult +from ._spatial import ( EdgeStructure, _resolve_spatial_graph, naturalize_rho, ) -from locpick.models.base import ( +from .base import ( BaseChoiceModel, SpatialMixin, _compute_fit_statistics, @@ -39,9 +40,8 @@ _safe_inv, _sandwich_inv, ) -from locpick.models.mixed import ParamDistribution, _resolve_draws -from locpick.models.nested import NestingTree, naturalize_nest_params -from locpick.results.fit_result import FitResult +from .mixed import ParamDistribution, _resolve_draws +from .nested import NestingTree, naturalize_nest_params class ChoiceModel(BaseChoiceModel, SpatialMixin): @@ -272,7 +272,7 @@ def _prepare_random_params(self, arrays: ChoiceArrays) -> None: def _build_per_nest_edges(self, arrays: ChoiceArrays) -> None: """Build per-nest EdgeStructure / EdgeDataJAX from the global graph.""" - from locpick._jax.data import EdgeDataJAX + from .._jax.data import EdgeDataJAX n_nests = self._nests.n_nests self._edge_structs = [] @@ -443,29 +443,29 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: # Pure MNL / SCL if not self._is_nested and not self._is_mixed: if self._is_spatial: - from locpick._jax.builders import build_scl_objective + from .._jax.builders import build_scl_objective return build_scl_objective( arrays, self._edge_struct, self._allocation, self._edge_list ) - from locpick._jax.builders import build_mnl_objective + from .._jax.builders import build_mnl_objective return build_mnl_objective(arrays) # Nested (no random) if self._is_nested and not self._is_mixed: if self._is_spatial: - from locpick._jax.builders import build_nested_scl_objective + from .._jax.builders import build_nested_scl_objective return build_nested_scl_objective(arrays, self._nest_matrix, self._edge_data_list) - from locpick._jax.builders import build_nested_objective + from .._jax.builders import build_nested_objective return build_nested_objective(arrays, self._nest_matrix) # Mixed (no nests) if self._is_mixed and not self._is_nested: if self._is_spatial: - from locpick._jax.builders import build_mscl_objective + from .._jax.builders import build_mscl_objective return build_mscl_objective( arrays, @@ -476,7 +476,7 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: self._random_distributions, self._draws, ) - from locpick._jax.builders import build_mixed_logit_objective + from .._jax.builders import build_mixed_logit_objective return build_mixed_logit_objective( arrays, @@ -488,7 +488,7 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: # Mixed Nested if self._is_nested and self._is_mixed: if self._is_spatial: - from locpick._jax.builders import build_mnscl_objective + from .._jax.builders import build_mnscl_objective return build_mnscl_objective( arrays, @@ -498,7 +498,7 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: self._random_distributions, self._draws, ) - from locpick._jax.builders import build_mixed_nested_objective + from .._jax.builders import build_mixed_nested_objective return build_mixed_nested_objective( arrays, @@ -883,7 +883,7 @@ def probabilities(self, data=None, beta=None, alpha=None) -> np.ndarray: arrays = self._arrays if data is not None: - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if not isinstance(data, ChoiceTable): raise TypeError("data must be a ChoiceTable") @@ -899,10 +899,10 @@ def probabilities(self, data=None, beta=None, alpha=None) -> np.ndarray: def _probabilities_mnl(self, arrays, data, beta) -> np.ndarray: """Compute MNL or SCL probabilities.""" - from locpick._kernels.mnl_numpy import mnl_probs_numpy + from .._kernels.mnl_numpy import mnl_probs_numpy if self._is_spatial: - from locpick.models.scl import _scl_log_probs_numpy + from .scl import _scl_log_probs_numpy k = arrays.design_matrix.shape[1] if beta is None: @@ -916,7 +916,7 @@ def _probabilities_mnl(self, arrays, data, beta) -> np.ndarray: float(beta[k]) if beta.size > k else float(self._result.coefficients.values[k]) ) - from locpick._sampling.correction import get_sampling_correction + from .._sampling.correction import get_sampling_correction log_probs = _scl_log_probs_numpy( beta_use, @@ -939,7 +939,7 @@ def _probabilities_mnl(self, arrays, data, beta) -> np.ndarray: n_alts = arrays.n_alts utilities = (dm @ beta).reshape(n_obs, n_alts) - from locpick._sampling.correction import apply_sampling_correction + from .._sampling.correction import apply_sampling_correction utilities = apply_sampling_correction(utilities, arrays) @@ -954,7 +954,7 @@ def _probabilities_complex(self, arrays, data, beta, alpha) -> np.ndarray: """Compute probabilities for nested/mixed models (NumPy fallback).""" # For nested models, use the NumPy kernel if self._is_nested and not self._is_mixed: - from locpick.models.nested import _nested_logit_probs_numpy + from .nested import _nested_logit_probs_numpy k = arrays.design_matrix.shape[1] n_nests = self._nests.n_nests @@ -971,7 +971,7 @@ def _probabilities_complex(self, arrays, data, beta, alpha) -> np.ndarray: if alpha is None: alpha = np.zeros(n_nests) - from locpick._sampling.correction import get_sampling_correction + from .._sampling.correction import get_sampling_correction return _nested_logit_probs_numpy( np.asarray(beta, dtype=np.float64), @@ -986,7 +986,7 @@ def _probabilities_complex(self, arrays, data, beta, alpha) -> np.ndarray: # For mixed models, use the NumPy kernel if self._is_mixed and not self._is_nested: - from locpick.models.mixed import _mixed_logit_probs_numpy + from .mixed import _mixed_logit_probs_numpy k_fixed = self._k_fixed k_random = self._k_random @@ -1000,7 +1000,7 @@ def _probabilities_complex(self, arrays, data, beta, alpha) -> np.ndarray: beta_random_means = beta[k_fixed : k_fixed + k_random] beta_random_spreads = beta[k_fixed + k_random :] - from locpick._sampling.correction import get_sampling_correction + from .._sampling.correction import get_sampling_correction return _mixed_logit_probs_numpy( beta_fixed, @@ -1024,8 +1024,8 @@ def _probabilities_complex(self, arrays, data, beta, alpha) -> np.ndarray: def _probabilities_mixed_nested_numpy(self, arrays, beta, alpha) -> np.ndarray: """Compute mixed nested logit probabilities (NumPy fallback).""" - from locpick._sampling.correction import get_sampling_correction - from locpick.models.nested import _nested_logit_probs_numpy + from .._sampling.correction import get_sampling_correction + from .nested import _nested_logit_probs_numpy k_total = arrays.design_matrix.shape[1] k_fixed = self._k_fixed @@ -1156,7 +1156,7 @@ def utilities(self, data=None, beta=None) -> np.ndarray: n_alts = arrays.n_alts V = (dm @ beta).reshape(n_obs, n_alts) - from locpick._sampling.correction import apply_sampling_correction + from .._sampling.correction import apply_sampling_correction V = apply_sampling_correction(V, arrays) return V @@ -1186,7 +1186,7 @@ def simulate(self, data=None, n_draws: int = 1, seed: Optional[int] = None) -> p Simulated choices with columns ``draw``, ``obs_id``, ``alt_id``, and ``probability``. """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before simulation.") @@ -1240,7 +1240,7 @@ def simulate(self, data=None, n_draws: int = 1, seed: Optional[int] = None) -> p def _resolve_me_data(self, data=None): """Resolve data for marginal effects computation.""" - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before computing marginal effects.") @@ -1515,7 +1515,7 @@ def covariance_robust(self, data=None) -> np.ndarray: np.ndarray, shape (n_parameters, n_parameters) Sandwich (robust) covariance matrix. """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated first.") @@ -1552,7 +1552,7 @@ def covariance_clustered(self, data=None, groups=None) -> np.ndarray: np.ndarray, shape (n_parameters, n_parameters) Cluster-robust covariance matrix. """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated first.") @@ -1628,7 +1628,7 @@ def _observation_scores(self, arrays) -> np.ndarray: def _mnl_observation_scores(self, arrays) -> np.ndarray: """Compute MNL observation scores analytically.""" - from locpick._kernels.mnl_numpy import mnl_observation_scores_numpy + from .._kernels.mnl_numpy import mnl_observation_scores_numpy dm = np.asarray(arrays.design_matrix, dtype=np.float64) chosen = np.asarray(arrays.chosen, dtype=np.float64).reshape(arrays.n_obs, arrays.n_alts) @@ -1641,7 +1641,7 @@ def _mnl_observation_scores(self, arrays) -> np.ndarray: else: available = np.ones((n_obs, n_alts), dtype=np.float64) - from locpick._sampling.correction import get_sampling_correction + from .._sampling.correction import get_sampling_correction inclusion_probs = get_sampling_correction(arrays) diff --git a/locpick/models/mixed.py b/locpick/models/mixed.py index 5602704..3051c71 100644 --- a/locpick/models/mixed.py +++ b/locpick/models/mixed.py @@ -52,15 +52,15 @@ import numpy as np import pandas as pd -from locpick._jax.objective import Objective -from locpick._kernels.constants import NEG_INF -from locpick._solvers import Solver, SolverResult -from locpick.data.arrays import ChoiceArrays -from locpick.models._spatial import ( +from .._jax.objective import Objective +from .._kernels.constants import NEG_INF +from .._solvers import Solver, SolverResult +from ..data.arrays import ChoiceArrays +from ..results.fit_result import FitResult +from ._spatial import ( naturalize_rho, ) -from locpick.models.base import BaseChoiceModel, SpatialMixin, _safe_inv, _sandwich_inv -from locpick.results.fit_result import FitResult +from .base import BaseChoiceModel, SpatialMixin, _safe_inv, _sandwich_inv # --------------------------------------------------------------------------- # Distribution specifications @@ -657,7 +657,7 @@ class MixedMNL(BaseChoiceModel, SpatialMixin): Examples -------- >>> from locpick import ChoiceTable - >>> from locpick.models.mixed import MixedLogit, ParamDistribution + >>> from .mixed import MixedLogit, ParamDistribution >>> ct = ChoiceTable.from_tables(choosers, alternatives, chosen) >>> model = MixedLogit( ... ct, formula="cost + time - 1", @@ -814,7 +814,7 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: random_distributions = self._random_distributions if self._is_spatial: - from locpick._jax.builders import build_mscl_objective + from .._jax.builders import build_mscl_objective return build_mscl_objective( arrays, @@ -828,7 +828,7 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: backend = (self._backend or os.environ.get("LOCPICK_MIXED_BACKEND", "")).lower() if backend != "numpy": - from locpick._jax.builders import build_mixed_logit_objective + from .._jax.builders import build_mixed_logit_objective return build_mixed_logit_objective( arrays, @@ -1139,7 +1139,7 @@ def utilities(self, data=None, beta_fixed=None, beta_random_means=None): V = (dm @ beta_full).reshape(n_obs, n_alts) # Add sampling correction if present - from locpick._sampling.correction import apply_sampling_correction + from .._sampling.correction import apply_sampling_correction V = apply_sampling_correction(V, arrays) @@ -1171,7 +1171,7 @@ def simulate(self, data=None, n_draws: int = 1, seed: Optional[int] = None) -> p Simulated choices with columns ``draw``, ``obs_id``, ``alt_id``, and ``probability``. """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before simulation.") @@ -1240,7 +1240,7 @@ def marginal_effect(self, data=None, variable: Optional[str] = None) -> pd.Serie pd.Series Direct marginal effects, indexed by (obs_id, alt_id). """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before computing marginal effects.") @@ -1286,7 +1286,7 @@ def cross_marginal_effect(self, data=None, variable: Optional[str] = None) -> pd pd.Series Cross-marginal effects, indexed by (obs_id, alt_id). """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before computing marginal effects.") @@ -1340,7 +1340,7 @@ def elasticity(self, data=None, variable: Optional[str] = None) -> pd.Series: pd.Series Direct elasticities, indexed by (obs_id, alt_id). """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before computing elasticities.") @@ -1394,7 +1394,7 @@ def cross_elasticity(self, data=None, variable: Optional[str] = None) -> pd.Seri pd.Series Cross-elasticities, indexed by (obs_id, alt_id). """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before computing elasticities.") @@ -1442,7 +1442,7 @@ def covariance_robust(self, data=None) -> np.ndarray: np.ndarray, shape (n_parameters, n_parameters) Sandwich (robust) covariance matrix. """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated first.") @@ -1481,7 +1481,7 @@ def covariance_clustered(self, data=None, groups=None) -> np.ndarray: np.ndarray, shape (n_parameters, n_parameters) Cluster-robust covariance matrix. """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated first.") @@ -1623,7 +1623,7 @@ def probabilities( draws = self._draws # Resolve canonical sampling correction tensor. - from locpick._sampling.correction import get_sampling_correction + from .._sampling.correction import get_sampling_correction sampling_correction = get_sampling_correction(arrays) diff --git a/locpick/models/mixed_nested.py b/locpick/models/mixed_nested.py index 7ea9c67..dcae25a 100644 --- a/locpick/models/mixed_nested.py +++ b/locpick/models/mixed_nested.py @@ -46,23 +46,23 @@ import numpy as np import pandas as pd -from locpick._jax.objective import Objective -from locpick._solvers import Solver, SolverResult -from locpick.data.arrays import ChoiceArrays -from locpick.models._spatial import ( +from .._jax.objective import Objective +from .._solvers import Solver, SolverResult +from ..data.arrays import ChoiceArrays +from ..results.fit_result import FitResult +from ._spatial import ( EdgeStructure, _resolve_spatial_graph, naturalize_rho, ) -from locpick.models.base import ( +from .base import ( BaseChoiceModel, SpatialMixin, _compute_fit_statistics, _compute_null_ll, ) -from locpick.models.mixed import ParamDistribution, _resolve_draws -from locpick.models.nested import NestingTree, naturalize_nest_params -from locpick.results.fit_result import FitResult +from .mixed import ParamDistribution, _resolve_draws +from .nested import NestingTree, naturalize_nest_params class MixedNestedMNL(BaseChoiceModel, SpatialMixin): @@ -106,9 +106,9 @@ class MixedNestedMNL(BaseChoiceModel, SpatialMixin): Examples -------- >>> from locpick import ChoiceTable - >>> from locpick.models.mixed_nested import MixedNestedMNL - >>> from locpick.models.nested import NestingTree, NestSpec - >>> from locpick.models.mixed import ParamDistribution + >>> from .mixed_nested import MixedNestedMNL + >>> from .nested import NestingTree, NestSpec + >>> from .mixed import ParamDistribution >>> ct = ChoiceTable.from_tables(choosers, alternatives, chosen) >>> nests = NestingTree([ ... NestSpec("transit", alt_ids=[0, 1, 2]), @@ -220,7 +220,7 @@ def _pre_fit(self, arrays: ChoiceArrays) -> None: self._resolve_spatial_graph() self._validate_graph_size(arrays) - from locpick._jax.data import EdgeDataJAX + from .._jax.data import EdgeDataJAX n_nests = self._nests.n_nests self._edge_structs = [] @@ -301,7 +301,7 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: ) if self._is_spatial: - from locpick._jax.builders import build_mnscl_objective + from .._jax.builders import build_mnscl_objective return build_mnscl_objective( arrays, @@ -315,7 +315,7 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: # Try JAX backend first backend = (self._backend or os.environ.get("LOCPICK_MIXED_NESTED_BACKEND", "")).lower() if backend != "numpy": - from locpick._jax.builders import build_mixed_nested_objective + from .._jax.builders import build_mixed_nested_objective return build_mixed_nested_objective( arrays, @@ -546,7 +546,7 @@ def probabilities(self, data=None, beta=None, alpha=None): arrays = self._arrays if data is not None: - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if not isinstance(data, ChoiceTable): raise TypeError("data must be a ChoiceTable") @@ -562,7 +562,7 @@ def _probabilities_jax(self, arrays, beta=None, alpha=None): """Compute probabilities using JAX backend.""" import jax.numpy as jnp - from locpick._jax.data import ChoiceDataJAX + from .._jax.data import ChoiceDataJAX k_total = arrays.design_matrix.shape[1] k_fixed = k_total - len(self._random_col_indices) @@ -662,7 +662,7 @@ def _probabilities_jax(self, arrays, beta=None, alpha=None): V = np.asarray(v_fixed) + v_random # Nested logit probabilities for this draw - from locpick.models.nested import _nested_logit_probs_numpy + from .nested import _nested_logit_probs_numpy _nested_logit_probs_numpy( np.concatenate([beta_fixed, np.zeros(0)]), # beta only, no nest params in utility @@ -688,7 +688,7 @@ def _probabilities_jax(self, arrays, beta=None, alpha=None): def _probabilities_numpy(self, arrays, beta=None, alpha=None): """Compute probabilities using NumPy backend (fallback).""" - from locpick._sampling.correction import get_sampling_correction + from .._sampling.correction import get_sampling_correction k_total = arrays.design_matrix.shape[1] k_fixed = k_total - len(self._random_col_indices) @@ -869,7 +869,7 @@ def utilities(self, data=None, beta=None): arrays = self._arrays if data is not None: - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if not isinstance(data, ChoiceTable): raise TypeError("data must be a ChoiceTable") @@ -898,7 +898,7 @@ def utilities(self, data=None, beta=None): V = np.zeros((n_obs, n_alts)) # Add sampling correction if present - from locpick._sampling.correction import apply_sampling_correction + from .._sampling.correction import apply_sampling_correction V = apply_sampling_correction(V, arrays) diff --git a/locpick/models/mnl.py b/locpick/models/mnl.py index 4196a2c..9a87912 100644 --- a/locpick/models/mnl.py +++ b/locpick/models/mnl.py @@ -12,13 +12,14 @@ import numpy as np import pandas as pd -from locpick._solvers import Solver, SolverResult -from locpick.data.arrays import ChoiceArrays -from locpick.data.problem import EstimationProblem -from locpick.models._spatial import ( +from .._solvers import Solver, SolverResult +from ..data.arrays import ChoiceArrays +from ..data.problem import EstimationProblem +from ..results.fit_result import FitResult +from ._spatial import ( naturalize_rho, ) -from locpick.models.base import ( +from .base import ( BaseChoiceModel, SpatialMixin, _compute_fit_statistics, @@ -26,7 +27,6 @@ _safe_inv, _sandwich_inv, ) -from locpick.results.fit_result import FitResult class MNL(BaseChoiceModel, SpatialMixin): @@ -158,7 +158,7 @@ def probabilities(self, data=None, beta=None): np.ndarray, shape (n_obs, n_alts) Choice probabilities for each observation and alternative. """ - from locpick._kernels.mnl_numpy import mnl_probs_numpy + from .._kernels.mnl_numpy import mnl_probs_numpy if self._arrays is None: raise RuntimeError("Model must be estimated before prediction.") @@ -171,7 +171,7 @@ def probabilities(self, data=None, beta=None): ) if self._is_spatial: - from locpick.models.scl import _scl_log_probs_dispatch + from .scl import _scl_log_probs_dispatch k = arrays.design_matrix.shape[1] if beta is None: @@ -186,7 +186,7 @@ def probabilities(self, data=None, beta=None): else: rho = float(self._result.coefficients.values[k]) - from locpick._sampling.correction import get_sampling_correction + from .._sampling.correction import get_sampling_correction log_probs = _scl_log_probs_dispatch( beta_use, @@ -211,7 +211,7 @@ def probabilities(self, data=None, beta=None): # Systematic utility with sampling correction utilities = (dm @ beta).reshape(n_obs, n_alts) - from locpick._sampling.correction import apply_sampling_correction + from .._sampling.correction import apply_sampling_correction utilities = apply_sampling_correction(utilities, arrays) @@ -265,7 +265,7 @@ def utilities(self, data=None, beta=None): V = (dm @ beta).reshape(n_obs, n_alts) # Add sampling correction if present - from locpick._sampling.correction import apply_sampling_correction + from .._sampling.correction import apply_sampling_correction V = apply_sampling_correction(V, arrays) @@ -297,7 +297,7 @@ def simulate(self, data=None, n_draws: int = 1, seed: Optional[int] = None) -> p Simulated choices with columns ``draw``, ``obs_id``, ``alt_id``, and ``probability``. """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before simulation.") @@ -381,7 +381,7 @@ def marginal_effect(self, data=None, variable: Optional[str] = None) -> pd.Serie pd.Series Direct marginal effects, indexed by (obs_id, alt_id). """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before computing marginal effects.") @@ -431,7 +431,7 @@ def cross_marginal_effect(self, data=None, variable: Optional[str] = None) -> pd pd.Series Cross-marginal effects, indexed by (obs_id, alt_id). """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before computing marginal effects.") @@ -481,7 +481,7 @@ def elasticity(self, data=None, variable: Optional[str] = None) -> pd.Series: pd.Series Direct elasticities, indexed by (obs_id, alt_id). """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before computing elasticities.") @@ -529,7 +529,7 @@ def cross_elasticity(self, data=None, variable: Optional[str] = None) -> pd.Seri pd.Series Cross-elasticities, indexed by (obs_id, alt_id). """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before computing elasticities.") @@ -575,7 +575,7 @@ def covariance_robust(self, data=None) -> np.ndarray: np.ndarray, shape (n_parameters, n_parameters) Sandwich (robust) covariance matrix. """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated first.") @@ -614,7 +614,7 @@ def covariance_clustered(self, data=None, groups=None) -> np.ndarray: np.ndarray, shape (n_parameters, n_parameters) Cluster-robust covariance matrix. """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated first.") @@ -713,7 +713,7 @@ def _observation_scores(self, arrays) -> np.ndarray: self._observation_scores_cache[cache_key] = scores return scores - from locpick._kernels.mnl_numpy import mnl_observation_scores_numpy + from .._kernels.mnl_numpy import mnl_observation_scores_numpy dm = np.asarray(arrays.design_matrix, dtype=np.float64) chosen = np.asarray(arrays.chosen, dtype=np.float64).reshape(arrays.n_obs, arrays.n_alts) @@ -726,7 +726,7 @@ def _observation_scores(self, arrays) -> np.ndarray: else: available = np.ones((n_obs, n_alts), dtype=np.float64) - from locpick._sampling.correction import get_sampling_correction + from .._sampling.correction import get_sampling_correction inclusion_probs = get_sampling_correction(arrays) @@ -781,7 +781,7 @@ def _build_objective(self, arrays: ChoiceArrays): is provided for debugging and benchmarking. """ if self._is_spatial: - from locpick._jax.builders import build_scl_objective + from .._jax.builders import build_scl_objective return build_scl_objective( arrays, self._edge_struct, self._allocation, self._edge_list @@ -792,7 +792,7 @@ def _build_objective(self, arrays: ChoiceArrays): backend = (self._backend or os.environ.get("LOCPICK_MNL_BACKEND", "")).lower() if backend == "numpy": return self._build_objective_numpy(arrays) - from locpick._jax.builders import build_mnl_objective + from .._jax.builders import build_mnl_objective return build_mnl_objective(arrays) @@ -823,7 +823,7 @@ def _build_objective_numpy(self, arrays: ChoiceArrays): Delegates to the canonical MNL kernels in :mod:`locpick._kernels.mnl_numpy` to avoid code duplication. """ - from locpick._kernels.mnl_numpy import ( + from .._kernels.mnl_numpy import ( mnl_gradient_numpy, mnl_log_likelihood_numpy, ) @@ -843,7 +843,7 @@ def _build_objective_numpy(self, arrays: ChoiceArrays): else: available = np.ones((n_obs, n_alts), dtype=np.float64) - from locpick._sampling.correction import get_sampling_correction + from .._sampling.correction import get_sampling_correction inclusion_probs = get_sampling_correction(arrays) @@ -871,7 +871,7 @@ def gradient(beta: np.ndarray) -> np.ndarray: inclusion_probs=inclusion_probs, ) - from locpick._jax.objective import Objective + from .._jax.objective import Objective return Objective(fn=log_likelihood, grad=gradient) diff --git a/locpick/models/nested.py b/locpick/models/nested.py index fc4382a..d008d36 100644 --- a/locpick/models/nested.py +++ b/locpick/models/nested.py @@ -39,15 +39,16 @@ import numpy as np import pandas as pd -from locpick._jax.objective import Objective -from locpick._solvers import Solver, SolverResult -from locpick.data.arrays import ChoiceArrays -from locpick.models._spatial import ( +from .._jax.objective import Objective +from .._solvers import Solver, SolverResult +from ..data.arrays import ChoiceArrays +from ..results.fit_result import FitResult +from ._spatial import ( EdgeStructure, _resolve_spatial_graph, naturalize_rho, ) -from locpick.models.base import ( +from .base import ( BaseChoiceModel, SpatialMixin, _compute_fit_statistics, @@ -55,7 +56,6 @@ _safe_inv, _sandwich_inv, ) -from locpick.results.fit_result import FitResult # --------------------------------------------------------------------------- # Nest specification @@ -266,7 +266,7 @@ def _nested_logit_probs_numpy( else: avail = np.ones((n_obs, n_alts), dtype=np.float64) - from locpick._kernels.constants import NEG_INF + from .._kernels.constants import NEG_INF utilities = np.where(avail > 0, utilities, NEG_INF) @@ -414,7 +414,7 @@ class NestedMNL(BaseChoiceModel, SpatialMixin): Examples -------- >>> from locpick import ChoiceTable, MultinomialLogit - >>> from locpick.models.nested import NestedLogit, NestSpec, NestingTree + >>> from .nested import NestedLogit, NestSpec, NestingTree >>> nests = NestingTree([ ... NestSpec("transit", alt_ids=[0, 1, 2]), ... NestSpec("auto", alt_ids=[3, 4]), @@ -480,7 +480,7 @@ def _pre_fit(self, arrays: ChoiceArrays) -> None: self._validate_graph_size(arrays) # Build per-nest EdgeStructure / EdgeDataJAX from the global graph. - from locpick._jax.data import EdgeDataJAX + from .._jax.data import EdgeDataJAX n_nests = self._nests.n_nests self._edge_structs = [] @@ -528,14 +528,14 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: nest_matrix = self._nest_matrix if self._is_spatial: - from locpick._jax.builders import build_nested_scl_objective + from .._jax.builders import build_nested_scl_objective return build_nested_scl_objective(arrays, nest_matrix, self._edge_data_list) # Try JAX backend first (default when available) backend = (self._backend or os.environ.get("LOCPICK_NESTED_BACKEND", "")).lower() if backend != "numpy": - from locpick._jax.builders import build_nested_objective + from .._jax.builders import build_nested_objective return build_nested_objective(arrays, nest_matrix) @@ -776,7 +776,7 @@ def utilities(self, data=None, beta=None): V = (dm @ beta).reshape(n_obs, n_alts) # Add sampling correction if present - from locpick._sampling.correction import apply_sampling_correction + from .._sampling.correction import apply_sampling_correction V = apply_sampling_correction(V, arrays) @@ -808,7 +808,7 @@ def simulate(self, data=None, n_draws: int = 1, seed: Optional[int] = None) -> p Simulated choices with columns ``draw``, ``obs_id``, ``alt_id``, and ``probability``. """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before simulation.") @@ -877,7 +877,7 @@ def marginal_effect(self, data=None, variable: Optional[str] = None) -> pd.Serie pd.Series Direct marginal effects, indexed by (obs_id, alt_id). """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before computing marginal effects.") @@ -924,7 +924,7 @@ def cross_marginal_effect(self, data=None, variable: Optional[str] = None) -> pd pd.Series Cross-marginal effects, indexed by (obs_id, alt_id). """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before computing marginal effects.") @@ -975,7 +975,7 @@ def elasticity(self, data=None, variable: Optional[str] = None) -> pd.Series: pd.Series Direct elasticities, indexed by (obs_id, alt_id). """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before computing elasticities.") @@ -1023,7 +1023,7 @@ def cross_elasticity(self, data=None, variable: Optional[str] = None) -> pd.Seri pd.Series Cross-elasticities, indexed by (obs_id, alt_id). """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated before computing elasticities.") @@ -1069,7 +1069,7 @@ def covariance_robust(self, data=None) -> np.ndarray: np.ndarray, shape (n_parameters, n_parameters) Sandwich (robust) covariance matrix. """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated first.") @@ -1108,7 +1108,7 @@ def covariance_clustered(self, data=None, groups=None) -> np.ndarray: np.ndarray, shape (n_parameters, n_parameters) Cluster-robust covariance matrix. """ - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable if self._arrays is None: raise RuntimeError("Model must be estimated first.") @@ -1223,7 +1223,7 @@ def probabilities(self, data=None, beta=None, alpha=None): nest_matrix = self._nests.build_nest_matrix(alt_ids) # Resolve canonical sampling correction tensor. - from locpick._sampling.correction import get_sampling_correction + from .._sampling.correction import get_sampling_correction sampling_correction = get_sampling_correction(arrays) diff --git a/locpick/models/sar_mnl.py b/locpick/models/sar_mnl.py index d470505..8ee1f92 100644 --- a/locpick/models/sar_mnl.py +++ b/locpick/models/sar_mnl.py @@ -27,15 +27,15 @@ import numpy as np import pandas as pd -from locpick._solvers import Solver, SolverResult -from locpick.data.arrays import ChoiceArrays -from locpick.models._spatial_weights import resolve_spatial_weights -from locpick.models.base import ( +from .._solvers import Solver, SolverResult +from ..data.arrays import ChoiceArrays +from ..results.fit_result import FitResult +from ._spatial_weights import resolve_spatial_weights +from .base import ( BaseChoiceModel, _compute_fit_statistics, _compute_null_ll, ) -from locpick.results.fit_result import FitResult class SARMNL(BaseChoiceModel): @@ -158,7 +158,7 @@ def _fit_linearized_gmm(self) -> FitResult: self._arrays = arrays self._pre_fit(arrays) - from locpick._kernels.sar_mnl_numpy import fit_linearized_gmm + from .._kernels.sar_mnl_numpy import fit_linearized_gmm result_dict = fit_linearized_gmm(arrays, self._W_sparse) @@ -204,7 +204,7 @@ def _build_objective(self, arrays: ChoiceArrays): Auto-selects dense solve (n_alts ≤ 2000) or conjugate gradient (n_alts > 2000) based on the estimator setting. """ - from locpick._jax.sar_kernels import build_sar_mnl_objective + from .._jax.sar_kernels import build_sar_mnl_objective # Auto-select estimator if self._estimator == "auto": @@ -306,7 +306,7 @@ def probabilities(self, data=None, beta=None, rho=None): np.ndarray, shape (n_obs, n_alts) Choice probabilities for each observation and alternative. """ - from locpick._kernels.mnl_numpy import mnl_probs_numpy + from .._kernels.mnl_numpy import mnl_probs_numpy if self._arrays is None: raise RuntimeError("Model must be estimated before prediction.") @@ -335,7 +335,7 @@ def probabilities(self, data=None, beta=None, rho=None): V_base = (dm @ beta).reshape(n_obs, n_alts) # Sampling correction - from locpick._sampling.correction import apply_sampling_correction + from .._sampling.correction import apply_sampling_correction V_base = apply_sampling_correction(V_base, arrays) @@ -394,7 +394,7 @@ def utilities(self, data=None, beta=None, rho=None): V_base = (dm @ beta).reshape(n_obs, n_alts) - from locpick._sampling.correction import apply_sampling_correction + from .._sampling.correction import apply_sampling_correction V_base = apply_sampling_correction(V_base, arrays) @@ -445,7 +445,7 @@ def marginal_effects(self, data=None, variable: Optional[str] = None): if self._arrays is None: raise RuntimeError("Model must be estimated before computing marginal effects.") - from locpick.data.choicetable import ChoiceTable + from ..data.choicetable import ChoiceTable ct = self._data arrays = self._arrays diff --git a/locpick/results/diagnostics.py b/locpick/results/diagnostics.py index e31c9be..81168e0 100644 --- a/locpick/results/diagnostics.py +++ b/locpick/results/diagnostics.py @@ -14,7 +14,7 @@ from scipy.linalg import cho_factor, cho_solve if TYPE_CHECKING: - from locpick.results.fit_result import FitResult + from .fit_result import FitResult @dataclass diff --git a/locpick/spec/model_spec.py b/locpick/spec/model_spec.py index d8d6138..27aae3a 100644 --- a/locpick/spec/model_spec.py +++ b/locpick/spec/model_spec.py @@ -198,7 +198,7 @@ def _build_from_scoped_terms(self, data) -> Any: import numpy as np import pandas as pd - from locpick.data import ChoiceArrays + from ..data import ChoiceArrays df = data.to_frame() n_obs = data.n_observations From 798c5913c777a65b6b5f0f8e3e356209380321fa Mon Sep 17 00:00:00 2001 From: knaaptime Date: Fri, 19 Jun 2026 19:34:48 -0700 Subject: [PATCH 5/7] consolidate sar into choicemodel --- README.md | 25 +- ...ivelike_locpick_household_tract_demo.ipynb | 6 +- .../user-guide/spatial_models_demo.ipynb | 382 +------------ locpick/__init__.pyi | 3 - locpick/_jax/sar_kernels.py | 468 +++++++++++++++- locpick/models/__init__.pyi | 3 - locpick/models/choice_model.py | 476 +++++++++++++++- locpick/models/sar_mnl.py | 530 ------------------ tests/test_sar_mnl.py | 78 ++- 9 files changed, 1007 insertions(+), 964 deletions(-) delete mode 100644 locpick/models/sar_mnl.py diff --git a/README.md b/README.md index a430a33..59f1a62 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ - **Large-scale urban models**: 100K+ choosers, 1K+ alternatives - **Sampling-based estimation**: Most alternatives are irrelevant; only a sampled subset is evaluated per chooser -- **Spatial correlation**: Nearby alternatives share unobserved attributes (via `graph=` on any model) +- **Spatial dependence**: Nearby alternatives can affect each other or share unobserved attributes (via `graph=` on any model) - **Heterogeneous preferences**: Mixed logit for random taste variation - **Nested structure**: Nested logit for hierarchical choice (e.g., county → tract → block) - **JAX-native computation**: JIT-compiled kernels, GPU acceleration, automatic differentiation @@ -13,21 +13,18 @@ The package is **not** a general-purpose ML library. It is specifically for stru ## Features -LocPick can automate the creation of choice tables for estimation or simulation, using census choice sets, uniform or weighted random sampling of alternatives, generated interaction terms, and cartesian merges. A unique feature is the implementation of *spatial* choice models, which take one of two forms. The [Bhat et al](https://linkinghub.elsevier.com/retrieve/pii/S0191261503000055) approach is similar to a spatial error model, assuming that nearby alternatives are more similar (closer substitutes). The SAR style approach assumes that the structural utility of each alternative $V$ has a simultaneous autoregressive structure, and is estimated with either [PML](http://dx.doi.org/10.1016/j.regsciurbeco.2009.09.004) or [GMM](https://www.sciencedirect.com/science/article/pii/S0166046217300625) +LocPick can automate the creation of choice tables for estimation or simulation, using census choice sets, uniform or weighted random sampling of alternatives, generated interaction terms, and cartesian merges. A unique feature is the implementation of *spatial* choice models, which take one of two forms. -It also provides tools for Monte Carlo simulation of choices given probability distributions from fitted models, with fast algorithms for independent or capacity-constrained choices. - -LocPick includes native classic and spatially-correlated Multinomial Logit, Nested Logit, and Mixed Logit estimators, and an internal data pipeline designed around pandas inputs, xarray-backed alignment, and NumPy/JAX-ready arrays. +- The [Bhat et al](https://linkinghub.elsevier.com/retrieve/pii/S0191261503000055) approach is similar to a spatial error model, assuming that nearby alternatives are more similar (closer substitutes). +- The SAR style approach assumes that the structural utility of each alternative $V$ has a simultaneous autoregressive structure, and is estimated with either [PML](http://dx.doi.org/10.1016/j.regsciurbeco.2009.09.004) or [GMM](https://www.sciencedirect.com/science/article/pii/S0166046217300625) -## Installation - -Install LocPick with Pip or Conda: +It also provides tools for Monte Carlo simulation of choices given probability distributions from fitted models, with fast algorithms for independent or capacity-constrained choices. -```bash -pip install locpick -``` +LocPick includes estimators for classic, spatially-correlated, and simultaneous autoregressive: -```bash -conda install locpick --channel conda-forge -``` +- Multinomial Logit +- Nested Logit +- Mixed Logit and +- Mixed/Nested +models, and an internal data pipeline designed around pandas inputs, xarray-backed alignment, and NumPy/JAX-ready arrays. diff --git a/docs/source/user-guide/livelike_locpick_household_tract_demo.ipynb b/docs/source/user-guide/livelike_locpick_household_tract_demo.ipynb index 5a9b4c9..e0300a3 100644 --- a/docs/source/user-guide/livelike_locpick_household_tract_demo.ipynb +++ b/docs/source/user-guide/livelike_locpick_household_tract_demo.ipynb @@ -61,7 +61,7 @@ "from livelike.config import up_expanded_attributes_household\n", "from pymedm import PMEDM\n", "\n", - "from locpick import MNL, ChoiceTable" + "from locpick import ChoiceModel, ChoiceTable" ] }, { @@ -435,7 +435,7 @@ "source": [ "formula_1 = \"median_contract_rent + median_home_value + median_household_income + p_poverty_rate\"\n", "\n", - "model_1 = MNL(ct, formula=formula_1)\n", + "model_1 = ChoiceModel(ct, formula=formula_1)\n", "result_1 = model_1.fit()" ] }, @@ -474,7 +474,7 @@ "_interaction_terms = [k for k in interactions.keys() if k in ct._ds.data_vars]\n", "formula_2 = \" + \".join(_base_terms + _interaction_terms)\n", "\n", - "model_2 = MNL(ct, formula=formula_2)\n", + "model_2 = ChoiceModel(ct, formula=formula_2)\n", "result_2 = model_2.fit()\n", "print(result_2.summary())" ] diff --git a/docs/source/user-guide/spatial_models_demo.ipynb b/docs/source/user-guide/spatial_models_demo.ipynb index f7c0237..95108c5 100644 --- a/docs/source/user-guide/spatial_models_demo.ipynb +++ b/docs/source/user-guide/spatial_models_demo.ipynb @@ -18,7 +18,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -40,7 +40,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -49,7 +49,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -58,7 +58,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -67,42 +67,16 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAx0AAAM6CAYAAADkMC4dAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjksIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvJkbTWQAAAAlwSFlzAAAewgAAHsIBbtB1PgABAABJREFUeJzsnQd4I+X19a8sy7LcrXXvW9heWdilJUACIdQECITO7lJD+ZME0hMSQr5USAIp1GXpvbdQQ8vCrrf3Xtx777Zsfc957TGyrDIzmhmN5Pt7oqyx5dFoJNvvee+951jcbrebGIZhGIZhGIZhdCJGrwMzDMMwDMMwDMOw6GAYhmEYhmEYRne40sEwDMMwDMMwjK6w6GAYhmEYhmEYRldYdDAMwzAMwzAMoyssOhiGYRiGYRiG0RUWHQzDMAzDMAzD6AqLDoZhGIZhGIZhdIVFB8MwDMMwDMMwusKig2EYhmEYhmEYXWHRwTAMwzAMwzCMrrDoYBiGYRiGYRhGV1h0MAzDMAzDMAyjKyw6GIZhGIZhGIbRFRYdDMMwDMMwDMPoCosOhmEYhmEYhmF0JVbfwzORSm9vL23btk18nJmZSbGx/FZhGIZhGIYJhsvlooaGBvHxvHnzKD4+ni8aiw7GHxAcS5Ys4QvEMAzDMAyjktLSUjr66KP5+nF7FcMwDMMwDMMwesM9M4xP0FLlqdJzc3P5SjEMwzAMwwShpqZmtFvEcz010WHRwfh+Y3jMcEBwFBQU8JViGIZhGIZRstDmmdhR2L2KYRiGYRiGYRhdYdHBMAzDMAzDMIyusOhgGIZhGIZhGEZXWHQwDMMwDMMwDKMrLDoYhmEYhmEYhtEVFh0MwzAMwzAMw+gKiw6GYRiGYRiGYXSFRQfDMAzDMAzDMJEpOtrb2+nZZ5+lW2+9lU488USaNm0apaamUlxcHGVlZdFJJ51Ef/7zn6mpqSnosQ4dOkQ/+MEPaO7cuZScnEyJiYk0ffp0uvHGG2nHjh2anjfO59e//jUtWLBAnG9KSor4GJ+Tc64SX3zxBV1++eVUUlJC8fHxImDvm9/8prgmcnG5XPTAAw/QV7/6VZFo6XA4xHW8/vrraefOnSqfIcMwDMMwDMMYjFsn3n//fTcOH+yWkZHhfuedd/we54EHHnDHxcX5/X587d///rcm51xaWurOzc31+1h5eXnudevWBT3OHXfc4Y6JifF7nLPPPtvd09MT8BiNjY3upUuX+j2G3W53r1y50q0XFRUVo4+FjxmGYRiGYRheQ6lF1/aqwsJCuuKKK+iee+6hl19+Wez+r169mp577jm64IILyGq1UmNjI51zzjm0devWcd+PqsB1111H/f39ourw29/+lv73v//RunXr6MEHHxS7/vgaKh4vvvhiSOdaVVVFZ599NtXU1IjI+h//+Mf06aefihs+xueqq6vprLPOEvf1x8MPPyyqIkNDQzR16lRauXIllZaW0quvvkonn3yyuM8bb7xBV199td9jDA4O0nnnnUdr164V/42P//Of/4j/vvfee0WlqK+vj6699lp69913Q3reDMMwDMMwDKM7bp1wuVxB7/PKK6+M7qafd955Y77W1dXlzsrKEl9LSkpyb9u2bdz3t7W1uefNmyfuk5OT4+7s7FR9vldeeeXouTz//PPjvo7PSV9fvny5z2O0tLS409LSxH2KiorcDQ0N464JqhzScT755BOfx1m1atXofW644YZxX9+3b587JSVFfP2II45wDwwMuLWGKx0MwzAMwzC8hjJ9pQNVjGB8+9vfppkzZ4qPUVHwBDv79fX14uNbbrlFzHN4g3mLv/71r+Lj2tpaevTRR1Wda11dHT355JPi49NOO01UYbzB5/A18Pjjj4vv8eahhx6i1tZW8fGf/vQnysjIGHdN/v3vf49em7/85S8+z0f6fHp6us/7oMLzs5/9THy8b98+eu211xQ/Z4ZhGIZhGIaZMO5VGAoHvb29Yz6PFiqJ008/3e/3YyAdg9pAbYvV66+/LlqawPLly/3eb9myZeJf3Bff4w1aqCQxhJYoXxQUFNApp5wiPn7//feps7NzzNchIqQh8e9+97uUkJAQ8FwAWtcYhmEYhmEYxqyEVXTs2rWLNm/eLD6WKh4Szc3Nox9nZ2f7PQZmLZxOp/j4888/F45PSvnss89GP4bTlj88v4bZEk8wW4LZDXDssccKl65gx8Fchqe4UnIuOTk5wsHL17kwDMMwDMMwjJmINfoBu7u7xSA2hqlhmStVGNBC5asCAtra2vwez+12C3teaeG/f//+cQJGjvgBGFbHYt4fsL1FFQOPJ32PZ4VCEjzBHt/z6ziONGDueS5yj7N3716qqKigrq6uMdcsGJWVlQG/joF6hmEYhmEYhokY0YFZ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pFctmlUq93x3miRw0jgREYURgw4iohhd6Vg0s1T+77vTGXAQEVHYMeggIopyZtPIoOM0BBxnMOAgIqLIwKCDiCjKDY85Tp1RIr9hwEFERBGEQQcRUQw5ZUaJ/PbMGUypIiKiiMKgg4goyvX129W/J08vkd9yhYOIiCIQgw4ioijXP2CXE6cVy71nTvfYyYqIiChcEsP2zEREpItjpxTJBQdWMuAgIqKIxaCDiCjKXXJIdbg3gYiIyCOuwxMRERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaEYdBARERERkaESjX14ilZ9fX2O6zt37gzrthARERFFC+fjJufjqXjHoINc2rt3r+P6vHnz+C4RERERBXA8VVVVxfeN6VVERERERGQ0k91utxv+LBR1urq6ZNWqVep6fn6+JCZyUczXJVVtZWjp0qVSXFxs6H6iyML9H9+4/+Mb9398c97/n3zyieO4aerUqZKcnBzmrYsMPJIkl/ABmTt3Lt+dICDgKCsr43sYp7j/4xv3f3zj/o9v5eXl/PvvArtXERERERGRoRh0EBERERGRoRh0EBERERGRoRh0EBERERGRoRh0EBERERGRoRh0EBERERGRoRh0EBERERGRoTgckIiIiIiIDMWVDiIiIiIiMhSDDiIiIiIiMhSDDiIiIiIiMhSDDiIiIiIiMhSDDiIiIiIiMhSDDiIiIiIiMhSDDiIiIiIiMhSDDiIiIiIiMhSDDiIiIiIiMhSDDiIiIiIiMhSDDoort956q5hMJr8u+J7hqqqq/H6cLVu2BLTN3d3d8tlnn8nvf/97Of/882X8+PFiNpsdj+uvbdu2yQ033CATJ06U1NRUycnJkXnz5slvfvMb6ejokFgWjftf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zZdISUlRzzF27FivXc7c4ec/dvf/kgj6+8+gg+La73//e8eHEQN2AjV79mz1GImJifbdu3f7/H233HKL4/kff/xxw4IOOPTQQx3b+Mknn4y4/Z577nE8JrYrHkTD/n/00Ucd98EAuOE2bNhgz8jIcLQ57O3tHXGff/7zny6/7q5lbjDvRTSJl/0P/Pzru/+PO+44xz531aJ6x44d9rKyMkc70+3btwe0/9H+dGBgwOVt+FmbNm2a4zHeeustl/fj5z++938kff4ZdFBcmzt3rvqQJSQkqAOvQKxZs8bxYcWEUn/48ktn586d6jbny/jx44d8n/Nl/fr1Lh/nyy+/dJwRSUtLs99xxx32Tz/91P7f//7XfsUVVzgeD32/A52gGm2iYf+jVzsm5Gr3O/300+1vvvmmOnuFP5oFBQXq65iS+/rrr7t8jMrKSntJSYn92muvtT/77LPqj87y5cvVH6lf/OIX9tzcXMfjH3LIIfauri57PIiX/Q/8/Ou7/9etW6fOCOP7bTab+hzh4BPzGB588EF7aWmpY5/dfffdAe9/BJrV1dVq0v1//vMftR/ffvttNZfF+XOL293h5z++938kff4ZdFDcWrt2reODdvzxxwf8OD/5yU8CTm3x5ZfOu+++67iPLxdPKyY446WdFXV1wS8cd0FLrImW/Q979+51/IF0dcGZtP/3//6fx4MOX352cEDraYhYLImn/a/h51/f/Y+DzMLCQrf7xWQy2X/5y1+6/X5fDzo9fWZxIHnvvfd63E5+/uN7/0fS55/dqyhu6VFAOjAwIM8884y6jhZ2J598skQyDBH66quv5P7771etUdFCDwPhxowZI2eccYZ8//vfF5vNJvEgmvY/Wh5+8skn8sgjj6iiQbRHRFEgir3RC/76669XHVXcefLJJ1WR4KeffiqbNm1SxY0ockQvdnQ7Ouigg1THtQMPPFDiRTztfw0///rufxTofv3112qYJmYlbN68WbXgRav2hQsXyrXXXiszZ86UYFx55ZVqEBw+v1u2bFGtWvG5xfyGE044QS677DJ13RN+/uN7/0fS59+EyMPQZyAiIiIiorjGlrlERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERGQoBh1ERERERMSgg4iIiIiIohdXOoiIiIiIyFAMOoiIiIiIyFAMOoiIiIiISIz0/wEGcVMI+ACIngAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": { - "image/png": { - "height": 413, - "width": 398 - } - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "dc.plot()" ] }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -135,77 +109,18 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[31mSignature:\u001b[39m\n", - "simulate_scl(\n", - " n_obs: \u001b[33m'int'\u001b[39m = \u001b[32m10000\u001b[39m,\n", - " n_alts: \u001b[33m'int'\u001b[39m = \u001b[32m12\u001b[39m,\n", - " alt_params: \u001b[33m'dict[str, float] | None'\u001b[39m = \u001b[38;5;28;01mNone\u001b[39;00m,\n", - " rho: \u001b[33m'float'\u001b[39m = \u001b[32m0.7\u001b[39m,\n", - " adjacency: \u001b[33m'np.ndarray | None'\u001b[39m = \u001b[38;5;28;01mNone\u001b[39;00m,\n", - " seed: \u001b[33m'int'\u001b[39m = \u001b[32m1234\u001b[39m,\n", - ") -> \u001b[33m'SCLDataset'\u001b[39m\n", - "\u001b[31mDocstring:\u001b[39m\n", - "Generate synthetic SCL choice data with known parameters.\n", - "\n", - "The default DGP creates a circular adjacency graph with 12 zones,\n", - "includes both alternative-level and chooser×alternative interaction\n", - "terms, and simulates choices using the SCL probability formula.\n", - "\n", - "Parameters\n", - "----------\n", - "n_obs : int, default 10000\n", - " Number of observations (decision-makers).\n", - "n_alts : int, default 12\n", - " Number of alternatives (zones).\n", - "alt_params : dict, optional\n", - " Mapping of alternative-level column name → true coefficient.\n", - " Default: ``{\"cost\": -0.5, \"time\": -0.1}``.\n", - "rho : float, default 0.7\n", - " True dissimilarity parameter ρ ∈ (0, 1].\n", - "adjacency : np.ndarray, optional\n", - " Binary adjacency matrix of shape ``(n_alts, n_alts)``.\n", - " Default: circular graph where zone *i* is adjacent to\n", - " ``(i-1) % n_alts`` and ``(i+1) % n_alts``.\n", - "seed : int, default 1234\n", - " Random seed for reproducibility.\n", - "\n", - "Returns\n", - "-------\n", - "SCLDataset\n", - "\u001b[31mFile:\u001b[39m ~/Dropbox/projects/locpick/locpick/dgp.py\n", - "\u001b[31mType:\u001b[39m function" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "simulate_scl?" ] }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Observations: 2000\n", - "Alternatives: 179\n", - "True rho: 0.7\n", - "Adjacency shape: (179, 179)\n" - ] - } - ], + "outputs": [], "source": [ "# Generate synthetic SCL data with spatial correlation\n", "n_obs = 2000\n", @@ -245,21 +160,9 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Models configured:\n", - " Spatial MNL: MNL\n", - " Spatial NestedMNL: NestedMNL\n", - " Spatial MixedMNL: MixedMNL\n", - " Spatial MixedNestedMNL: MixedNestedMNL\n" - ] - } - ], + "outputs": [], "source": [ "# Common formula for all models\n", "formula = \"cost + time - 1\"\n", @@ -330,38 +233,9 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "=== Spatial MNL ===\n", - "============================================================\n", - "Spatially Correlated Logit Estimation Results\n", - "============================================================\n", - "Observations: 2000\n", - "Alternatives: 179\n", - "Parameters: 3\n", - "DF residual: 1997\n", - "------------------------------------------------------------\n", - "Log-likelihood: -7883.9502\n", - "LL (null): -10374.7716\n", - "AIC: 15773.9004\n", - "BIC: 15790.7031\n", - "Rho-squared: 0.2401\n", - "Adj. rho-sq: 0.2398\n", - "------------------------------------------------------------\n", - "Parameter Coef Std.Err t P>|t|\n", - "------------------------------------------------------------\n", - "cost -0.4940 0.0172 -28.659 0.0000\n", - "time -0.1993 0.0070 -28.287 0.0000\n", - "rho 0.7088 0.1051 6.743 0.0000\n", - "============================================================\n" - ] - } - ], + "outputs": [], "source": [ "# Fit spatial MNL\n", "result_scl = model_scl.fit()\n", @@ -371,41 +245,9 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "=== Spatial NestedMNL ===\n", - "============================================================\n", - "Nested Spatially Correlated Logit Estimation Results\n", - "============================================================\n", - "Observations: 2000\n", - "Alternatives: 179\n", - "Parameters: 6\n", - "DF residual: 1994\n", - "------------------------------------------------------------\n", - "Log-likelihood: -7887.6327\n", - "LL (null): -10374.7716\n", - "AIC: 15787.2654\n", - "BIC: 15820.8708\n", - "Rho-squared: 0.2397\n", - "Adj. rho-sq: 0.2392\n", - "------------------------------------------------------------\n", - "Parameter Coef Std.Err t P>|t|\n", - "------------------------------------------------------------\n", - "cost -0.5268 0.0143 -36.768 0.0000\n", - "time -0.2140 0.0051 -42.008 0.0000\n", - "rho_urban 1.0000 nan nan 1.0000\n", - "rho_suburban 1.0000 0.0060 166.820 0.0000\n", - "lambda_urban 1.0000 nan nan 1.0000\n", - "lambda_suburban 0.9989 0.0985 10.140 0.0000\n", - "============================================================\n" - ] - } - ], + "outputs": [], "source": [ "# Fit spatial NestedMNL\n", "result_nested_scl = model_nested_scl.fit()\n", @@ -415,39 +257,9 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "=== Spatial MixedMNL ===\n", - "============================================================\n", - "Mixed Spatially Correlated Logit Estimation Results\n", - "============================================================\n", - "Observations: 2000\n", - "Alternatives: 179\n", - "Parameters: 4\n", - "DF residual: 1996\n", - "------------------------------------------------------------\n", - "Log-likelihood: -7883.9503\n", - "LL (null): -10374.7716\n", - "AIC: 15775.9006\n", - "BIC: 15798.3042\n", - "Rho-squared: 0.2401\n", - "Adj. rho-sq: 0.2397\n", - "------------------------------------------------------------\n", - "Parameter Coef Std.Err t P>|t|\n", - "------------------------------------------------------------\n", - "cost -0.4940 0.0172 -28.659 0.0000\n", - "rho 0.7089 0.1051 6.744 0.0000\n", - "mean_time -0.1993 0.0070 -28.287 0.0000\n", - "sd_time -11.5144 101.0278 -0.114 0.9093\n", - "============================================================\n" - ] - } - ], + "outputs": [], "source": [ "# Fit spatial MixedMNL\n", "result_mixed_scl = model_mixed_scl.fit()\n", @@ -457,50 +269,9 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/knaaptime/Dropbox/projects/locpick/locpick/models/mixed.py:214: UserWarning: The balance properties of Sobol' points require n to be a power of 2.\n", - " uniform = sampler.random(n=n_total)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "=== Spatial MixedNestedMNL ===\n", - "============================================================\n", - "Mixed Nested Spatially Correlated Logit Estimation Results\n", - "============================================================\n", - "Observations: 2000\n", - "Alternatives: 179\n", - "Parameters: 7\n", - "DF residual: 1993\n", - "------------------------------------------------------------\n", - "Log-likelihood: -7887.6327\n", - "LL (null): -10374.7716\n", - "AIC: 15789.2655\n", - "BIC: 15828.4718\n", - "Rho-squared: 0.2397\n", - "Adj. rho-sq: 0.2391\n", - "------------------------------------------------------------\n", - "Parameter Coef Std.Err t P>|t|\n", - "------------------------------------------------------------\n", - "cost -0.5267 0.0140 -37.620 0.0000\n", - "rho_urban 1.0000 nan nan 1.0000\n", - "rho_suburban 1.0000 0.0027 376.534 0.0000\n", - "lambda_urban 0.9920 0.8306 1.194 0.2324\n", - "lambda_suburban 1.0000 nan nan 1.0000\n", - "mean_time -0.2139 0.0051 -42.057 0.0000\n", - "sd_time 114.1125 3108247384245457486086144.0000 0.000 1.0000\n", - "============================================================\n" - ] - } - ], + "outputs": [], "source": [ "# Fit spatial MixedNestedMNL\n", "result_mixed_nested_scl = model_mixed_nested_scl.fit()\n", @@ -519,27 +290,9 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " Log-Likelihood Null LL AIC BIC \\\n", - "Spatial MNL -7883.9502 -10374.7716 15773.9004 15790.7031 \n", - "Spatial NestedMNL -7887.6327 -10374.7716 15787.2654 15820.8708 \n", - "Spatial MixedMNL -7883.9503 -10374.7716 15775.9006 15798.3042 \n", - "Spatial MixedNestedMNL -7887.6327 -10374.7716 15789.2655 15828.4718 \n", - "\n", - " Rho² Adj. Rho² n_params \n", - "Spatial MNL 0.2401 0.2398 3.0 \n", - "Spatial NestedMNL 0.2397 0.2392 6.0 \n", - "Spatial MixedMNL 0.2401 0.2397 4.0 \n", - "Spatial MixedNestedMNL 0.2397 0.2391 7.0 \n" - ] - } - ], + "outputs": [], "source": [ "# Collect fit statistics for comparison\n", "results = {\n", @@ -578,74 +331,9 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Spatial MNL probabilities shape: (2000, 179)\n", - "Probabilities sum to 1: True\n", - "\n", - "First 3 decision-makers' probabilities:\n", - "[[0.0008 0.0038 0.0002 0.0428 0.0206 0. 0.0199 0.0003 0.0014 0.0001\n", - " 0.0002 0. 0.0066 0.0127 0.0146 0. 0.0073 0.009 0.0109 0.0043\n", - " 0.0002 0.0043 0.0003 0.0011 0.0015 0.0003 0.0009 0.0005 0.0002 0.0008\n", - " 0. 0.0001 0.0004 0.0003 0.0757 0.0018 0.0012 0. 0.0048 0.0025\n", - " 0.0018 0.0125 0.0004 0.0003 0.0002 0.0001 0. 0.0003 0.0008 0.\n", - " 0.0002 0.0816 0.0004 0.0226 0.0004 0.0001 0.0071 0.0009 0.0002 0.0001\n", - " 0.0011 0.0028 0. 0.0004 0.0208 0.0019 0. 0.001 0.0002 0.0232\n", - " 0.0002 0.0293 0.0021 0.0574 0.039 0.0006 0.0005 0.0005 0.0046 0.0015\n", - " 0.002 0.0045 0.0016 0.0081 0.0043 0.0003 0. 0.0007 0.0002 0.0039\n", - " 0.0016 0.0068 0.0012 0. 0.0001 0. 0.014 0.0023 0.0003 0.0189\n", - " 0.0003 0.0004 0.0004 0.0012 0.0036 0.0003 0.0047 0.0003 0. 0.0039\n", - " 0.0002 0.0017 0.0003 0.0026 0.0051 0.0111 0.0138 0.013 0.0003 0.0003\n", - " 0.0008 0.0013 0.0001 0.0003 0.0075 0.0157 0.0278 0.0001 0.0178 0.0013\n", - " 0.0008 0.0003 0.0001 0.0024 0. 0.0015 0. 0.0016 0.005 0.\n", - " 0.0017 0. 0.0018 0.0038 0.0002 0.0052 0.0008 0.0006 0.0191 0.002\n", - " 0.0004 0.0409 0.0001 0.0012 0.003 0.0014 0. 0.0005 0.0219 0.0265\n", - " 0.0056 0.0001 0. 0.0463 0.0012 0.0005 0.0006 0. 0.0018 0.0017\n", - " 0.0033 0.0004 0.0004 0.0004 0.0151 0.0011 0.0006 0.0001 0.0011]\n", - " [0.0008 0.0038 0.0002 0.0428 0.0206 0. 0.0199 0.0003 0.0014 0.0001\n", - " 0.0002 0. 0.0066 0.0127 0.0146 0. 0.0073 0.009 0.0109 0.0043\n", - " 0.0002 0.0043 0.0003 0.0011 0.0015 0.0003 0.0009 0.0005 0.0002 0.0008\n", - " 0. 0.0001 0.0004 0.0003 0.0757 0.0018 0.0012 0. 0.0048 0.0025\n", - " 0.0018 0.0125 0.0004 0.0003 0.0002 0.0001 0. 0.0003 0.0008 0.\n", - " 0.0002 0.0816 0.0004 0.0226 0.0004 0.0001 0.0071 0.0009 0.0002 0.0001\n", - " 0.0011 0.0028 0. 0.0004 0.0208 0.0019 0. 0.001 0.0002 0.0232\n", - " 0.0002 0.0293 0.0021 0.0574 0.039 0.0006 0.0005 0.0005 0.0046 0.0015\n", - " 0.002 0.0045 0.0016 0.0081 0.0043 0.0003 0. 0.0007 0.0002 0.0039\n", - " 0.0016 0.0068 0.0012 0. 0.0001 0. 0.014 0.0023 0.0003 0.0189\n", - " 0.0003 0.0004 0.0004 0.0012 0.0036 0.0003 0.0047 0.0003 0. 0.0039\n", - " 0.0002 0.0017 0.0003 0.0026 0.0051 0.0111 0.0138 0.013 0.0003 0.0003\n", - " 0.0008 0.0013 0.0001 0.0003 0.0075 0.0157 0.0278 0.0001 0.0178 0.0013\n", - " 0.0008 0.0003 0.0001 0.0024 0. 0.0015 0. 0.0016 0.005 0.\n", - " 0.0017 0. 0.0018 0.0038 0.0002 0.0052 0.0008 0.0006 0.0191 0.002\n", - " 0.0004 0.0409 0.0001 0.0012 0.003 0.0014 0. 0.0005 0.0219 0.0265\n", - " 0.0056 0.0001 0. 0.0463 0.0012 0.0005 0.0006 0. 0.0018 0.0017\n", - " 0.0033 0.0004 0.0004 0.0004 0.0151 0.0011 0.0006 0.0001 0.0011]\n", - " [0.0008 0.0038 0.0002 0.0428 0.0206 0. 0.0199 0.0003 0.0014 0.0001\n", - " 0.0002 0. 0.0066 0.0127 0.0146 0. 0.0073 0.009 0.0109 0.0043\n", - " 0.0002 0.0043 0.0003 0.0011 0.0015 0.0003 0.0009 0.0005 0.0002 0.0008\n", - " 0. 0.0001 0.0004 0.0003 0.0757 0.0018 0.0012 0. 0.0048 0.0025\n", - " 0.0018 0.0125 0.0004 0.0003 0.0002 0.0001 0. 0.0003 0.0008 0.\n", - " 0.0002 0.0816 0.0004 0.0226 0.0004 0.0001 0.0071 0.0009 0.0002 0.0001\n", - " 0.0011 0.0028 0. 0.0004 0.0208 0.0019 0. 0.001 0.0002 0.0232\n", - " 0.0002 0.0293 0.0021 0.0574 0.039 0.0006 0.0005 0.0005 0.0046 0.0015\n", - " 0.002 0.0045 0.0016 0.0081 0.0043 0.0003 0. 0.0007 0.0002 0.0039\n", - " 0.0016 0.0068 0.0012 0. 0.0001 0. 0.014 0.0023 0.0003 0.0189\n", - " 0.0003 0.0004 0.0004 0.0012 0.0036 0.0003 0.0047 0.0003 0. 0.0039\n", - " 0.0002 0.0017 0.0003 0.0026 0.0051 0.0111 0.0138 0.013 0.0003 0.0003\n", - " 0.0008 0.0013 0.0001 0.0003 0.0075 0.0157 0.0278 0.0001 0.0178 0.0013\n", - " 0.0008 0.0003 0.0001 0.0024 0. 0.0015 0. 0.0016 0.005 0.\n", - " 0.0017 0. 0.0018 0.0038 0.0002 0.0052 0.0008 0.0006 0.0191 0.002\n", - " 0.0004 0.0409 0.0001 0.0012 0.003 0.0014 0. 0.0005 0.0219 0.0265\n", - " 0.0056 0.0001 0. 0.0463 0.0012 0.0005 0.0006 0. 0.0018 0.0017\n", - " 0.0033 0.0004 0.0004 0.0004 0.0151 0.0011 0.0006 0.0001 0.0011]]\n" - ] - } - ], + "outputs": [], "source": [ "# Predict probabilities using model.probabilities() (no arguments uses fitted parameters)\n", "probs_scl = model_scl.probabilities()\n", @@ -667,25 +355,9 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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K9rbCy+8kI08DVFhLD6kF48e0hqSzmbgwIFpm222yeYnT56cnn/++SaXv+666woVQ/fbb7922UeaFl/4//jHPxaSDDfddNNmD1n+5Tx06+ajZykRlPnRj36UllxyybJG7BaPxoygTinRLiIuxIS4WHD22WeXXPbggw8utIaI1kwR+KBlxowZk/r06VN4v/7kk08cSgAAaAfiqjSMIzU3ODa+/+YDYyPpodRAwaaMHDmy0PI44iA//OEPF4kH4gtf+EL2/TcXhROgFjg3oW1j/i0R14aitXKPHv9r6BnVDI877riK39vjdtFtrrnrq//4xz+y+RisENemqilaSufHLETMvdS1j6997Wtpyy23zOYjRhxVFZuz2267Zf9/9NFHWfVEgM7O1WGoQXn7g7DaaqtVbb0xKiUS3GK65557CtuKVo1RjSySImI0R7QgPPLII7PWkOWIYMtf//rXrDXkqFGj0korrZRVeowL8tG6MD70RZnuDz/8sOQ6Yn/yfSuW/67h1HDUafHfmvKf//wnXXLJJdkH2PXXXz+7zzFaZsCAAdn9juS/uC+1oDho9tWvfjV95jOfydp35m6++eZCu+9yRSXOww8/PEs8WXzxxbMP4xtuuGHWaqOSyljFj1ecV81tM0bL7rzzztkon/79+2fHfNlll822ffTRR7eohHp8gI8vHzGKOdYb7U9jvTEqOkYpxzkcZdLLKbcfiTuxH/E8iP2KSmIrrLBCFlSMymLljEiKRKGG52c8v2LUVAQ04xzLn19HHHFE1lKmre/vY489VtinWD5P8ivni2icH/lty30tKEfxqPPmAsP53+OL6l577VXRdqKtfZzXX//619Pqq6+e+vXrlz2u0e72K1/5Sjr55JPTtGnTmlxHfv8nTZpU+F28xjX2mtSw/XBj50N8GY7txmtPnGfxRTXOuXJfy+I8HDx4cOHvUfa/OWeddVZh+TgHKz3vGpo4cWKh/UEkzpf6sl38flO8zXhPa3jsikX131LHNDd27NjCMjEf4tweP3582mqrrbLXynis4+///Oc/F7htvA9F64No9RDLxXke71fxXhWPRbQcinZLU6dObXSb8fjn4rxo7FyIx749xCjMXFOVGPKLKyEuksQ5VErsf3HrkxjJWg3x+MR+xDkbIz7jeRiPUTwv43jF+0O0sIg2760Rx6Hc8674s1CxV199NbuAFO+7sZ+9evXK3rdiHcOHD88+J0SbkebeM+P1fvTo0dn8G2+8kf72t7+16r4BAADlEVf9fxbluGp87887z1x++eVNxgevv/767MJ/S6sl5qKFcy4G+pVTqbEr2GijjQrz0eWn+BpAxHbycyuPN0SMM5IsdthhhyxmF9cQ4u/F8Ytqx20bDoSO8yO/hrHKKqtk8cM4Dypp21pODKuhKEIQMdx4/kSb1Hj+REeMqAQWz6uINxTHb1t7zaRhvOP000/PEo7ivke8I57DsS+RGFRp/DcGwcZzPuIl8RjGYxKxql/+8pdNXgfqKudmXBOL18Mdd9wxO4di2XgsI25/4IEHpjvuuKPF+x2vHcccc0z63Oc+lw1gzs+R6Ijy+uuv10wsLtYfxyrWF8+lOAciFhbH9tNPP2329o3Fz1uiscey4d+Kr7NGzLDUc6hUnLHceO8777yTtd6N4xAx2Dgu8R4ZbYSjPW+51QHjtSiOb1wTiXMqzoH82kacF7H+eM+J18dyjnW1Yv7VENdm4lpycYw9Wj2XI45HntRY7rWl1r63lxJVG/Njv9Zaa2X3qynxepkr9X5XLK5T5OJzDECnVw/UnG233Ta+AWfTbrvtVrX1jhw5srDeu+++u/6mm26qHzBgQOF3DadevXrV//KXv2xyndOmTatfZpllSq6jeBo4cGD97bff3uh6Yn/KWUc+vfTSSwvcvvhvpZx//vn13bt3L2v9m2++ef2MGTOavO+XXHJJYfn99tuvvpo+/fTT+tVWW62w/osuuqjwt7XWWqvw+1/96ldlrzPuf8+ePUve5xVXXLH+/vvvL+t+FT9ecV6Vsuuuu5Z1vOvq6uqPOuqo+vnz55d1X+69994FjkNT0wknnFByPf/973/rv/GNbzS7jnieXHfddU3u0yqrrLLA+XnDDTfUL7nkkiXX2adPn/qbb765ze/vBhtsUPjb5ZdfXtb2LrzwwsJtNt100/pqve7EeufMmVO/9NJLZz+vuuqq2bnemDiGcV7Ectttt132u913372wrtNOO63kNocNG1bWsVpsscXqx40bV3I9lbwmxfOmqfPht7/9bX3v3r0Xut16661XcpuNueeee+q7detWWOaWW24puf8PPfRQdh/zZa+44or61tppp50K67vmmmvKetybm4rFa06pY5qLx774PHj66afrP/e5zzW67scee6xwu3/84x/1K6+8ctn7NW/evEa32dwUj317iPfwfJuLL754yeU22mijwnJnnnlms+v9+9//Xlg+3jNnz57dqv2cPHlyfb9+/co6dvGa8Oijjza5vng+lTrWxX8rZ4r3smK/+c1vstfmcm678cYbN3vfzzvvvMLy3/3ud1t4BAEAgEqIqy6acdXi9X35y1/OfnfooYcWfve3v/2t5G0322yzwnfgN954o/62224r6zt+cQwjj8dFDCv/XcSxyom9NRVbLY6vNBULq6bi+1XOY/O73/1ugcf/tddeK/k4x98i1tjYeROx1LaK24b33nuvfquttmpyXWPGjMmWa3gdpbnjVCqGlXv11VfrR48eXdbzJz9/q3HNJHzyySf1p5xySqNxyeKpR48e9SeddFLJWG2x448/foH4ZMPps5/9bBavaxjD6yrnZsS61lhjjWYfj3jM//Of/zS5jw1jyL///e+z63Kl1hnXG5o736sdi2t4Lej999+v33HHHZtcb8Tni493Ofe9pc+zpt5Piv9WzlRuDLIxca2uqetBMcX1jv/7v//LrpGU8uabby4Qz21uuuOOO+rbK+Zf6rWpUvFeW3ytslTctPg1JH9PzT/nxbF84YUXGr1dvI6tvvrq2XJxLSqOd7nvu+Xac889C+v79re/3ezyr7/++gLH7Pnnn29y+bgP+XW0eL19++23W73PAB3pf2nlQE0ZMmTIAlXxouJcJe2cyxEjc6JyV4yIipFxUd0q/o8KYjESL34/Z86crLJbjI459NBDG13PrFmzslFAISqyxWidGCEWI69iHS+99FLWsjVG20R1oaieGCNgGo4eWXnllbMRQ+HXv/514ff57xqKEUKVitFkeSvFGPEWxzRGWcaopXfffTc9+eST6amnnsr+HpWNorR27HuMHmxv9957b3bsQuzfLrvsUvhbVE2MKnx5e8hSx6hYHNMYjZmL+xSVv6JKWIwWjPsbFZ1idOhRRx1VtfuRV6SLUUwxKiwqNcYIsWgnGqOsY0TXa6+9lo0A+/nPf56dc1GtrCkxajFGOBW3h40RSV/60peyEYRR7S8ex5hixFI+0quhN998M22++ebZ8ysX50SMvo3qWLF/MYo1zts4P6J0eox2/uY3v9ns/Y7KhdESNc63qMoWozPjnI3HNJ5f8+fPz0Ytxjr/9a9/NVkZtbX399vf/na2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//tCe6Jo81x8vrdGfNEubvNd/FpT65qryNpYm5HGKr22x7kQr0nRGqu48mW0emqpYcOGZVMeOG3qvT/aRucjY/v379/qthflvvfGfn3729+uWhJtpY9VvBbmQd3WniPF7rvvvsL8VlttVdb6AQCA1hFXXfTiquUaOnRoevTRRwvxp7wLRXs45phjCkkk8b27nKSmRVW147YxaDcfpB2thc8+++ySt49YYnSsqIaoyHnooYcucL9aU42s0phUXI+JAeF5ImFx7LAlxzGeP8VVIGMAbanuTJGcVXxtqSuImHPErooHETf1OMT1sj//+c+Fnw855JBmtxFV784555wm45uR2JyLQesNOwa1Rywu9rGpuHt09SmuFhkD6TtaXBPJk8iixXa1khOjjXKe4B6JuPGcL/f6WjyeDZPUy41hz5w5c4FrRc3FKGsl5v+Xv/wljRkzpnAtefHFF18gSbElvva1ry1wbemkk05K7SGuwxdfe40YfqnHPqrW5pVr43W5nNeDWFdeRCYeoy222KJq+w7QESQmQg2aMmVKGj16dNpggw2yAMkbb7xRctlIvonKasVf0ONLbnMjQqKKXozgiS+IDb98xPbiw2E+GiOWbeyLa3xYjQ+OIb6IxBehd999d4Fl3n///ewLTmyrb9++Zd3/ddddtzBfrdYWxSOXzjrrrEaDPxGYGjlyZBagKHdfqyGCdrfddlvh54bV45qz6qqrZpURS43qjKTVvBVnJClFkmnD4EIkZcSo4VtvvbVQYbE14vzbdtttCz8fcMAB2XndUDy+sVx8ASvnmP/iF78o3JcIqESFuMbWG+I+xpeaaM/a0N577134shVfgmIE6bhx49KsWbMaXVd8yYvRiHGbKJXfXqp1fxsqDqIUt4g48MADU0eIL++RXJdPedCsUjFKNB85FoGxSLpsGPCL3//617/OKkfGdhq2a2ruNen666+vSvJua1xwwQWFpMp43kSwp7gdRgS643UgT9KM86harSTivaEtWoy3tagiHOd9tEwoVZUgRjUefvjhhZ+LX8OKg3v5a1W06C31fGypiy++eIGLOIcddlj22tRaP/nJTwrzcb6ceOKJCwXgIlm3uCpjfD6I51RrxGtrfh5Ggnqss2FyfLx2feMb38gCttV8763ks8QjjzySVllllSy4G1UOGxPnTRy74gtXjZ0jxe6+++5G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ZNOVV16ZXfiIqgwAAACwKIlWzQ3bNVcqqihWUkkxkhBjAgAAAAAAFqRiYhU8/vjjabfddsuSEvv165fOOOOM9I9//CPddddd6Vvf+la2zHPPPZe222679OGHH7Z4OwceeGAhKXHUqFHpxhtvTFOmTEnjx49Pa6yxRvr000/TqaeemiVBAgAAsAiqq6vdCQAAAAAAWGSomFgFRx11VFYdsUePHun2229PG220UeFvm2++eVpzzTXT8ccfn5599tl07rnnZsmDlZo0aVJWETGMGTMm3XDDDal79+7Zz8OGDUvbb7992mCDDbJ20rGtXXbZJQ0YMKAadw8AAAAAAAAAgBozd+7crEDWrFmzsvkoaAUserp375569+6dllhiidS3b99UVyPFAiQmtlK0ab7nnnsKFQ2LkxJzxx57bLrkkkvSM888k37+85+nE088MS222GIVbefss88unEgXXHBBISkxN3DgwDRu3Li05557ppkzZ2ZVFGO7AAAAAAAAAAB0HfX19WnGjBnZBDB//vw0Z86crNtvnz590mc+85nUrVvHN1KWmNhK0U45d8ABBzS6TDzQ++67b5aQGEmDkcg4evTosrcR2e3RFjrE7QYNGtTocjvvvHOW+fr++++niRMnSkwEAAAAAAAAAOhi3njjjSwBqVhUSGtY5ApYNHzyySdZwnL4+OOPs467q6yySodXTpSY2Er33Xdf9n+UwYxWyqWMHDmyMH///fdXlJg4ZcqULKu14Xoa6tmzZxoxYkTWTjpuM2/evIorMwIAANCJ1XX731RranGfAAAAAKATmj179gJJicsss0xWxKpXr14dnoQEdIxPP/00K3z35ptvZkmKkZwYLd779evXoQ+JKwOtFO2Zw5AhQ1KPHqXzPNdee+2FblPpNhqup6ntRInO559/vqLtAAAAAAAAAABQu959993C/HLLLZdNvXv3lpQIi7Bu3bplCcorrLBC4XcffPBB6mgqJrYyC33GjBnZfKn2yrmllloqq6oY2aivvvpqRdspXr657QwePHiB2w0dOrSsbUyfPr3ZMsAAAAAAAAAAAHScjz76qDA/YMAADwVQEBUSo3JqtHWOqokdTWJiKxRnlpZT+jJPTIzSmW21ndhGrpLtFCc0AgAA0ElFq5ZabNdSi/sEAAAAAJ1QtGkN0dWze/fuHb07QI1VTuzevXvWaTd/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pr/85S/pkUceyaoZRrJj3mn1jjvuaLSl9JNPPpn++te/Fn7+8Y9/nP2ueDr00EPLvi/REjnaG++3337Z9u+777706KOPZu2vf/SjH2VJf5FMefjhh6e//e1vqb3k7Zn//Oc/p3fffXeBv7333nvZ74uXq9SJJ55Y6Jx7yimntHp/u7IeHb0DAAAAAAAAAAAAteaII45Ixx13XDb//e9/P1144YVpzJgxWRLgl7/85bTGGmtUtL5IcIxkx2iBHK2Pc1/96lfT1ltvnbbaaqss4S8SC6Pdc7Hu3bun5557rtG20iNHjsxaM5922mlZUmBUBYwWy9HuOff5z38+9evXr/BztH+O37VUVBzca6+9Ckl6uaj6uN1226Xvfve72f164oknsv2KhMr2EI/PgAEDsqTE6667Ln3rW98q/C1+nj17dvb3WK4lll9++ezxieTQu+66K917773Z/WRhKiYCAABAV1HXrXYnAAAAAIBO5uijj07/93//V/j5lVdeSb/61a/SN7/5zSxBMJILo31yVOHLKxo255xzzlkgKTE3atSoQhLdww8/vFBiYl1dXaNJicVOPfXUrFJh7Eu0dm5LkdjYMCmx2JJLLpklSYb7778/vfPOO6k9ROXJ3XbbLZu//PLLG23jvOuuu2bLtdTxxx9fSPJUNbE0VwYAAAAAAAAAAAAaJlZ165bGjx+fbrvttjR69Ojs52JvvfVWuuaaa9L222+fhg8fnl544YUmj2G0Pt5hhx1K/r04CfLOO+9scl3RVvr111/Pqij+61//yqZnnnkmDRo0qFCdsT3NmjUrvfzyy+mpp54q7M9iiy1W+Ht77k/epjnaS8c+5Uml8XPx31sqkj+jImSIionNPVaLKq2cAQAAAAAAAAAASvja176WTTNnzkx///vfs4qGjzzySJbo9t5772XLxO823XTT7Pcrrrhio+uJNsc9epRO1/riF7+YevbsmebOnZsl9jUUlRCvuOKKLFnywQcfTB9//HHJdc2YMaPNH8/Yxrnnnpv++Mc/pueff77JqpHtsT+5TTbZJK2++urpxRdfzI7XySefnFVLjP1bddVVs7+3VrT4/vWvf509/lE1ccstt6zKvnclKiYCAABAV1FXV7sTAACLnAMnPLTQBAAAnVlUPPz617+exo4dm7VvjoqJF198cfb78MYbbzTZ2ne55ZZrcv2RtLj00ktn8//9738X+Nvs2bPTdtttl1X7u+eee5pMSgzN/b21IgFz7bXXTj/5yU/Sv//972ZbWbf1/jS09957L9C+OW/rHL+PttitFY95tPoOkydPzqpqsiCJiQAAAAAAAAAAABXq1atXOuCAA9JVV11V+N3EiROzNsuNKSchrlSC3xlnnFFIfhs5cmS69tpr09SpU9OHH36YPvnkk+x2MUXVxqbWUw1R0XG33XZL77zzTtau+ZhjjkmTJk3KEjMjgTLfl+LW1m25P43J2zVHq+tf/epX2f/FCYvVEImJeVLqqaeeWrX1dhVaOVOxz2/3g1TXs1/NHrmZD/2qo3cBANrVUsMOd8QBAAAAAACgg2y99dZp8ODB6dVXX83aPUfC3rLLLrvQclFhsSnz58/Pbh/yyol5Ut9FF12UzUcb4r/97W+pW7fG69Hlt29Lsf1okxyinfG3vvWtDtuXUoYMGZI22mij9MADD6Tvfe972e+GDx+ePvvZz1ZtG0sssUTW0jlaRUcr7z/96U9pvfXWq9r6OzsVEwEAAKCrqOtWuxMAAAAAQBe20korFeZLJQ3+85//zJIPS3n88cezaoTh85//fOH30db5zTffzOajUmGp9Uf1xLwyYGOq0cI4PPXUU4X5PfbYo+RykazXkfKqiVHFsfjnavrud7+bBg4cmM2fdtpp7V4Zspa5MgAAAAAAAAAAANBCH330UXr66acLVfSKqx0WiwTDP//5zyXXc/HFFxfmt9xyy8J8cTJjbKuU8ePHp3nz5pX8e+/evQvzc+bMSS1Vzv5EO+vf/e53qSPtvvvuqX///lnL7X79+jWZRNlSsd4TTjihkFj6xz/+serb6KwkJgIAAAAAAAAAADSoPvjlL3853XzzzVmSXSnxtyOOOCJ98MEH2c/bb799k5UJjznmmEZbOk+aNKmQyLfBBhukYcOGFf4WbaEHDBiQzV999dWFqorFHnroofSDH/ygycdwmWWWST179szmX3jhhRY/3muuuWZh/tJLL210mRNPPDE9+uijqSNFguj777+fVUyMxyevbFhthx12WFphhRWy+XHjxrXJNjqjHh29AwAAAECVRLCrFtsmV6k9CNSKadOmpV/84hfplltuyeZjxPWQIUOyNjqHHnpoWnzxxauynQgyX3LJJemJJ55IM2fOzIKbm266aRboHDFiRLO3j7Yx119/fbriiivSI488kt5+++2szU+sZ/jw4emAAw5IW2+9dVX2FQAAAKArmjJlShozZkxaeeWV04477pg22mijtMoqq2RV+N5999302GOPZVUOn3zyyWz5JZdcMp1++ukl17feeutllRUj8TAS9yJGE5ULb7311nTeeedllQh79OiRfv3rXy9wu4jpfPOb38x+H+2gI0Z09NFHZzGp9957L7v9BRdckFXvi5bS//73vxvdfqw7Eh7//ve/Z/u9/vrrpy9+8YtpscUWKyTylar2WCxiSsstt1z6z3/+k04++eT0yiuvZAmZkfg3derU9Pvf/z7dddddaeONN8621dX16dMnff/7309HHXVUmjFjRtm3e/PNN9OECROaXS4e50022SR1NhITAQAAAKBMkYwYQeAI+Ba3q4kR6TFddNFFWSB49dVXb/ExjRHcu+66azYav1gEeGO68sor09ixY9Mpp5xSch2xfxEsv+eeexb620svvZRN11xzTdbO5rLLLiuMlAcAAADg/yXxxQDPSB577bXXsqTAhgmDDasIXnXVVWnVVVctuUwkAR5++OHpO9/5TvZ/QxGjiQqEUamxoTPOOCNL8ovExEiY3HPPPRf4eyQURhvhU089tWRiYoiEyEi2fOedd9Jee+21wN9OO+20LO7UnL59+2YxpYg/RSwrkiJjKrbZZpulX/3qV+nzn/98WhQcfPDB6ac//Wl2rpTrueeeywYPN2e//fbrlImJNVhGAQAAAABqz+OPP55VRYykvxh9HsHgf/zjH9no729961uFYOJ2222XtfppqQMPPLCQlDhq1Kh04403ZsHm8ePHpzXWWCNrDxQB5kiCLCUC03lS4mqrrZYFhu+77770t7/9LQuQ5m1rIjkxRtcDAAAAsKDevXtnSWaRDPjDH/4wbbPNNtlg1EjK6969e1piiSXS2muvnQ38jIGk//rXv7JKiM056KCDsjhNxJmiumEkI0ZFxn333TerwLjHHns0eruoxhj7EhUZ11133Wz/Ika1zjrrpOOOOy6LXX31q19tdvsRu4p41g477JBtP6+WWKmomvjwww+nvffeu7CeaDk9cuTIrCV1bCOO1aIiHo+oHsn/U1cfPV2gGdOnT0+DBw/O5nsN3S/V9exXs8ds5kO/6uhdAIB2tdSwhUdTURmfH6BrfGdZc7X/fWd59dVX06BBg9Ii+X1txLGprveSqdbUz34vzZl8ziL5+NC1RJJgJPvFaPl77703a91TLBL+jj/++Gw+gtWRPFipSZMmZaPJQ4xcv+GGG7JAdy5awUSAO1pIL7XUUunFF19MAwYMWGAd0bZ5ww03zOYjWB6j6KO9ULG4fbQOipZD0QooRv5H4LgtXps87wEWXQdOeGih343ff1iH7AsAQFfx/PPPF9r9RoU+OoeoohidMKLyXTmte6E9XyPaKpanYiIAAAAANCPaNOcVCKOiYcOkxHDsscdmI9TDz3/+8zRv3ryKj+vZZ5+d/R/JiFHlsDgpMUSlw3HjxmXzM2fOzKooNhQj53NHHXXUQkmJ4TOf+UyhTUxUYHzwwQcr3lcAAAAAgFIkJgIAAABAM6Kdci5P6Fso0NatW9ZyJ08azBMZyxXtn6PFTRg9enTJkck777xz1iooTJw4caG/z507tzAfFRNLibbQuTlz5lS0rwAAAAAATZGYCAAAAF1FXbfanaCTu++++7L/+/btm7VSLmXkyJGF+fvvv7+ibUyZMqWQIFi8noZ69uyZRowYUbhNw8qMa621VmE+Wj2X8sILLzR6GwAAAACA1nJlAAAAAACa8cwzz2T/DxkyJPXo0aPkcmuvvfZCtylX8fLF62lqO/Pnz0/PP//8An/beuut06qrrprNn3/++WnWrFkL3X769OlpwoQJ2Xy0pV533XUr2lcAAAAAgKaUjqICAAAAAGn27NlpxowZ2ZEo1V45t9RSS2VVFSMZ8NVXX63o6BUv39x2Bg8evMDthg4dWvi5V69e6YorrkhjxozJqiKut9566Xvf+176/Oc/n1VXfOSRR9LZZ5+dtZuOBMZLLrmk4kc5Ehub8sYbb1S8TgAAAACg65CYCAAAAABN+OCDDwrz/fr1a/ZY5YmJH374YZttJ7aRa2w7X/nKV9Jjjz2WfvGLX2TTIYccssDfY/0//OEP06GHHpoGDhyYKlWcGAkAAABA015++WWHiEWOVs4AAADQVdTV1e4EnbxiYq5nz57NLh8VC8PHH3/cZtvJt1FqO/X19en666/PpqiS2FAkM1599dXplltuqWgfAQAAAADKoWIiAAAAADShd+/ehfm5c+c2e6zmzJmT/d+nT582206+jca28+mnn6Y99tgjXXfdddnPBx54YDrssMPSOuuskz755JP0z3/+M2vlfNNNN6X9998/PfHEE+mcc86paF+ba1MdrZyHDx9e0ToBAAAAgK5DYiIAAAAANKF///6F+XLaM0cb53LbPrd0O/k2GtvOBRdcUEhKHDt2bDrttNMW+PvGG2+c/vSnP6V99903/eEPf0jnnntu2nzzzdN2221X9r4OGjSo7GUBAAAAgEWPVs4AAADQVdR1q90JOrGoZDhw4MBsfvr06U0uO3PmzELS4ODBgyvaTnGyX3PbKa5Y2HA748ePLyQ6fv/73y+5jjPPPLMwf9FFF1W0rwAAAAAATXFlAAAAAACaEW2Qw9SpU9P8+fNLLvfss88udJtyDR06tNH1NLWdHj16pCFDhizwt2eeeaawvl69ejWZCLn88suXtT0AAAAAgEpITAQAAACAZmyyySbZ/1EN8ZFHHim53KRJkxZomVyJYcOGpZ49ey60nobmzp2bJk+evNBtcpGsGJpKoMzNmzdvgdsAAAAAta179+7Z/5988kmqr6/v6N0Bakh9fX322hC6dev4tMCO3wMAAACgOurqaneCTm7HHXcszF9yySWNLvPpp5+myy67LJsfMGBAGjVqVEXbiNbLW2yxRTZ/5513lmznPHHixPT+++9n8zvttNNCf19ttdWy///1r3+ld999t+T24u///e9/F7gNAAAAUNvyAYqRgPTRRx919O4ANWTOnDmFhOWGg5k7gsREAAAAAGjG8OHD06abbprNjx8/Pj3wwAMLLXPOOecU2igfeeSRabHFFlvg7xMmTEh1dXXZNHbs2Ea3c9xxxxWqHR522GGFEc65GTNmpBNOOKGQ/HjQQQcttI4xY8YUApHHHHNMo9UTZs+enb773e8Wfv7617/uHAAAAIBOYIkllijMx4BDVROBXD6YOfTt2zd1NImJAAAAAFCG888/P/Xp0ydLGtxqq63ST37yk6yl8t13350OPvjgdPzxx2fLrbXWWunYY49t0THdfPPN0x577JHN33TTTWn06NHZ/w8//HBWqXHEiBFp2rRp2d/POuustNRSSy20jkhGXG655bL5uM1Xv/rVdMUVV2QtqKdMmZJ++9vfpg022CDb77DOOuuk/fff3zkAAAAAnUC/fv2yQY/hww8/zDouzJo1S4IiLMI++eST9M4772RT8WtFR+vR0TsAAAAAVEnWNrkGxyBq5UwXsf7666drrrkm7b333tno45NOOmmhZSIp8ZZbbsnaMrfUxRdfnK3/1ltvzZIH8wTCXLdu3dIpp5ySJUM2ZuDAgemvf/1r2nnnndNLL72U7r///mxqzBe/+MV044031kRrFwAAAKB5ERdYeeWV02uvvZYlI0ZyYkyRrNi9e3eHEBYx9fX1C3VdWXbZZWsi3icxEQAAAADKFG2Sn3jiiax6YiQgRlWCCPINGTIk7brrrunwww9Piy++eKuOZ1RljHVfeeWVWfvnxx9/PL377rtp+eWXz9pJxzY22mijJtcRCYdPPvlkuvTSS9Of/vSnbJ+jvVNcpIhqipFkGfu7++67L9RyGgAAAKhtMSCyODkxxP/R5QFYtC255JJpmWWWSbVAYiIAAAAAVGCVVVZJ5557bjZVItolV9Iyea+99sqmlurbt2869NBDswkAAADoesmJ0bkhqiVG54W5c+cuVDUNWDR07949Gyw9YMCA1Lt371QrJCYCAABAl2rlXJdqTi3uEwAAAAB0gbbOSyyxRDYB1JpuHb0DAAAAAAAAAAAAQNchMREAAAAAAAAAAACoGomJAAAAAAAAAAAAQNX0qN6qAAAAgA5V1+1/U62pxX0CAAAAAADajCsDAAAAAAAAAAAAQNVITAQAAAAAAAAAAACqRitnAAAA6Crq6v431Zpa3CcAAAAAAKDNqJgIAAAAAAAAAAAAVI3ERAAAAAAAAAAAAKBqtHIGAACALqNbSnW1OAaxFvcJAAAAAABoK64MAAAAAAAAAAAAAFUjMREAAAAAAAAAAACoGq2cAQAAoKuoq/vfVGtqcZ8AAAAAAIA2IzGRiv3rlh+nQYMGOXIAUCNmPvSrVMuWGnZ4R+8CAAAAAAAAAO1IK2cAAAAAAAAAAACgalRMBAAAgC7VyrkGxyBq5QwAAAAAAIuUGrxaAQAAAAAAAAAAAHRWEhMBAAAAAAAAAACAqtHKGQAAALqKaONck62ca3CfAAAAAACANuPKAAAAAAAAAAAAAFA1EhMBAAAAAAAAAACAqpGYCAAAAAAAAAAAAFRNj+qtCgAAAOhQdXX/m2pNLe4TAAAAAADQZlRMBAAAAAAAAAAAAKpGYiIAAAAAAAAAAABQNVo5AwAAQFdR1+1/U62pxX0CAAAAAADajCsDAAAAAAAAAAAAQNVITAQAAAAAAAAAAACqRitnAAAA6Crq6v431Zpa3CcAoNM5cMJDC/1u/P7DOmRfAAAAgKapmAgAAAAAAAAAAABUjcREAAAAAAAAAAAAoGq0cgYAAIAu1cq5BscgauUMAAAAAACLlBq8WgEAAAAAAAAAAAB0VhITAQAAAAAAAAAAgKrRyhkAAAC6VCvnulRzanGfAAAAAACANqNiIgAAAAAAAAAAAFA1EhMBAAAAAAAAAACAqtHKGQAAALqIuvhXk22Ta3GfAAAAAACAtqJiIgAAAAAAAAAAAFA1EhMBAAAAAAAAAACAqpGYCAAAAAAAAAAAAFRNj+qtCgAAAOhQdXWprq6u5h6E+hrcJwAAAAAAoO2omAgAAAAAAAAAAABUjcREAAAAAAAAAAAAoGq0cgYAAICuIjom12LX5FrcJwAAAAAAoM2omNhKjz76aDrzzDPTNttskwYPHpx69eqV+vXrl9Zaa620//77p/vuu68qD9TYsWNTXV1dWdM999xTlW0CAAAAAAAAAABApVRMbIWRI0eme++9d6Hfz507Nz3//PPZdOmll6Z99tknXXTRRalnz56t2RwAAAAAAAAAAADUPImJrfDaa69l/6+00kpp1113TZtuumn6zGc+kz755JP0wAMPpHPOOSdb5g9/+EOaP39+uvLKK6vyoD355JNN/n211VarynYAAADoXPJK+rWmFvcJAAAAAABoOxITW2HttdfO2jh/4xvfSN27d1/gbyNGjMgqJW688cbp3//+d7rqqqvSd77znSx5sbU+//nPt3odAAAAAAAAAAAA0Ba6tclaFxE333xz2m233RZKSswNHDgwq5qYu/7669tx7wAAAAAAAAAAAKD9qZjYxjbbbLPC/AsvvNDWmwMAAGARppUzAAAAAABQC1RMbGNz5879fwe7m8MNAAAAAAAAAABA1yZTro1NmjSpML/22mtXZZ2jR49OyyyzTOrZs2dabrnlsqqMZ511Vpo5c2ZV1g8AAAAAAAAAAAAtpZVzG/r000+zhMHcbrvtVpX13nnnnYX5t99+O0t+jGncuHFpwoQJaYcddqh4ndOnT2/y72+88UaL9hUAAID2o5UzAAAAAABQCyQmtqHzzjsvTZkyJZvfaaed0oYbbtiq9a277rppxx13TMOHD08rrbRSmjdvXnruuefSFVdckW6//fb07rvvpm984xvpz3/+c9pmm20qWvfgwYNbtW8AAKXMfOhXDg50EUsNOzzVqvq5H3b0LgAAAAAAAPD/k5jYRqKC4fe///1sPtotX3jhha1a31FHHZXGjh270O+//OUvp3333Tf99re/TYccckj65JNP0kEHHZSmTp2a+vTp06ptAgAAAAAAAAAAQKUkJraBp556KquQOH/+/NSrV6907bXXpuWXX75V6xwwYECTfz/44IPTww8/nC666KL0+uuvp4kTJ6ZvfvObZa//1VdfbbaVc1RqBAAAoHZp5QwAAAAAANQCiYlV9tJLL6WtttoqzZw5M3Xv3j1dddVVaeTIkak9RHJiJCbmFRsrSUwcNGhQG+4ZAAAAAAAAAAAAi4puHb0DXUlUKtxyyy2z/6NKxcUXX5xVTmwvQ4cOLcy/9tpr7bZdAAAAAAAAAAAAyElMrJIZM2ak0aNHpxdffDH7+Ze//GXad999U3uqr69v1+0BAAAAAAAAAABAQ1o5V8F7772Xtt566/T0009nP5911lnpsMMOS+0t335YaaWV2n37AAAAdLC6/3+qNbW4TwAAAAAAQJtRMbGVPvroo7TddtulRx99NPv55JNPTieccELqCL/97W8L8yNHjuyQfQAAAAAAAAAAAGDRJjGxFebOnZt22mmn9Pe//z37+cgjj0w//vGPK17PhAkTUl1dXTaNHTt2ob8/+eSTaerUqc0mJY4fPz6bX2GFFbL9AgAAAAAAAAAAgPamlXMr7Lnnnun222/P5jfffPN04IEHpn/9618ll+/Zs2daa621Kt7OI488kg466KA0atSotM0226R11103LbPMMmn+/Pnp2WefTZdffnm64447smW7d++eJSn27du3FfcMAACAzigf9FZranGfAAAAAACAtiMxsRUmTpxYmP/b3/6WvvCFLzS5/CqrrJJefvnlFm3rk08+SXfeeWc2lRLJilE1cfvtt2/RNgAAAAAAAAAAAKC1JCZ2Attuu22WcPjAAw+kxx57LL311lvpnXfeSfX19WnppZdO6623Xvra176W9t9//7TEEkt09O4CAAAAAAAAAACwCJOY2AqRGFgNkVAYUynLLbdc+r//+79sAgAAgFKiY3Ittk2uwV0CAAAAAADaULe2XDkAAAAAAAAAAACwaJGYCAAAAAAAAAAAAFSNVs4AAADQRdTFvxrsmxz7BQAAAAAALDpUTAQAAAAAAAAAAACqRmIiAAAAAAAAAAAAUDUSEwEAAKCLiDbOtTpBVzJt2rR03HHHpXXWWSf17ds3Lb300mn48OHpZz/7Wfroo4+qtp2rr746bb311mnFFVdMvXv3TquuumraZ5990uTJk5u83f7771/xc3TChAlV228AAAAAgB4OAQAAAACU55Zbbknf/OY303vvvVf4XSQjPvTQQ9l00UUXpVtvvTWtvvrqLT6ks2fPTrvuumu6+eabF/j9K6+8kk1XXnllGjt2bDrllFOq9rB99rOfrdq6AAAAAAAkJgIAAABAGR5//PG02267ZYmI/fr1SyeeeGIaNWpU+vjjj7Pqhr///e/Tc889l7bbbrssSTGWaYkDDzywkJQY6z/yyCPTSiutlJ588sl05plnphdeeCGdeuqpWSXFgw46aKHbn3HGGVlFx6bMnDkzbbbZZunTTz9Na665Ztpoo42cAwAAAABA1UhMBAAAAIAyHHXUUVlSYo8ePdLtt9++QDLf5ptvniX4HX/88enZZ59N5557bpY8WKlJkyZlFRHDmDFj0g033JC6d++e/Txs2LC0/fbbpw022CBrJx3b2mWXXdKAAQMWWMfKK6+cTU258MILs6TEsO+++3r8AQAAAICq6lbd1QEAAAAdpq6GJ+jkogLiPffcU6ho2FiFwWOPPTats8462fzPf/7zNG/evIq3c/bZZ2f/RzLiBRdcUEhKzA0cODCNGzeuUPVw/PjxLbo/l112WfZ/XV1d2meffVq0DgAAAACAUiQmAgAAAEAzbrzxxsL8AQcc0HigrVu3QvXBSBrMExnL9eGHH6a77rormx89enQaNGhQo8vtvPPOaYkllsjmJ06cWPFj9/zzz6fJkydn8yNHjkyrrLJKxesAAAAAAGiKxEQAAAAAaMZ9992X/d+3b9+slXIpkeiXu//++ys6rlOmTElz5sxZaD0N9ezZM40YMaJwm0orM+bVEoM2zgAAAABAW5CYCAAAAF1FXV3WlrXWptgv6OyeeeaZ7P8hQ4akHj16lFxu7bXXXug2lW6j4Xqa2s78+fOzCojlqq+vT5dffnk2v/jii6dddtmlon0EAAAAAChH6SgqAAAAAJBmz56dZsyYkR2JUu2Vc0sttVRWVXHWrFnp1VdfrejoFS/f3HYGDx68wO2GDh1a1jbuvffe9PLLL2fzO+20U+rfv39qienTpzf59zfeeKNF6wUAAAAAugaJiQAAAADQhA8++KAw369fv2aPVZ6Y+OGHH7bZdmIbuUq2U9zGeb/99kstVZwYCQAAAADQkMREAAAA6CIKrZNrTC3uE1RaMTHXs2fPZpfv1atX9v/HH3/cZtvJt1HJdmL9119/fTa/8sorpy222KKi/QMAAAAAKJfERAAAAABoQu/evQvzc+fObfZYzZkzJ/u/T58+bbadfBuVbOfGG29M77//fja/9957p27duqWWaq5NdbRyHj58eIvXDwAAAAB0bhITAQAAAKAJ/fv3r6htcrRxLrftc0u3k2+jku0Ut3Hed999U2sMGjSoVbcHAAAAALq2lg+LBgAAAGqylXMtTu1l2rRp6bjjjkvrrLNO6tu3b1p66aWzqm0/+9nP0kcffVSVbTz99NPpiCOOSOuuu25aYoklspa7yy67bBo1alQ677zz0gcffFCV7VA7opLhwIEDs/np06c3uezMmTMLSYODBw9ucbJfc9sprlhYznbeeuutdPvtt2fzG2ywQRo6dGhF+wYAAAAAUAmJiQAAAECXcMstt6QvfOEL6ZxzzknPPvtslogYSWIPPfRQ+t73vpe+9KUvpRdffLFV24h1r7feeulXv/pV+te//pUlIc6bNy/NmDEj3XPPPemYY47JEhafeOKJqt0vakMku4apU6em+fPnl1wuzr2GtylXcbJg8Xqa2k6PHj3SkCFDml33FVdckT755JNsfr/99qtovwAAAAAAKiUxEQAAAOj0Hn/88bTbbrul9957L2tre8YZZ6R//OMf6a677krf+ta3smWee+65tN1225XVircx1157bVaNMZLSokri0UcfnSVDPvjgg+nKK69Mm2yySbbcK6+8kr72ta9l+0LXkT++UQ3xkUceKbncpEmTCvMbb7xxRdsYNmxYdm41XE9Dc+fOTZMnT17oNuW0cV5sscXSnnvuWdF+AQAAAABUSmIiAAAAdBV1NTy1saOOOiqrkBjV46Jd7UknnZQ22mijtPnmm6ff/e536eyzzy5UmTv33HNbtI3TTz+9MD9x4sRsPdtuu23WKjoSve6777608847Z39/44030vjx46t076gFO+64Y2H+kksuaXSZTz/9tJAAOGDAgKy9dyX69++ftthii2z+zjvvLNnOOc6/999/P5vfaaedml3vk08+mSXvhjhn87bUAAAAAABtRWIiAAAA0KlFq+ZooxwOPPDALCGxoWOPPbbQVvfnP/951n65EpEEFq2bQ7SEjsqLjTnttNMK81Gxka4jElA33XTTbD6STh944IFGW30/88wz2fyRRx6ZVScsNmHChFRXV5dNY8eObXQ7UZUzRGXOww47rNB+ORdtw0844YRC8uNBBx3U7L5feumlhfl99923jHsLAAAAANA6EhMBAACATu3GG28szB9wwAGNLtOtW7dCQtbMmTMLiYzlita5udVXX73kcmussUZhfs6cORVtg9p3/vnnpz59+mRJg1tttVX6yU9+krVUvvvuu9PBBx+cjj/++Gy5tdZaK0uGbYmo8rnHHntk8zfddFMaPXp09v/DDz+cVWocMWJEmjZtWvb3s846Ky211FJNri8SG6PVeFh66aXT17/+9RbtFwAAAABAJXpUtDQAAABQs/JKbLWmrfcpWiiHvn37pg022KDkciNHjizM33///VnCV7mi9W0kdf33v/9NL774YsnlXnjhhcJ8JKfRtay//vrpmmuuSXvvvXdWRTNahjcUj/stt9yStWVuqYsvvjhb/6233polPcbUMNH2lFNOyZIhm3PHHXdkrcVDJDz27NmzxfsFAAAAAFAuFRMBAACATi1vnTtkyJDUo0fpMZhrr732QrepxLe//e3s/0cffTTddtttjS5z+umnZ/937969rBa7dD5jxoxJTzzxRDr66KOzJMTFF188a6m84YYbpnHjxqXHHnssOxdbI6oyRnLjFVdckSXQLrfccllC4eDBg9Nee+2VJdaWagXd0B/+8IfCvDbOAAAAAEB7UTERAAAAaDd55bamDBo0qOz1zZ49O82YMaOs20XL26iqOGvWrPTqq6+mSp188slZO90777wz7bTTTunwww9PW2yxRVZNMaooXnjhhWnSpElZUuIvfvGLtM4661S8DTqHVVZZJZ177rnZVIn9998/m8oVSYgxtUYkN8YEAAAAANCeJCYCAAAA7Wb48OHNLlNfX1/2+j744IPCfL9+/ZpdPk9M/PDDD8veRvH6o1LihAkT0llnnZXOOeecbCq28847p+OPPz59+ctfrnj9AAAAAADQVUhMBAAAqBFLDTs81bKZD/0q1arp06enNVe7NC3q6urqsqnWtOU+RcXEXLS6bU6vXr2y/z/++OMWbS8qJl511VVZhcTGRDXF5ZdfPquWuMQSS7RoGwAAAAAA0Nl16+gdAAAAABYdU6ZMydooNzVVonfv3oX5uXPnNrv8nDlzsv/79OlT8b5ff/31abPNNkt/+9vf0rrrrptuuOGG9M4772TbfeGFF9KZZ56Z5s2bl7V0/spXvpLefPPNircBAAAAAABdgYqJAAAAQLtZccUV06BBg6q2vv79+xfmy2nPHG2cy237XOytt95K+++/f5bY+LnPfS794x//yNpC51ZfffV04oknZq2qR48enZ566ql0xBFHpOuuu66i7QAAAAAAQFegYiIAAAB0EXXxr64Gp9R2rZyjYuLAgQMLLb2bMnPmzEJi4uDBgyvaztVXX1247UknnbRAUmKxLbbYIpvCxIkTs20CAAAAAMCiRmIiAAAA0Kmts8462f9Tp05N8+fPL7ncs88+u9BtyvXMM88U5r/0pS81uewGG2yQ/f/pp5+mf//73xVtBwAAAAAAugKJiQAAAECntskmm2T/R0XDRx55pORykyZNKsxvvPHGFW2jR48ehfmmkh/DvHnzGr0dAAAAAAAsKiQmAgAAQBfR4S2bm5ja0o477liYv+SSSxpdJqoXXnbZZdn8gAED0qhRoyraxmqrrVaYv++++5pc9t57783+j/u96qqrVrQdAAAAAADoCiQmAgAAAJ3a8OHD06abbprNjx8/Pj3wwAMLLXPOOecU2jEfeeSRabHFFlvg7xMmTCgkUY4dO3ah22+33XaFBMszzjgjvfbaa43uy+9+97v08MMPZ/MjRoxIyyyzTBXuIQAAAAAAdC76CQEAAACd3vnnn5+1Z/7444/TVlttlU466aSsKmL8fPXVV2cJg2GttdZKxx57bMXrX3vttdMBBxyQLr744iwpcf31109HHXVUlhDZv3//9Oqrr2bbufLKK7Plu3fvns4888yq308AAAAAAOgMJCYCAABAVxEF/dq2a3LLtMM+RaLgNddck/bee+/0/vvvZ4mJDUVS4i233JIlErbEBRdckGbNmpVt5+23304nn3xyo8v17ds3S4TcbLPNWrQdAAAAAADo7LRyBgAAALqEMWPGpCeeeCIdffTRWRLi4osvngYMGJA23HDDNG7cuPTYY4+lIUOGtHj9vXr1yqoi/u1vf0v77rtvto1IQuzRo0daeuml00YbbZROOeWU9Oyzz6a99tqrqvcNAAAAAAA6ExUTAQAAgC5jlVVWSeeee242VWL//ffPpnJEi+iYAAAAAACAxklMBAAAgC6iri6m2uvlXIO7BAAAAAAAtCGtnAEAAAAAAAAAAICqkZgIAAAAAAAAAAAAVI1WzgAAANBFRBvn2mzlXHv7BAAAAAAAtB0VEwEAAAAAAAAAAICqkZgIAAAAAAAAAAAAVI3ERAAAAAAAAAAAAKBqelRvVQAAAECHqqtLdXV1tfcg1OI+AQAAAAAAbUbFRAAAAAAAAAAAAKBqJCYCAAAAAAAAAAAAVaOVMwAAAHQV0TG5Frsm1+I+AQAAAAAAbUbFRAAAAAAAAAAAAKBqJCYCAAAAAAAAAAAAVaOVMwAAAHQRdXV12VRranGfAAAAAACAtqNiIgAAAAAAAAAAAFA1EhMBAAAAAAAAAACAqtHKGQAAALoIrZwBAAAAAIBaoGIiAAAAAAAAAAAAUDUSEwEAAAAAAAAAAICq0coZAAAAuoi6+FdXl2pxvwAAAAAAgEWHiokAAAAAAAAAAABA1UhMBAAAAAAAAAAAAKpGYiIAAAAAAAAAAABQNT2qtyoAAACgI9XV1WVTranFfQIAAAAAANqOiokAAAAAAAAAAABA1UhMBAAAAAAAAAAAAKpGK2cAAADoKqJjci12Ta7FfQIAAAAAANqMiokAAAAAAAAAAABA1aiYCAAAUCNmPvSrjt4FAAAAAAAAaDWJiQAAANBF1NXVZVOtqcV9AgAAAAAA2o5WzgAAAAAAAAAAAEDVSEwEAAAAAAAAAAAAqkYrZwAAAOgitHIGAAAAAABqgYqJAAAAAAAAAAAAQNVITAQAAAAAAAAAAACqRitnAAAA6CLq6v431Zpa3CcAAAAAAKDtqJgIAAAAAAAAAAAAVI3ERAAAAAAAAAAAAKBqtHIGAACALtXKufb6JtfgLgEAAAAAAG1IxcRWigs+5UybbbZZVR6wq6++Om299dZpxRVXTL17906rrrpq2meffdLkyZOrsn4AAAAAAAAAAABoDRUTO4nZs2enXXfdNd18880L/P6VV17JpiuvvDKNHTs2nXLKKR22jwAAAAAAAAAAACAxsUq+853vpEMPPbTk3/v27duq9R944IGFpMRRo0alI488Mq200krpySefTGeeeWZ64YUX0qmnnppVUjzooINatS0AAAAAAAAAAABoKYmJVbLccsulz3/+86ktTJo0KauIGMaMGZNuuOGG1L179+znYcOGpe233z5tsMEGadq0aen4449Pu+yySxowYECb7AsAAAA1rC6lurpUe2pxnwAAAAAAgDbTre1WTbWcffbZ2f+RjHjBBRcUkhJzAwcOTOPGjcvmZ86cmcaPH+/gAwAAAAAAAAAA0CEkJta4Dz/8MN11113Z/OjRo9OgQYMaXW7nnXdOSyyxRDY/ceLEdt1HAAAAAAAAAAAAyElMrHFTpkxJc+bMyeZHjhxZcrmePXumESNGFG4zb968dttHAAAAakNdXV3NTgAAAAAAwKJDYmKVXHfddemzn/1s6tOnT+rfv39ac80103777ZfuvvvuVq33mWeeKcyvvfbaTS6b/33+/Pnp+eefb9V2AQAAAAAAAAAAoCV6tOhWLOTpp59e4OepU6dm02WXXZZ23HHHNGHChLTkkktWfOReffXVwnypNs65wYMHL3C7oUOHlr2d6dOnN/n3N954o+x1AQAAAAAAAAAAsOiSmNhKiy++eNp+++3TFltskVUs7NevX3r77bfTpEmT0m9+85v0zjvvpBtvvDHtsMMO6Y477kiLLbZYRev/4IMPCvOx7qb07du3MP/hhx9WtJ3ipEYAAAA6p2iYXItdk2twlwAAAAAAgDYkMbGVXnvttTRgwICFfj969Oh0xBFHpG222SY99thjWaLihRdemL773e9WtP7Zs2cX5nv27Nnksr169SrMf/zxxxVtBwAAAAAAAAAAAKpBYmIrNZaUmFt++eXT9ddfn9ZZZ500d+7c9Mtf/rLixMTevXsX5mMdTZkzZ05hvk+fPi1uGV2qlfPw4cMrWicAAAAAAAAAAACLHomJbWz11VfPqifecsstaerUqen1119PK620Utm379+/f9ntmWfNmlV22+eGBg0aVNHyAAAA1J5u3eqyqdbU4j4BAAAAAABtp1sbrpv/39ChQxdo/dzShMHp06eXXfVw8ODBjj8AAAAAAAAAAADtTmJiO6ivr69KUuOzzz7b5LL533v06JGGDBnS4m0CAAAAAAAAAABAS0lMbAdPP/10Yb6SNs5h2LBhqWfPntn8pEmTSi43d+7cNHny5IVuAwAAwKKjrq52JwAAAAAAYNEhMbGNvfjii+mOO+7I5ldfffW08sorV3T7/v37py222CKbv/POO0u2c544cWJ6//33s/mddtqp1fsNAAAAAAAAAAAALSExsRX+/Oc/p/nz55f8+1tvvZV22WWXNG/evOznww47bKFlJkyYkOrq6rJp7Nixja7nuOOOy/6PbcU6PvnkkwX+PmPGjHTCCSdk8wMGDEgHHXRQa+4WAAAAAAAAAAAAtFiPlt+UI444Iks6/MY3vpE22mijtOqqq6Y+ffpkiYL33HNP+s1vfpPeeeed7EBtsskmjSYmlmPzzTdPe+yxR7r66qvTTTfdlEaPHp2OOuqorC30k08+mc4444w0bdq0bNmzzjorLbXUUh4cAACARVA+8K3W1OI+AQAAAAAAbUdiYiu9/vrr6Ze//GU2lRKJixdddFHq1atXi7dz8cUXZ62ab7311nT33XdnU7Fu3bqlU045JR188MEt3gYAAAAAAAAAAAC0lsTEVrj00kvTpEmT0gMPPJBefPHFrFJiJA/269cvDR48OH3lK19J++23X1ZNsbWiEuMtt9ySrrzyyqz98+OPP57efffdtPzyy6dNN900HX744VXZDgAAAAAAAAAAALSGxMRWGDlyZDa1xv77759N5dprr72yCQAAAICOMW3atPSLX/wiG0Qa89ElY8iQIWm33XZLhx56aFp88cWrsp2rr746XXLJJemJJ55IM2fOTCussEI2QPWwww5LI0aMKHs9s2bNyga6Tpw4MT377LPZ4NoBAwaklVdeOW288cZpzJgxaauttqrKPgMAAAAABImJAAAA0EXU1f1vqjW1uE/QUpGM+M1vfjO99957hd999NFH6aGHHsqmiy66KN16661p9dVXb/E2Zs+enXbdddd08803L/D7V155JZuio8bYsWPTKaec0uy67r777nTAAQdktyv2n//8J5see+yxdN9990lMBAAAAACqSmIiAAAAAJTh8ccfz6oiRiJiv3790oknnphGjRqVPv7446y64e9///v03HPPpe222y5LUoxlWuLAAw8sJCXG+o888si00korpSeffDKdeeaZ6YUXXkinnnpqWnHFFdNBBx1Ucj133nlnVg0xEh379++fvv3tb6ctttgiLb/88untt99OL7/8crrtttvSW2+95fEHAAAAAKpKYiIAAAAAlOGoo47KkhJ79OiRbr/99rTRRhsV/rb55punNddcMx1//PFZu+Rzzz03Sx6s1KRJk7KKiCGSCm+44YbUvXv37Odhw4al7bffPm2wwQZZC+nY1i677JK1ZW4oEg/32GOPLClxnXXWyfZ30KBBCy138MEHp7lz53r8AQAAAICq6lbd1QEAAAAdpa6urmYn6OyiAuI999xTqGhYnJSYO/bYY7MkwPDzn/88zZs3r+LtnH322dn/kYx4wQUXFJIScwMHDkzjxo3L5mfOnJnGjx/f6HqimuM777yTevXqlSU3NpaUmOvZs2fF+wkAAAAA0BSJiQAAAADQjBtvvLEwf8ABBzQeaOvWLe27776FpME8kbFcH374Ybrrrruy+dGjR5dMJtx5553TEksskc1PnDhxob+/++67haqLe+65Z/rsZz9b0X4AAAAAALSWxEQAAAAAaMZ9992X/d+3b9+slXIpI0eOLMzff//9FR3XKVOmpDlz5iy0nsYqHI4YMaJwm4aVGf/85z+njz/+OJvfddddC7//4IMP0vPPP5/+85//VLRfAAAAAACVkpgIAAAAXUWttnHWypku4Jlnnsn+HzJkSOrRo0fJ5dZee+2FblPpNhqup6ntzJ8/P0s2LDZ58uTCfLSc/stf/pI23njjrMriWmutlZZffvm00korpWOOOSbNmDGjon0EAAAAAChH6SgqAAAAAJBmz55dSOAr1V45t9RSS2VVFWfNmpVeffXVio5e8fLNbWfw4MEL3G7o0KGFn59++uns/yWXXDJddNFF6fjjj1/o9m+88UY677zz0nXXXZf++te/LnD7ckyfPr3Jv8f6AQAAAIBFl8REAAAAAGhCtEDO9evXr9ljlScmfvjhh222ndhGruF2/vvf/2b/RzvnE044IfXq1Sv98Ic/THvvvXdadtllswqLZ599drrsssuyBMMddtghPfbYY2Xdt8YSIwEAAAAAGtLKGQAAALqIvGtyLU7Q2Ssm5nr27Nns8pEImCcGttV28m00tp1Iigxz585N9fX16Q9/+EOWoLjyyitn6/3c5z6XLr300vTtb387W27q1KnpN7/5TUX7CgAAAADQFImJAAAAANCE3r17F+Yj2a85c+bMyf7v06dPm20n30Zj2ylez4gRI9Kuu+7a6DrOPPPMQoLjVVddVdG+RvvopqYpU6ZUtD4AAAAAoGvRyhkAAAAAmtC/f//CfDntmfOKhZW0Rq50O/k2GttO8Xq22WabkutYZpll0oYbbpj+/ve/p8cffzzNmzcvLbbYYmXt66BBg8paDgAAAABYNKmYCAAAAF1EXfyrq8Ep6eVM5xYVCAcOHJjNT58+vcllZ86cWUgaHDx4cEXbKU72a247UZUw13A7xT83l0CYL/vJJ5+kd955p6L9BQAAAAAoRWIiAAAAADRjnXXWyf6fOnVqmj9/fsnlnn322YVuU66hQ4c2up6mttOjR480ZMiQBf72uc99rjAfCYdNKf57rAsAAAAAoBokJgIAAABAMzbZZJPs/6iG+Mgjj5RcbtKkSYX5jTfeuKLjOmzYsNSzZ8+F1tPQ3Llz0+TJkxe6Te6rX/1qYf6FF15ocpv53/v06ZOWXnrpivYXAAAAAKAUiYkAAADQRdTV1e4End2OO+5YmL/kkksaXebTTz9Nl112WTY/YMCANGrUqIq20b9//7TFFltk83feeWfJds4TJ05M77//fja/0047LfT3SExcdtlls/kbb7wx1dfXN7qel156Kf3zn//M5r/yla+kbt2ECgEAAACA6hBtBAAAAIBmDB8+PG266abZ/Pjx49MDDzyw0DLnnHNOeuaZZ7L5I488Mi222GIL/H3ChAmprq4um8aOHdvodo477rjs/2gXfdhhhy3UinnGjBnphBNOKCQ/HnTQQQuto3v37oX1PPfcc2ncuHELLTNv3rx06KGHZsmU4ZBDDnEOAAAAAABVIzERAAAAAMpw/vnnZy2PI2lwq622Sj/5yU+ylsp33313Ovjgg9Pxxx+fLbfWWmulY489tkXHdPPNN0977LFHNn/TTTel0aNHZ/8//PDDWaXGESNGpGnTpmV/P+uss9JSSy3V6Hq++93vpi996UvZ/Iknnpj22Wef9Ne//jU9+uij6dprr82SLP/yl79kf992223TN77xDecAAAAAAFA1Paq3KgAAAADoutZff/10zTXXpL333jtrpXzSSScttEwkJd5yyy1ZW+aWuvjii7P133rrrVnSY0zFouXyKaeckiVDltK7d+908803pzFjxqRHHnkkXX755dnUUCQlXn311VkVRwAAAACAalExEQAAALqIvEVsLU7QVUSi3xNPPJGOPvroLAlx8cUXz1oqb7jhhlnL5MceeywNGTKkVduIqoyR3HjFFVdkFROXW2651LNnzzR48OC01157pfvvv79kK+hiK664YlbR8Te/+U0aOXJkWnbZZbP20iussELafvvt08SJE1udRAkAAAAA0BgVEwEAAACgAqussko699xzs6kS+++/fzaVK5IQY2qNHj16ZJUVm6quCAAAAABQbSomAgAAAAAAAAAAAFWjYiIAAAB0EdExuRa7JtfiPgEAAAAAAG1HxUQAAAAAAAAAAACgaiQmAgAAAAAAAAAAAFWjlTMAAAB0EXV1ddlUa2pxnwAAAAAAgLajYiIAAAAAAAAAAABQNRITAQAAAAAAAAAAgKrRyhkAAAC6iOiYXItdk2txnwAAAAAAgLajYiIAAAAANeWss85Kb7zxRkfvBgAAAAAALSQxEQAAAICactJJJ6VVVlkljRkzJt14441p/vz5Hb1LAAAAAABUQGIiAAAAdKlWznU1OHX0kaEzimTEW2+9NX3jG99IK6+8cjruuOPSU0891dG7BQAAAABAGSQmAgAAAFBTnnzyyXTUUUelgQMHpvr6+vT222+n8847L33hC19II0aMSL///e/TBx980NG7CQAAAABACRITAQAAAKgpn/vc59K5556bXnvttTRx4sSspXP37t2zJMWHHnooHXLIIWnFFVdM++23X5o0aVJH7y4AAFV04ISHFpoAAIDOR2IiAAAAADWpR48eaccdd0x/+tOf0vTp09PZZ5+d1llnnSxB8aOPPkqXX3552nzzzdOQIUPSmWeemSUy8v+xdydgWlZl48DvGZFVBBRREVQMF8gyU1BEBTFRU9y3XFFcKjUtt7Jcyi1NUSvXRJDc9w1NwwTBNNEUd1IUEVJwwYVdlv91Tt/Mf1gGZoZZXmZ+P6/zPWee9zzPOfPid3VxvM99AwAAAADUPYGJAAAAUF8URRQVYEvrghXVrl27OP300+P111+P559/Po4//vhYffXVc5Die++9F+ecc05suOGG8cMf/jDuu++++Oabb3zpAAAAAAB1RGAiAAAAACuV7t27x/XXXx8fffRRDB06NNZZZ50coDh//vx44okn4qCDDor11lsvfvnLX+YxAAAAAADULoGJAAAAAKx0JkyYEJdeemmce+65MWXKlCjK6TkjByim9umnn8Yf/vCHXOZ54MCBdb1cAAAAAIAGpVFdLwAAAACoHikwqyQ4q5AU4ppYOc2aNSuXaR48eHCMHDmyNAgx6dKlSwwYMCCOOOKIeO2112LQoEFx77335mfOOOOMXAr68MMPr+tfAQAAAACgQZAxEQAAAICC9txzz8Xxxx8f6667bhx11FExYsSIWLBgQTRr1iz69+8fo0ePjjfeeCN+8YtfxFprrRV9+vSJ2267LcaNGxdbbrllDl688sor6/rXAAAAAABoMGRMBAAAAKDgfPTRRzF06NCcHfGdd97J90qyI2611VZx7LHHxqGHHhotW7Ys9x0bbrhh/P73v49dd901/vOf/9Ta2gEAAAAAGjqBiQAAAFBPpILJhVg1uQCXRIHbY4894sknn8xZEUuCEVu3bp0DEY877rjYYostKvyuTp065evMmTNrbL0AAAAAACxKYCIAAAAABeXxxx8v7e+44445O+IBBxwQTZs2rfS7mjdvnt9RVIhRuwAAAAAA9ZTARAAAAAAKSrt27eKoo47KAYkbb7zxCr2rffv2MWLEiGpbGwAAAAAAyycwEQAAAOqJlBGuELPCFeKaKGyTJk2KRo1sWwEAAAAArKzs8AIAAABQUG6//fZ83WeffWL11Vev0DPTp0+P+++/P/ePPPLIGl0fAAAAAADLJjARAAAAgILSv3//nGlz6623jq5du1bomSlTpuTniouLBSYCAAAAANQxgYkAAABQT6SKyYVYNbkQ10T9tXDhwrpeAgAAAABAg1fc4L8BAAAAAFZ68+bNy9dGjZzDBQAAAACoawITAQAAAFjpjRs3Ll/XWGONul4KAAAAAECD5wg5AAAA1BNFRUW5FZpCXBOF5Zlnnlnq/TFjxsSnn366zGfnzJkT48ePj8svvzz/u/a9732vhlYJAAAAAEBFCUwEAAAAoE717t17iQDWhQsXxjHHHFPhd6Tx6R0nnHBCDawQAAAAAIDKUMoZAAAAgDqXAgtL2tLuLa916NAhrrnmmthnn33q9PcAAAAAAEDGRAAAAADq2NNPP13aT0GGffr0ydkPBw0aFJ06dSr3uTSmadOmse6660bHjh1rabUAAAAAACyPUs4AAABQT6QgrcXL4RaCQlwThaVXr15Lvd+9e/fo2rVrra8HAAAAAIAVIzARAAAAgILy/vvv5+t6661X10sBAAAAAKAKBCYCAAAAUFA22GCDul4CAAAAAAArQGAiAAAA1BOpYnIhVk0uxDUBAAAAAAA1R2AiAAAAAHXid7/7XWn/3HPPXer9qij7LgAAAAAAap/ARAAAAADqxPnnnx9F/5dSs2wwYdn7VSEwEQAAAACgbglMBAAAgHoiBXKtSDBXTSnENVE4Fi5cWKn7AAAAAAAUPoGJAAAAANSJBQsWVOo+AAAAAAArh+K6XgAAAAAAAAAAAABQf8iYCAAAAPVFUSqbHIWnENcEAAAAAADUGBkTAQAAAAAAAAAAgGojMBEAAAAAAAAAAACoNko5AwAAQD1RVFSUW6EpxDVRGPr06VMj/7499dRT1f5eAAAAAAAqTmAiAAAAAHVixIgROZBw4cKFK/yukvcIhAUAAAAAqHsCEwEAAACoEzvuuKNAQgAAAACAekhgIgAAANQTqWByIVZNLsAlUUAZEwEAAAAAqH+K63oBAAAAAAAAAAAAQP0hMBEAAAAAAAAAAACoNgITAQAAAAAAAAAAgGrTqPpeBQAAANSl4qKi3ApNIa4JAAAAAACoOQITAQAAAKgTq6yySr4WFRXFvHnzlrhfFYu/CwAAAACA2icwEQAAAIA6sXDhwkrdBwAAAABg5SAwEQAAAOqJVDG5EKsmF+KaKAznnXdepe4DAAAAALByEJgIAAAAQJ0QmAgAAAAAUD8V1/UCAAAAAAAAAAAAgPpDxkQAAACoJ4qKinIrNIW4JgAAAAAAoObImLgCevfuXfoffSraRowYUel5zj///Bp9PwAAANQXEydOjNNPPz26dOkSLVq0iDXWWCO6d+8el19+ecycObNa5xo+fHj0798/OnfunOdq1apVbLLJJnHAAQfEddddF9OnT6/W+Rq6KVOmxFNPPRX33HNPbqmf7gEAAAAAUHhkTKxFxcXFsfHGG9fmlAAAANBgDBs2LA477LD48ssvS++lYMQxY8bkdtNNN8Vjjz0WG2200QrNM23atDj66KPjoYceWuKzr776Kt5555247777okePHvG9731vheZq6BYsWBA33HBDXHvttfHmm28udUzXrl3jpz/9aZxwwgl57wUAAAAAgLonMHEFDB48OGbMmLHMMWnT/OCDD879nXfeOdZbb70VmTJee+21ZX7eqVOnFXo/AAAAK6/iov+1QlMbaxo7dmwcdNBBORBxtdVWi1/96lex0047xaxZs+LOO++Mv/zlLzFu3LjYY489cpBiGlMVKehxl112iZdeein/nN53yCGH5KyJ8+fPjw8++CC//957763m37DhSdkQ0/f78ssv558XLlxY7t7LSSedFDfffHM88sgjsc4669TySgEAAAAAWJzAxBVQkSDAv/71r6X9I488MlbU5ptvvsLvAAAAgPrm1FNPzUGJjRo1iieffDJnKyzRp0+fXMHgzDPPjLfffjsGDhwY5557bpXm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4/zzz49zzjlnhX4XAAAAAICaYne8Gn3zzTf5JHuy1lprxe677x61pSRLY9K+fftamxcAAIDCUVz0v1ZoanpN22+/fQ5MTNkQX3rppdhmm22WOm7kyJGl/Z49e1ZqjpRVbv3118/Z8caPH7/MsWU/X2+99So1T0PTq1evWJmMHTs2Z0VMgYipHPivfvWr2GmnnWLWrFl5T+gvf/lLjBs3LvbYY48cpFjZkuElBgwYUBqUmN5/yimn5P2e1157LS6++OL879i5556bS6Mfe+yxy3zX3nvvHRdeeGG5nzdu3LhKawQAAAAAWBaBidXo8ccfj08++ST3Dz300FrNinDDDTestJv6AAAAsCL22WefuOSSS3I/lb1dWmDiggULSqsctG7dOgd7Vdb+++8fV155ZUyZMiX++c9/xnbbbbfUcffff39pP5Xdpf449dRTc1Bi2vN58skno0ePHqWf9enTJzbeeOM488wzc3bOgQMH5uDBykoBtCkjYtKvX7944IEHYpVVVsk/d+vWLfbaa6/YaqutcpBsmuuAAw7I/06XJ322+eabV+n3BQAAAACoquIqP8kyyzgfeeSRFfqGhgwZEkVFRbmlEjyLSyfhUymq5QUlDho0KPfXWWed2Hffff3pAAAA0GB07969NAAw/f34ueeeW2LMFVdcEW+99Vbup+xzq666aqX+fl4SlNa0adPc/9nPfpYzNC7u1ltvjREjRuR+yprXoUOHavgNKQQpA2LJn23KaFg2KLHEaaedVlom/KqrrsrVNSrrsssuy9cUjHjttdeWBiWWaNu2bVx66aW5P23atNI9IQAAAACAQiIwsZqkjeCSEjvpFPr3v//9anlvKkG12WabxS677JJP2v/973+Pf//73/HCCy/kQMi+ffvGj3/84zw2bVSnIMUWLVpUy9wAAACsXEoC6wqx1bSrr746mjVrFvPmzct/V04ZFJ9//vl4+umn44QTTsiZ5ZJNNtkkB49VRSrl/Lvf/a707+spIPKWW27J/X/84x9x0kknRf/+/fPnq6++es6uSP3x4IMPlvaPPvropY4pLi4uPaya9opKAhkravr06fHUU0/lftoLKi+wdb/99sv/ji2eoRMAAAAAoFAo5VxN7rrrrpgzZ06lsiVW1Pz582P48OG5lWfNNdfMJ+RTOR8AAABoaLbccsv8d/PDDz88vvrqqzj7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jhTQOWvWrHJ/l6+//joHcH722Wf55wEDBkSTJk2W+x0AAAAAABRUYGLJ5nAq51xR7dq1y9eSDVIAAABg2UoCngqxQWWss8468c0338Ts2bNjo402WuSzBx54IH7961/H2muvnQMUU2vVqlUO4kuBjE2bNq2xL/vqq6+OZs2a5aDBvn37xiWXXJJLKqd504HcM888M4/bZJNN4rTTTqtytshDDjkk9x9++OHYZZdd8vXFF1/MmRq33XbbmDhxYv7897//fbRp02aJd6T7HTp0iOOOOy5uueWWHMSZ9udSpsm05m9/+9vx5JNP5rGbbrppuUGSAAAAAABV1ShqwRdffLFIsGFFpM3npLi4VmInAQAAACgg5e0Jpcx+F1xwQW6ff/55DhJca621aiUANlX4SOWPU+bGVEr57LPPXmJMCkocNmxYLstcVTfffHN+/2OPPZaDHlNb/Ls555xzcjBkedJ3c9NNN+VWnh133DFuv/32WGONNaq8VgAAAACAOgtMTCe3P/nkk0plPywp4Zw2lgEAAABgcXURUNevX7949dVXc/bEFIA4adKkaNy4cXTu3DkOPPDAOOmkk6J58+YrNEfKypjenYIGU/nnsWPH5oO/KUtkKied5ujRo0e5z19++eXx1FNP5XLTaY8tlX9Oz6d1tW/fPrbZZpv40Y9+lLM+ymgKAAAAAKy0gYldu3aNkSNH5rIxO+20U4WeSRuvaWN0q622qvH1AQAAQH2QEsYVYtXkQlwTrIgNNtggBg4cmFtl9O/fP7eKOvTQQ3OrrK233jo3AAAAAIB6HZi41157xYgRI+Laa6+NE088cbmn2QcPHhxPPPFEDkzcd999a2OJAAAAABSol156KYYPHx6vv/56LlGcpP2lzTffPH7wgx842AoAAAAA0BADE0844YRcQuajjz6KXXbZJYYOHRrf/va3lxj34YcfxmWXXRbXXXddDkrceOONq3QqHAAAAICV34svvpgPuaZrec4+++ycHfCaa66RJRAAAAAAoCEFJjZr1iweeOCB6NOnT7zyyivx3e9+NzbddNPSz3/84x/HJ598Ev/5z3/yzwsXLoyWLVvGvffeG8XFxbWxRAAAAFjpFRcV5VZoCnFNFL677747jjjiiJg3b17eK0oaN26cMyWmn6dNmxZz587N98eMGRM9e/aMW2+9NQ488MA6XjkAAAAAALUW9detW7f45z//mUvspM3jt99+u/SzZ599NsaNG5fvp9alS5d8L40FAAAAoGFJ+0ZHHXVUfPPNN7HKKqvET37ykxx8OGPGjPjvf/+bq3KkfsqkmD5r1KhRHnvkkUcusucEAAAAAEA9zphY4jvf+U6MHTs2hg0bFg899FDePJ46dWrMnz8/1lxzzdhyyy1jr732iv3331+mRAAAAIAG6tJLL405c+ZE06ZN47HHHovevXsvMSYFLH7/+9/P7aCDDordd989P3PZZZfFzTffXCfrBgAAAACgDgITS+yxxx65AQAAANUnFUwuxKLJhbgmCttTTz0VRUVFceqppy41KHFxvXr1ymN///vfx/Dhw2tljQAAAAAAFEApZwAAAACoiE8++SRff/jDH1b4Cys5BFvyLAAAAAAA9Tww8ZprrolPP/20NqYCAAAAYCW31lpr5Wsq5VxRTZo0yde2bdvW2LoAAAAAACigwMSTTz452rdvn0+u33777TFz5szamBYAAAAalFT6tlAbVEbPnj3zdcyYMRV+5oUXXsjX7bff3pcNAAAAANBQSjnPmzcv/va3v8URRxwRa6+9dhx++OHx+OOPx/z582trCQAAAACsBH7xi1/EKqusEhdffHGFSjNPnTo1Lrnkklh11VXj5z//ea2sEQAAAACAOg5M/Ne//hWnnHJKDkhcuHBhzJgxI+64447Yc889cybFn/3sZ/H888/XxlIAAAAAKHDdunWLG264IQccbrPNNvHggw/GggULlhiX7j300EPRo0ePHMB43XXXRffu3etkzQAAAAAA/H+NopY2k1O74oor4h//+Efcdttt8cADD8RXX32VN42vueaa3DbccMOcSfFHP/pRbLbZZrWxNAAAAADqyDHHHLPMz7t27Rpjx46N/fffP9q0aRNbbrlltGvXLpcHnzJlSrzyyivx+eef57FbbLFFjB49Op599tkYNGhQLf0GAAAAAADUWWBiieLi4vjBD36Q2/XXXx+PPPJIDlJMJZ3nzp0b77//flx44YW5pY3mFKR48MEHx7rrrlubywQAAICVUnHR/1qhKcQ1URiGDBmSgwyXJX2eKnCkAMR04LWsdL9kTApgTC0RmAgAAAAA0IACE8tq0qRJHHDAAbl98cUXce+998btt98ezzzzTC7D8+9//ztefvnlOPPMM3PQIgAAAAD1y/rrr7/cwEQAAAAAAFY+dRaYWFbr1q3j2GOPzW3y5Mk5QPGSSy7JAYvz58+v6+UBAAAAUAMmTJjgewUAAAAAqIcKIjCxxOuvv55LO99xxx3x5Zdf1vVyAAAAYKWSMs8VYva5QlwTAAAAAABQjwMTJ06cmAMRU0DiG2+8ke8tXLgwX5s3bx577713Ha8QAAAAAAAAAAAAKOjAxM8//zzuueeeHIz4z3/+MwcilgQjrrLKKvGDH/wgDj/88Nhnn32iRYsWdbFEAAAAAArMvHnzYtq0abnfpk2baNSozs/cAgAAAACwFLW2eztr1qx46KGH4vbbb48nnngibyQnJQGJ3bt3j8MOOywOOeSQWGuttWprWQAAAFCvqJpMffPWW2/FtddeG8OHD4933nmndC8plQjfeOONY5dddokf//jH0bVr17peKgAAAAAAtRmYeOSRR8aDDz4YM2bMyD+XbCCnzeNDDz00ByR27ty5NpYCAAAAwEriV7/6VVx++eWxYMGC0v2kEunncePGxX/+85+47rrr4owzzoiLL764ztYKAAAAAEAtBybeeuutpf127drFwQcfnEs1d+vWrTamBwAAAGAlc/LJJ+dMiSUBiV26dIltttkm1llnnXxvypQp8cILL8Sbb74Z8+fPj0svvTQfir366qvreukAAAAAAA1erQQmtmjRIvbdd9+cGTGV1ykuLm7wXzwAAABUt1TaNrVCU4hrorA9++yzcc011+R/d1KJ5htvvDG22267pY597rnncinn1157Lf785z/nA7HljQUAAAAAoB4FJk6dOjWaNWtWG1MBAAAAsJK74YYb8rVTp045SLFVq1blju3Ro0c888wzsdVWW8X7778f119/vcBEAAAAAICGEJgoKBEAAACAiho1alTOlvjLX/5ymUGJJdKYs846K0444YT8LAAAAACsrAYMGbPEvUH9u9XJWqDgAxMBAACAmldc9L9WaApxTRS2jz/+OF+33HLLCj/z/e9/P1+nTJlSY+sCAAAAAKAOAhOHDh1a2j/yyCOXer8qyr4LAAAAgPqtadOmMXfu3JgxY0aFn5k+fXq+NmnSpAZXBgAAAABArQcm9u/fP5fZSa1sMGHJ/apY/F0AAAAA1G+dOnWKsWPHxsMPPxw77rhjhZ555JFH8nWjjTaq4dUBAAAAALA8xVHNFi5cmFt596vSAAAAgOVLZwJLDgwWVvOnR+X88Ic/zHtCf/7zn+Opp55a7vg05k9/+lP+9y09CwAAAABAPcqY+P7771fqPgAAAAAs7tRTT81BiV9//XXsvvvucdxxx8UxxxwTW265ZRQX/++c7YIFC+Lll1+OQYMGxU033RTz5s2LVq1a5WcBAAAAAKhHgYkbbLBBpe4DAAAAwOLatm0bd999d+y1114xd+7cuP7663Nr3LhxrLHGGjkz4meffZY/S1J2xfTZPffcE2uuuaYvFAAAAACgvpVyBgAAAIAV1bdv33j++edj6623zoGHqc2ZMyc++uij+O9//5v7Jfe7desW//rXv+IHP/iBLx4AAAAAoL5lTCxPKrWTTrJfeOGFse6661bomU8++STOOuus/FwqyQMAAAAsW9H/tUJTiGti5fC9730vXnjhhRgzZkwMHz48Xn/99fj888/zZylz4uabb56DEVNgIgAAAAAADSwwcciQITnA8LTTTqtwYOJXX31V+pzARAAAAICG45lnnsnXtI+08cYb58BDwYcAAAAAACsPpZwBAAAAKCi9e/eOnXbaKZ599tm6XgoAAAAAAIWaMbEqZs+ena9NmjSp66UAAADASqG4qCi3QlOIa6KwrbbaajFjxoz4zne+U9dLAQAAAACgPmVMLDkRv/baa9f1UgAAAACoReuvv36+zpw50/cOAAAAALASqpGMib/73e+Wev/aa6+Ndu3aLfPZOXPmxPjx4+Phhx+OoqKi6NmzZ00sEQAAAIACtccee8Rbb70Vw4cPjx122KGulwMAAAAAQCEEJp5//vk5qLCshQsXxnXXXVfhd6TxTZs2jTPOOKMGVggAAAD1T/qreCFWTS7ENVHYfv7zn8fNN98cV111VRx44IGx+eab1/WSAAAAAAAohFLOKbCwpKUgxdTK3iuvNWnSJDbccMM47LDD4rnnnostttiippYIAAAAQAFaZ5114tFHH42WLVvmahoXX3xxTJgwoa6XBQAAAABAXWZMXLBgwSI/FxcX58DE119/Pbp27VoTUwIAAABQT2y00Ub5Onfu3Pj666/jnHPOyW211VaL1q1bxyqrrFLus2kPavz48bW4WgAAAAAAaiUwcXHrr79+3hRu3LhxbUwHAAAADdP/VSwoOIW4Jgra4tkRU5WNJAUpprYsBfn/AwAAAAAADUytBCYqtQMAAABARR111FG+LAAAAACAlVitBCYCAAAAQEUNHjzYlwUAAAAAsBITmAgAAAD1RKpgW4hVbAtxTQAAAAAAQD0KTHz66afjwQcfjLFjx8ann34as2bNioULF5Y7vqioKMaPH1+rawQAAACg9qU9on//+9/xwQcfxPz586N9+/bRrVu3aNy4sT8OAAAAAICVSK0FJk6dOjUOOeSQGDlyZP65vGDEFIhY9rP0MwAAAAD124033hjnnXde3kMqq3nz5nHKKafEb3/721hllVXqbH0AAAAAABRYYOI333wTu+++e7zyyis56HDLLbfMJ96HDRuWAw8PP/zwmDZtWj4R/9///jff+/73vx+bb755bSwPAAAAgDp0/vnnxwUXXLDUw6wzZsyISy65JN588824//7762iFAAAAAAAUXGDikCFD4uWXX84Bh4MHD46jjjoq3njjjRyYmNxyyy2lYx966KE48cQT82bzL3/5y9h///1rY4kAAACw0isuKsqt0BTimigcr7/+elx44YWlQYk9e/aMbbfdNho1ahSvvvpqPPnkk7msc9ozuuuuu+Lggw+u6yUDAAAAAFAIgYn33Xdfvu622245KHFZ9t5775wpceutt47+/fvHd7/73dh4441rY5kAAAAA1EEJ5wULFuRAxBR4uO+++y7y+Ysvvhi77rprfPHFF3HDDTcITAQAAAAAWAkU18YkY8eOLS3ZvDSLl+j51re+Faecckou1XP11VfXxhIBAAAAqAOjR4/O+0YnnHDCEkGJSTq8mjIqpv2jf/3rXzmIEQAAAACAwlYrgYmff/55vnbq1Kn0XuPGjUv7M2fOXOKZnXfeOV///ve/18YSAQAAYKWXKiYXaoPyTJgwIV/32muvcsfss88++Tp79uz4+OOPfZkAAAAAAAWuVgITS4IQywYjrr766qX9yZMnL/FM06ZNy/0MAAAAgPrhq6++ytd111233DHrrLNOaf/rr7+ulXUBAAAAAFDggYnrr79+vk6ZMqX03tprrx0tW7bM/VSGZ3FvvPFGvqZSPgAAAADUTyWlmYuLy9+mKrs/NH/+/FpZFwAAAAAABR6Y+P3vfz9fX3755UXu77jjjrFw4cK4+uqrY86cOaX3v/zyy7jsssvypnPXrl1rY4kAAACw0itK/xQVYAuHDgEAAAAAoCGplcDEnXfeOQcgDhs2bJH7P/7xj0sDFr/zne/E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+OIYP358nHvuubl6RvqeKuqkk07KlTbatWuXAzsBAAAAatKAIWOWen9Q/26+eGgAimtjkg033DCXL04btPVFOrleonHjxsscm8r5lEgb1VWda3nzlJ2rKvMAAAAAFIJ77703br755th6661zhsLyghKT9NkjjzwSW221VX7m7rvvrrF1nXrqqTkosVGjRjlT49lnnx09evSIPn36xI033hiXXXZZHvf222/HwIEDqzTHyJEjc0bEpF+/fvH3v/899t5775wR8phjjonnn38+1l9//fz5mWeeGV988UWF3vvQQw/FAw88EGuttVacddZZVVobAAAAAEBBBSY2a9YsX9MGan3RtGnT0v7cuXOXOXbOnDlLfBdVmWt585Sdq7LzpNLPy2ovvPBCpdcNAAAAUBU33HBDFBUVxS9+8YtYZZVVljs+jTnttNNyiecUIFgTUgbEESNGlGY0TAGJi0tr6NKlS+5fddVVucR0ZZUEN6bfKZWyXvz3b9u2bVx66aW5P23atJxNsiLVOFK2xOTyyy+vNxVNAAAAAIAGHphYcqp9/vz5UV+0bNmytL+8sskzZswo7VelhE/JXBUpz1wyV2Xn6dChwzJbKg0EAABAYUuBXMUF2NK6oDJeffXVfN1kk00q/EzJ2FTuuCY8+OCDpf2jjz56qWOKi4vjyCOPLA0aLAlkrKi09/PUU0/l/i677JL3ZJZmv/32i9VXXz3377///uW+N5WcnjRpUvTu3bt0fQAAAAAAK31gYt++ffN19OjRUV+U3RhOG7vLkjIOlujYsWOV51rePGXnqso8AAAAAIUgZfhLpk6dWuFnSsaWPFvdRo0ala8tWrTIZaPL06tXr9J+ZffCUsWKkmoYZd+zuMaNG8e2225b+syyMjP+61//iuuuuy4/k64AAAAAAPUmMPGUU07JpYVTqZjJkydHfdC1a9fS/ttvv73MsWU/LynnU5W5vvzyy/j444/LHffRRx/FV199VeV5AAAAAArBBhtskK9Dhw6t8DMlY9dff/0aWdNbb72Vr507d45GjRqVO26zzTZb4pnKzrH4e5Y1z7x58+Kdd95Z6pgUsHjcccfFggUL4owzzljuOwEAAAAAVqrAxI033jhuv/32mDlzZj7Nnfpz586NlVmnTp2iffv2uT9y5Mhljn3mmWdKS1pvuOGGlZ5r++23L+0va66yn/Xs2bPS8wAAALDy/yW/UBtUxt577x0LFy6MO++8My677LLljk+HYe+4445cNnzfffet9i979uzZ8emnn+Z+eeWVS7Rp0yZnVVy8ikZFlB2/vHnKVssob54//OEPubT1RhttFL/+9a+jOqXKHstq6QAtAAAAANBwlX+8uxr16dMnX9daa614//3344gjjogBAwbkgMW0WbvKKquU+2zaUH7qqaei0KR1pU3yVAInZUR8/vnnS0volJXul2RMTOPTc5W11157xU9+8pN8un3w4MFx8MEHL3XckCFD8rW4uDg/AwAAALAy+uUvf5kzIE6ZMiV+9atf5aDDo446Krp16xbt2rXL+yvpszFjxsRf//rXeOWVV/Jz66yzTpx11lnVvp6y5aFXW2215Y5PgYkzZsyI6dOn19g8JcGPydLmeffdd+OCCy7I/WuuuSZXM6lOZQMjAQAAAADqJDBxxIgRiwTkpRPvc+bMiddff73cZ9L4NK4qgXy15dRTT42//OUvuWTOySefnDMjlt3knTVrVr6fpBI/afzS9O7duzTbYQrcXDyrYtpUP+yww/JG+xNPPBH33ntvHHDAAYuMueeee/JnSQr8TM8AAAAArIxat24dw4cPj1133TUmT54cr776apx22mnljk97SCnD4N/+9rf8bE1kTCzRuHHj5Y5v0qRJ6d5QTc1TMkd58/z4xz/O7zvwwANjt912q9Q6AAAAAABWisDEHXfcseACDEePHp1PjpcoKceTpPsl2QdL9O/ff4l3bLLJJnH66afH73//+3jxxRdz+eR0Kv9b3/pWjB8/Pi699NJ4+eWX89gzzjgjZ4isqosuuihvrn/yySfxox/9KM+355575s8effTRuOKKK0qzUl544YVVngcAAACgEHTt2jXeeOON+O1vf5v3aaZNm7bUcakax9FHHx3nnnturL766jWylqZNm5b2586du9zx6UBuUtkshZWZp2SOpc2Tvq9UgSR9H1dddVXUhOWVqU6lnLt3714jcwMAAAAAha/WMiYWmptuuiluueWWpX727LPP5ra8wMSSgMGpU6fGzTffnIMQDznkkCXGpLLVKxosmMrjPPLII7HPPvvExx9/nIMeUysrZUl88MEHc4YAAAAAGp50JrDAzgVmhbgmVg4psC4dxrzkkkvipZdeitdeey0HKKYMiWussUZ85zvfia222qpC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ePXVV3N/xx13rJU1AgAAAABQS4GJEydOjJqw/vrr18h7AQAAACg8w4cPj9133z3WXHPNCu03NW7cOPbff//4/PPP87M77bRTrawTAAAAAIBaCEzs1KlT1ERWhXnz5lX7ewEAAAAoTHfffXe+HnjggdGsWbPljm/evHkcfPDBce2118Zdd90lMBEAAAAAoI4VV+fLFi5cWCMNAAAAgIbjpZdeyodV+/TpU+FnSrIkvvDCCzW4MgAAAAAAaj1j4uDBg6vzdQAAAEAlTx8WF9XzU5E0CJMmTap0dY4NN9wwXydPnlxj6wIAAAAAoA4CE4866qjqfB0AAAAADdDMmTPztTKVNErGfvXVVzW2LgAAAAAAKkbSAgAAAAAKylprrZWvb7/9doWfKRm75ppr1ti6AAAAAACoB4GJY8aMiZNPPrmulwEAAAArhaKiwm1QGd26dcsZEIcOHVrhZ4YMGRJFRUXx/e9/35cNAAAAAFDHCi4wcdKkSXHJJZdE165dY9ttt41rr722rpcEAAAAQC064IAD8vWpp56KK664Yrnj05h//OMfuX/ggQfW+PoAAAAAAFgJAhNnzJgRt9xyS+y8886x4YYbxm9+85sYN25cPhkPAAAAQMNy8MEHxxZbbJH3hs4888zYf//9Y9SoUfHNN9+Ujpk3b16+t99+++UxKVvi5ptvHocffnidrh0AAAAAgIhGdfUlpI3l4cOH55I8Dz74YMycObP0ftKhQ4e8sZw2ngEAAIDlKy4qyq3QFOKaKGwpyPCBBx6Inj17xkcffZT3jlJbddVVY4011siff/bZZ6WBimk/qX379vHQQw/lzwAAAAAAaGAZE9944418ir1jx46x2267xe23354zJqYN5HTvtNNOi+eeey4mTpwYV111Veywww61vUQAAABgJZX2E04//fTo0qVLtGjRIgexde/ePS6//PLSQ5HVLQXOtW7dOgfEpda7d+8amaehSVU1Xn755dh7773zz2nvaO7cufHxxx/n7zz1Sw64psOt//73v/MzAAAAAAA0kIyJn3zySdx22205O+LYsWPzvZKN47Rx/8UXX+SN+8suuywOOuig2lgSAAAAUM8MGzYsDjvssPjyyy9L76VgxDFjxuR20003xWOPPRYbbbRRtc578sknLzIn1addu3Y5c+K4cePyn10KVPz000/zZ23bto3vf//7sccee8TGG2/sawcAAAAAaAiBiXPmzMnlc1Iw4pNPPhnz588vDUZs0qRJ/PCHP4zDDz88X5s1a1ZTywAAAIAGo7jof63Q1Maa0kHIdNgxBSKuttpq8atf/Sp22mmnmDVrVtx5553xl7/8JQe3pSC2FKSYxlSHRx55JO67774cQDd16tRqeSdL2nTTTXMDAAAAAKCBBiaOHj06ByPec8898dVXX+V7KSAxZURMZZlTMGL6DwWtWrWq7qkBAACABurUU0/NQYmNGjXKByR79OhR+lmfPn1yRr0zzzwz3n777Rg4cGCce+65Kzzn9OnT48QTT8z9VCr6yCOPXOF3AgAAAABAfVBc3S/ccccdY9CgQbmEUQpI7NKlS1x00UXx/vvvx8iRI+O4444TlAgAAABUm5QBccSIEbk/YMCARYISS5x22ml5jyK56qqr4ptvvlnhec8+++z48MMPc2bGI444YoXfBwAAAAAA9UWNlXJu2bJlXH311dG/f/+amgIAAAAoqyiiqKgw11WTHnzwwdL+0UcfvdQxxcXFOaNhKvE8bdq0HMi4yy67VHnOF154Ia655ppo3LhxXHfddVV+T0M3ceLE0v7666+/1PtVUfZdAAAAAADUk8DElCkxlTNKWQpSeaRUvvnQQw+NDh061MR0AAAAQAM2atSofG3RokVstdVW5Y7r1atXaX/06NFVDkycN29eHH/88bFgwYI466yzYtNNN63Se4jo1KlT/hqKiory97r4/apY/F0AAAAAANSDUs7pPwYce+yxuVxzClB8/fXXczaCDTfcMJc2uvnmm+Orr76q7mkBAACABuqtt97K186dO0ejRuWfwdxss82WeKYqLr/88hg7dmx861vfyuWcqbq0d1TSyrtflQYAAAAAQD3LmNizZ8/c/vznP8dDDz0Ut9xySzz55JP5pPozzzyT20knnRR77LFHHHHEEfHDH/6wupcAAAAADVJxFOVWaMqu6aOPPlru+MpUXJg9e3Z8+umnFXquTZs2OavijBkz4sMPP4yqeO+99+J3v/td7l977bXRtGnTKr2H/xk8eHCl7gMAAAAA0IBLOSeNGzeOAw88MLdPPvkkbr311hg6dGjOKJD+o8H999+fW+vWrWtqCQAAAECB6d69+3LHVCbj3ddff13aX2211ZY7viQwcfr06VEVJ5xwQsyaNSsOPvjg6Nu3b5Xewf931FFHVeo+AAAAAAANPDCxrLXWWit+/vOf55ZKOw8ZMiTuuOOOnCVh2rRpUVT0v8wJv/jFL+LZZ5+NAw44IHbYYYfaWBr1UJtuJ0Uhmzbmz3W9BACoVYX+v82J/30GWHmlw49lD0kuT5MmTfI1BRdWVjpwOXz48Fh99dXjyiuvrPTzAAAAAADQUBTX9oSbb755XH755blk0mOPPRaHHHJILnuUsiH897//zSWge/fuHeuuu2789Kc/jaeeeqq2lwgAAADUkBdeeCHvCSyrVUbZUspz585d7vg5c+bka7NmzSo1TyoXfdppp+X+RRddlPctAAAAAACAOsyYuDTFxcWx22675ZbKLt11113x17/+NUaPHp2DFKdMmRI33HBD3HjjjTFv3ry6WiYAAACsNFJBgv8rSlBQyq4pBfR16NCh2t7dsmXL0n5FyjOnMs4VLftcVqrykIITt95663yQEgAAAAAAKMDAxMX/I8Kxxx6b24QJE3JppFtvvTXefffdul4aAAAAUMBSxsS2bdvmoMFJkyYtc+y0adNKAxM7duxY4TlShYd0mDLp06dP3H333cscP3Xq1Ljzzjtzv1OnTrHNNttUeK6G5ne/+12NvPfcc8+tkfcCAAAAALASBSaWteGGG+bN49SeffbZ0o1/AAAAgKXp0qVLjBo1Kh9wTFUXGjVa+nbH22+/vcgzFVW2RPRll1223PFvvfVW/OhHP8r9o446SmDiMpx//vlRVANpPgUmAgAAAADUreIoYD179ozrr7++rpcBAAAAK4XiosJtNWn77bfP15QN8aWXXip33MiRIxfZc6AwLFy4cJmtKmMAAAAAAKhbBR2YCAAAALA8++yzT2l/8ODBSx2zYMGCGDp0aO63bt06dtppp0pVd1heYFzZYLhevXqV3hsyZIg/wGVIfy7ltffeey+6deuWv8fdd9897rnnnvjggw9i9uzZuaX+vffemz9LY9LY999/Pz8LAAAAAEDdEpgIAAAArNS6d+8eO+ywQ+4PGjQonnvuuSXGXHHFFbnEcnLKKafEqquuusjnKYAwlRROLZUXpm59+eWX0bdv3/j3v/+dA0qHDRsW+++/f3Ts2DEaN26cW+rvt99++bO//vWvOVvmD37wg/wsAAAAAAB1S2AiAAAA1BPFRUUF22ra1VdfHc2aNYt58+blgLZLLrkknn/++Xj66afjhBNOiDPPPDOP22STTeK0006r8fWwYq688sp4991347jjjovDDz98ueMPO+ywPHb8+PE5CBUAAAAAgLolMBEAAABY6W255ZZx1113xeqrrx7Tp0+Ps88+O3r06BF9+vSJG2+8sTQoMWXXa9myZV0vl+W47777cvbKAw88sMLf1UEHHZSv999/v+8XAAAAAKCOCUwEAAAA6oV+/frFq6++Gj//+c9zEGLz5s2jdevWsfXWW8ell14aL7/8cnTu3Lmul0kFTJgwIV9btWpV4e+rZOwHH3zgOwYAAAAAqGON6noBAAAAQPVIFZNroWpypdXmmjbYYIMYOHBgbpXRv3//3FbEwoULV+h5/r9VV101X1977bX4/ve/X6GvJo0t+ywAAAAAAHVHxkQAAAAACsoWW2yRAz1TpsuZM2cud3wak8am8s/f/e53a2WNAAAAAACUT2AiAAAAAAXl2GOPzddx48ZF796945VXXil37NixY2OnnXaKt99+O/98/PHH19o6AQAAAABYOqWcAQAAoJ4oiqIoLsBazmldUBmHHXZYPPDAA3H//ffHSy+9FFtttVV85zvfiW7dukW7du1yZsQpU6bEmDFjSks4J/vtt18ceuihvmwAAAAAgDomMBEAAACAgnPXXXfFqaeeGtddd10sWLAgXn311UWCEEukks8pUPGkk06KgQMH1slaAQAAAABYlFLOAAAAABScVVZZJf70pz/Fyy+/HD/5yU9i4403Lg1ELGmdO3fOn6Uxf/zjH6NRI2dwAQAAAAAKgd1aAAAAqCdSFecCrORckGti5ZFKOF9zzTW5P2fOnPjiiy9yUGKbNm2iSZMmdb08AAAAAACWQmAiAAAAACuFFIi49tpr1/UyAAAAAABYDqWcAQAAAAAAAAAAgGojYyIAAAAABevdd9+NoUOHxnPPPRcff/xxzJo1K/72t79F586dS8e8/vrrMXHixGjRokX06tWrTtcLAAAAAIDARAAAAKhXZREKsTRCIa6JwrdgwYI466yz4qqrrsr9hQsX5vtFRUUxd+7cRcZ++OGHseeee0ajRo3i/fffj/XWW6+OVg0AAAAAQOK/DQAAAABQcE444YQYOHBgzJ8/P9q3bx8HHHBAuWN333332GijjfLYe++9t1bXCQAAAADAkgQmAgAAAFBQRowYEYMGDcr9s88+OyZMmBB33333Mp858MADc1bFp59+upZWCQAAAABAeRqV+wkAAACwUkklblMrNIW4Jgrb9ddfn68//OEP48ILL6zQM927d8/XN954o0bXBgAAAADA8smYCAAAAEBBee6553JA64ABAyr8TIcOHfL1448/rsGVAQAAAABQEQITAQAAACgoU6dOzddOnTpV+JlGjf5XGOSbb76psXUBAAAAAFAxSjkDAABAPZEKJhdi0eRCXBOFrVmzZjF37tyYOXNmhZ+ZOHFivrZp06YGVwYAAAAAQEXImAgAAABAQSnJlPjyyy9X+JlHH300X7t27Vpj6wIAAAAAoGIEJgIAAABQUPr27RsLFy6MG2+8MRYsWLDc8S+99FL89a9/jaKiothtt91qZY0AAAAAAJRPYCIAAADUE8VFRQXboDJOOumkXM75tddei+OOOy6++eabcsfed999ORgxlX5effXV4/jjj/dlAwAAAADUsQYbmDh16tRc4ufcc8+N3XffPdq2bZtP1afWv3//Cr1j9uzZ8dBDD8XJJ58c22yzTayxxhqx6qqr5muPHj3i/PPPj48++qha1rvhhhuWrm9ZLY0DAAAAWJmtt9568cc//jFnTRwyZEhstNFG8dOf/rT080GDBsVPfvKT2HjjjeOggw6Kzz77LO+LpAyLrVq1qvH1TZw4MU4//fTo0qVLtGjRIu8Fde/ePS6//PKYOXNmtc1z5513xq677hrrrrtuNG3aNO/7HHHEEfH8888v87k5c+bEAw88EL/61a/iBz/4QWyyySal+1ZrrrlmbLfddnlPbNKkSdW2VgAAAACAshpFA7X22muv0POvvvpqbL/99vH1118v8dm0adPyBnFqAwcOjJtuuilvkgMAAABQMQMGDMjBhj/72c9i8uTJccMNN+Sfk6uuuipfU+Bi0qRJk7j++uvjwAMPrPGvd9iwYXHYYYfFl19+WXovBSOOGTMmt7QP9Nhjj+VgyqpKh2HT75IO1Zb1wQcf5Hb77bfnA7HnnHPOUp//8MMPY7/99lvqZ59//nk899xzuaV9q2uvvTaOPPLIKq8VAAAAAGBpGmxgYlkdO3bMJ9yffPLJCj/z1VdflQYl9uzZM/bcc8/Yeuut86nzTz75JO6///68EZ3GHHroodGyZcucmXFF7b333nHhhReW+3njxo1XeA4AAABWXoomU58cc8wx0bdv3xyI+PDDD8e77767RGbFvfbaK84444xaqSIxduzYfPg0BSKuttpqOSPhTjvtFLNmzcrZDf/yl7/EuHHjYo899shBimlMVYMyS4IS0/tPOeWUaN++fS5tffHFF8f48eNzxsOUSfHYY49d6jvatWuXn+3WrVtssMEGeWzKmJiCPFNw5W233RYzZszIlUPWWmutatm3AgAAAACIhh6YmDZv08Zsail74oQJE6JTp04Vfr64uDhvRJ933nnRtWvXJT5Pm+ZpQ3ffffeN+fPn53LP77zzTunJ/qpq3bp1bL755iv0DgAAAICVRYcOHXKJ5NTSQdGpU6fmvZZ0OLRt27a1upZTTz01ByU2atQoH3Dt0aNH6Wd9+vTJpaXPPPPMePvtt3M2wrT/VFkjR47MGRGTfv365ZLMq6yySv457WOlQMytttoql5NOcx1wwAF5v6islK3x448/LncfKu1XHX/88bkayDfffBO/+c1vBCYCAAAAANWqOBqo3/72tznLYVVLOm+33XZx1113LTUosWx2w5KyOekk+yuvvFLl9QIAAAA0FCnIL7XBgwcvcn/11VePzp07x6abblrrQYkpA+KIESNKMxqWDUoscdppp+WqHEnK8piC/irrsssuy9cUjJjKLJcEJZZIv/ell16a+9OmTYtBgwYt9UDt8g7Hdu/ePXbeeefc//e//x3Tp0+v9FoBAAAAAMrTYAMTa0sqmVMiBScCAAAAsGyjRo3KmQNrozxzRT344IOl/aOPPnqpY1JA4JFHHlkaNFgSyFhRKTjwqaeeyv1ddtklZ4tcmnQQNgVpJvfff39UVYsWLUr7c+bMqfJ7AAAAAAAWJzCxhpXd1E2b0wAAAFBTUoK0Qm1QGe3atcvXxUsU13WwZEkwXyqlXJ5evXqV9kePHl2pOV544YXSvaSy71lc48aNY9ttty19piqZGVNJ7H/84x+lWRhTaWwAAAAAgOoiUq6GpdP9JTbbbLMVft8zzzwT3/3ud/MmePPmzaNTp05x8MEH51P7CxcuXOH3AwAAANS1LbbYIl//85//RKF466238jWVkm7UqFG548ru/5Q8U9k5Fn/PsuaZN29evPPOOxV6fwp6fP/99+Mvf/lLbLfddjmrY3LKKadUap0AAAAAAMtT/i4qK2zs2LExbNiw3P/2t78dXbt2XeF3ps3jsiZMmJDb3XffHT179oy77ror1ltvvUq/d9KkScv8/KOPPqr0OwEAAACq4thjj42//e1vcf311+cDmXVt9uzZ8emnn+Z+eeWVS7Rp0yYfKJ0xY0Z8+OGHlZqn7PjlzdOxY8dFnitv3ymVk95pp53Kfc9hhx0WZ5xxRlSWvSQAAAAAYFkEJtaQdAI9baLPnz8//3zxxRev0PtSiZ699tor+vbtG5tvvnm0atUqvvjii3juuefiuuuuyxvQzz77bOyyyy75Xvq8MspuZi/P5nv8Jooar1aF34KkTbeTCv6LmDbmz3W9BADq0f+2+N8VgNrzv7LJhVc3uQCXRIHbb7/94vDDD49bb701jjnmmPjTn/6Ug/3qytdff13aX2215e+JlAQmTp8+vcbmKft9VHaeZMMNN8yBn7vuumtURWX2kgAAAACAhkdgYg056aST4sUXX8z9o446KgcVrogXXnghWrduvcT93r1757kOOOCAePLJJ3PJn9/+9rcxcODAFZoPAAAAoK4MHTo0dt5553j11VfjlltuiYceeij69esX3/3ud3NGwlVWWWWZzx955JHVnjGx7OHR5WnSpEm+zpo1q8bmKZljefN069YtXnvttdKDtBMnToyHH344/vrXv8bRRx8dF1xwQQwYMKBS6wQAAAAAWB6BiTXgkksuiZtuuin3t9pqq7jmmmtW+J1LC0os0bJly1zK+Vvf+lZ89tlnceONN8bvf//7Cm2Ul1heaaFUyrl79+6VWjMAAABAVfTv33+R7J/Tpk3LgXQVkZ6r7sDEpk2blvbnzp273PEpADBp1qxZjc1TMsfy5kmZFVP1jRJpr2rffffNGSn32GOPXPFj8uTJce6551ZqrfaSAAAAAIBlEZhYzW644YY4++yzc3/TTTeNxx9/vFZKDaXSzYccckgOgkylglK2xu22267Cz3fo0KFG1wcAAEDNK/6/VmgKcU0UvoULFy7z59qUDoVWpmxy2pupaNnnqs5TMkdV5klSRspTTjklLrvsslx946CDDorNNtusws/bSwIAAAAAlkVgYjW644474qc//Wnub7DBBjF8+PBYa621orZ07dq1tJ9OugMAAACsjN5///0oJCmTYdu2bePTTz+NSZMmLXNsyu5YEjTYsWPHSs1TNtgvzbP11ltXKGNhZecpsffee+fAxAULFsT9999fetgWAAAAAGBFCUysJg8//HAuE5Q2ctddd9146qmnav3keF1mDgAAAACoLunAZ6Hp0qVLjBo1Kt59992YN29eNGq09G21t99+e5FnqnrotOx7ljVPWkfnzp2jKsoeqP3ggw+q9A4AAAAAgKVRTakapCDEVO4mbUqvueaa8fe//z2+9a1vRW178803S/vt27ev9fkBAACoW0VFRQXbYGW3/fbb52vKhvjSSy+VO27kyJGl/Z49e1Zqjm7dukXjxo2XeM/i5s6dG88///wSz1RW2YobVSkHDQAAAABQHhkTV9A///nPXPZmzpw5sfrqq8cTTzwR3/72t6O2ffnll3HXXXflfvPmzZdZ6gcAAACg0KS9lUGDBsXjjz+es/fNnz8/H7zcaaed4oQTTsiHQevSPvvsE5dccknuDx48OLbZZpslxqRKGkOHDs391q1b57VXRsuWLWPnnXfO38Hw4cNzOeelVeRIZZe/+uqr3N93332r+BtF3HPPPaX973znO1V+DwBAeQYMGbPEvUH9u/nCAACgAZAxcQW88sorsccee+ST8i1atIjHHnssttpqq0q/p3fv3qUZJCZMmLDE53/7299i1qxZ5T7/9ddf54yNn332Wf55wIAB0aRJk0qvAwAAAKAuvPPOO/mg58knn5z3V954441cqvgf//hHnHPOOblU8dNPP12nfzjdu3ePHXbYIfdTAOVzzz23xJgrrrgi3nrrrdw/5ZRTYtVVV13k8yFDhpTuAZ1//vlLnef000/P11SZ48QTT8wBmmV9+umncdZZZ5UGPx577LFLvOOOO+7Ih1iX5e67744bbrgh91u1ahV77bXXMscDAAAAAFRGg82YOHr06Hj33XcX2dQtke6njeKy+vfvv8jP48ePj1133TW++OKL/POFF16YN3Fff/31cuds165dbpX1+9//Pg477LDYb7/9ctmgVCY6lddJc6dN8Ouuuy4+/PDDPHbTTTctd2MbAACA+i0VTC7EosmFuCYKK1NiCop77733yh2TguzSvshrr7221AyCteXqq6/O5ZnTAdK+ffvG2WefnbMipp/vvPPOuPHGG/O4TTbZJE477bQqzdGnT5845JBD8vsefvjh2GWXXeLUU0/N2SPT73/RRRfFxIkTS/eM2rRps8Q7UsDh8ccfn7M87rjjjnm/KO1bpcO148aNi3vvvTcHgCYpSDL9XmusscYKfTcAAAAAAGU12MDEm266KW655Zalfvbss8/mtqzAxFGjRsXUqVNLf/75z3++3DnPO++8KgcNfv7553nNqZUnbTTffvvtNpIBAACAlcatt96ag+VSgFy3bt1y4N22224bjRo1ildffTX/nAL0UunilJHwyiuvrLO1brnllnHXXXfF4YcfnteTAhMXl4IShw0blssyV9XNN9+c35+CB1OmyMWzRRYXF+dMkqnEdXmmT5+ev9vUypOCGv/0pz/lA7EAAAAAANWpwQYmrkwuv/zyeOqpp3J2xLRRn7I7pmyJzZs3z6flt9lmm/jRj36UT+qnTXwAAACAlcWDDz6Yr126dImRI0dGkyZNSj9LgYrp85RR8dFHH40HHnigTgMTk379+uWAyZRlMAUgTpo0KRo3bpzLTR944IFx0kkn5T2bFdGsWbP87nQANVX1GDt2bN4LWnvttXM56TRHjx49yn3+tttui+HDh+eAxrTWKVOmxCeffJLX2bZt2/jOd74Tu+22Wxx66KFLzbgIAAAAALCiGmxgYtrUXbxcc2WkDIqLZ1GsqhEjRizz86233jo3AAAAWJZ0WK0QD6wV4pooHCnoLv07ksoVlw1KLCtlJkyBiR9++GEu65zKEtelDTbYIAYOHJhbTe4npcDB1CprvfXWi6OOOio3AAAAAIC6UFwnswIAAABARK4MkXzve98r9/so+9lnn33mewMAAAAAKHACEwEAAACoM7Nnz87XZZU/btq06RLjAQAAAAAoXAITAQAAAAAAAAAAgGrTqPpeBQAAANT16cNCPIFYiGsCAAAAAABqjsBEAAAAAOrctddeG+3atauWceeee241rgwAAAAAgMoSmAgAAABAnbvuuuuW+XlRUVGFxiUCEwEAAAAA6pbARAAAAKgviopKg7cKSiGuiYKycOHCantXQf7/AAAAAABAAyMwEQAAAIA68/TTT/v2AQAAAADqGYGJAAAAANSZXr16+fYBAAAAAOoZgYkAAABQT6QCtoVYxLYQ1wQAAABQ3w0YMmap9wf171brawGg4Smu6wUAAAAAAAAAAAAA9YfARAAAAAAAAAAAAKDaKOUMAAAA9amUcwHWTS7AJQEAAAAAADVIxkQAAAAAAAAAAACg2ghMBAAAAAAAAAAAAKqNUs4AAABQTxRHUW6FphDXBAAAAAAA1BwZEwEAAAAAAAAAAIBqIzARAAAAAAAAAAAA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o3rbbbeN3/zmNzF58uQVfvdbb72Vr61atYp11lmn3HHrrrturL766os8AwAAAMCK69KlS76+++67MW/evHLHvf3220s8U1FlyyqXfc+y5mnUqFF07ty53HGzZs2Kfv36xZgx/yvpe8YZZ+Q9KwAAAACAmiQwsZqMHDkypk6dGt9880189tln8a9//SsuuuiivDF8ww03rNC7S07AV6RETskJ+bKn5isincJfVvvoo4+quHoAAABqS1EB/wMru+233z5fUzbEl156aZl7RCV69uxZqTm6deuWD70u/p7FzZ07N55//vklnllc2qfaf//9S9/14x//OC677LJKrQkAAAAAoCoaVekpSm200Uax3377RY8ePUqDAt97772477774t57743Zs2fnTd+ioqI4/vjjq/TNpXI7yWqrrbbcsS1atMjX6dOnV2qOypYWAgAAAGhI9tlnn7jkkktyf/DgwbHNNtssMWbBggUxdOjQ3G/dunXstNNOlZqjZcuWsfPOO8fjjz8ew4cPz4dFl3ZQ9f7774+vvvoq9/fdd9+lvmv+/Plx6KGH5nclRxxxRFx77bWVWg8AAAAAQFXJmLgC0sZvKt/zhz/8IQcnphPqqR188MFx9913x8MPPxyrrrpqHvvzn/88Pv744yrNk4Ibk/JOv5fVpEmT0jI9AAAAAFSP7t27xw477JD7gwYNiueee26JMVdccUW89dZbuX/KKaeU7guVGDJkSD68mtr555+/1HlOP/30fE3lok888cQcYFjWp59+GmeddVZp8OOxxx67xDsWLlwYxx13XD40m6SsiSmYMs0LAAAAAFAbZExcAa1atVrm53vuuWecd9558Zvf/CZmzpyZN61//etfV3qepk2b5udTmZ7lmTNnTr42a9asUnMsr/RzKuWcNuABAAAoXCnmqBDjjgpxTVAVV199dS7PnA6E9u3bN84+++ycFTH9fOedd8aNN96Yx22yySZx2mmnVWmOPn36xCGHHJLflw697rLLLnHqqadG+/bt47XXXouLLrooJk6cmMf+/ve/jzZt2iw1uDEFIiabb755XmdJwGR50jgAAAAAgOoiMLGGpdPp55xzTj6pPnLkyCoFJqYyPikwsSLlmWfMmFHhss9lLa0sEAAAAAD/35Zbbhl33XVXHH744bmUcgr4W1wKShw2bFjez6mqm2++Ob//sccei6effjq3soqLi/N+0wknnLDU5++7777S/uuvvx5bbbXVcudMe1cAAAAAA4aMWeJLGNS/my8GqDSBiTWsXbt20bZt2/jkk09i8uTJVXpHChqcMmVKTJo0qcKZDzt27FiluQAAAAAoX79+/eLVV1/N2RNTAGLar2ncuHF07tw5DjzwwDjppJOiefPmK/QVpkoY6d233357Lv88duzY+OKLL2LttdfO5aTTHD169PDHBAAAtUywDgBAxQlMrAUreuK8a9eu8dJLL8WXX34ZH3/8cayzzjrllltOp+mTLl26rNCcAAAAACzdBhtsEAMHDsytMvr3759bRR166KG5VdaECRMq/QwAAAAAQHUqrta3sYSpU6fGZ599lvvt27ev0je0/fbbl/ZTOejylP2sZ8+e/jQAAAAamKICbgAAAAAAQMMhMLGG3XjjjaUZE3v16lWld+y1115RXPy/P6rBgweXOy6V9knS2PQMAAAAAAAAAAAA1DaBiVWUSuK8/PLLyxzz6KOPxgUXXJD7TZs2jaOPPnqp43r37h1FRUW5La3UTirdfNhhh+X+E088Effee+8SY+655578WXLEEUeUW+4ZAAAAAAAAAAAAalKjaKBGjx4d7777bunPn376aWk/3S/JPliif//+i/ycAgh32mmn6NGjR/Tr1y++973vRbt27XJ2xPfeey8HD6ZWki3x8ssvj/XWW6/K673ooovib3/7W3zyySfxox/9KF588cXYc889SwMgr7jiitxfa6214sILL6zyPAAAAKzECrVuciGuCQAAAAAAqDENNjDxpptuiltuuWWpnz377LO5LSswscRzzz2XW3maN28eV155ZRx//PErtN6OHTvGI488Evvss098/PHHcemll+ZWVsqS+OCDD0aHDh1WaC4AAAAAAAAAAACoqgYbmLiittpqq7j11ltzUGLKXvjRRx/lrIvz5s2LNm3axLe//e3Yeeed49hjj82ZFKvDNttsE6+99lpcffXVOQCxpOxzp06dYu+9945TTz011lxzzWqZCwAAAFZGEydOjD/+8Y8xbNiw3G/SpEl07tw5DjrooPjpT3+aDxBW1VdffRWPPfZYPPXUU/HSSy/ligkzZ86MVq1a5X2AVNkg7QO0bt26Wn8nAAAAAABY2TTYwMRUqnnxcs2V0bJlyzjssMNyW1EjRoyo8Ni2bdvGBRdc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MWNGPmiwX79+RQf7tVROw4yFhZYDAAAAAFBqNZsxccUVV2z3MtdZZ512LxMAAACAtlt33XXD448/Ht58880wb9680KVL481qr7322gLzFGK99dZrdDnNlRPrMWDAgILKAQAAAAAotZoNTGwo3lUeG4rHjh3b6nlWWWWV8NJLL7U43XnnnRduvPHGNH7UUUeFttprr73COeec0+TrXbt2bXMZAAAAACxo6623ToGJMRvis88+GzbbbLNGV9Gjjz6aH99qq60KWo2DBg1KbTtz585Ny/n1r3/d6HTx9aeeemqBeQAAAAAAyknNBibGLpxjw20cYvbE2DVz//79Wz3/YostFtZff/1mp5k/f3545JFH0njv3r3D3nvv3eZ6L7XUUi2WCwAAQG2q+8dQbsqxTlCo2K4Tb0CNrrnmmkYDE7/99ttw3XXX5dtwtt9++4LKiO1HO+64Y7j33nvDgw8+mLpzbti9c87tt98eZs2alcb32WcfGxMAAAAAKDudQo06++yzw+67717SLp1jA/J7772Xxvfff//Qs2fPkpUFAAAAQOkMHjw4DBkyJI2PHDkyjB8/fpFpLr744vDqq6+m8RNPPDHd2NrQqFGjQl1dXRpGjBjRaDmnnHJKeozdRR9//PHpxteGPv744/CrX/0qH/x4zDHHZPQOAQAAAACyU7MZE9tD7g75rLpxBgAAAKDjXHrppal75jlz5oShQ4eG008/PWVFjH/fdNNN4corr0zTrb322uHkk08uqowddtghHHzwwWl5d955Z9hpp53CSSedFPr06RNeeumlcO6554YpU6akac8///yw9NJLN7qc//3f/01DY95///0UJNlQvKm2V69eRdUZAAAAAGBhAhNL5PPPPw+jR49O46uttlrYZpttSlUUAAAAJHV13w3lphzrBMXYZJNNws033xwOP/zw1JVyDExcWAxKHDNmTOqWuVhXX311Wv4999wTHn744TQ01KlTp3DGGWeEY489tsllxHap2GNIY15//fVw9NFHL/DcdtttJzCRmjJ81MRFnhs5bFCH1AUAAACgGtVsV86ldtttt4XZs2en8SOPPDJ10ZOFxx57LGy44YZh8cUXT11D9+/fPxx00EGpsbm+vj6TMgAAAABo3B577BFefPHF8Itf/CIFIcb2mdil8sCBA8MFF1wQnn/++TBgwIA2rb4ePXqk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+8K+++4b+vbtm8518TH+HZ/P0ieffBLOOuussNFGG4Ull1wyLLHEEmk8Phdfq3RZbJevvvoq/PWvfw0///nPw2abbZY+Byy22GLpcYsttkjHwfTp0zOpb7z2t+Z4jdPV8jYZNWpUq89tcdosOFaaN3ny5IKvO23Zj2vhWAGAWm0fo7Zk0dbXEd8PqB5Ztmu2V1sO1SOLNlznQCqtzTp+bvz3f//39Dkyfp6M1/34+TJ+zoyfN6ku5dQ+X8g5l+pRjr9HOA/WlnL67cV5sLS6lHj5VLExY8aEww47LHz22WcLXCwmTpyYhquuuircc889YY011ii6jPiB6oADDkgnpIbeeeedNNx4443pQ9UZZ5wRatm2224bHnvssUWenzt3bpg0aVIarr322hTMGbdL165dO6SelI5jpXQ6deoU1lprrRKWUP1WXHHFTJZTX18ffvrTn4Yrr7xygefffffdcMcdd6ThJz/5Sbjiiiva/AU5Xsf22muvRb60v/jii2mI59L4hX/gwIGhVrdLXA9bb711+Pzzzxd5bcaMGenmgThccsklaX0deOCBbSqvFmR1rLQnx0pprLPOOiVaMgBQie1j1B5tfVSL9mzLobZpw6XS26zfeuutsNtuu4XXX399gedfe+21NMTPlPE30V133TWTetPxtM/T0crt9wjnwdrTEfug3146hsBEivLCCy+kAIPY0Brv2DnttNPC9ttvH+bMmZMytv3pT39KH57jh+jYCBunKcbw4cPzQYlx+SeeeGLo06dPeOmll9Idw/ECFSOoYybFY445pma3ZvyCE8V1EwM5hwwZElZdddUwf/78MH78+HDxxRenaa6//vowb9689OUlC3E7NKd///6ZlFMNfvazn4XjjjuuyddjRoW2cKy03jXXXBO+/PLLZqd55ZVXwkEHHZTGd9xxx7DKKqu0afs4Vv5PzPIQ7/IcO3ZswevxN7/5Tb6BZ5NNNgmnnnpqWHPNNdO14MILLwzPP/98en355ZcP55xzTtHbK54v99hjj/DBBx+ELl26hF/+8pdh9913T6/Fa1IMtHvvvffSc88++2yb949K3S6zZs3KByVutdVWaX3EQM1ll102fPTRR+H2229PDWZxmkMPPTT07t073fHUVjFgtLntWy3B9205VnLuv//+9NmgKfHO7bZwrLROPEe0dB2IzjvvvPxntKOO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jTcWFEtJrrXOg5TTby/Og6XR9O280ES605xevXq1uI7iB+n4BTF+gClVObnG3ajQcmrF7373u9TlbxQbi2LK97bYYIMNwt577x0GDx4c+vTpkz5gvP766+HPf/5zGDt2bJg5c2bYb7/9wl133RV++MMfhloWs3ntueeeYccdd0x3IsX9ORfEecUVV6QPZrH7xnjH0QMPPBAWW2yxgpbvWCnv4BDHSjbacz/PldXaa1yx5dSC2HBxzDHHhPnz56e/f/vb37ZpefEuu3g+HTp0aFh//fVTVst4vRk/fny4/PLL05fXJ554ImUojc/F12tVvEFg3333DVtssUW+wfLvf/97ariPd4jFH5N++tOfpob7n/zkJ0WV4VjJxpQpU/J3LW+55ZapkaatHCsAUD3tY5BFW197fD+gOrWlXVObJeXShuscSKlkdZ4rpo0tt5xu3boVVGdqSyHt8506dUrX+1133TVstNFGqbe8uG8+99xzKaAsZrSLNypsv/326TNozFYLWV1rnQcph99enAdLrB4KMGXKlPq428ThiCOOaHH6fv36pWnXXHPNgtbzj370o3w5b731VrPTjhw5Mj/t9ddfX1A5teCRRx6p79KlS1o/K6ywQv3777/fpuXNmDGj2devuOKK/Pbo06dP/ezZs+trWXPrK26LTTbZJL++Lr300oKX71jJztSpU+s7deqUtsXmm2/e5uU5Vpr29ttv5/f7o446qsV1+a//+q/56R966KFmp42v56b9t3/7t4K3W24fGDJkSIvTxmnitJ07d66vxe3SkmOOOSbT5TV3TM2aNat+6NCh+fJ+8Ytf1NfqNpk5c2b9t99+2+Trd911V/1iiy2WltmzZ8/66dOnF1U3x0o2+/a5556b38bxM1QWavFYAYBqbR+Dtrb1tdf3A6pTW9o127Mth9pSSBuucyCV0Ga9xhprpOfj58WWxM+dueXEY4Hq0pHt881d8+fOnZvmzy1rn332aXPdKE8d9XuE8yBt2Qez+u3FebC0dOVMQbp3754fnzt3bovT51KJ9+jRo2TlNExXXmg51e7ll19Od03Pmzcv3TkVu7pcccUV27TMllLJH3vssekOnOi9994Lt99+e6hlza2vuC3inSIxs1H0n//5nwUv37GSnRtuuCF1hRTFtPRt5VjJTnvu57mySnmNqwXnnXdeuOqqq9L4pptu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xkQAKELMmJfrQmPmzJmhW7duLc4zevToFHTXu3fvkqzzXHch/fv3D3feeWer54vTN6Zz586hPVx99dVh5MiRaXzjjTcOJ510Uthss83CKquskgIec/U48sgjw/XXX58CGUstZkI8++yzU1l/+ctfwmmnnZaenzFjRrj33nvT+IEHHhgWW2yxJrfDVlttFa644opWl5nrqhoAAAAA6Djl1var3Tc72n0BANqXwEQAKMItt9xScOa+mBHvtttuC0cffXRJ1vmyyy6bHj/44IPwve99r6DunDvSn/70p/S45pprpgyFPXr0aHS6GBTYXmJXzrFrjwkTJqQMibnAxLj9YrfLTXXjnNsOcRt89NFHYf3112+3OgMAAAAA1df2q903O9p9AQDaV2VELABAmcl1zbHyyiuHSy65pMXpf/WrX4UpU6ak+QptnKqrq2vVdJtsskm45557UiPYE088EbbddttQCV5++eX0uNdeezUZlBgzFz733HPtWq8YeBgDE2P9XnzxxbDhhhuGG2+8Mb22+uqrhy222KLJ7fDKK6+EN954I7zzzjthtdVWa9d6AwAAAADl3/ar3Ve7LwBAtROYCAAFevvtt8O4cePS+H777RcOPvjgFud55plnwsUXXxweffTR1Ei16qqrtrq87t27p8evv/662eliYN+5556bxi+88MKKCUycN29eeowBlU2JXVO/9957La6nr776qsX11Fpxu/7yl79MXaXErInLLLNMeOyxx/JBi001HO65555p+tx2uOyyyzKpDwAAAABQPW2/2n21+wIAVLtOHV0BAKg0119/fcrgF+2///6tmic3XZwvzl+IeGdu9OGHH4bPP/+8yekGDRoUhg4dmsZj5sSzzjqr2eVOnjw5/OUvfwkdLXafEd11112Ndtf81ltvheOOO67V6ylOn4UVVlgh/NM//VMaj+spZkv89ttv09+HHnpok/PFBst11103jV9++eVh5MiRzZbzt7/9Lb13AAAAAKB22n61+2r3BQCodgITAaBAucalGLg2ZMiQVs2z2Wabhb59+y4wf2ttueWW6TEGxf30pz8NTz31VJg0aVJ4880309DQNddck2/Q+td//dew+eabhyuvvDKMHz8+PP/88+HBBx9M3Y/EAMYBAwaE//7v/w4d7cgjj0yP7777bnqv8T3ELpRjdsIRI0aETTfdNHz66afhBz/4QavWU8yu+Mc//jEF/OXWUQzqLMbhhx+eHqdOnRrOO++8fFfN6623XpPzdO7cOdx8882hV69eqTHymGOOCbvsskvqyuXpp59OXVLfd999aXlbbbVV2GCDDdLd1AAAAABA7bT9avfV7gsAUO105QwABXjiiSfywYD77LNP6NSpdTH+sdvffffdN/zHf/xHeP3111OAWmywao0ddtghBRjGgMSYtS8ODeXu4I369OmTghAPOOCAMHHixFROHJqyxBJLhI524oknhgceeCCMHTs2vPbaa+FHP/rRAq/36NEjBfWNGTMmBfU15ZRTTgm33XZb6so5BnA2dNRRR4VRo0YVXLe999479OzZM3UzPXPmzHw3zi2JwYZxX4l3S8cg0vvvvz8N5bwdAAAAAKCWtXfbr3Zf7b4AANVOxkQAKEAMkGvYZW8hGk7fcDktiQ1gMWjvN7/5Tdhoo41SJr7Y2NWU1VZbLTV+3XHHHeHggw8O/fv3T8F1iy22WFh++eXTnbgnn3xyytLXUjfD7SHWKwYdxoa7gQMHprrGYMSY0TEGGMZgxBho2ZKNN944BWUecsghYdVVVw3dunVrc93iut5rr70W2BZxnbbGhhtuGF555ZVw7bXXpgDHfv36he7du4euXbumrJbbbbdd2qbPPvtsOPPMM9tcVwAAAACgctp+tftq9wUAqHZ19Q3TLAEAUDFiAGbsZnr48OHhqquu6ujqAAAAAAAAAEAiYyIAQIWaNWtWelxyySU7uioAAAAAAAAAkCcwEQCgAk2ePDl89tlnaXzttdfu6OoAAAAAAAAAQF6X/xsFAKDcvfTSS2HKlCnhnHPOSX/X1dWFnXfeuaOrBQAAAAAAAAB5AhMBACrIhhtuuMDfJ5xwQlh99dU7rD4AAAAAAAAAsDCBiQAAFaZHjx5hnXXWCUcffXQKTAQAAAAAAACAciIwEQCggtTX13d0FQAAAAAAAACgWZ2afxkAAAAAAAAAAACg9QQmAgAAAAAAAAAAAJkRmAgAAAAAAAAAAABkRmAiAAAAAAAAAAAAkBmBiQAAAAAAAAAAAEBmBCYCAAAAAAAAAAAAmRGYCAAAAAAAAAAAAGRGYCIAAAAAAAAAAACQGYGJAAAAAAAAAAAAQGYEJgIAAAAAAAAAAACZEZgIAAAAAAAAAAAAZEZgIgAAAAAAAAAAAJAZgYkAAAAAAAAAAABAZgQmAgAAAAAAAAAAAJkRmAgAAAAAAAAAAABkRmAiAAAAAAAAAAAAkBmBiQAAAAAAAAAAAEBmBCYCAAAAAAAAAAAAmRGYCAAAAAAAAAAAAISs/H8ozAG3SenHkAAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": { - "image/png": { - "height": 491, - "width": 1299 - } - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "\n", diff --git a/locpick/__init__.pyi b/locpick/__init__.pyi index 0cfbf33..e774b6e 100644 --- a/locpick/__init__.pyi +++ b/locpick/__init__.pyi @@ -101,9 +101,6 @@ from .dgp import ( from .dgp import ( simulate_scl as simulate_scl, ) -from .models import ( - SARMNL as SARMNL, -) from .models import ( ChoiceModel as ChoiceModel, ) diff --git a/locpick/_jax/sar_kernels.py b/locpick/_jax/sar_kernels.py index 4e343b9..7b1c045 100644 --- a/locpick/_jax/sar_kernels.py +++ b/locpick/_jax/sar_kernels.py @@ -27,9 +27,10 @@ compute_ll_contribs, compute_utilities, mnl_log_probs, + nested_log_probs, ) from .objective import Objective -from .transforms import ParamTransform +from .transforms import Identity, ParamTransform, Sigmoid, SoftPlus, Tanh # Threshold for switching from dense solve to conjugate gradient. _DENSE_CUTOFF = 2000 @@ -307,3 +308,468 @@ def _grad_jax(params): param_names=param_names, transform=transform, ) + + +# --------------------------------------------------------------------------- +# SAR + Nested +# --------------------------------------------------------------------------- + + +def _sar_nested_ll_core( + params, + design_matrix, + available, + chosen, + weights, + inclusion_probs, + W_dense, + nest_matrix, + n_obs, + n_alts, + n_nests, +): + """SAR + Nested PML log-likelihood. + + Layout: [beta_1..k, alpha_rho, alpha_lambda_1..M] + Spatial filter applied to utilities, then nested GEV. + """ + k = design_matrix.shape[1] + beta = params[:k] + alpha_rho = params[k] + alpha_lambdas = params[k + 1 : k + 1 + n_nests] + rho = jnp.tanh(alpha_rho) + lambdas = 1.0 / (1.0 + jnp.exp(-alpha_lambdas)) # sigmoid → (0, 1] + + V_base = compute_utilities( + design_matrix, + beta, + n_obs, + n_alts, + inclusion_probs=inclusion_probs, + available=available, + ) + + A = jnp.eye(n_alts) - rho * W_dense + V_filtered = jax.scipy.linalg.solve(A, V_base.T).T + D = jnp.diag(jax.scipy.linalg.inv(A)) + V_star = V_filtered / D[None, :] + + log_probs = nested_log_probs(V_star, lambdas, nest_matrix, available) + return compute_ll(log_probs, chosen, weights) + + +def build_sar_nested_objective(arrays, W_sparse, nest_matrix) -> Objective: + """Build Objective for SAR + Nested estimation.""" + data = ChoiceDataJAX.from_arrays(arrays) + W_dense = jnp.array(W_sparse.toarray(), dtype=jnp.float64) + n_obs = arrays.n_obs + n_alts = arrays.n_alts + k = arrays.design_matrix.shape[1] + n_nests = nest_matrix.shape[1] + nest_matrix_jax = jnp.array(nest_matrix, dtype=jnp.float64) + + @jax.jit + def _ll_jax(params): + return _sar_nested_ll_core( + params, + data.design_matrix, + data.available, + data.chosen, + data.weights, + data.inclusion_probs, + W_dense, + nest_matrix_jax, + n_obs, + n_alts, + n_nests, + ) + + @jax.jit + def _grad_jax(params): + return jax.grad(_sar_nested_ll_core, argnums=0)( + params, + data.design_matrix, + data.available, + data.chosen, + data.weights, + data.inclusion_probs, + W_dense, + nest_matrix_jax, + n_obs, + n_alts, + n_nests, + ) + + param_names = list(arrays.param_names) + ["rho"] + param_names += [f"lambda_{i}" for i in range(n_nests)] + # Transform: Identity for beta, Tanh for rho, Sigmoid for lambdas + transforms = [Identity()] * k + [Tanh()] + [Sigmoid(0, 1)] * n_nests + transform = ParamTransform(transforms) + + return Objective.from_jax( + ll_fn=_ll_jax, + grad_fn=_grad_jax, + param_names=param_names, + transform=transform, + ) + + +# --------------------------------------------------------------------------- +# SAR + Mixed +# --------------------------------------------------------------------------- + + +def _sar_mixed_ll_core( + params, + dm_fixed, + dm_random, + available, + chosen, + weights, + inclusion_probs, + W_dense, + dist_codes, + draws, + n_obs, + n_alts, + k_fixed, + k_random, + n_draws, +): + """SAR + Mixed simulated PML log-likelihood. + + Layout: [beta_fixed, alpha_rho, mean_*, sd_*] + Spatial filter applied to fixed utility; random part added after. + """ + beta_fixed = params[:k_fixed] + alpha_rho = params[k_fixed] + beta_random_means = params[k_fixed + 1 : k_fixed + 1 + k_random] + beta_random_spreads_raw = params[k_fixed + 1 + k_random :] + rho = jnp.tanh(alpha_rho) + spreads = jnp.log1p(jnp.exp(beta_random_spreads_raw)) # softplus + + A = jnp.eye(n_alts) - rho * W_dense + A_inv = jax.scipy.linalg.inv(A) + D = jnp.diag(A_inv) + + # Fixed utility (spatially filtered + normalised) + if dm_fixed is not None and k_fixed > 0: + V_fixed_base = (dm_fixed @ beta_fixed).reshape(n_obs, n_alts) + else: + V_fixed_base = jnp.zeros((n_obs, n_alts), dtype=jnp.float64) + + if inclusion_probs is not None: + V_fixed_base = V_fixed_base + jnp.log(jnp.maximum(inclusion_probs, 1e-30)) + + V_fixed_filtered = jax.scipy.linalg.solve(A, V_fixed_base.T).T + V_fixed_star = V_fixed_filtered / D[None, :] + + # Simulated likelihood: average over draws + means = beta_random_means[None, :] + + def _prob_single_draw(r): + z_r = draws[:, r, :] + beta_normal = means + spreads * z_r + beta_lognormal = jnp.exp(jnp.clip(means + spreads * z_r, -50, 50)) + t = 1.0 / (1.0 + 0.2316419 * jnp.abs(z_r)) + d = 0.3989422804014327 + poly = t * ( + 0.319381530 + + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))) + ) + phi_z = jnp.where( + z_r >= 0, + 1.0 - d * jnp.exp(-0.5 * z_r * z_r) * poly, + d * jnp.exp(-0.5 * z_r * z_r) * poly, + ) + beta_uniform = means + spreads * (2.0 * phi_z - 1.0) + abs_z = jnp.abs(z_r) + tri_sign = jnp.where(z_r >= 0, 1.0, -1.0) + beta_triangular = means + spreads * tri_sign * (jnp.sqrt(2.0 * abs_z) - 1.0) + + beta_r = jnp.where( + dist_codes == 0, + beta_normal, + jnp.where( + dist_codes == 1, + beta_lognormal, + jnp.where(dist_codes == 2, beta_triangular, beta_uniform), + ), + ) + + V_random = (dm_random @ beta_r).reshape(n_obs, n_alts) + V_total = V_fixed_star + V_random + V_masked = jnp.where(available > 0, V_total, -1e30) + log_sum_exp = jax.scipy.special.logsumexp(V_masked, axis=1) + log_probs = V_masked - log_sum_exp[:, None] + log_probs = jnp.where(available > 0, log_probs, -1e30) + # Per-obs probability for this draw + return jnp.exp((log_probs * chosen).sum(axis=1)) + + probs_sim = jax.vmap(_prob_single_draw)(jnp.arange(n_draws)).mean(axis=0) + ll = jnp.sum(jnp.log(jnp.maximum(probs_sim, 1e-30)) * weights) + return ll + + +def build_sar_mixed_objective( + arrays, W_sparse, random_col_indices, random_distributions, draws +) -> Objective: + """Build Objective for SAR + Mixed estimation.""" + data = ChoiceDataJAX.from_arrays( + arrays, + draws=draws, + random_col_indices=random_col_indices, + random_distributions=random_distributions, + ) + W_dense = jnp.array(W_sparse.toarray(), dtype=jnp.float64) + n_obs = arrays.n_obs + n_alts = arrays.n_alts + k_fixed = len(data.fixed_col_indices) if data.fixed_col_indices else 0 + k_random = len(random_col_indices) + n_draws = draws.shape[1] + + @jax.jit + def _ll_jax(params): + return _sar_mixed_ll_core( + params, + data.dm_fixed, + data.dm_random, + data.available, + data.chosen, + data.weights, + data.inclusion_probs, + W_dense, + data.dist_codes, + data.draws, + n_obs, + n_alts, + k_fixed, + k_random, + n_draws, + ) + + @jax.jit + def _grad_jax(params): + return jax.grad(_sar_mixed_ll_core, argnums=0)( + params, + data.dm_fixed, + data.dm_random, + data.available, + data.chosen, + data.weights, + data.inclusion_probs, + W_dense, + data.dist_codes, + data.draws, + n_obs, + n_alts, + k_fixed, + k_random, + n_draws, + ) + + param_names_list = list(arrays.param_names) + fixed_names = [name for i, name in enumerate(param_names_list) if i not in random_col_indices] + random_names = [param_names_list[i] for i in random_col_indices] + display_names = ( + fixed_names + + ["rho"] + + [f"mean_{n}" for n in random_names] + + [f"sd_{n}" for n in random_names] + ) + + transforms = ( + [Identity()] * k_fixed + [Tanh()] + [Identity()] * k_random + [SoftPlus()] * k_random + ) + transform = ParamTransform(transforms) + + return Objective.from_jax( + ll_fn=_ll_jax, + grad_fn=_grad_jax, + param_names=display_names, + transform=transform, + ) + + +# --------------------------------------------------------------------------- +# SAR + Mixed + Nested +# --------------------------------------------------------------------------- + + +def _sar_mixed_nested_ll_core( + params, + dm_fixed, + dm_random, + available, + chosen, + weights, + inclusion_probs, + W_dense, + nest_matrix, + dist_codes, + draws, + n_obs, + n_alts, + k_fixed, + k_random, + n_draws, + n_nests, +): + """SAR + Mixed + Nested simulated PML log-likelihood. + + Layout: [beta_fixed, alpha_rho, alpha_lambda_1..M, mean_*, sd_*] + """ + beta_fixed = params[:k_fixed] + alpha_rho = params[k_fixed] + alpha_lambdas = params[k_fixed + 1 : k_fixed + 1 + n_nests] + beta_random_means = params[k_fixed + 1 + n_nests : k_fixed + 1 + n_nests + k_random] + beta_random_spreads_raw = params[k_fixed + 1 + n_nests + k_random :] + rho = jnp.tanh(alpha_rho) + lambdas = 1.0 / (1.0 + jnp.exp(-alpha_lambdas)) + spreads = jnp.log1p(jnp.exp(beta_random_spreads_raw)) + + A = jnp.eye(n_alts) - rho * W_dense + A_inv = jax.scipy.linalg.inv(A) + D = jnp.diag(A_inv) + + if dm_fixed is not None and k_fixed > 0: + V_fixed_base = (dm_fixed @ beta_fixed).reshape(n_obs, n_alts) + else: + V_fixed_base = jnp.zeros((n_obs, n_alts), dtype=jnp.float64) + if inclusion_probs is not None: + V_fixed_base = V_fixed_base + jnp.log(jnp.maximum(inclusion_probs, 1e-30)) + + V_fixed_filtered = jax.scipy.linalg.solve(A, V_fixed_base.T).T + V_fixed_star = V_fixed_filtered / D[None, :] + + means = beta_random_means[None, :] + + def _prob_single_draw(r): + z_r = draws[:, r, :] + beta_normal = means + spreads * z_r + beta_lognormal = jnp.exp(jnp.clip(means + spreads * z_r, -50, 50)) + t = 1.0 / (1.0 + 0.2316419 * jnp.abs(z_r)) + d = 0.3989422804014327 + poly = t * ( + 0.319381530 + + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))) + ) + phi_z = jnp.where( + z_r >= 0, + 1.0 - d * jnp.exp(-0.5 * z_r * z_r) * poly, + d * jnp.exp(-0.5 * z_r * z_r) * poly, + ) + beta_uniform = means + spreads * (2.0 * phi_z - 1.0) + abs_z = jnp.abs(z_r) + tri_sign = jnp.where(z_r >= 0, 1.0, -1.0) + beta_triangular = means + spreads * tri_sign * (jnp.sqrt(2.0 * abs_z) - 1.0) + + beta_r = jnp.where( + dist_codes == 0, + beta_normal, + jnp.where( + dist_codes == 1, + beta_lognormal, + jnp.where(dist_codes == 2, beta_triangular, beta_uniform), + ), + ) + + V_random = (dm_random @ beta_r).reshape(n_obs, n_alts) + V_total = V_fixed_star + V_random + log_probs = nested_log_probs(V_total, lambdas, nest_matrix, available) + return jnp.exp((log_probs * chosen).sum(axis=1)) + + probs_sim = jax.vmap(_prob_single_draw)(jnp.arange(n_draws)).mean(axis=0) + ll = jnp.sum(jnp.log(jnp.maximum(probs_sim, 1e-30)) * weights) + return ll + + +def build_sar_mixed_nested_objective( + arrays, W_sparse, nest_matrix, random_col_indices, random_distributions, draws +) -> Objective: + """Build Objective for SAR + Mixed + Nested estimation.""" + data = ChoiceDataJAX.from_arrays( + arrays, + draws=draws, + random_col_indices=random_col_indices, + random_distributions=random_distributions, + ) + W_dense = jnp.array(W_sparse.toarray(), dtype=jnp.float64) + n_obs = arrays.n_obs + n_alts = arrays.n_alts + k_fixed = len(data.fixed_col_indices) if data.fixed_col_indices else 0 + k_random = len(random_col_indices) + n_draws = draws.shape[1] + n_nests = nest_matrix.shape[1] + nest_matrix_jax = jnp.array(nest_matrix, dtype=jnp.float64) + + @jax.jit + def _ll_jax(params): + return _sar_mixed_nested_ll_core( + params, + data.dm_fixed, + data.dm_random, + data.available, + data.chosen, + data.weights, + data.inclusion_probs, + W_dense, + nest_matrix_jax, + data.dist_codes, + data.draws, + n_obs, + n_alts, + k_fixed, + k_random, + n_draws, + n_nests, + ) + + @jax.jit + def _grad_jax(params): + return jax.grad(_sar_mixed_nested_ll_core, argnums=0)( + params, + data.dm_fixed, + data.dm_random, + data.available, + data.chosen, + data.weights, + data.inclusion_probs, + W_dense, + nest_matrix_jax, + data.dist_codes, + data.draws, + n_obs, + n_alts, + k_fixed, + k_random, + n_draws, + n_nests, + ) + + param_names_list = list(arrays.param_names) + fixed_names = [name for i, name in enumerate(param_names_list) if i not in random_col_indices] + random_names = [param_names_list[i] for i in random_col_indices] + display_names = ( + fixed_names + + ["rho"] + + [f"lambda_{i}" for i in range(n_nests)] + + [f"mean_{n}" for n in random_names] + + [f"sd_{n}" for n in random_names] + ) + + transforms = ( + [Identity()] * k_fixed + + [Tanh()] + + [Sigmoid(0, 1)] * n_nests + + [Identity()] * k_random + + [SoftPlus()] * k_random + ) + transform = ParamTransform(transforms) + + return Objective.from_jax( + ll_fn=_ll_jax, + grad_fn=_grad_jax, + param_names=display_names, + transform=transform, + ) diff --git a/locpick/models/__init__.pyi b/locpick/models/__init__.pyi index 42cf136..a0e0598 100644 --- a/locpick/models/__init__.pyi +++ b/locpick/models/__init__.pyi @@ -45,9 +45,6 @@ from .nested import ( from .nested import ( naturalize_nest_params as naturalize_nest_params, ) -from .sar_mnl import ( - SARMNL as SARMNL, -) from .scl import ( EdgeStructure as EdgeStructure, ) diff --git a/locpick/models/choice_model.py b/locpick/models/choice_model.py index 3414cfb..315b265 100644 --- a/locpick/models/choice_model.py +++ b/locpick/models/choice_model.py @@ -94,6 +94,12 @@ class ChoiceModel(BaseChoiceModel, SpatialMixin): Additional options passed to the solver constructor. backend : str, optional Computation backend hint. + estimator : str, optional + SAR estimation method (only relevant when ``lag=True``). + ``"auto"`` (default) selects ``"pml"`` (dense solve) for + n_alts ≤ 2000 and ``"pml_cg"`` (conjugate gradient) for larger + alternative sets. ``"linearized_gmm"`` uses the two-step GMM + estimator (Carrión-Flores et al. 2018) for very large J. Examples -------- @@ -113,6 +119,7 @@ def __init__( nests: Optional[NestingTree] = None, random_params: Optional[dict[str, ParamDistribution]] = None, graph=None, + lag: bool = False, n_draws: Optional[int] = None, draw_type: Optional[str] = None, seed: int = 42, @@ -121,6 +128,7 @@ def __init__( solver: Union[str, Solver] = "lbfgs", solver_options: Optional[dict] = None, backend: Optional[str] = None, + estimator: str = "auto", ): # Handle the legacy `problem` parameter by wrapping it as EstimationProblem if problem is not None: @@ -141,6 +149,8 @@ def __init__( self._nests = nests self._random_params = random_params self._graph_input = graph + self._lag = lag # True = SAR spatial lag, False = SCL (default) + self._estimator = estimator # SAR estimator: auto, pml, pml_cg, linearized_gmm # Mixed logit settings self._n_draws = n_draws if n_draws is not None else 100 @@ -160,6 +170,8 @@ def __init__( self._edge_structs = None self._edge_data_list = None + # SAR spatial state (None when lag=True is not used) + self._W_sparse = None # CSR sparse for SAR kernels # Random parameter state (built in _pre_fit) self._random_col_indices: Optional[list[int]] = None self._random_distributions: Optional[list[str]] = None @@ -177,6 +189,16 @@ def __init__( def _is_spatial(self) -> bool: return self._graph_input is not None + @property + def _is_spatial_lag(self) -> bool: + """True when SAR (lag=True) spatial model is active.""" + return self._is_spatial and self._lag + + @property + def _is_spatial_scl(self) -> bool: + """True when SCL (lag=False) spatial model is active.""" + return self._is_spatial and not self._lag + @property def _is_nested(self) -> bool: return self._nests is not None @@ -193,12 +215,16 @@ def model_type(self) -> str: parts.append("Mixed") if self._is_nested: parts.append("Nested") - if self._is_spatial: + if self._is_spatial_lag: + parts.append("Spatial Autoregressive") + elif self._is_spatial_scl: parts.append("Spatially Correlated") if not parts: return "Multinomial Logit" if len(parts) == 1 and parts[0] == "Spatially Correlated": return "Spatially Correlated Logit" + if len(parts) == 1 and parts[0] == "Spatial Autoregressive": + return "Spatial Autoregressive Logit" if len(parts) == 1 and parts[0] == "Nested": return "Nested Logit" if len(parts) == 1 and parts[0] == "Mixed": @@ -209,6 +235,61 @@ def model_type(self) -> str: # Pre-fit: build model-specific data structures # ------------------------------------------------------------------ + def fit(self, **kwargs) -> FitResult: + """Estimate the model and return results. + + Dispatches to the PML estimator (JAX autodiff) or the + linearized GMM estimator based on the ``estimator`` setting. + """ + if self._is_spatial_lag and self._estimator == "linearized_gmm": + return self._fit_linearized_gmm() + return super().fit(**kwargs) + + def _fit_linearized_gmm(self) -> FitResult: + """Two-step linearized GMM estimation (Carrión-Flores et al. 2018).""" + arrays = self._get_arrays() + self._arrays = arrays + self._pre_fit(arrays) + + from .._kernels.sar_mnl_numpy import fit_linearized_gmm + + result_dict = fit_linearized_gmm(arrays, self._W_sparse) + + beta = result_dict["beta"] + rho = result_dict["rho"] + se = result_dict["se"] + ll = result_dict["log_likelihood"] + + utility_param_names = list(arrays.param_names) + k = len(utility_param_names) + display_values = np.concatenate([beta, [rho]]) + display_names = utility_param_names + ["rho"] + model_type = "Spatial Autoregressive Logit (Linearized GMM)" + n_params = len(display_values) + + std_errors = se[: k + 1] + + coefficients = pd.Series(display_values, index=display_names, name="coefficient") + std_err_series = pd.Series(std_errors, index=display_names, name="std_error") + ll_null = _compute_null_ll(arrays) + + stats = _compute_fit_statistics( + ll=ll, + ll_null=ll_null, + n_obs=arrays.n_obs, + n_params=n_params, + n_alts=arrays.n_alts, + coefficients=coefficients, + std_errors=std_err_series, + model_type=model_type, + solver_name="linearized_gmm", + solver_result_raw=result_dict, + ) + + self._result = FitResult(spec=self._spec, **stats) + self._clear_caches() + return self._result + def _pre_fit(self, arrays: ChoiceArrays) -> None: """Build nest matrix, random parameter structure, and spatial graph.""" # Build nest matrix if nests are provided @@ -222,12 +303,21 @@ def _pre_fit(self, arrays: ChoiceArrays) -> None: # Resolve spatial graph if graph is provided if self._is_spatial: - self._resolve_spatial_graph() - self._validate_graph_size(arrays) + if self._is_spatial_lag: + # SAR: resolve W via _spatial_weights resolver + from ._spatial_weights import resolve_spatial_weights - # Build per-nest edge structures for nested spatial models - if self._is_nested: - self._build_per_nest_edges(arrays) + self._W_sparse = resolve_spatial_weights( + self._graph_input, arrays.n_alts, row_standardize=True + )[1] # get the CSR sparse + else: + # SCL: resolve via edge structure (existing behavior) + self._resolve_spatial_graph() + self._validate_graph_size(arrays) + + # Build per-nest edge structures for nested spatial models + if self._is_nested: + self._build_per_nest_edges(arrays) def _prepare_random_params(self, arrays: ChoiceArrays) -> None: """Identify random parameter columns and generate draws.""" @@ -324,7 +414,7 @@ def _get_solver_inputs(self, arrays: ChoiceArrays): fixed_mask = None names_base = param_names_all if self._is_spatial: - # SCL: [beta, alpha_rho] + # SCL or SAR: [beta, alpha_rho] x0 = np.concatenate([x0_base, np.zeros(1)]) names = list(names_base) + ["alpha_rho"] else: @@ -335,7 +425,15 @@ def _get_solver_inputs(self, arrays: ChoiceArrays): # --- Nested (no random) --- if self._is_nested and not self._is_mixed: n_nests = self._nests.n_nests - if self._is_spatial: + if self._is_spatial_lag: + # SAR + Nested: [beta, alpha_rho, alpha_lambda_1..M] + x0 = np.concatenate([np.zeros(k), np.zeros(1), self._nests.initial_alphas()]) + names = ( + param_names_all + + ["alpha_rho"] + + [f"alpha_lambda_{name}" for name in self._nests.nest_names] + ) + elif self._is_spatial_scl: # Nested SCL: [beta, alpha_rho_1..M, alpha_lambda_1..M] x0 = np.concatenate( [ @@ -360,7 +458,7 @@ def _get_solver_inputs(self, arrays: ChoiceArrays): k_fixed = self._k_fixed k_random = self._k_random if self._is_spatial: - # MSCL: [beta_fixed, alpha_rho, mean_*, sd_*] + # SAR + Mixed or MSCL: [beta_fixed, alpha_rho, mean_*, sd_*] x0 = np.concatenate( [ np.zeros(k_fixed), @@ -395,7 +493,25 @@ def _get_solver_inputs(self, arrays: ChoiceArrays): fixed_param_names = [ name for name in param_names_all if name not in self._random_params ] - if self._is_spatial: + if self._is_spatial_lag: + # SAR + Mixed + Nested: [beta_fixed, alpha_rho, alpha_lambda_1..M, mean_*, sd_*] + x0 = np.concatenate( + [ + np.zeros(k_fixed), + np.zeros(1), + self._nests.initial_alphas(), + np.zeros(k_random), + np.full(k_random, 0.1), + ] + ) + names = ( + fixed_param_names + + ["alpha_rho"] + + [f"alpha_lambda_{name}" for name in self._nests.nest_names] + + [f"mean_{name}" for name in self._random_param_names] + + [f"sd_{name}" for name in self._random_param_names] + ) + elif self._is_spatial_scl: # Mixed Nested SCL: [beta_fixed, alpha_rho_1..M, alpha_lambda_1..M, mean_*, sd_*] x0 = np.concatenate( [ @@ -440,9 +556,20 @@ def _get_solver_inputs(self, arrays: ChoiceArrays): def _build_objective(self, arrays: ChoiceArrays) -> Objective: """Build optimization objective based on active features.""" - # Pure MNL / SCL + # Pure MNL / SCL / SAR if not self._is_nested and not self._is_mixed: - if self._is_spatial: + if self._is_spatial_lag: + from .._jax.sar_kernels import build_sar_mnl_objective + + # Auto-select estimator + if self._estimator == "auto": + if arrays.n_alts <= 2000: + self._estimator = "pml" + else: + self._estimator = "pml_cg" + use_cg = self._estimator == "pml_cg" + return build_sar_mnl_objective(arrays, self._W_sparse, use_cg=use_cg) + elif self._is_spatial_scl: from .._jax.builders import build_scl_objective return build_scl_objective( @@ -454,7 +581,11 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: # Nested (no random) if self._is_nested and not self._is_mixed: - if self._is_spatial: + if self._is_spatial_lag: + from .._jax.sar_kernels import build_sar_nested_objective + + return build_sar_nested_objective(arrays, self._W_sparse, self._nest_matrix) + elif self._is_spatial_scl: from .._jax.builders import build_nested_scl_objective return build_nested_scl_objective(arrays, self._nest_matrix, self._edge_data_list) @@ -464,7 +595,17 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: # Mixed (no nests) if self._is_mixed and not self._is_nested: - if self._is_spatial: + if self._is_spatial_lag: + from .._jax.sar_kernels import build_sar_mixed_objective + + return build_sar_mixed_objective( + arrays, + self._W_sparse, + self._random_col_indices, + self._random_distributions, + self._draws, + ) + elif self._is_spatial_scl: from .._jax.builders import build_mscl_objective return build_mscl_objective( @@ -487,7 +628,18 @@ def _build_objective(self, arrays: ChoiceArrays) -> Objective: # Mixed Nested if self._is_nested and self._is_mixed: - if self._is_spatial: + if self._is_spatial_lag: + from .._jax.sar_kernels import build_sar_mixed_nested_objective + + return build_sar_mixed_nested_objective( + arrays, + self._W_sparse, + self._nest_matrix, + self._random_col_indices, + self._random_distributions, + self._draws, + ) + elif self._is_spatial_scl: from .._jax.builders import build_mnscl_objective return build_mnscl_objective( @@ -524,10 +676,17 @@ def _build_fit_result( k = arrays.design_matrix.shape[1] param_names_all = list(arrays.param_names) - # --- Pure MNL / SCL --- + # --- Pure MNL / SCL / SAR --- if not self._is_nested and not self._is_mixed: - if self._is_spatial: - # Layout: [beta_1..k, alpha_rho] + if self._is_spatial_lag: + # SAR: Layout [beta_1..k, alpha_rho], rho = tanh(alpha_rho) + beta = all_params[:k] + alpha_rho = all_params[k] + rho = np.tanh(alpha_rho) + display_values = np.concatenate([beta, [rho]]) + display_names = param_names_all + ["rho"] + elif self._is_spatial_scl: + # SCL: Layout [beta_1..k, alpha_rho], rho = sigmoid(alpha_rho) beta = all_params[:k] alpha_rho = all_params[k] rho = naturalize_rho(alpha_rho) @@ -545,8 +704,24 @@ def _build_fit_result( # --- Nested (no random) --- if self._is_nested and not self._is_mixed: n_nests = self._nests.n_nests - if self._is_spatial: - # Layout: [beta, alpha_rho_1..M, alpha_lambda_1..M] + if self._is_spatial_lag: + # SAR + Nested: Layout [beta, alpha_rho, alpha_lambda_1..M] + beta = all_params[:k] + alpha_rho = all_params[k] + alpha_lambda = all_params[k + 1 : k + 1 + n_nests] + rho = np.tanh(alpha_rho) + lambdas = naturalize_nest_params(alpha_lambda) + display_values = np.concatenate([beta, [rho], lambdas]) + display_names = ( + param_names_all + + ["rho"] + + [f"lambda_{name}" for name in self._nests.nest_names] + ) + std_errors = self._compute_se_sar_nested( + all_params, arrays, k, n_nests, rho, lambdas + ) + elif self._is_spatial_scl: + # SCL + Nested: Layout [beta, alpha_rho_1..M, alpha_lambda_1..M] beta = all_params[:k] alpha_rho = all_params[k : k + n_nests] alpha_lambda = all_params[k + n_nests : k + 2 * n_nests] @@ -580,8 +755,25 @@ def _build_fit_result( if self._is_mixed and not self._is_nested: k_fixed = self._k_fixed k_random = self._k_random - if self._is_spatial: - # Layout: [beta_fixed, alpha_rho, mean_*, sd_*] + if self._is_spatial_lag: + # SAR + Mixed: Layout [beta_fixed, alpha_rho, mean_*, sd_*] + beta_fixed = all_params[:k_fixed] + alpha_rho = all_params[k_fixed] + rho = np.tanh(alpha_rho) + beta_random_means = all_params[k_fixed + 1 : k_fixed + 1 + k_random] + beta_random_spreads = all_params[k_fixed + 1 + k_random :] + display_values = np.concatenate( + [beta_fixed, [rho], beta_random_means, beta_random_spreads] + ) + display_names = ( + list(self._fixed_names) + + ["rho"] + + [f"mean_{n}" for n in self._random_param_names] + + [f"sd_{n}" for n in self._random_param_names] + ) + std_errors = self._compute_se_sar_mixed(all_params, arrays, k_fixed, rho) + elif self._is_spatial_scl: + # SCL + Mixed (MSCL): Layout [beta_fixed, alpha_rho, mean_*, sd_*] beta_fixed = all_params[:k_fixed] alpha_rho = all_params[k_fixed] rho = naturalize_rho(alpha_rho) @@ -616,8 +808,32 @@ def _build_fit_result( name for name in param_names_all if name not in self._random_params ] - if self._is_spatial: - # Layout: [beta_fixed, alpha_rho_1..M, alpha_lambda_1..M, mean_*, sd_*] + if self._is_spatial_lag: + # SAR + Mixed + Nested: Layout [beta_fixed, alpha_rho, alpha_lambda_1..M, mean_*, sd_*] + beta_fixed = all_params[:k_fixed] + alpha_rho = all_params[k_fixed] + alpha_lambda = all_params[k_fixed + 1 : k_fixed + 1 + n_nests] + beta_random_means = all_params[ + k_fixed + 1 + n_nests : k_fixed + 1 + n_nests + k_random + ] + beta_random_spreads = all_params[k_fixed + 1 + n_nests + k_random :] + rho = np.tanh(alpha_rho) + lambdas = naturalize_nest_params(alpha_lambda) + display_values = np.concatenate( + [beta_fixed, [rho], lambdas, beta_random_means, np.abs(beta_random_spreads)] + ) + display_names = ( + fixed_param_names + + ["rho"] + + [f"lambda_{name}" for name in self._nests.nest_names] + + [f"mean_{name}" for name in self._random_param_names] + + [f"sd_{name}" for name in self._random_param_names] + ) + std_errors = self._compute_se_sar_mixed_nested( + all_params, arrays, k_fixed, n_nests, rho, lambdas + ) + elif self._is_spatial_scl: + # SCL + Mixed + Nested: Layout [beta_fixed, alpha_rho_1..M, alpha_lambda_1..M, mean_*, sd_*] beta_fixed = all_params[:k_fixed] alpha_rho = all_params[k_fixed : k_fixed + n_nests] alpha_lambda = all_params[k_fixed + n_nests : k_fixed + 2 * n_nests] @@ -692,8 +908,13 @@ def _compute_se(self, all_params, arrays, display_values, display_names): try: hess = self._compute_hessian(all_params) se_unconstrained = self._compute_std_errors_from_hessian(hess) - if self._is_spatial: - # Delta method: SE(rho) = rho*(1-rho)*SE(alpha_rho) + if self._is_spatial_lag: + # SAR delta method: SE(rho) = (1 - rho^2) * SE(alpha_rho) + rho = display_values[k] + se_rho = (1.0 - rho**2) * se_unconstrained[k] + std_errors = np.concatenate([se_unconstrained[:k], [se_rho]]) + elif self._is_spatial_scl: + # SCL delta method: SE(rho) = rho*(1-rho)*SE(alpha_rho) rho = display_values[k] se_rho = rho * (1.0 - rho) * se_unconstrained[k] std_errors = np.concatenate([se_unconstrained[:k], [se_rho]]) @@ -704,7 +925,11 @@ def _compute_se(self, all_params, arrays, display_values, display_names): if hess_inv is not None: se = np.sqrt(np.maximum(np.diag(hess_inv), 0)) se[se == 0] = np.nan - if self._is_spatial: + if self._is_spatial_lag: + rho = display_values[k] + se_rho = (1.0 - rho**2) * se[k] + std_errors = np.concatenate([se[:k], [se_rho]]) + elif self._is_spatial_scl: rho = display_values[k] se_rho = rho * (1.0 - rho) * se[k] std_errors = np.concatenate([se[:k], [se_rho]]) @@ -827,6 +1052,72 @@ def _compute_se_mixed_nested_scl(self, all_params, arrays, k_fixed, n_nests, rho ) return std_errors + def _compute_se_sar_nested(self, all_params, arrays, k, n_nests, rho, lambdas): + """Compute SEs for SAR + Nested. Layout: [beta, alpha_rho, alpha_lambda_1..M].""" + n_params = len(all_params) + std_errors = np.full(n_params, np.nan) + try: + hess = self._compute_hessian(all_params) + se_raw = self._compute_std_errors_from_hessian(hess) + # SAR: rho = tanh(alpha_rho), SE(rho) = (1 - rho^2) * SE(alpha_rho) + se_rho = (1.0 - rho**2) * se_raw[k] + # Lambda: sigmoid, SE(lambda) = lambda*(1-lambda)*SE(alpha_lambda) + se_lambda = lambdas * (1.0 - lambdas) * se_raw[k + 1 : k + 1 + n_nests] + std_errors = np.concatenate([se_raw[:k], [se_rho], se_lambda]) + except Exception: + hess_inv = self._get_hessian_inverse() + if hess_inv is not None: + se_raw = np.sqrt(np.maximum(np.diag(hess_inv), 0)) + se_raw[se_raw == 0] = np.nan + se_rho = (1.0 - rho**2) * se_raw[k] + se_lambda = lambdas * (1.0 - lambdas) * se_raw[k + 1 : k + 1 + n_nests] + std_errors = np.concatenate([se_raw[:k], [se_rho], se_lambda]) + return std_errors + + def _compute_se_sar_mixed(self, all_params, arrays, k_fixed, rho): + """Compute SEs for SAR + Mixed. Layout: [beta_fixed, alpha_rho, mean_*, sd_*].""" + n_params = len(all_params) + std_errors = np.full(n_params, np.nan) + try: + hess = self._compute_hessian(all_params) + se_raw = self._compute_std_errors_from_hessian(hess) + # SAR: rho = tanh(alpha_rho), SE(rho) = (1 - rho^2) * SE(alpha_rho) + se_rho = (1.0 - rho**2) * se_raw[k_fixed] + std_errors = np.concatenate([se_raw[:k_fixed], [se_rho], se_raw[k_fixed + 1 :]]) + except Exception: + hess_inv = self._get_hessian_inverse() + if hess_inv is not None: + se_raw = np.sqrt(np.maximum(np.diag(hess_inv), 0)) + se_raw[se_raw == 0] = np.nan + se_rho = (1.0 - rho**2) * se_raw[k_fixed] + std_errors = np.concatenate([se_raw[:k_fixed], [se_rho], se_raw[k_fixed + 1 :]]) + return std_errors + + def _compute_se_sar_mixed_nested(self, all_params, arrays, k_fixed, n_nests, rho, lambdas): + """Compute SEs for SAR + Mixed + Nested. + Layout: [beta_fixed, alpha_rho, alpha_lambda_1..M, mean_*, sd_*].""" + n_params = len(all_params) + std_errors = np.full(n_params, np.nan) + try: + hess = self._compute_hessian(all_params) + se_raw = self._compute_std_errors_from_hessian(hess) + se_rho = (1.0 - rho**2) * se_raw[k_fixed] + se_lambda = lambdas * (1.0 - lambdas) * se_raw[k_fixed + 1 : k_fixed + 1 + n_nests] + std_errors = np.concatenate( + [se_raw[:k_fixed], [se_rho], se_lambda, se_raw[k_fixed + 1 + n_nests :]] + ) + except Exception: + hess_inv = self._get_hessian_inverse() + if hess_inv is not None: + se_raw = np.sqrt(np.maximum(np.diag(hess_inv), 0)) + se_raw[se_raw == 0] = np.nan + se_rho = (1.0 - rho**2) * se_raw[k_fixed] + se_lambda = lambdas * (1.0 - lambdas) * se_raw[k_fixed + 1 : k_fixed + 1 + n_nests] + std_errors = np.concatenate( + [se_raw[:k_fixed], [se_rho], se_lambda, se_raw[k_fixed + 1 + n_nests :]] + ) + return std_errors + def _make_fit_result( self, solver_result: SolverResult, @@ -898,10 +1189,46 @@ def probabilities(self, data=None, beta=None, alpha=None) -> np.ndarray: return self._probabilities_mnl(arrays, data, beta) def _probabilities_mnl(self, arrays, data, beta) -> np.ndarray: - """Compute MNL or SCL probabilities.""" + """Compute MNL, SCL, or SAR probabilities.""" from .._kernels.mnl_numpy import mnl_probs_numpy - if self._is_spatial: + if self._is_spatial_lag: + # SAR: spatial filter + variance normalisation + k = arrays.design_matrix.shape[1] + if beta is None: + coef_vals = np.asarray(self._result.coefficients.values, dtype=np.float64) + beta_use = coef_vals[:k] + rho = float(coef_vals[k]) + else: + beta = np.asarray(beta, dtype=np.float64) + beta_use = beta[:k] + rho = ( + float(beta[k]) if beta.size > k else float(self._result.coefficients.values[k]) + ) + + dm = np.asarray(arrays.design_matrix, dtype=np.float64) + n_obs = arrays.n_obs + n_alts = arrays.n_alts + W_dense = np.asarray(self._W_sparse.toarray(), dtype=np.float64) + + V_base = (dm @ beta_use).reshape(n_obs, n_alts) + from .._sampling.correction import apply_sampling_correction + + V_base = apply_sampling_correction(V_base, arrays) + + A = np.eye(n_alts) - rho * W_dense + V_filtered = np.linalg.solve(A, V_base.T).T + D = np.diag(np.linalg.inv(A)) + V_star = V_filtered / D[None, :] + + if arrays.available is not None: + available = np.asarray(arrays.available, dtype=np.float64).reshape(n_obs, n_alts) + else: + available = np.ones((n_obs, n_alts), dtype=np.float64) + + return mnl_probs_numpy(V_star, available, inclusion_probs=None) + + if self._is_spatial_scl: from .scl import _scl_log_probs_numpy k = arrays.design_matrix.shape[1] @@ -1259,6 +1586,97 @@ def _resolve_me_data(self, data=None): ) return ct, probs, df, index + def marginal_effects(self, data=None, variable: Optional[str] = None): + """Compute average direct, indirect, and total marginal effects. + + In the SAR-MNL model (``lag=True``), a change in an attribute of + alternative *j* affects not only *j*'s utility but also neighbouring + alternatives through the spatial multiplier + :math:`(I - \\rho W)^{-1}`. + + Following LeSage & Pace (2009): + + - **Direct effect**: impact on own alternative. + - **Indirect effect**: spillover to neighbouring alternatives. + - **Total effect**: direct + indirect. + + Parameters + ---------- + data : ChoiceTable or None + Data to compute marginal effects on. If None, uses + estimation data. + variable : str + Name of the variable to compute marginal effects for. + + Returns + ------- + dict + Dictionary with keys ``"direct"``, ``"indirect"``, + ``"total"``, each mapping to a ``pd.Series`` indexed by + alternative ID. + """ + if not self._is_spatial_lag: + raise ValueError( + "marginal_effects() is only available for SAR models (lag=True). " + "Use marginal_effect() for non-spatial models." + ) + if self._arrays is None: + raise RuntimeError("Model must be estimated before computing marginal effects.") + + from ..data.choicetable import ChoiceTable + + ct = self._data + arrays = self._arrays + if data is not None: + if not isinstance(data, ChoiceTable): + raise TypeError("data must be a ChoiceTable") + arrays = data.to_arrays( + formula=self._spec.formula, + spec=self._spec if self._spec.formula is None else None, + ) + ct = data + + n_obs = arrays.n_obs + n_alts = arrays.n_alts + k = arrays.design_matrix.shape[1] + + coef_vals = np.asarray(self._result.coefficients.values, dtype=np.float64) + beta = coef_vals[:k] + rho = float(coef_vals[k]) + + param_names = list(arrays.param_names) + if variable not in param_names: + raise ValueError(f"Variable '{variable}' not found in parameters: {param_names}") + beta_r = beta[param_names.index(variable)] + + probs = self.probabilities(data=data) + W_dense = np.asarray(self._W_sparse.toarray(), dtype=np.float64) + A = np.eye(n_alts) - rho * W_dense + Z_mat = np.linalg.inv(A) + + direct = np.zeros(n_alts) + indirect = np.zeros(n_alts) + for k_alt in range(n_alts): + direct[k_alt] = ( + beta_r * Z_mat[k_alt, k_alt] * np.mean(probs[:, k_alt] * (1 - probs[:, k_alt])) + ) + for j_alt in range(n_alts): + if j_alt != k_alt: + indirect[k_alt] += ( + -beta_r * Z_mat[k_alt, j_alt] * np.mean(probs[:, k_alt] * probs[:, j_alt]) + ) + + total = direct + indirect + + df = ct.to_frame() + alt_ids = df[ct.alt_id_col].values.reshape(n_obs, n_alts)[0] + + return { + "direct": pd.Series(direct, index=alt_ids, name=f"direct_{variable}"), + "indirect": pd.Series(indirect, index=alt_ids, name=f"indirect_{variable}"), + "total": pd.Series(total, index=alt_ids, name=f"total_{variable}"), + } + def marginal_effect(self, data=None, variable: Optional[str] = None) -> pd.Series: """Compute direct marginal effects for a variable. diff --git a/locpick/models/sar_mnl.py b/locpick/models/sar_mnl.py deleted file mode 100644 index 8ee1f92..0000000 --- a/locpick/models/sar_mnl.py +++ /dev/null @@ -1,530 +0,0 @@ -"""Spatial Autoregressive Multinomial Logit (SAR-MNL) model. - -Implements the pseudo maximum likelihood (PML) estimator from -Smirnov (2010): a spatial autoregressive lag in the systematic -utility of alternatives (spatial locations), with variance -normalisation by ``diag((I - ρW)^{-1})`` for consistency. - -The model specifies: - -.. math:: - - V_j = \\rho \\sum_k w_{jk} V_k + Z_j \\beta + X_{ij} \\gamma - -yielding reduced-form utilities :math:`V^* = (I - \\rho W)^{-1} -(Z\\beta + X\\gamma)`, normalised by :math:`D = \\text{diag}((I - -\\rho W)^{-1})`, with standard MNL choice probabilities. - -Estimation is via JAX autodiff through the spatial solve and -variance normalisation. No log-determinant Jacobian is needed -(this is pseudo-ML, not full ML). -""" - -from __future__ import annotations - -from typing import Optional, Union - -import numpy as np -import pandas as pd - -from .._solvers import Solver, SolverResult -from ..data.arrays import ChoiceArrays -from ..results.fit_result import FitResult -from ._spatial_weights import resolve_spatial_weights -from .base import ( - BaseChoiceModel, - _compute_fit_statistics, - _compute_null_ll, -) - - -class SARMNL(BaseChoiceModel): - r"""Spatial Autoregressive Multinomial Logit (SAR-MNL). - - Specifies a spatial autoregressive lag in the systematic utility - of alternatives (spatial locations): - - .. math:: - - V_j = \rho \sum_k w_{jk} V_k + Z_j \beta + X_{ij} \gamma - - yielding reduced-form utilities :math:`V^* = (I - \rho W)^{-1} - (Z\beta + X\gamma)`, normalised by :math:`D = \text{diag}((I - - \rho W)^{-1})`, with standard MNL choice probabilities. - - Estimation is via pseudo maximum likelihood (PML, Smirnov 2010) - with JAX autodiff through the spatial solve and variance - normalisation. No log-determinant Jacobian is needed (this is - pseudo-ML, not full ML). - - Parameters - ---------- - data : ChoiceTable or EstimationProblem - The choice data. - formula : str, optional - Formulaic formula string. - spec : ModelSpec, optional - ModelSpec object. - W : libpysal.graph.Graph, scipy.sparse, or np.ndarray - J×J spatial weights matrix connecting alternatives (locations). - Row-standardised internally. Zero diagonal. A ``libpysal.graph.Graph`` - is the preferred input type (matching bayespecon). ``scipy.sparse`` - and dense ``np.ndarray`` are also accepted and converted internally. - weights : str or array-like, optional - Observation weights. - availability : str or array-like, optional - Alternative availability. - solver : str or Solver, optional - Solver for PML optimisation. Default "lbfgs". - solver_options : dict, optional - backend : str, optional - estimator : str, optional - "auto" (default), "pml", "pml_cg", or "linearized_gmm". - ``auto`` selects ``pml`` (dense solve) for n_alts ≤ 2000 and - ``pml_cg`` (conjugate gradient) for larger alternative sets. - - Examples - -------- - >>> from locpick import ChoiceTable, SARMNL - >>> from libpysal.graph import Graph - >>> ct = ChoiceTable.from_tables(choosers, alternatives, chosen, sample_size=10) - >>> W = Graph.build_knn(gdf, k=7).transform("r") - >>> model = SARMNL(ct, formula="chosen ~ cost + time", W=W) - >>> result = model.fit() - >>> print(result.summary()) - """ - - def __init__( - self, - data, - formula: Optional[str] = None, - spec=None, - W=None, - weights: Optional[Union[str, np.ndarray]] = None, - availability: Optional[Union[str, np.ndarray]] = None, - solver: Union[str, Solver] = "lbfgs", - solver_options: Optional[dict] = None, - backend: Optional[str] = None, - estimator: str = "auto", - ): - super().__init__( - data=data, - formula=formula, - spec=spec, - solver=solver, - solver_options=solver_options, - backend=backend, - weights=weights, - availability=availability, - ) - if W is None: - raise ValueError("W (spatial weights matrix) is required for SARMNL.") - self._W_input = W - self._W_sparse = None # resolved at fit time - self._estimator = estimator - - # ------------------------------------------------------------------ - # Properties - # ------------------------------------------------------------------ - - @property - def W(self): - """The spatial weights matrix (libpysal.graph.Graph).""" - return self._W_input - - # ------------------------------------------------------------------ - # Estimation - # ------------------------------------------------------------------ - - def _pre_fit(self, arrays: ChoiceArrays) -> None: - """Resolve the spatial weights matrix.""" - self._W_sparse = resolve_spatial_weights( - self._W_input, arrays.n_alts, row_standardize=True - )[1] # get the CSR sparse - - def fit(self, **kwargs) -> FitResult: - """Estimate the model and return results. - - Dispatches to the PML estimator (JAX autodiff) or the - linearized GMM estimator based on the ``estimator`` setting. - """ - if self._estimator == "linearized_gmm": - return self._fit_linearized_gmm() - return super().fit(**kwargs) - - def _fit_linearized_gmm(self) -> FitResult: - """Two-step linearized GMM estimation (Carrión-Flores et al. 2018).""" - arrays = self._get_arrays() - self._arrays = arrays - self._pre_fit(arrays) - - from .._kernels.sar_mnl_numpy import fit_linearized_gmm - - result_dict = fit_linearized_gmm(arrays, self._W_sparse) - - beta = result_dict["beta"] - rho = result_dict["rho"] - se = result_dict["se"] - ll = result_dict["log_likelihood"] - - utility_param_names = list(arrays.param_names) - k = len(utility_param_names) - display_values = np.concatenate([beta, [rho]]) - display_names = utility_param_names + ["rho"] - model_type = "SAR-MNL (Linearized GMM)" - n_params = len(display_values) - - # SEs: first k are beta SEs, last is rho SE - std_errors = se[: k + 1] - - coefficients = pd.Series(display_values, index=display_names, name="coefficient") - std_err_series = pd.Series(std_errors, index=display_names, name="std_error") - ll_null = _compute_null_ll(arrays) - - stats = _compute_fit_statistics( - ll=ll, - ll_null=ll_null, - n_obs=arrays.n_obs, - n_params=n_params, - n_alts=arrays.n_alts, - coefficients=coefficients, - std_errors=std_err_series, - model_type=model_type, - solver_name="linearized_gmm", - solver_result_raw=result_dict, - ) - - self._result = FitResult(spec=self._spec, **stats) - self._clear_caches() - return self._result - - def _build_objective(self, arrays: ChoiceArrays): - """Build the PML objective using JAX. - - Auto-selects dense solve (n_alts ≤ 2000) or conjugate gradient - (n_alts > 2000) based on the estimator setting. - """ - from .._jax.sar_kernels import build_sar_mnl_objective - - # Auto-select estimator - if self._estimator == "auto": - if arrays.n_alts <= 2000: - self._estimator = "pml" - else: - self._estimator = "pml_cg" - - use_cg = self._estimator == "pml_cg" - return build_sar_mnl_objective(arrays, self._W_sparse, use_cg=use_cg) - - def _get_solver_inputs(self, arrays: ChoiceArrays): - """Get initial values, param names, bounds, fixed mask. - - Appends an unconstrained ``alpha_rho`` (initial 0.0) so the - spatial autoregressive parameter is estimated alongside the - utility coefficients. ``rho = tanh(alpha_rho) ∈ (-1, 1)``. - """ - x0, names, bounds, fixed_mask = super()._get_solver_inputs(arrays) - x0 = np.concatenate([x0, np.zeros(1)]) - names = list(names) + ["alpha_rho"] - return x0, names, bounds, fixed_mask - - def _build_fit_result(self, solver_result: SolverResult, arrays: ChoiceArrays) -> FitResult: - """Build a FitResult from solver output.""" - all_params = solver_result.coefficients - utility_param_names = list(arrays.param_names) - k = len(utility_param_names) - - # Layout: [beta_1..k, alpha_rho] - beta = all_params[:k] - alpha_rho = all_params[k] - rho = np.tanh(alpha_rho) # ρ ∈ (-1, 1) - - display_values = np.concatenate([beta, [rho]]) - display_names = utility_param_names + ["rho"] - model_type = "Spatial Autoregressive Multinomial Logit" - n_params = len(display_values) - - # Standard errors via Hessian - std_errors = np.full(n_params, np.nan) - try: - hess = self._compute_hessian(all_params) - se_unconstrained = self._compute_std_errors_from_hessian(hess) - # Delta method: SE(rho) = (1 - rho^2) * SE(alpha_rho) - se_rho = (1.0 - rho**2) * se_unconstrained[k] - std_errors = np.concatenate([se_unconstrained[:k], [se_rho]]) - except Exception: - if solver_result.hessian is not None: - try: - se = np.sqrt(np.maximum(np.diag(solver_result.hessian), 0)) - se[se == 0] = np.nan - se_rho = (1.0 - rho**2) * se[k] - std_errors = np.concatenate([se[:k], [se_rho]]) - except Exception: - pass - - coefficients = pd.Series(display_values, index=display_names, name="coefficient") - std_err_series = pd.Series(std_errors, index=display_names, name="std_error") - ll = solver_result.log_likelihood - ll_null = _compute_null_ll(arrays) - - stats = _compute_fit_statistics( - ll=ll, - ll_null=ll_null, - n_obs=arrays.n_obs, - n_params=n_params, - n_alts=arrays.n_alts, - coefficients=coefficients, - std_errors=std_err_series, - model_type=model_type, - solver_name=solver_result.solver_name, - solver_result_raw=solver_result.raw, - ) - - return FitResult(spec=self._spec, **stats) - - # ------------------------------------------------------------------ - # Prediction - # ------------------------------------------------------------------ - - def probabilities(self, data=None, beta=None, rho=None): - """Compute choice probabilities under the SAR-MNL model. - - Uses the full PML model: spatially-filtered + variance-normalised - utilities, then standard MNL softmax. - - Parameters - ---------- - data : ChoiceTable or None - Data to predict on. If None, uses estimation data. - beta : np.ndarray or None - Utility coefficients. If None, uses estimated values. - rho : float or None - Spatial autoregressive parameter. If None, uses estimated value. - - Returns - ------- - np.ndarray, shape (n_obs, n_alts) - Choice probabilities for each observation and alternative. - """ - from .._kernels.mnl_numpy import mnl_probs_numpy - - if self._arrays is None: - raise RuntimeError("Model must be estimated before prediction.") - - arrays = self._arrays - if data is not None: - arrays = data.to_arrays( - formula=self._spec.formula, - spec=self._spec if self._spec.formula is None else None, - ) - - n_obs = arrays.n_obs - n_alts = arrays.n_alts - k = arrays.design_matrix.shape[1] - - if beta is None: - coef_vals = np.asarray(self._result.coefficients.values, dtype=np.float64) - beta = coef_vals[:k] - if rho is None: - rho = float(self._result.coefficients.values[k]) - - dm = np.asarray(arrays.design_matrix, dtype=np.float64) - W_dense = np.asarray(self._W_sparse.toarray(), dtype=np.float64) - - # Base utilities - V_base = (dm @ beta).reshape(n_obs, n_alts) - - # Sampling correction - from .._sampling.correction import apply_sampling_correction - - V_base = apply_sampling_correction(V_base, arrays) - - # Spatial filter + variance normalisation - A = np.eye(n_alts) - rho * W_dense - V_filtered = np.linalg.solve(A, V_base.T).T - D = np.diag(np.linalg.inv(A)) - V_star = V_filtered / D[None, :] - - # Availability - if arrays.available is not None: - available = np.asarray(arrays.available, dtype=np.float64).reshape(n_obs, n_alts) - else: - available = np.ones((n_obs, n_alts), dtype=np.float64) - - return mnl_probs_numpy(V_star, available, inclusion_probs=None) - - def utilities(self, data=None, beta=None, rho=None): - """Compute spatially-filtered + variance-normalised utilities. - - Parameters - ---------- - data : ChoiceTable or None - Data to compute utilities on. If None, uses estimation data. - beta : np.ndarray or None - Utility coefficients. If None, uses estimated values. - rho : float or None - Spatial autoregressive parameter. If None, uses estimated value. - - Returns - ------- - np.ndarray, shape (n_obs, n_alts) - Spatially-filtered and variance-normalised utilities. - """ - if self._arrays is None: - raise RuntimeError("Model must be estimated before prediction.") - - arrays = self._arrays - if data is not None: - arrays = data.to_arrays( - formula=self._spec.formula, - spec=self._spec if self._spec.formula is None else None, - ) - - n_obs = arrays.n_obs - n_alts = arrays.n_alts - k = arrays.design_matrix.shape[1] - - if beta is None: - beta = np.asarray(self._result.coefficients.values[:k], dtype=np.float64) - if rho is None: - rho = float(self._result.coefficients.values[k]) - - dm = np.asarray(arrays.design_matrix, dtype=np.float64) - W_dense = np.asarray(self._W_sparse.toarray(), dtype=np.float64) - - V_base = (dm @ beta).reshape(n_obs, n_alts) - - from .._sampling.correction import apply_sampling_correction - - V_base = apply_sampling_correction(V_base, arrays) - - A = np.eye(n_alts) - rho * W_dense - V_filtered = np.linalg.solve(A, V_base.T).T - D = np.diag(np.linalg.inv(A)) - V_star = V_filtered / D[None, :] - - return V_star - - # ------------------------------------------------------------------ - # Marginal effects (LeSage & Pace 2009) - # ------------------------------------------------------------------ - - def marginal_effects(self, data=None, variable: Optional[str] = None): - """Compute average direct, indirect, and total marginal effects. - - In the SAR-MNL model, a change in an attribute of alternative - *j* affects not only *j*'s utility but also neighbouring - alternatives through the spatial multiplier - :math:`(I - \\rho W)^{-1}`. - - Following LeSage & Pace (2009), the marginal effect of variable - *r* on the probability of choosing alternative *k* is an - :math:`J \\times J` matrix. Summary measures are: - - - **Direct effect**: average of diagonal elements (impact on - own alternative). - - **Indirect effect**: average of off-diagonal row sums - (spillover to neighbouring alternatives). - - **Total effect**: direct + indirect. - - Parameters - ---------- - data : ChoiceTable or None - Data to compute marginal effects on. If None, uses - estimation data. - variable : str - Name of the variable to compute marginal effects for. - - Returns - ------- - dict - Dictionary with keys ``"direct"``, ``"indirect"``, - ``"total"``, each mapping to a ``pd.Series`` indexed by - alternative ID. - """ - if self._arrays is None: - raise RuntimeError("Model must be estimated before computing marginal effects.") - - from ..data.choicetable import ChoiceTable - - ct = self._data - arrays = self._arrays - if data is not None: - if not isinstance(data, ChoiceTable): - raise TypeError("data must be a ChoiceTable") - arrays = data.to_arrays( - formula=self._spec.formula, - spec=self._spec if self._spec.formula is None else None, - ) - ct = data - - n_obs = arrays.n_obs - n_alts = arrays.n_alts - k = arrays.design_matrix.shape[1] - - # Get estimated parameters - coef_vals = np.asarray(self._result.coefficients.values, dtype=np.float64) - beta = coef_vals[:k] - rho = float(coef_vals[k]) - - # Get the beta for the requested variable - param_names = list(arrays.param_names) - if variable not in param_names: - raise ValueError(f"Variable '{variable}' not found in parameters: {param_names}") - beta_r = beta[param_names.index(variable)] - - # Compute probabilities - probs = self.probabilities(data=data) - - # Spatial multiplier (I - rho*W)^{-1} - W_dense = np.asarray(self._W_sparse.toarray(), dtype=np.float64) - A = np.eye(n_alts) - rho * W_dense - Z_mat = np.linalg.inv(A) # (n_alts, n_alts) - - # Marginal effect matrix for variable r, alternative k: - # ME_{k,r} = P_k * (beta_r * Z_{kk} - sum_l P_l * beta_r * Z_{lk}) - # = beta_r * P_k * (Z_{kk} - sum_l P_l * Z_{lk}) - # But P varies across choosers. For the average marginal effect, - # we average over choosers: - # AME_{k,r} = beta_r * avg(P_k) * (Z_{kk} - sum_l avg(P_l) * Z_{lk}) - - # Compute the J×J marginal effect matrix (averaged over choosers) - # ME[j, k] = beta_r * avg_probs[k] * (Z[k, j] - sum_l avg_probs[l] * Z[l, j]) - # But the standard LeSage-Pace formulation for MNL is: - # dP_k/dX_j = P_k * (delta_{kj} - P_j) * beta_r * Z[j, ...] - # This is complex — we use the simpler average approach: - # - # For each alternative k, the direct effect is: - # dP_k/dX_k = beta_r * Z[k,k] * P_k * (1 - P_k) - # The indirect (spillover) effect from j to k (j != k) is: - # dP_k/dX_j = -beta_r * Z[k,j] * P_k * P_j - # But with the spatial multiplier, Z replaces the identity. - - # Direct effects: average over choosers of - # beta_r * Z[k,k] * P_ik * (1 - P_ik) - direct = np.zeros(n_alts) - indirect = np.zeros(n_alts) - for k_alt in range(n_alts): - # Direct: own-alternative effect - direct[k_alt] = ( - beta_r * Z_mat[k_alt, k_alt] * np.mean(probs[:, k_alt] * (1 - probs[:, k_alt])) - ) - # Indirect: spillover from neighbours - # Sum over j != k of dP_k/dX_j = -beta_r * sum_{j!=k} Z[k,j] * P_k * P_j - for j_alt in range(n_alts): - if j_alt != k_alt: - indirect[k_alt] += ( - -beta_r * Z_mat[k_alt, j_alt] * np.mean(probs[:, k_alt] * probs[:, j_alt]) - ) - - total = direct + indirect - - # Get alternative IDs from the data - df = ct.to_frame() - alt_ids = df[ct.alt_id_col].values.reshape(n_obs, n_alts)[0] - - return { - "direct": pd.Series(direct, index=alt_ids, name=f"direct_{variable}"), - "indirect": pd.Series(indirect, index=alt_ids, name=f"indirect_{variable}"), - "total": pd.Series(total, index=alt_ids, name=f"total_{variable}"), - } diff --git a/tests/test_sar_mnl.py b/tests/test_sar_mnl.py index 55ed9fa..596eb11 100644 --- a/tests/test_sar_mnl.py +++ b/tests/test_sar_mnl.py @@ -14,7 +14,7 @@ import numpy as np import numpy.testing as npt -from locpick import SARMNL, ChoiceModel +from locpick import ChoiceModel from locpick.dgp import simulate_sar_mnl @@ -24,10 +24,11 @@ class TestSARMNLRecovery: def test_sar_mnl_recovers_beta_params(self): """SAR-MNL should recover beta coefficients within tolerance.""" dataset = simulate_sar_mnl(n_obs=5000, n_alts=50, rho=0.3, seed=2026) - model = SARMNL( + model = ChoiceModel( dataset.choice_table, formula="alt_attr + obs_x_alt - 1", - W=dataset.W, + graph=dataset.W, + lag=True, ) result = model.fit() @@ -43,10 +44,11 @@ def test_sar_mnl_recovers_beta_params(self): def test_sar_mnl_recovers_rho(self): """SAR-MNL should recover the spatial autoregressive parameter ρ.""" dataset = simulate_sar_mnl(n_obs=5000, n_alts=50, rho=0.3, seed=2026) - model = SARMNL( + model = ChoiceModel( dataset.choice_table, formula="alt_attr + obs_x_alt - 1", - W=dataset.W, + graph=dataset.W, + lag=True, ) result = model.fit() @@ -62,10 +64,11 @@ def test_sar_mnl_rho_zero_recovers_mnl(self): """When ρ=0, SAR-MNL should recover standard MNL estimates.""" dataset = simulate_sar_mnl(n_obs=5000, n_alts=50, rho=0.0, seed=42) # Fit SAR-MNL - sar_model = SARMNL( + sar_model = ChoiceModel( dataset.choice_table, formula="alt_attr + obs_x_alt - 1", - W=dataset.W, + graph=dataset.W, + lag=True, ) sar_result = sar_model.fit() # Fit standard MNL (via ChoiceModel without W) @@ -85,10 +88,11 @@ def test_sar_mnl_rho_zero_recovers_mnl(self): def test_sar_mnl_recovers_rho_low_spatial_dep(self): """SAR-MNL should detect low spatial dependence (ρ=0.05) is near zero.""" dataset = simulate_sar_mnl(n_obs=5000, n_alts=50, rho=0.05, seed=2026) - model = SARMNL( + model = ChoiceModel( dataset.choice_table, formula="alt_attr + obs_x_alt - 1", - W=dataset.W, + graph=dataset.W, + lag=True, ) result = model.fit() # Low ρ is hard to distinguish from zero — check it's not wildly off @@ -98,10 +102,11 @@ def test_sar_mnl_recovers_rho_low_spatial_dep(self): def test_sar_mnl_recovers_rho_moderate_spatial_dep(self): """SAR-MNL should recover ρ at moderate spatial dependence (ρ=0.5).""" dataset = simulate_sar_mnl(n_obs=5000, n_alts=50, rho=0.5, seed=2026) - model = SARMNL( + model = ChoiceModel( dataset.choice_table, formula="alt_attr + obs_x_alt - 1", - W=dataset.W, + graph=dataset.W, + lag=True, ) result = model.fit() npt.assert_allclose( @@ -114,10 +119,11 @@ def test_sar_mnl_recovers_rho_moderate_spatial_dep(self): def test_sar_mnl_smaller_n_alts(self): """SAR-MNL should work with a small number of alternatives.""" dataset = simulate_sar_mnl(n_obs=3000, n_alts=12, rho=0.2, n_neighbors=3, seed=42) - model = SARMNL( + model = ChoiceModel( dataset.choice_table, formula="alt_attr + obs_x_alt - 1", - W=dataset.W, + graph=dataset.W, + lag=True, ) result = model.fit() # Should converge and produce finite estimates @@ -133,11 +139,26 @@ def test_sar_mnl_w_input_types(self): W_sparse = sp.csr_array(W_graph.sparse) # scipy.sparse W_dense = W_sparse.toarray() # dense numpy - model1 = SARMNL(dataset.choice_table, "alt_attr + obs_x_alt - 1", W=W_graph) + model1 = ChoiceModel( + dataset.choice_table, + "alt_attr + obs_x_alt - 1", + graph=W_graph, + lag=True, + ) result1 = model1.fit() - model2 = SARMNL(dataset.choice_table, "alt_attr + obs_x_alt - 1", W=W_sparse) + model2 = ChoiceModel( + dataset.choice_table, + "alt_attr + obs_x_alt - 1", + graph=W_sparse, + lag=True, + ) result2 = model2.fit() - model3 = SARMNL(dataset.choice_table, "alt_attr + obs_x_alt - 1", W=W_dense) + model3 = ChoiceModel( + dataset.choice_table, + "alt_attr + obs_x_alt - 1", + graph=W_dense, + lag=True, + ) result3 = model3.fit() npt.assert_allclose( @@ -156,10 +177,11 @@ def test_sar_mnl_w_input_types(self): def test_sar_mnl_probabilities_sum_to_one(self): """Choice probabilities should sum to 1 across alternatives.""" dataset = simulate_sar_mnl(n_obs=1000, n_alts=20, rho=0.2, seed=42) - model = SARMNL( + model = ChoiceModel( dataset.choice_table, formula="alt_attr + obs_x_alt - 1", - W=dataset.W, + graph=dataset.W, + lag=True, ) model.fit() probs = model.probabilities() @@ -173,10 +195,11 @@ def test_sar_mnl_probabilities_sum_to_one(self): def test_sar_mnl_marginal_effects_structure(self): """Marginal effects: direct + indirect = total; indirect > 0 when ρ > 0.""" dataset = simulate_sar_mnl(n_obs=1000, n_alts=20, rho=0.3, seed=42) - model = SARMNL( + model = ChoiceModel( dataset.choice_table, formula="alt_attr + obs_x_alt - 1", - W=dataset.W, + graph=dataset.W, + lag=True, ) model.fit() me = model.marginal_effects(variable="alt_attr") @@ -192,10 +215,11 @@ def test_sar_mnl_marginal_effects_structure(self): def test_sar_mnl_gmm_recovers_rho(self): """Linearized GMM should recover ρ at moderate spatial dependence.""" dataset = simulate_sar_mnl(n_obs=5000, n_alts=50, rho=0.3, seed=2026) - model = SARMNL( + model = ChoiceModel( dataset.choice_table, formula="alt_attr + obs_x_alt - 1", - W=dataset.W, + graph=dataset.W, + lag=True, estimator="linearized_gmm", ) result = model.fit() @@ -210,18 +234,20 @@ def test_sar_mnl_cg_matches_dense(self): """CG path should give similar results to dense path.""" dataset = simulate_sar_mnl(n_obs=1000, n_alts=20, rho=0.2, seed=42) - model_dense = SARMNL( + model_dense = ChoiceModel( dataset.choice_table, formula="alt_attr + obs_x_alt - 1", - W=dataset.W, + graph=dataset.W, + lag=True, estimator="pml", ) result_dense = model_dense.fit() - model_cg = SARMNL( + model_cg = ChoiceModel( dataset.choice_table, formula="alt_attr + obs_x_alt - 1", - W=dataset.W, + graph=dataset.W, + lag=True, estimator="pml_cg", ) result_cg = model_cg.fit() From 0841b893f7cd48a60024dcb7c913bb365d29827a Mon Sep 17 00:00:00 2001 From: knaaptime Date: Sat, 20 Jun 2026 19:25:32 -0700 Subject: [PATCH 6/7] condense sar into choicemodel --- locpick/_jax/builders.py | 5 - locpick/_jax/data.py | 3 +- locpick/_jax/sar_kernels.py | 16 +- locpick/dgp.py | 18 +- locpick/models/sar_mnl.py | 530 ++++++++++++++++++++++++++++++++++++ 5 files changed, 554 insertions(+), 18 deletions(-) create mode 100644 locpick/models/sar_mnl.py diff --git a/locpick/_jax/builders.py b/locpick/_jax/builders.py index 0bae1d1..54be6ec 100644 --- a/locpick/_jax/builders.py +++ b/locpick/_jax/builders.py @@ -134,11 +134,6 @@ def _grad_jax(beta): # --------------------------------------------------------------------------- -# --------------------------------------------------------------------------- -# SCL objective -# --------------------------------------------------------------------------- - - # Top-level JIT'd kernels — cached across all SCL objectives @jax.jit def _scl_ll_kernel(params, data): diff --git a/locpick/_jax/data.py b/locpick/_jax/data.py index eb2a693..6497739 100644 --- a/locpick/_jax/data.py +++ b/locpick/_jax/data.py @@ -1,8 +1,7 @@ """JAX-ready data containers for choice model estimation. These containers hold pre-converted JAX arrays, built once and shared -across all solvers. They replace the ad-hoc numpy→JAX conversion that -was duplicated inside each model's ``_build_*_jax`` closure. +across all solvers. """ from __future__ import annotations diff --git a/locpick/_jax/sar_kernels.py b/locpick/_jax/sar_kernels.py index 7b1c045..0be620f 100644 --- a/locpick/_jax/sar_kernels.py +++ b/locpick/_jax/sar_kernels.py @@ -495,9 +495,13 @@ def _prob_single_draw(r): beta_lognormal, jnp.where(dist_codes == 2, beta_triangular, beta_uniform), ), - ) + ) # (n_obs, k_random) - V_random = (dm_random @ beta_r).reshape(n_obs, n_alts) + # Random utility: broadcast per-obs random coefficients with design matrix + V_random = jnp.sum( + dm_random.reshape(n_obs, n_alts, k_random) * beta_r[:, None, :], + axis=2, + ) V_total = V_fixed_star + V_random V_masked = jnp.where(available > 0, V_total, -1e30) log_sum_exp = jax.scipy.special.logsumexp(V_masked, axis=1) @@ -672,9 +676,13 @@ def _prob_single_draw(r): beta_lognormal, jnp.where(dist_codes == 2, beta_triangular, beta_uniform), ), - ) + ) # (n_obs, k_random) - V_random = (dm_random @ beta_r).reshape(n_obs, n_alts) + # Random utility: broadcast per-obs random coefficients with design matrix + V_random = jnp.sum( + dm_random.reshape(n_obs, n_alts, k_random) * beta_r[:, None, :], + axis=2, + ) V_total = V_fixed_star + V_random log_probs = nested_log_probs(V_total, lambdas, nest_matrix, available) return jnp.exp((log_probs * chosen).sum(axis=1)) diff --git a/locpick/dgp.py b/locpick/dgp.py index 2fd774f..d6a5dd8 100644 --- a/locpick/dgp.py +++ b/locpick/dgp.py @@ -2006,17 +2006,21 @@ def simulate_sar_mnl( choosers = pd.DataFrame({"obs_feature": obs_feature}, index=obs_ids) alt_ids = pd.Index(np.arange(n_alts), name="aid") - alt_attr = rng.standard_normal(n_alts) - alternatives = pd.DataFrame({"alt_attr": alt_attr}, index=alt_ids) + alt_data = {col: rng.standard_normal(n_alts) for col in alt_params} + alternatives = pd.DataFrame(alt_data, index=alt_ids) # --- Interactions (chooser × alternative) -------------------------- interaction_index = pd.MultiIndex.from_product([obs_ids, alt_ids], names=["oid", "aid"]) obs_feat_tiled = np.repeat(obs_feature, n_alts) - alt_attr_tiled = np.tile(alt_attr, n_obs) - obs_x_alt_values = obs_feat_tiled * alt_attr_tiled - interactions = { - "obs_x_alt": pd.Series(obs_x_alt_values, index=interaction_index, name="obs_x_alt") - } + # Use the first alternative column as the basis for interaction terms + first_alt_col = next(iter(alt_params)) + first_alt_values = alternatives[first_alt_col].to_numpy() + first_alt_tiled = np.tile(first_alt_values, n_obs) + interactions = {} + for col in interaction_params: + interactions[col] = pd.Series( + obs_feat_tiled * first_alt_tiled, index=interaction_index, name=col + ) # --- Base utilities: V_base = Zβ + Xγ (n_obs × n_alts) ------------- V_base = np.zeros((n_obs, n_alts)) diff --git a/locpick/models/sar_mnl.py b/locpick/models/sar_mnl.py new file mode 100644 index 0000000..8ee1f92 --- /dev/null +++ b/locpick/models/sar_mnl.py @@ -0,0 +1,530 @@ +"""Spatial Autoregressive Multinomial Logit (SAR-MNL) model. + +Implements the pseudo maximum likelihood (PML) estimator from +Smirnov (2010): a spatial autoregressive lag in the systematic +utility of alternatives (spatial locations), with variance +normalisation by ``diag((I - ρW)^{-1})`` for consistency. + +The model specifies: + +.. math:: + + V_j = \\rho \\sum_k w_{jk} V_k + Z_j \\beta + X_{ij} \\gamma + +yielding reduced-form utilities :math:`V^* = (I - \\rho W)^{-1} +(Z\\beta + X\\gamma)`, normalised by :math:`D = \\text{diag}((I - +\\rho W)^{-1})`, with standard MNL choice probabilities. + +Estimation is via JAX autodiff through the spatial solve and +variance normalisation. No log-determinant Jacobian is needed +(this is pseudo-ML, not full ML). +""" + +from __future__ import annotations + +from typing import Optional, Union + +import numpy as np +import pandas as pd + +from .._solvers import Solver, SolverResult +from ..data.arrays import ChoiceArrays +from ..results.fit_result import FitResult +from ._spatial_weights import resolve_spatial_weights +from .base import ( + BaseChoiceModel, + _compute_fit_statistics, + _compute_null_ll, +) + + +class SARMNL(BaseChoiceModel): + r"""Spatial Autoregressive Multinomial Logit (SAR-MNL). + + Specifies a spatial autoregressive lag in the systematic utility + of alternatives (spatial locations): + + .. math:: + + V_j = \rho \sum_k w_{jk} V_k + Z_j \beta + X_{ij} \gamma + + yielding reduced-form utilities :math:`V^* = (I - \rho W)^{-1} + (Z\beta + X\gamma)`, normalised by :math:`D = \text{diag}((I - + \rho W)^{-1})`, with standard MNL choice probabilities. + + Estimation is via pseudo maximum likelihood (PML, Smirnov 2010) + with JAX autodiff through the spatial solve and variance + normalisation. No log-determinant Jacobian is needed (this is + pseudo-ML, not full ML). + + Parameters + ---------- + data : ChoiceTable or EstimationProblem + The choice data. + formula : str, optional + Formulaic formula string. + spec : ModelSpec, optional + ModelSpec object. + W : libpysal.graph.Graph, scipy.sparse, or np.ndarray + J×J spatial weights matrix connecting alternatives (locations). + Row-standardised internally. Zero diagonal. A ``libpysal.graph.Graph`` + is the preferred input type (matching bayespecon). ``scipy.sparse`` + and dense ``np.ndarray`` are also accepted and converted internally. + weights : str or array-like, optional + Observation weights. + availability : str or array-like, optional + Alternative availability. + solver : str or Solver, optional + Solver for PML optimisation. Default "lbfgs". + solver_options : dict, optional + backend : str, optional + estimator : str, optional + "auto" (default), "pml", "pml_cg", or "linearized_gmm". + ``auto`` selects ``pml`` (dense solve) for n_alts ≤ 2000 and + ``pml_cg`` (conjugate gradient) for larger alternative sets. + + Examples + -------- + >>> from locpick import ChoiceTable, SARMNL + >>> from libpysal.graph import Graph + >>> ct = ChoiceTable.from_tables(choosers, alternatives, chosen, sample_size=10) + >>> W = Graph.build_knn(gdf, k=7).transform("r") + >>> model = SARMNL(ct, formula="chosen ~ cost + time", W=W) + >>> result = model.fit() + >>> print(result.summary()) + """ + + def __init__( + self, + data, + formula: Optional[str] = None, + spec=None, + W=None, + weights: Optional[Union[str, np.ndarray]] = None, + availability: Optional[Union[str, np.ndarray]] = None, + solver: Union[str, Solver] = "lbfgs", + solver_options: Optional[dict] = None, + backend: Optional[str] = None, + estimator: str = "auto", + ): + super().__init__( + data=data, + formula=formula, + spec=spec, + solver=solver, + solver_options=solver_options, + backend=backend, + weights=weights, + availability=availability, + ) + if W is None: + raise ValueError("W (spatial weights matrix) is required for SARMNL.") + self._W_input = W + self._W_sparse = None # resolved at fit time + self._estimator = estimator + + # ------------------------------------------------------------------ + # Properties + # ------------------------------------------------------------------ + + @property + def W(self): + """The spatial weights matrix (libpysal.graph.Graph).""" + return self._W_input + + # ------------------------------------------------------------------ + # Estimation + # ------------------------------------------------------------------ + + def _pre_fit(self, arrays: ChoiceArrays) -> None: + """Resolve the spatial weights matrix.""" + self._W_sparse = resolve_spatial_weights( + self._W_input, arrays.n_alts, row_standardize=True + )[1] # get the CSR sparse + + def fit(self, **kwargs) -> FitResult: + """Estimate the model and return results. + + Dispatches to the PML estimator (JAX autodiff) or the + linearized GMM estimator based on the ``estimator`` setting. + """ + if self._estimator == "linearized_gmm": + return self._fit_linearized_gmm() + return super().fit(**kwargs) + + def _fit_linearized_gmm(self) -> FitResult: + """Two-step linearized GMM estimation (Carrión-Flores et al. 2018).""" + arrays = self._get_arrays() + self._arrays = arrays + self._pre_fit(arrays) + + from .._kernels.sar_mnl_numpy import fit_linearized_gmm + + result_dict = fit_linearized_gmm(arrays, self._W_sparse) + + beta = result_dict["beta"] + rho = result_dict["rho"] + se = result_dict["se"] + ll = result_dict["log_likelihood"] + + utility_param_names = list(arrays.param_names) + k = len(utility_param_names) + display_values = np.concatenate([beta, [rho]]) + display_names = utility_param_names + ["rho"] + model_type = "SAR-MNL (Linearized GMM)" + n_params = len(display_values) + + # SEs: first k are beta SEs, last is rho SE + std_errors = se[: k + 1] + + coefficients = pd.Series(display_values, index=display_names, name="coefficient") + std_err_series = pd.Series(std_errors, index=display_names, name="std_error") + ll_null = _compute_null_ll(arrays) + + stats = _compute_fit_statistics( + ll=ll, + ll_null=ll_null, + n_obs=arrays.n_obs, + n_params=n_params, + n_alts=arrays.n_alts, + coefficients=coefficients, + std_errors=std_err_series, + model_type=model_type, + solver_name="linearized_gmm", + solver_result_raw=result_dict, + ) + + self._result = FitResult(spec=self._spec, **stats) + self._clear_caches() + return self._result + + def _build_objective(self, arrays: ChoiceArrays): + """Build the PML objective using JAX. + + Auto-selects dense solve (n_alts ≤ 2000) or conjugate gradient + (n_alts > 2000) based on the estimator setting. + """ + from .._jax.sar_kernels import build_sar_mnl_objective + + # Auto-select estimator + if self._estimator == "auto": + if arrays.n_alts <= 2000: + self._estimator = "pml" + else: + self._estimator = "pml_cg" + + use_cg = self._estimator == "pml_cg" + return build_sar_mnl_objective(arrays, self._W_sparse, use_cg=use_cg) + + def _get_solver_inputs(self, arrays: ChoiceArrays): + """Get initial values, param names, bounds, fixed mask. + + Appends an unconstrained ``alpha_rho`` (initial 0.0) so the + spatial autoregressive parameter is estimated alongside the + utility coefficients. ``rho = tanh(alpha_rho) ∈ (-1, 1)``. + """ + x0, names, bounds, fixed_mask = super()._get_solver_inputs(arrays) + x0 = np.concatenate([x0, np.zeros(1)]) + names = list(names) + ["alpha_rho"] + return x0, names, bounds, fixed_mask + + def _build_fit_result(self, solver_result: SolverResult, arrays: ChoiceArrays) -> FitResult: + """Build a FitResult from solver output.""" + all_params = solver_result.coefficients + utility_param_names = list(arrays.param_names) + k = len(utility_param_names) + + # Layout: [beta_1..k, alpha_rho] + beta = all_params[:k] + alpha_rho = all_params[k] + rho = np.tanh(alpha_rho) # ρ ∈ (-1, 1) + + display_values = np.concatenate([beta, [rho]]) + display_names = utility_param_names + ["rho"] + model_type = "Spatial Autoregressive Multinomial Logit" + n_params = len(display_values) + + # Standard errors via Hessian + std_errors = np.full(n_params, np.nan) + try: + hess = self._compute_hessian(all_params) + se_unconstrained = self._compute_std_errors_from_hessian(hess) + # Delta method: SE(rho) = (1 - rho^2) * SE(alpha_rho) + se_rho = (1.0 - rho**2) * se_unconstrained[k] + std_errors = np.concatenate([se_unconstrained[:k], [se_rho]]) + except Exception: + if solver_result.hessian is not None: + try: + se = np.sqrt(np.maximum(np.diag(solver_result.hessian), 0)) + se[se == 0] = np.nan + se_rho = (1.0 - rho**2) * se[k] + std_errors = np.concatenate([se[:k], [se_rho]]) + except Exception: + pass + + coefficients = pd.Series(display_values, index=display_names, name="coefficient") + std_err_series = pd.Series(std_errors, index=display_names, name="std_error") + ll = solver_result.log_likelihood + ll_null = _compute_null_ll(arrays) + + stats = _compute_fit_statistics( + ll=ll, + ll_null=ll_null, + n_obs=arrays.n_obs, + n_params=n_params, + n_alts=arrays.n_alts, + coefficients=coefficients, + std_errors=std_err_series, + model_type=model_type, + solver_name=solver_result.solver_name, + solver_result_raw=solver_result.raw, + ) + + return FitResult(spec=self._spec, **stats) + + # ------------------------------------------------------------------ + # Prediction + # ------------------------------------------------------------------ + + def probabilities(self, data=None, beta=None, rho=None): + """Compute choice probabilities under the SAR-MNL model. + + Uses the full PML model: spatially-filtered + variance-normalised + utilities, then standard MNL softmax. + + Parameters + ---------- + data : ChoiceTable or None + Data to predict on. If None, uses estimation data. + beta : np.ndarray or None + Utility coefficients. If None, uses estimated values. + rho : float or None + Spatial autoregressive parameter. If None, uses estimated value. + + Returns + ------- + np.ndarray, shape (n_obs, n_alts) + Choice probabilities for each observation and alternative. + """ + from .._kernels.mnl_numpy import mnl_probs_numpy + + if self._arrays is None: + raise RuntimeError("Model must be estimated before prediction.") + + arrays = self._arrays + if data is not None: + arrays = data.to_arrays( + formula=self._spec.formula, + spec=self._spec if self._spec.formula is None else None, + ) + + n_obs = arrays.n_obs + n_alts = arrays.n_alts + k = arrays.design_matrix.shape[1] + + if beta is None: + coef_vals = np.asarray(self._result.coefficients.values, dtype=np.float64) + beta = coef_vals[:k] + if rho is None: + rho = float(self._result.coefficients.values[k]) + + dm = np.asarray(arrays.design_matrix, dtype=np.float64) + W_dense = np.asarray(self._W_sparse.toarray(), dtype=np.float64) + + # Base utilities + V_base = (dm @ beta).reshape(n_obs, n_alts) + + # Sampling correction + from .._sampling.correction import apply_sampling_correction + + V_base = apply_sampling_correction(V_base, arrays) + + # Spatial filter + variance normalisation + A = np.eye(n_alts) - rho * W_dense + V_filtered = np.linalg.solve(A, V_base.T).T + D = np.diag(np.linalg.inv(A)) + V_star = V_filtered / D[None, :] + + # Availability + if arrays.available is not None: + available = np.asarray(arrays.available, dtype=np.float64).reshape(n_obs, n_alts) + else: + available = np.ones((n_obs, n_alts), dtype=np.float64) + + return mnl_probs_numpy(V_star, available, inclusion_probs=None) + + def utilities(self, data=None, beta=None, rho=None): + """Compute spatially-filtered + variance-normalised utilities. + + Parameters + ---------- + data : ChoiceTable or None + Data to compute utilities on. If None, uses estimation data. + beta : np.ndarray or None + Utility coefficients. If None, uses estimated values. + rho : float or None + Spatial autoregressive parameter. If None, uses estimated value. + + Returns + ------- + np.ndarray, shape (n_obs, n_alts) + Spatially-filtered and variance-normalised utilities. + """ + if self._arrays is None: + raise RuntimeError("Model must be estimated before prediction.") + + arrays = self._arrays + if data is not None: + arrays = data.to_arrays( + formula=self._spec.formula, + spec=self._spec if self._spec.formula is None else None, + ) + + n_obs = arrays.n_obs + n_alts = arrays.n_alts + k = arrays.design_matrix.shape[1] + + if beta is None: + beta = np.asarray(self._result.coefficients.values[:k], dtype=np.float64) + if rho is None: + rho = float(self._result.coefficients.values[k]) + + dm = np.asarray(arrays.design_matrix, dtype=np.float64) + W_dense = np.asarray(self._W_sparse.toarray(), dtype=np.float64) + + V_base = (dm @ beta).reshape(n_obs, n_alts) + + from .._sampling.correction import apply_sampling_correction + + V_base = apply_sampling_correction(V_base, arrays) + + A = np.eye(n_alts) - rho * W_dense + V_filtered = np.linalg.solve(A, V_base.T).T + D = np.diag(np.linalg.inv(A)) + V_star = V_filtered / D[None, :] + + return V_star + + # ------------------------------------------------------------------ + # Marginal effects (LeSage & Pace 2009) + # ------------------------------------------------------------------ + + def marginal_effects(self, data=None, variable: Optional[str] = None): + """Compute average direct, indirect, and total marginal effects. + + In the SAR-MNL model, a change in an attribute of alternative + *j* affects not only *j*'s utility but also neighbouring + alternatives through the spatial multiplier + :math:`(I - \\rho W)^{-1}`. + + Following LeSage & Pace (2009), the marginal effect of variable + *r* on the probability of choosing alternative *k* is an + :math:`J \\times J` matrix. Summary measures are: + + - **Direct effect**: average of diagonal elements (impact on + own alternative). + - **Indirect effect**: average of off-diagonal row sums + (spillover to neighbouring alternatives). + - **Total effect**: direct + indirect. + + Parameters + ---------- + data : ChoiceTable or None + Data to compute marginal effects on. If None, uses + estimation data. + variable : str + Name of the variable to compute marginal effects for. + + Returns + ------- + dict + Dictionary with keys ``"direct"``, ``"indirect"``, + ``"total"``, each mapping to a ``pd.Series`` indexed by + alternative ID. + """ + if self._arrays is None: + raise RuntimeError("Model must be estimated before computing marginal effects.") + + from ..data.choicetable import ChoiceTable + + ct = self._data + arrays = self._arrays + if data is not None: + if not isinstance(data, ChoiceTable): + raise TypeError("data must be a ChoiceTable") + arrays = data.to_arrays( + formula=self._spec.formula, + spec=self._spec if self._spec.formula is None else None, + ) + ct = data + + n_obs = arrays.n_obs + n_alts = arrays.n_alts + k = arrays.design_matrix.shape[1] + + # Get estimated parameters + coef_vals = np.asarray(self._result.coefficients.values, dtype=np.float64) + beta = coef_vals[:k] + rho = float(coef_vals[k]) + + # Get the beta for the requested variable + param_names = list(arrays.param_names) + if variable not in param_names: + raise ValueError(f"Variable '{variable}' not found in parameters: {param_names}") + beta_r = beta[param_names.index(variable)] + + # Compute probabilities + probs = self.probabilities(data=data) + + # Spatial multiplier (I - rho*W)^{-1} + W_dense = np.asarray(self._W_sparse.toarray(), dtype=np.float64) + A = np.eye(n_alts) - rho * W_dense + Z_mat = np.linalg.inv(A) # (n_alts, n_alts) + + # Marginal effect matrix for variable r, alternative k: + # ME_{k,r} = P_k * (beta_r * Z_{kk} - sum_l P_l * beta_r * Z_{lk}) + # = beta_r * P_k * (Z_{kk} - sum_l P_l * Z_{lk}) + # But P varies across choosers. For the average marginal effect, + # we average over choosers: + # AME_{k,r} = beta_r * avg(P_k) * (Z_{kk} - sum_l avg(P_l) * Z_{lk}) + + # Compute the J×J marginal effect matrix (averaged over choosers) + # ME[j, k] = beta_r * avg_probs[k] * (Z[k, j] - sum_l avg_probs[l] * Z[l, j]) + # But the standard LeSage-Pace formulation for MNL is: + # dP_k/dX_j = P_k * (delta_{kj} - P_j) * beta_r * Z[j, ...] + # This is complex — we use the simpler average approach: + # + # For each alternative k, the direct effect is: + # dP_k/dX_k = beta_r * Z[k,k] * P_k * (1 - P_k) + # The indirect (spillover) effect from j to k (j != k) is: + # dP_k/dX_j = -beta_r * Z[k,j] * P_k * P_j + # But with the spatial multiplier, Z replaces the identity. + + # Direct effects: average over choosers of + # beta_r * Z[k,k] * P_ik * (1 - P_ik) + direct = np.zeros(n_alts) + indirect = np.zeros(n_alts) + for k_alt in range(n_alts): + # Direct: own-alternative effect + direct[k_alt] = ( + beta_r * Z_mat[k_alt, k_alt] * np.mean(probs[:, k_alt] * (1 - probs[:, k_alt])) + ) + # Indirect: spillover from neighbours + # Sum over j != k of dP_k/dX_j = -beta_r * sum_{j!=k} Z[k,j] * P_k * P_j + for j_alt in range(n_alts): + if j_alt != k_alt: + indirect[k_alt] += ( + -beta_r * Z_mat[k_alt, j_alt] * np.mean(probs[:, k_alt] * probs[:, j_alt]) + ) + + total = direct + indirect + + # Get alternative IDs from the data + df = ct.to_frame() + alt_ids = df[ct.alt_id_col].values.reshape(n_obs, n_alts)[0] + + return { + "direct": pd.Series(direct, index=alt_ids, name=f"direct_{variable}"), + "indirect": pd.Series(indirect, index=alt_ids, name=f"indirect_{variable}"), + "total": pd.Series(total, index=alt_ids, name=f"total_{variable}"), + } From 12389b3460ea68ec5dc98a64207304ab74e02ae1 Mon Sep 17 00:00:00 2001 From: knaaptime Date: Sun, 21 Jun 2026 08:55:10 -0700 Subject: [PATCH 7/7] update docs --- docs/source/api.rst | 7 - docs/source/user-guide/sar_mnl.md | 82 ++- docs/source/user-guide/sar_mnl_demo.ipynb | 137 ++++- .../user-guide/spatial_models_demo.ipynb | 34 +- .../user-guide/spatial_models_lag_demo.ipynb | 506 ++++++++++++++++++ 5 files changed, 710 insertions(+), 56 deletions(-) create mode 100644 docs/source/user-guide/spatial_models_lag_demo.ipynb diff --git a/docs/source/api.rst b/docs/source/api.rst index cbdc7ac..bff8edb 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -50,13 +50,6 @@ Models ChoiceModel :no-index: -.. currentmodule:: locpick.models.sar_mnl - -.. autosummary:: - :toctree: generated/ - - SARMNL :no-index: - .. currentmodule:: locpick.models.nested .. autosummary:: diff --git a/docs/source/user-guide/sar_mnl.md b/docs/source/user-guide/sar_mnl.md index 9f2d965..3423706 100644 --- a/docs/source/user-guide/sar_mnl.md +++ b/docs/source/user-guide/sar_mnl.md @@ -1,16 +1,17 @@ # Spatial Autoregressive Multinomial Logit (SAR-MNL) ```{note} -This user guide covers the `SARMNL` model class, which implements a -spatial autoregressive lag in the utility of alternatives (spatial -locations) using the pseudo maximum likelihood (PML) estimator from -Smirnov (2010). +This user guide covers SAR-MNL estimation via ``ChoiceModel`` with +``graph=`` and ``lag=True``, which implements a spatial autoregressive +lag in the utility of alternatives (spatial locations) using the pseudo +maximum likelihood (PML) estimator from Smirnov (2010). ``` ## Overview -The `SARMNL` class models spatial spillover in the **systematic utility** -of alternatives via a spatial autoregressive (SAR) lag: +The `ChoiceModel` class with `lag=True` models spatial spillover in the +**systematic utility** of alternatives via a spatial autoregressive (SAR) +lag: $$V_j = \rho \sum_k w_{jk} V_k + Z_j \beta + X_{ij} \gamma$$ @@ -23,6 +24,20 @@ normalised by $D = \text{diag}((I - \rho W)^{-1})$ for consistency (Smirnov 2010). Choice probabilities follow standard MNL softmax over the spatially-filtered, variance-normalised utilities. +### SAR vs SCL + +The `graph=` parameter can be used with two spatial mechanisms: + +| `lag=` | Mechanism | ρ range | Transform | +|--------|-----------|---------|-----------| +| `False` (default) | SCL (GEV paired nests) | (0, 1] | Sigmoid | +| `True` | SAR (spatial autoregressive lag) | (-1, 1) | Tanh | + +SAR models spatial spillover in the **systematic utility** via the +spatial multiplier $(I - \rho W)^{-1}$. SCL models spatial correlation +via GEV paired nests between adjacent alternatives. They are different +mechanisms for the same spatial weights matrix. + ### Key features - **PML estimator** (Smirnov 2010): consistent, no log-determinant needed @@ -31,11 +46,12 @@ the spatially-filtered, variance-normalised utilities. - **Linearized GMM fallback** (Carrión-Flores et al. 2018): for very large J - **libpysal Graph support**: canonical W type, matching bayespecon - **Marginal effects**: direct, indirect, and total (LeSage & Pace 2009) +- **Composable with Mixed and Nested logit**: SAR + Mixed, SAR + Nested, SAR + Mixed + Nested ## Quick Start ```python -from locpick import ChoiceTable, SARMNL +from locpick import ChoiceTable, ChoiceModel from libpysal.graph import Graph # Build spatial weights matrix connecting alternatives @@ -45,7 +61,7 @@ W = Graph.build_knn(gdf, k=7).transform("r") ct = ChoiceTable.from_tables(choosers, alternatives, chosen_alternatives=choices) # Estimate SAR-MNL -model = SARMNL(ct, formula="cost + time - 1", W=W) +model = ChoiceModel(ct, formula="cost + time - 1", graph=W, lag=True) result = model.fit() print(result.summary()) ``` @@ -64,13 +80,13 @@ variance normalisation $\text{diag}((I - \rho W)^{-1})$. ```python # Auto-select (default) -model = SARMNL(ct, formula="cost + time - 1", W=W) +model = ChoiceModel(ct, formula="cost + time - 1", graph=W, lag=True) # Force dense solve -model = SARMNL(ct, formula="cost + time - 1", W=W, estimator="pml") +model = ChoiceModel(ct, formula="cost + time - 1", graph=W, lag=True, estimator="pml") # Force conjugate gradient -model = SARMNL(ct, formula="cost + time - 1", W=W, estimator="pml_cg") +model = ChoiceModel(ct, formula="cost + time - 1", graph=W, lag=True, estimator="pml_cg") ``` ### Linearized GMM @@ -83,13 +99,13 @@ inversion entirely via a two-step procedure: 2. **Step 2**: Two-stage least squares (TSLS) with instruments $[X, WX]$ ```python -model = SARMNL(ct, formula="cost + time - 1", W=W, estimator="linearized_gmm") +model = ChoiceModel(ct, formula="cost + time - 1", graph=W, lag=True, estimator="linearized_gmm") result = model.fit() ``` ## Spatial Weights Matrix -`SARMNL` accepts `libpysal.graph.Graph` as the canonical W type +`ChoiceModel` accepts `libpysal.graph.Graph` as the canonical W type (matching the bayespecon package). `scipy.sparse` matrices and dense NumPy arrays are also accepted for convenience. @@ -106,6 +122,46 @@ W = Graph.build_contiguity(gdf, rook=False).transform("r") W = Graph.build_distance_band(gdf, threshold=1000).transform("r") ``` +## Composing with Mixed and Nested Logit + +SAR composes naturally with mixed logit (random coefficients) and nested +logit (hierarchical choice). The spatial filter is applied to utilities +*before* the GEV/softmax/mixed-logit logic, so the mechanisms are +independent. + +### SAR + Nested + +```python +from locpick import ChoiceModel, NestingTree + +nests = NestingTree(...) +model = ChoiceModel(ct, formula="cost + time - 1", graph=W, lag=True, nests=nests) +result = model.fit() +``` + +### SAR + Mixed + +```python +from locpick import ChoiceModel, ParamDistribution + +random_params = {"time": ParamDistribution("normal", "time")} +model = ChoiceModel( + ct, formula="cost + time - 1", graph=W, lag=True, + random_params=random_params, n_draws=200, +) +result = model.fit() +``` + +### SAR + Mixed + Nested + +```python +model = ChoiceModel( + ct, formula="cost + time - 1", graph=W, lag=True, + nests=nests, random_params=random_params, n_draws=200, +) +result = model.fit() +``` + ## Marginal Effects In the SAR-MNL model, a change in an attribute of alternative $j$ diff --git a/docs/source/user-guide/sar_mnl_demo.ipynb b/docs/source/user-guide/sar_mnl_demo.ipynb index 6c11de7..81bcc19 100644 --- a/docs/source/user-guide/sar_mnl_demo.ipynb +++ b/docs/source/user-guide/sar_mnl_demo.ipynb @@ -40,10 +40,80 @@ "import numpy as np\n", "import pandas as pd\n", "\n", - "from locpick import SARMNL, ChoiceModel\n", + "from locpick import ChoiceModel\n", "from locpick.dgp import simulate_sar_mnl" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "95219f10", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28ae38ee", + "metadata": {}, + "outputs": [], + "source": [ + "import geosnap as gsp" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55d92adc", + "metadata": {}, + "outputs": [], + "source": [ + "datasets = gsp.DataStore()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e8f0e2b3", + "metadata": {}, + "outputs": [], + "source": [ + "dc = gsp.io.get_acs(datasets, years=2019, level=\"tract\", state_fips=\"11\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d5f5463", + "metadata": {}, + "outputs": [], + "source": [ + "dc.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3589ee6d", + "metadata": {}, + "outputs": [], + "source": [ + "from libpysal.graph import Graph\n", + "\n", + "dc_graph = Graph.build_contiguity(dc, rook=False)\n", + "\n", + "adj = dc_graph.sparse.todense()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "07983d83", + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "markdown", "id": "18e91aa6", @@ -67,7 +137,7 @@ "source": [ "# Generate synthetic SAR-MNL data\n", "# n_obs=2000 choosers, n_alts=12 alternatives, rho=0.5\n", - "dataset = simulate_sar_mnl(n_obs=2000, n_alts=12, rho=0.5, seed=42)\n", + "dataset = simulate_sar_mnl(n_obs=2000, n_alts=dc.shape[0], rho=0.5, seed=42, W=dc_graph)\n", "\n", "print(f\"Observations: {dataset.n_obs}\")\n", "print(f\"Alternatives: {dataset.n_alts}\")\n", @@ -94,6 +164,16 @@ "- **`solver`**: Default `\"lbfgs\"` (scipy L-BFGS-B, the fastest path)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "87784679", + "metadata": {}, + "outputs": [], + "source": [ + "dataset.choice_table.to_frame()" + ] + }, { "cell_type": "code", "execution_count": null, @@ -103,15 +183,13 @@ "source": [ "# Create the SAR-MNL model\n", "# W is the spatial weights matrix from the DGP (circular adjacency)\n", - "model_sar = SARMNL(\n", - " dataset.choice_table,\n", - " formula=\"cost + time + income_x_cost - 1\",\n", - " W=dataset.adjacency,\n", + "model_sar = ChoiceModel(\n", + " dataset.choice_table, formula=\"alt_attr + obs_x_alt - 1\", graph=dataset.W, lag=True\n", ")\n", "\n", "print(f\"Model: {model_sar}\")\n", - "print(f\"W shape: {dataset.adjacency.shape}\")\n", - "print(f\"W type: {type(dataset.adjacency).__name__}\")" + "print(f\"W shape: {dataset.W.n}\")\n", + "print(f\"W type: {type(dataset.W).__name__}\")" ] }, { @@ -149,7 +227,7 @@ "outputs": [], "source": [ "# Fit standard MNL (no spatial lag) for comparison\n", - "model_mnl = ChoiceModel(dataset.choice_table, formula=\"cost + time + income_x_cost - 1\")\n", + "model_mnl = ChoiceModel(dataset.choice_table, formula=\"alt_attr + obs_x_alt - 1\")\n", "t0 = time.perf_counter()\n", "result_mnl = model_mnl.fit()\n", "t_mnl = time.perf_counter() - t0\n", @@ -248,6 +326,16 @@ "print(f\"\\nMean absolute probability difference: {np.mean(np.abs(probs_sar - probs_mnl)):.6f}\")" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb21d792", + "metadata": {}, + "outputs": [], + "source": [ + "dc.assign(probs=probs_sar[0]).plot(\"probs\", scheme=\"quantiles\")" + ] + }, { "cell_type": "markdown", "id": "46140f2d", @@ -288,9 +376,8 @@ "source": [ "## Summary\n", "\n", - "The `SARMNL` class provides spatial autoregressive MNL estimation via pseudo maximum likelihood:\n", + "The `ChoiceModel` class provides spatial autoregressive MNL estimation via pseudo maximum likelihood:\n", "\n", - "- **Unified API**: Uses the same `ChoiceTable` + formula interface as `ChoiceModel`\n", "- **Spatial weights**: Accepts libpysal Graph, scipy.sparse, or dense ndarray for `W`\n", "- **JAX-accelerated**: Log-likelihood and gradient computed via JAX autodiff through the spatial solve\n", "- **Hybrid estimation**: scipy LBFGS solver + JAX kernels (the fastest path for all locpick models)\n", @@ -309,11 +396,37 @@ "Use **SAR-MNL** when you want a spatial lag in utilities (spillover effects).\n", "Use **SCL** when you want spatial correlation in the error structure (GEV)." ] + }, + { + "cell_type": "markdown", + "id": "71dc586c", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "id": "b8f831e6", + "metadata": {}, + "source": [] } ], "metadata": { + "kernelspec": { + "display_name": "locpick", + "language": "python", + "name": "python3" + }, "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" } }, "nbformat": 4, diff --git a/docs/source/user-guide/spatial_models_demo.ipynb b/docs/source/user-guide/spatial_models_demo.ipynb index 95108c5..c94298c 100644 --- a/docs/source/user-guide/spatial_models_demo.ipynb +++ b/docs/source/user-guide/spatial_models_demo.ipynb @@ -25,7 +25,7 @@ "import numpy as np\n", "import pandas as pd\n", "\n", - "from locpick import MNL, MixedMNL, MixedNestedMNL, NestedMNL\n", + "from locpick import ChoiceModel\n", "from locpick.dgp import simulate_scl\n", "from locpick.models.mixed import ParamDistribution\n", "from locpick.models.nested import NestingTree, NestSpec" @@ -107,15 +107,6 @@ "- `true_rho`: the ground-truth spatial correlation parameter" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "simulate_scl?" - ] - }, { "cell_type": "code", "execution_count": null, @@ -171,7 +162,7 @@ "adj = scl_dataset.adjacency\n", "\n", "# 1. Spatial MNL (SCL) — spatial correlation only\n", - "model_scl = MNL(\n", + "model_scl = ChoiceModel(\n", " ct,\n", " formula=formula,\n", " graph=adj,\n", @@ -185,7 +176,7 @@ " ]\n", ")\n", "\n", - "model_nested_scl = NestedMNL(\n", + "model_nested_scl = ChoiceModel(\n", " ct,\n", " formula=formula,\n", " graph=adj,\n", @@ -197,7 +188,7 @@ " \"time\": ParamDistribution(distribution=\"normal\", param=\"time\"),\n", "}\n", "\n", - "model_mixed_scl = MixedMNL(\n", + "model_mixed_scl = ChoiceModel(\n", " ct,\n", " formula=formula,\n", " graph=adj,\n", @@ -206,7 +197,7 @@ ")\n", "\n", "# 4. Spatial MixedNestedMNL — spatial + nesting + random coefficients\n", - "model_mixed_nested_scl = MixedNestedMNL(\n", + "model_mixed_nested_scl = ChoiceModel(\n", " ct,\n", " formula=formula,\n", " graph=adj,\n", @@ -387,14 +378,9 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] + "source": [ + "dc.assign(probs=probs_scl[0]).plot(\"probs\", scheme=\"quantiles\")" + ] }, { "cell_type": "code", @@ -406,9 +392,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python [conda env:locpick]", + "display_name": "locpick", "language": "python", - "name": "conda-env-locpick-py" + "name": "python3" }, "language_info": { "codemirror_mode": { diff --git a/docs/source/user-guide/spatial_models_lag_demo.ipynb b/docs/source/user-guide/spatial_models_lag_demo.ipynb new file mode 100644 index 0000000..9940680 --- /dev/null +++ b/docs/source/user-guide/spatial_models_lag_demo.ipynb @@ -0,0 +1,506 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Spatial Models Demo: SAR-MNL with `graph=` and `lag=True`\n", + "\n", + "This notebook demonstrates the four spatial logit model variants in `locpick`. When `lag=True` is combined with `graph=`, the model becomes a Spatial Autoregressive (SAR) logit — alternatives influence each other through a spatial weights matrix:\n", + "\n", + "- **`ChoiceModel(..., graph=g, lag=True)`** — SAR-MNL (spatial autoregressive logit)\n", + "- **`ChoiceModel(..., graph=g, nests=..., lag=True)`** — SAR + nesting\n", + "- **`ChoiceModel(..., graph=g, random_params=..., lag=True)`** — SAR + random taste variation\n", + "- **`ChoiceModel(..., graph=g, nests=..., random_params=..., lag=True)`** — SAR + nesting + random variation\n", + "\n", + "We use synthetic data generated by `simulate_sar_mnl` with a known spatial adjacency structure from DC census tracts." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "from locpick import ChoiceModel\n", + "from locpick.dgp import simulate_sar_mnl\n", + "from locpick.models.mixed import ParamDistribution\n", + "from locpick.models.nested import NestingTree, NestSpec" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import geosnap as gsp" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "datasets = gsp.DataStore()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dc = gsp.io.get_acs(datasets, years=2019, level=\"tract\", state_fips=\"11\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dc.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from libpysal.graph import Graph\n", + "\n", + "dc_graph = Graph.build_contiguity(dc, rook=False).transform(\"r\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Generate Synthetic Data\n", + "\n", + "We use `simulate_sar_mnl` to create a synthetic dataset with known spatial autoregressive structure. The DGP produces:\n", + "- `choosers`: household-level observations with a random feature\n", + "- `alternatives`: tract-level attributes (`cost`, `time`)\n", + "- `W`: a spatial weights matrix as a `libpysal.graph.Graph`\n", + "- `true_rho`: the ground-truth spatial autoregressive parameter" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Generate synthetic SAR-MNL data with spatial correlation\n", + "n_obs = 2000\n", + "n_alts = dc.shape[0]\n", + "\n", + "scl_dataset = simulate_sar_mnl(\n", + " n_obs=n_obs,\n", + " n_alts=n_alts,\n", + " alt_params={\"cost\": -0.5, \"time\": -0.2},\n", + " rho=0.5,\n", + " seed=42,\n", + " W=dc_graph,\n", + ")\n", + "\n", + "ct = scl_dataset.choice_table\n", + "print(f\"Observations: {ct.n_observations}\")\n", + "print(f\"Alternatives: {ct.n_alternatives}\")\n", + "print(f\"True rho: {scl_dataset.true_rho}\")\n", + "print(f\"W shape: {scl_dataset.W.sparse.shape}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Configure Spatial Models\n", + "\n", + "We configure four SAR model variants:\n", + "\n", + "1. **`ChoiceModel(graph=g, lag=True)`** — spatial autoregressive logit only\n", + "2. **`ChoiceModel(graph=g, nests=..., lag=True)`** — spatial + nesting structure\n", + "3. **`ChoiceModel(graph=g, random_params=..., lag=True)`** — spatial + random taste variation\n", + "4. **`ChoiceModel(graph=g, nests=..., random_params=..., lag=True)`** — spatial + nesting + random variation\n", + "\n", + "Each model shares the same `ChoiceTable` and formula but adds structural complexity. The `graph` parameter accepts a `libpysal.graph.Graph`, a `scipy.sparse` array, or a dense numpy adjacency matrix." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Common formula for all models — must match columns generated by simulate_sar_mnl\n", + "formula = \"cost + time - 1\"\n", + "\n", + "# Use the Graph from the DGP directly (preferred input type)\n", + "W = scl_dataset.W\n", + "\n", + "# 1. SAR MNL\n", + "model_scl = ChoiceModel(ct, formula=formula, graph=W, lag=True)\n", + "\n", + "# 2. SAR NestedMNL — spatial + nesting\n", + "nest_tree = NestingTree(\n", + " nests=[\n", + " NestSpec(name=\"urban\", alt_ids=list(range(0, n_alts // 2))),\n", + " NestSpec(name=\"suburban\", alt_ids=list(range(n_alts // 2, n_alts))),\n", + " ]\n", + ")\n", + "\n", + "model_nested_scl = ChoiceModel(ct, formula=formula, graph=W, nests=nest_tree, lag=True)\n", + "\n", + "# 3. SAR MixedMNL — spatial + random coefficients\n", + "random_params = {\n", + " \"time\": ParamDistribution(distribution=\"normal\", param=\"time\"),\n", + "}\n", + "\n", + "model_mixed_scl = ChoiceModel(\n", + " ct, formula=formula, graph=W, random_params=random_params, n_draws=100, lag=True\n", + ")\n", + "\n", + "# 4. SAR MixedNestedMNL — spatial + nesting + random coefficients\n", + "model_mixed_nested_scl = ChoiceModel(\n", + " ct,\n", + " formula=formula,\n", + " graph=W,\n", + " nests=nest_tree,\n", + " random_params=random_params,\n", + " n_draws=100,\n", + " lag=True,\n", + ")\n", + "\n", + "print(\"Models configured:\")\n", + "print(f\" Spatial MNL: {type(model_scl).__name__}\")\n", + "print(f\" Spatial NestedMNL: {type(model_nested_scl).__name__}\")\n", + "print(f\" Spatial MixedMNL: {type(model_mixed_scl).__name__}\")\n", + "print(f\" Spatial MixedNestedMNL: {type(model_mixed_nested_scl).__name__}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Fit Spatial Models\n", + "\n", + "Fit each model and inspect the estimated coefficients. SAR-MNL estimates a `rho` parameter that captures spatial autocorrelation. The nested variant adds `lambda` nest dissimilarity parameters. The mixed variant adds `sd_*` random-coefficient standard deviations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fit spatial MNL\n", + "result_scl = model_scl.fit()\n", + "print(\"=== Spatial MNL ===\")\n", + "print(result_scl.summary())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fit spatial NestedMNL\n", + "result_nested_scl = model_nested_scl.fit()\n", + "print(\"=== Spatial NestedMNL ===\")\n", + "print(result_nested_scl.summary())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fit spatial MixedMNL\n", + "result_mixed_scl = model_mixed_scl.fit()\n", + "print(\"=== Spatial MixedMNL ===\")\n", + "print(result_mixed_scl.summary())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fit spatial MixedNestedMNL\n", + "result_mixed_nested_scl = model_mixed_nested_scl.fit()\n", + "print(\"=== Spatial MixedNestedMNL ===\")\n", + "print(result_mixed_nested_scl.summary())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Visualize Spatial Autocorrelation\n", + "\n", + "The SAR spatial filter $(I - \\rho W)^{-1}$ creates spatial autocorrelation across alternatives. Since alternatives are spatial locations connected by $W$, nearby locations have more similar utilities than distant ones. We verify this by computing Moran's I on the utility vectors and mapping the spatial pattern." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Collect fit statistics for comparison\n", + "results = {\n", + " \"Spatial MNL\": result_scl,\n", + " \"Spatial NestedMNL\": result_nested_scl,\n", + " \"Spatial MixedMNL\": result_mixed_scl,\n", + " \"Spatial MixedNestedMNL\": result_mixed_nested_scl,\n", + "}\n", + "\n", + "comparison = pd.DataFrame(\n", + " {\n", + " name: {\n", + " \"Log-Likelihood\": r.log_likelihood,\n", + " \"Null LL\": r.log_likelihood_null,\n", + " \"AIC\": r.aic,\n", + " \"BIC\": r.bic,\n", + " \"Rho²\": r.rho_squared,\n", + " \"Adj. Rho²\": r.rho_bar_squared,\n", + " \"n_params\": r.n_parameters,\n", + " }\n", + " for name, r in results.items()\n", + " }\n", + ").T\n", + "\n", + "print(comparison.round(4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Predict Choice Probabilities\n", + "\n", + "Predict choice probabilities for the SAR model. Since we only have alternative-level attributes (`cost`, `time`), all choosers have the same probability vector — the spatial structure comes entirely from the spatial filter $(I - \\rho W)^{-1}$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Predict probabilities using model.probabilities() (no arguments uses fitted parameters)\n", + "probs_scl = model_scl.probabilities()\n", + "\n", + "print(f\"Spatial MNL probabilities shape: {probs_scl.shape}\")\n", + "print(f\"Probabilities sum to 1: {np.allclose(probs_scl.sum(axis=1), 1.0)}\")\n", + "print(\"\\nFirst 3 decision-makers' probabilities:\")\n", + "print(probs_scl[:3].round(4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Spatial Autocorrelation Diagnostics\n", + "\n", + "We verify that the SAR spatial filter creates spatial autocorrelation across alternatives by computing Moran's I on utility vectors and probability vectors. We also compare SAR probabilities against plain MNL to visualize the spatial smoothing effect." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from esda.moran import Moran\n", + "\n", + "# Fit plain MNL for comparison\n", + "model_mnl = ChoiceModel(ct, formula=formula)\n", + "result_mnl = model_mnl.fit()\n", + "probs_mnl = model_mnl.probabilities()\n", + "\n", + "# Get W as dense matrix for utility decomposition\n", + "W_dense = np.asarray(W.sparse.todense(), dtype=np.float64)\n", + "\n", + "# Moran's I on probability vectors\n", + "moran_sar = Moran(probs_scl[0], W)\n", + "moran_mnl = Moran(probs_mnl[0], W)\n", + "print(f\"Moran's I (SAR probs): {moran_sar.I:.4f} (p={moran_sar.p_sim:.4f})\")\n", + "print(f\"Moran's I (MNL probs): {moran_mnl.I:.4f} (p={moran_mnl.p_sim:.4f})\")\n", + "\n", + "# Show how spatial filter transforms utilities\n", + "rho_est = float(result_scl.coefficients[\"rho\"])\n", + "A = np.eye(n_alts) - rho_est * W_dense\n", + "A_inv = np.linalg.inv(A)\n", + "D = np.diag(A_inv)\n", + "\n", + "beta_est = np.array([result_scl.coefficients[\"cost\"], result_scl.coefficients[\"time\"]])\n", + "dm = np.asarray(model_scl._arrays.design_matrix, dtype=np.float64)\n", + "V_base = (dm @ beta_est).reshape(n_obs, n_alts)[0]\n", + "V_filtered = np.linalg.solve(A, V_base)\n", + "V_star = V_filtered / D\n", + "\n", + "# Moran's I on utility vectors (estimated rho)\n", + "moran_V_base = Moran(V_base, W)\n", + "moran_V_filtered = Moran(V_filtered, W)\n", + "moran_V_star = Moran(V_star, W)\n", + "print(f\"\\nMoran's I (V_base): {moran_V_base.I:.4f} (p={moran_V_base.p_sim:.4f})\")\n", + "print(f\"Moran's I (V_filtered): {moran_V_filtered.I:.4f} (p={moran_V_filtered.p_sim:.4f})\")\n", + "print(f\"Moran's I (V_star): {moran_V_star.I:.4f} (p={moran_V_star.p_sim:.4f})\")\n", + "\n", + "print(f\"\\nρ = {rho_est:.4f}\")\n", + "print(f\"V_base std: {V_base.std():.4f}\")\n", + "print(f\"V_filtered std: {V_filtered.std():.4f}\")\n", + "print(f\"V_star std: {V_star.std():.4f}\")\n", + "print(f\"D range: [{D.min():.4f}, {D.max():.4f}]\")\n", + "\n", + "# Map the difference between SAR and MNL probabilities\n", + "dc.assign(\n", + " sar_prob=probs_scl[0],\n", + " mnl_prob=probs_mnl[0],\n", + " diff=probs_scl[0] - probs_mnl[0],\n", + ").plot(\"diff\", scheme=\"quantiles\", legend=True, cmap=\"RdBu\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# --- Diagnostic: verify spatial filter creates spatial autocorrelation ---\n", + "# The SAR model says V_star = (I - ρW)^{-1} V_base / D\n", + "# If ρ > 0, the spatial filter should smooth V_base across neighbors,\n", + "# creating spatial autocorrelation in V_star across alternatives.\n", + "#\n", + "# Key insight: alternatives are spatial locations connected by W.\n", + "# The spatial filter creates correlation ACROSS ALTERNATIVES, not across choosers.\n", + "# With only alternative-level attributes (cost, time), V_base is the same\n", + "# for all choosers — the spatial structure comes entirely from (I - ρW)^{-1}.\n", + "\n", + "rho_true = scl_dataset.true_rho # 0.5\n", + "A_true = np.eye(n_alts) - rho_true * W_dense\n", + "A_true_inv = np.linalg.inv(A_true)\n", + "D_true = np.diag(A_true_inv)\n", + "\n", + "# V_base is the same for all choosers (only alt-level attributes)\n", + "V_base_vec = V_base # shape (n_alts,)\n", + "\n", + "# Apply spatial filter with TRUE rho\n", + "V_filtered_true = np.linalg.solve(A_true, V_base_vec)\n", + "V_star_true = V_filtered_true / D_true\n", + "\n", + "# Moran's I on utility vectors (TRUE rho)\n", + "moran_V_filtered_true = Moran(V_filtered_true, W)\n", + "moran_V_star_true = Moran(V_star_true, W)\n", + "\n", + "print(\"=== Spatial autocorrelation in utility vectors ===\")\n", + "print(f\"True ρ = {rho_true:.1f}, Estimated ρ = {rho_est:.4f}\")\n", + "print(f\"\\nMoran's I (V_base): {moran_V_base.I:.4f} (p={moran_V_base.p_sim:.4f})\")\n", + "print(f\"Moran's I (V_filtered, ρ̂): {moran_V_filtered.I:.4f} (p={moran_V_filtered.p_sim:.4f})\")\n", + "print(f\"Moran's I (V_star, ρ̂): {moran_V_star.I:.4f} (p={moran_V_star.p_sim:.4f})\")\n", + "print(\n", + " f\"Moran's I (V_filtered, ρ=0.5): {moran_V_filtered_true.I:.4f} (p={moran_V_filtered_true.p_sim:.4f})\"\n", + ")\n", + "print(f\"Moran's I (V_star, ρ=0.5): {moran_V_star_true.I:.4f} (p={moran_V_star_true.p_sim:.4f})\")\n", + "\n", + "print(f\"\\nV_base std: {V_base_vec.std():.4f}\")\n", + "print(f\"V_filtered std (ρ̂): {V_filtered.std():.4f}\")\n", + "print(f\"V_star std (ρ̂): {V_star.std():.4f}\")\n", + "print(f\"V_filtered std (ρ=0.5): {V_filtered_true.std():.4f}\")\n", + "print(f\"V_star std (ρ=0.5): {V_star_true.std():.4f}\")\n", + "print(f\"D range (ρ̂): [{D.min():.4f}, {D.max():.4f}]\")\n", + "print(f\"D range (ρ=0.5): [{D_true.min():.4f}, {D_true.max():.4f}]\")\n", + "\n", + "# Probabilities with true rho\n", + "probs_true = np.exp(V_star_true) / np.exp(V_star_true).sum()\n", + "moran_probs_true = Moran(probs_true, W)\n", + "moran_probs_mnl_vec = Moran(np.exp(V_base_vec) / np.exp(V_base_vec).sum(), W)\n", + "print(f\"\\nMoran's I (probs SAR, ρ̂): {moran_sar.I:.4f} (p={moran_sar.p_sim:.4f})\")\n", + "print(f\"Moran's I (probs SAR, ρ=0.5): {moran_probs_true.I:.4f} (p={moran_probs_true.p_sim:.4f})\")\n", + "print(\n", + " f\"Moran's I (probs MNL): {moran_probs_mnl_vec.I:.4f} (p={moran_probs_mnl_vec.p_sim:.4f})\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, axes = plt.subplots(2, 2, figsize=(14, 10))\n", + "\n", + "# 1. V_base (no spatial structure — random attributes)\n", + "dc.assign(V_base=V_base).plot(\"V_base\", ax=axes[0, 0], legend=True, cmap=\"viridis\")\n", + "axes[0, 0].set_title(f\"V_base (Moran's I = {moran_V_base.I:.3f})\")\n", + "\n", + "# 2. V_star with estimated rho (spatially filtered)\n", + "dc.assign(V_star=V_star).plot(\"V_star\", ax=axes[0, 1], legend=True, cmap=\"viridis\")\n", + "axes[0, 1].set_title(f\"V_star, ρ̂={rho_est:.3f} (Moran's I = {moran_V_star.I:.3f})\")\n", + "\n", + "# 3. V_star with true rho (stronger spatial pattern)\n", + "dc.assign(V_star_true=V_star_true).plot(\"V_star_true\", ax=axes[1, 0], legend=True, cmap=\"viridis\")\n", + "axes[1, 0].set_title(f\"V_star, ρ=0.5 (Moran's I = {moran_V_star_true.I:.3f})\")\n", + "\n", + "# 4. SAR vs MNL probability difference\n", + "dc.assign(\n", + " sar_prob=probs_scl[0],\n", + " mnl_prob=probs_mnl[0],\n", + " diff=probs_scl[0] - probs_mnl[0],\n", + ").plot(\"diff\", ax=axes[1, 1], scheme=\"quantiles\", legend=True, cmap=\"RdBu\")\n", + "axes[1, 1].set_title(\"SAR − MNL probability difference\")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "locpick", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}