├── .conda ├── bld.bat ├── build.sh └── meta.yaml ├── .envrc ├── .github └── workflows │ └── continuous-integration-workflow.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── LICENSE ├── README.rst ├── codecov.yml ├── docs ├── Makefile ├── make.bat ├── rtd_environment.yml └── source │ ├── _templates │ ├── custom-class-template.rst │ └── custom-module-template.rst │ ├── api.rst │ ├── conf.py │ ├── credits.rst │ ├── economics.rst │ ├── estimation.rst │ ├── index.rst │ ├── model_code.rst │ ├── references.rst │ ├── simulation.rst │ ├── tutorials.rst │ └── tutorials │ ├── replication │ ├── auxiliary_iskhakov.py │ ├── check_simulated_data_iskhakov.pickle │ ├── group_4.pkl │ ├── replication.ipynb │ ├── replication_iskhakov_et_al_2016.ipynb │ └── results_iskhakov.pickle │ └── simulation │ └── simulation_convergence.ipynb ├── environment.yml ├── ex_promotion_nbs.py ├── pyproject.toml ├── ruspy ├── __init__.py ├── config.py ├── estimation │ ├── __init__.py │ ├── config.py │ ├── criterion_function.py │ ├── estimation_transitions.py │ ├── mpec.py │ ├── nfxp.py │ └── pre_processing.py ├── model_code │ ├── __init__.py │ ├── choice_probabilities.py │ ├── cost_functions.py │ ├── demand_function.py │ └── fix_point_alg.py ├── simulation │ ├── __init__.py │ ├── simulation.py │ ├── simulation_functions.py │ └── simulation_model.py └── test │ ├── __init__.py │ ├── conftest.py │ ├── demand_test │ └── test_get_demand.py │ ├── estimation_tests │ ├── __init__.py │ ├── test_criterion_mpec.py │ ├── test_criterion_nfxp.py │ ├── test_estimation.py │ ├── test_mpec_functions.py │ ├── test_replication.py │ └── test_transition_estimate.py │ ├── old_tests │ ├── __init__.py │ ├── test_mpec_cubic_repl.py │ ├── test_mpec_hyperbolic_repl.py │ ├── test_mpec_linear_repl.py │ ├── test_mpec_quadratic_repl.py │ └── test_mpec_sqrt_repl.py │ ├── param_sim_test │ └── test_param_linear.py │ ├── ranodm_init.py │ ├── regression_sim_tests │ ├── __init__.py │ ├── regression_aux.py │ ├── test_cubic_regression.py │ ├── test_hyper_regression.py │ ├── test_linear_regression.py │ ├── test_quadratic_regression.py │ └── test_sqrt_regression.py │ ├── resources │ ├── demand_test │ │ └── get_demand.txt │ ├── estimation_test │ │ ├── choice_prob.txt │ │ ├── fixp.txt │ │ ├── mpec_constr_dev.txt │ │ ├── mpec_constraint.txt │ │ ├── mpec_like.txt │ │ ├── mpec_like_dev.txt │ │ ├── myop_cost.txt │ │ └── trans_mat.txt │ └── replication_test │ │ ├── group_4.pkl │ │ ├── repl_params_cubic.txt │ │ ├── repl_params_hyper.txt │ │ ├── repl_params_linear.txt │ │ ├── repl_params_quad.txt │ │ ├── repl_params_sqrt.txt │ │ ├── repl_test_trans.txt │ │ └── transition_count.txt │ └── simulation_output_test │ └── test_utility.py ├── setup.py └── tox.ini /.conda/bld.bat: -------------------------------------------------------------------------------- 1 | "%PYTHON%" setup.py install 2 | if errorlevel 1 exit 1 3 | -------------------------------------------------------------------------------- /.conda/build.sh: -------------------------------------------------------------------------------- 1 | $PYTHON setup.py install # Python command to install the script. 2 | -------------------------------------------------------------------------------- /.conda/meta.yaml: -------------------------------------------------------------------------------- 1 | {% set data = load_setup_py_data() %} 2 | 3 | package: 4 | name: ruspy 5 | version: {{ data.get('version') }} 6 | 7 | source: 8 | # git_url is nice in that it won't capture devenv stuff. However, it only captures 9 | # committed code, so pay attention. 10 | git_url: ../ 11 | 12 | build: 13 | number: 0 14 | noarch: python 15 | 16 | requirements: 17 | host: 18 | - python >=3.9, <3.10 19 | run: 20 | - python >=3.9, <3.10 21 | - statsmodels 22 | - numba 23 | - numpy >=1.23 24 | - pandas 25 | - pytest 26 | - pytest-xdist 27 | - scipy 28 | - estimagic >=0.4.3 29 | - nlopt 30 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | source activate ruspy 2 | 3 | # We need to be able to import our package 4 | export PYTHONPATH=$PWD:$PYTHONPATH 5 | 6 | export PROJECT_ROOT=$PWD -------------------------------------------------------------------------------- /.github/workflows/continuous-integration-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration Workflow 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - '*' 9 | 10 | jobs: 11 | run-tests: 12 | name: Run tests for ${{ matrix.os }} on ${{ matrix.python-version }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] 18 | python-version: ['3.9'] 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: conda-incubator/setup-miniconda@v2 22 | with: 23 | environment-file: environment.yml 24 | activate-environment: ruspy 25 | auto-update-conda: true 26 | python-version: ${{ matrix.python-version}} 27 | 28 | - name: Run pytest. 29 | shell: bash -l {0} 30 | run: pytest --cov-report=xml --cov=./ 31 | 32 | - name: Upload coverage report. 33 | if: runner.os == 'Linux' && matrix.python-version == '3.7' 34 | uses: codecov/codecov-action@v1 35 | with: 36 | token: df3e71e7-316d-4a04-b4d1-f76b90dfced7 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | __pycache__ 3 | pkl 4 | *.aux 5 | *.log 6 | *.bbl 7 | *.out 8 | *.blg 9 | *.pdf 10 | *.toc 11 | *.fls 12 | *.fdb_latexmk 13 | pip-wheel-metadata/ 14 | *.synctex.gz 15 | .ipynb* 16 | .pytest_cache 17 | ruspy.egg-info 18 | build 19 | test.ruspy.yml 20 | figures 21 | ruspy.rst 22 | modules.rst 23 | _generated 24 | *.db 25 | *.DS_Store 26 | results_ipopt.txt 27 | get_iskhakov_results 28 | 29 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceEconomics/ruspy/414e9f98e3b1cf19544b1ca3b1e544ac5751c1e9/.gitmodules -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Exclude every file or folder starting with a dot. 2 | exclude: ^\. 3 | repos: 4 | - repo: https://github.com/asottile/reorder_python_imports 5 | rev: v3.9.0 6 | hooks: 7 | - id: reorder-python-imports 8 | files: '(\.pyi?|wscript)$' 9 | - repo: https://github.com/pre-commit/pre-commit-hooks 10 | rev: v4.3.0 11 | hooks: 12 | - id: check-yaml 13 | exclude: 'meta\.yaml' 14 | - id: check-added-large-files 15 | args: ['--maxkb=10000'] 16 | - id: check-byte-order-marker 17 | types: [text] 18 | - id: check-merge-conflict 19 | - id: check-json 20 | - id: pretty-format-json 21 | args: [--autofix, --no-sort-keys] 22 | - id: trailing-whitespace 23 | - repo: https://github.com/asottile/pyupgrade 24 | rev: v3.2.2 25 | hooks: 26 | - id: pyupgrade 27 | args: [ 28 | --py36-plus 29 | ] 30 | - repo: https://github.com/asottile/blacken-docs 31 | rev: v1.12.1 32 | hooks: 33 | - id: blacken-docs 34 | additional_dependencies: [black==19.3b0] 35 | files: '(\.md|\.rst)$' 36 | - repo: https://github.com/psf/black 37 | rev: 22.10.0 38 | hooks: 39 | - id: black 40 | files: '(\.pyi?|wscript)$' 41 | language_version: python 42 | - repo: https://github.com/PyCQA/doc8 43 | rev: v1.0.0 44 | hooks: 45 | - id: doc8 46 | args: [--max-line-length, "88"] 47 | - repo: https://github.com/PyCQA/flake8 48 | rev: 5.0.4 49 | hooks: 50 | - id: flake8 51 | types: [python] 52 | additional_dependencies: [ 53 | flake8-alfred, flake8-bugbear, flake8-builtins, flake8-comprehensions, 54 | flake8-docstrings, flake8-eradicate, flake8-print, 55 | flake8-todo, pep8-naming, pydocstyle, 56 | ] 57 | # Harmonizing flake8 and black 58 | args: [ 59 | '--max-line-length=88', 60 | '--ignore=E203,E402,E501,E800,W503', 61 | '--select=B,C,E,F,W,T4,B9' 62 | ] 63 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | image: latest 5 | 6 | python: 7 | version: 3.8 8 | 9 | conda: 10 | environment: docs/rtd_environment.yml 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 OpenSourceEconomics 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ruspy 2 | ====== 3 | 4 | .. image:: https://anaconda.org/opensourceeconomics/ruspy/badges/version.svg 5 | :target: https://anaconda.org/OpenSourceEconomics/ruspy/ 6 | 7 | .. image:: https://anaconda.org/opensourceeconomics/ruspy/badges/platforms.svg 8 | :target: https://anaconda.org/OpenSourceEconomics/ruspy/ 9 | 10 | .. image:: https://readthedocs.org/projects/ruspy/badge/?version=latest 11 | :target: https://ruspy.readthedocs.io/ 12 | 13 | .. image:: https://img.shields.io/badge/License-MIT-yellow.svg 14 | :target: https://opensource.org/licenses/MIT 15 | 16 | .. image:: https://github.com/OpenSourceEconomics/ruspy/workflows/Continuous%20Integration%20Workflow/badge.svg 17 | :target: https://github.com/OpenSourceEconomics/ruspy/actions 18 | 19 | .. image:: https://codecov.io/gh/OpenSourceEconomics/robupy/branch/master/graph/badge.svg 20 | :target: https://codecov.io/gh/OpenSourceEconomics/robupy 21 | 22 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 23 | :target: https://github.com/psf/black 24 | 25 | ``ruspy`` is an open-source package for the simulation and estimation of a prototypical 26 | infinite-horizon dynamic discrete choice model based on 27 | 28 | Rust, J. (1987). `Optimal replacement of GMC bus engines: An empirical model of Harold Zurcher. `_ *Econometrica, 55* (5), 999-1033. 29 | 30 | You can install ``ruspy`` via conda with 31 | 32 | .. code-block:: bash 33 | 34 | $ conda config --add channels conda-forge 35 | $ conda install -c opensourceeconomics ruspy 36 | 37 | Please visit our `online documentation `_ for 38 | tutorials and other information. 39 | 40 | 41 | Citation 42 | -------- 43 | 44 | If you use ruspy for your research, do not forget to cite it with 45 | 46 | .. code-block:: bash 47 | 48 | @Unpublished{ruspy.2020, 49 | Author = {{ruspy}}, 50 | Title = {An open-source package for the simulation and estimation of a prototypical infinite-horizon dynamic discrete choice model based on Rust (1987)}, 51 | Year = {2020}, 52 | URL = {https://github.com/OpenSourceEconomics/ruspy/releases/v1.1}, 53 | } 54 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 50...100 3 | precision: 2 4 | round: down 5 | status: 6 | project: 7 | threshold: 5% 8 | patch: 9 | threshold: 5% 10 | 11 | ignore: 12 | - "setup.py" 13 | - "respy/config.py" 14 | - "ruspy/tests/**/*" 15 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/rtd_environment.yml: -------------------------------------------------------------------------------- 1 | name: rtd_ruspy 2 | 3 | channels: 4 | - conda-forge 5 | - nodefaults 6 | 7 | dependencies: 8 | - python=3.8 9 | - pip 10 | - sphinx 11 | 12 | - ipython 13 | - nbsphinx 14 | - numpydoc 15 | - sphinx-autobuild 16 | - sphinx-autoapi 17 | - numba 18 | - numpy 19 | - nlopt 20 | - pytest 21 | - estimagic 22 | 23 | - pip: 24 | - sphinx_rtd_theme 25 | -------------------------------------------------------------------------------- /docs/source/_templates/custom-class-template.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | :members: 7 | :show-inheritance: 8 | :inherited-members: 9 | {% block methods %} 10 | .. automethod:: __init__ 11 | 12 | {% if methods %} 13 | .. rubric:: {{ _('Methods') }} 14 | 15 | .. autosummary:: 16 | {% for item in methods %} 17 | ~{{ name }}.{{ item }} 18 | {%- endfor %} 19 | {% endif %} 20 | {% endblock %} 21 | 22 | {% block attributes %} 23 | {% if attributes %} 24 | .. rubric:: {{ _('Attributes') }} 25 | 26 | .. autosummary:: 27 | {% for item in attributes %} 28 | ~{{ name }}.{{ item }} 29 | {%- endfor %} 30 | {% endif %} 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /docs/source/_templates/custom-module-template.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. automodule:: {{ fullname }} 4 | 5 | {% block attributes %} 6 | {% if attributes %} 7 | .. rubric:: {{ _('Module Attributes') }} 8 | 9 | .. autosummary:: 10 | :toctree: 11 | :template: custom-class-template.rst 12 | {% for item in attributes %} 13 | {{ item }} 14 | {%- endfor %} 15 | {% endif %} 16 | {% endblock %} 17 | 18 | {% block functions %} 19 | {% if functions %} 20 | .. rubric:: {{ _('Functions') }} 21 | 22 | .. autosummary:: 23 | :toctree: 24 | {% for item in functions %} 25 | {{ item }} 26 | {%- endfor %} 27 | {% endif %} 28 | {% endblock %} 29 | 30 | {% block classes %} 31 | {% if classes %} 32 | .. rubric:: {{ _('Classes') }} 33 | 34 | .. autosummary:: 35 | :toctree: 36 | :template: custom-class-template.rst 37 | {% for item in classes %} 38 | {{ item }} 39 | {%- endfor %} 40 | {% endif %} 41 | {% endblock %} 42 | 43 | {% block exceptions %} 44 | {% if exceptions %} 45 | .. rubric:: {{ _('Exceptions') }} 46 | 47 | .. autosummary:: 48 | :toctree: 49 | {% for item in exceptions %} 50 | {{ item }} 51 | {%- endfor %} 52 | {% endif %} 53 | {% endblock %} 54 | 55 | {% block modules %} 56 | {% if modules %} 57 | .. rubric:: Modules 58 | 59 | .. autosummary:: 60 | :toctree: 61 | :template: custom-module-template.rst 62 | :recursive: 63 | {% for item in modules %} 64 | {{ item }} 65 | {%- endfor %} 66 | {% endif %} 67 | {% endblock %} 68 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | ### 2 | API 3 | ### 4 | 5 | Here the API of ruspy is documented. 6 | 7 | .. autosummary:: 8 | :toctree: _generated 9 | :template: custom-module-template.rst 10 | :recursive: 11 | 12 | ruspy 13 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration file for the Sphinx documentation builder. 3 | # 4 | # This file does only contain a selection of the most common options. For a 5 | # full list see the documentation: 6 | # http://www.sphinx-doc.org/en/master/config 7 | # -- Path setup -------------------------------------------------------------- 8 | # If extensions (or modules to document with autodoc) are in another directory, 9 | # add these directories to sys.path here. If the directory is relative to the 10 | # documentation root, use os.path.abspath to make it absolute, like shown here. 11 | # 12 | import os 13 | import sys 14 | 15 | sys.path.insert(0, os.path.abspath("../..")) 16 | print(sys.path) 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "ruspy" 21 | copyright = "2019, Maximilian Blesch" 22 | author = "Maximilian Blesch" 23 | 24 | # The short X.Y version 25 | version = "" 26 | # The full version, including alpha/delta/rc tags 27 | release = "" 28 | 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # If your documentation needs a minimal Sphinx version, state it here. 33 | # 34 | # needs_sphinx = '1.0' 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | "sphinx.ext.autodoc", 41 | "sphinx.ext.autosummary", 42 | "sphinx.ext.doctest", 43 | "sphinx.ext.todo", 44 | "sphinx.ext.coverage", 45 | "sphinx.ext.mathjax", 46 | "sphinx.ext.githubpages", 47 | "sphinx.ext.coverage", 48 | "sphinx.ext.extlinks", 49 | "sphinx.ext.ifconfig", 50 | "sphinx.ext.intersphinx", 51 | "sphinx.ext.mathjax", 52 | "sphinx.ext.todo", 53 | "sphinx.ext.viewcode", 54 | "numpydoc", 55 | "nbsphinx", 56 | ] 57 | 58 | autosummary_mock_imports = [ 59 | "ipopt", 60 | ] 61 | 62 | 63 | napoleon_numpy_docstring = True 64 | # Add any paths that contain templates here, relative to this directory. 65 | templates_path = ["_templates"] 66 | 67 | # The suffix(es) of source filenames. 68 | # You can specify multiple suffix as a list of string: 69 | # 70 | # source_suffix = ['.rst', '.md'] 71 | source_suffix = ".rst" 72 | 73 | # The master toctree document. 74 | master_doc = "index" 75 | 76 | # The language for content autogenerated by Sphinx. Refer to documentation 77 | # for a list of supported languages. 78 | # 79 | # This is also used if you do content translation via gettext catalogs. 80 | # Usually you set "language" from the command line for these cases. 81 | language = "en" 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | # This pattern also affects html_static_path and html_extra_path. 86 | exclude_patterns = [] 87 | 88 | # The name of the Pygments (syntax highlighting) style to use. 89 | pygments_style = "sphinx" 90 | 91 | 92 | # -- Options for HTML output ------------------------------------------------- 93 | 94 | # The theme to use for HTML and HTML Help pages. See the documentation for 95 | # a list of builtin themes. 96 | # 97 | html_theme = "sphinx_rtd_theme" 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | # 103 | # html_theme_options = {} 104 | 105 | # Add any paths that contain custom static files (such as style sheets) here, 106 | # relative to this directory. They are copied after the builtin static files, 107 | # so a file named "default.css" will overwrite the builtin "default.css". 108 | # html_static_path = ["_static"] 109 | 110 | # Custom sidebar templates, must be a dictionary that maps document names 111 | # to template names. 112 | # 113 | # The default sidebars (for documents that don't match any pattern) are 114 | # defined by theme itself. Builtin themes are using these templates by 115 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 116 | # 'searchbox.html']``. 117 | # 118 | html_sidebars = { 119 | "**": [ 120 | "relations.html", # needs 'show_related': True theme option to display 121 | "searchbox.html", 122 | ] 123 | } 124 | 125 | 126 | # -- Options for HTMLHelp output --------------------------------------------- 127 | 128 | # Output file base name for HTML help builder. 129 | htmlhelp_basename = "ruspydoc" 130 | 131 | 132 | # -- Options for LaTeX output ------------------------------------------------ 133 | 134 | latex_elements = { 135 | # The paper size ('letterpaper' or 'a4paper'). 136 | # 137 | # 'papersize': 'letterpaper', 138 | # The font size ('10pt', '11pt' or '12pt'). 139 | # 140 | # 'pointsize': '10pt', 141 | # Additional stuff for the LaTeX preamble. 142 | # 143 | # 'preamble': '', 144 | # Latex figure (float) alignment 145 | # 146 | # 'figure_align': 'htbp', 147 | } 148 | 149 | # Grouping the document tree into LaTeX files. List of tuples 150 | # (source start file, target name, title, 151 | # author, documentclass [howto, manual, or own class]). 152 | latex_documents = [ 153 | (master_doc, "ruspy.tex", "ruspy Documentation", "Maximilian Blesch", "manual") 154 | ] 155 | 156 | 157 | # -- Options for manual page output ------------------------------------------ 158 | 159 | # One entry per manual page. List of tuples 160 | # (source start file, name, description, authors, manual section). 161 | man_pages = [(master_doc, "ruspy", "ruspy Documentation", [author], 1)] 162 | 163 | 164 | # -- Options for Texinfo output ---------------------------------------------- 165 | 166 | # Grouping the document tree into Texinfo files. List of tuples 167 | # (source start file, target name, title, author, 168 | # dir menu entry, description, category) 169 | texinfo_documents = [ 170 | ( 171 | master_doc, 172 | "ruspy", 173 | "ruspy Documentation", 174 | author, 175 | "ruspy", 176 | "One line description of project.", 177 | "Miscellaneous", 178 | ) 179 | ] 180 | 181 | 182 | # -- Options for Epub output ------------------------------------------------- 183 | 184 | # Bibliographic Dublin Core info. 185 | epub_title = project 186 | 187 | # The unique identifier of the text. This can be a ISBN number 188 | # or the project homepage. 189 | # 190 | # epub_identifier = '' 191 | 192 | # A unique identification for the text. 193 | # 194 | # epub_uid = '' 195 | 196 | # A list of files that should not be packed into the epub file. 197 | epub_exclude_files = ["search.html"] 198 | 199 | 200 | # -- Extension configuration ------------------------------------------------- 201 | # Configuration for numpydoc 202 | numpydoc_xref_param_type = True 203 | numpydoc_xref_ignore = {"type", "optional", "default"} 204 | 205 | # Configuration for autodoc 206 | autosummary_generate = True 207 | # -- Options for todo extension ---------------------------------------------- 208 | 209 | # If true, `todo` and `todoList` produce output, else they produce nothing. 210 | todo_include_todos = True 211 | -------------------------------------------------------------------------------- /docs/source/credits.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Team 5 | ---- 6 | 7 | ---- 8 | BDFL 9 | ---- 10 | 11 | `Philipp Eisenhauer `_ 12 | 13 | ---------------- 14 | Development Lead 15 | ---------------- 16 | 17 | `Maximilian Blesch `_ 18 | 19 | ------------ 20 | Contributors 21 | ------------ 22 | 23 | `Sebastian Becker `_, `Pascal Heid `_, `Viktoria Kleinschmidt `_ 25 | 26 | 27 | 28 | Master Theses 29 | ------------- 30 | 31 | Below you find a list with past Master Theses, that used ruspy. If you think of using 32 | ruspy in your Master Thesis, please reach out to us and view the issues with a 33 | Master-Thesis tag on `github. `_ 34 | 35 | ------------------------------------------------------ 36 | Decision rule performance under model misspecification 37 | ------------------------------------------------------ 38 | by `Maximilian Blesch `_ 39 | 40 | I incorporate techniques from distributionally robust optimization into a dynamic 41 | investment model. This allows to explicitly account for ambiguity in the decision- 42 | making process. I outline an economic, mathematical, and computational model 43 | to study the seminal bus replacement problem (Rust, 1987) under potential model 44 | misspecification. I specify ambiguity sets for the transition dynamics of the model. 45 | These are based on empirical estimates, statistically meaningful, and computation- 46 | ally tractable. I analyze alternative policies in a series of computational exper- 47 | iments. I find that, given the structure of the model and the available data on 48 | past transitions, a policy simply ignoring model misspecification often outperforms 49 | its alternatives that are designed to explicitly account for it. 50 | 51 | 52 | --------------------------------------------------------------------------------- 53 | Mathematical Programming with Equilibrium Constraints: An Uncertainty Perspective 54 | --------------------------------------------------------------------------------- 55 | by `Pascal Heid `_ 56 | 57 | This thesis explores to which extent the Nested Fixed Point Algorithm (NFXP) as 58 | suggested by Rust (1987) differs from the Mathematical Programming with Equilibrium 59 | Constraints as introduced by Su and Judd (2012) by revisiting the Optimal Bus Engine 60 | Replacement Problem posed by the previous author. While previous studies focus on 61 | quantitative measures of speed and convergence rate, my focus lies on how the two 62 | approaches actually recover the true model when the simulation setup is less clean 63 | and more closely to what applied researchers typically face. For this comparison I 64 | draw on some techniques from the Uncertainty Quantification literature. I run a large 65 | scale simulation study in which I compare the two approaches among different model 66 | specifications by checking how accurate their counterfactual demand level predictions 67 | are. I can show that under realistic circumstances, the two approaches can yield 68 | considerably different predictions suggesting that they should be regarded as 69 | complements rather than competitors. 70 | -------------------------------------------------------------------------------- /docs/source/economics.rst: -------------------------------------------------------------------------------- 1 | Economic Model & Calibration 2 | ============================== 3 | 4 | Here the economic model of Rust (1987) is documented and two ways of calibrating 5 | its parameters are introduced. The first one is the nested fixed point algorithm 6 | (NFXP) initially suggested by Rust (1987) and the second one is mathematical 7 | programming with equilibrium constraints (MPEC) based on Su and Judd (2012). 8 | This builds the theoretic background for the estimation and simulation modules of ruspy. 9 | 10 | 11 | The Economic Model 12 | ------------------ 13 | 14 | The model is set up as an infinite horizon regenerative optimal stopping problem. It 15 | considers the dynamic decisions by a maintenance manager, Harold Zurcher, for a fleet of 16 | buses. As the buses are all identical and the decisions are assumed to be independent 17 | across buses, there are no indications of the bus in the following notation. Harold 18 | Zurcher makes repeated decisions :math:`a` about their maintenance in order to maximize 19 | his expected total discounted utility with respect to the expected mileage usage of the 20 | bus. Each month :math:`t`, a bus arrives at the bus depot in state :math:`s_t = (x_t, 21 | \epsilon_t(a_t))` containing the mileage since last engine replacement :math:`x_t` and 22 | other signs of wear and tear plus decision specific information :math:`\epsilon_t(a_t)`. 23 | Harold Zurcher is faced with the decision to either conduct a complete engine 24 | replacement :math:`(a_t = 1)` or to perform basic maintenance work :math:`(a_t = 0)`. 25 | The cost of maintenance :math:`c(x_t, \theta_1)` increases with the mileage state, 26 | while the cost of replacement :math:`RC` remains constant. Notationwise 27 | :math:`\theta_1` captures the structural parameters shaping the maintenance cost 28 | function. In the case of an engine replacement, the mileage state is reset to zero. 29 | 30 | The immediate utility of each action in month :math:`t` is assumed to be additively 31 | separable and given by: 32 | 33 | .. math:: 34 | 35 | \begin{align} 36 | u(a_t, x_t, \theta_1, RC) + \epsilon_t(a_t) \quad \text{with} \quad u(a_t, x_t, 37 | \theta_1, RC) = \begin{cases} 38 | -RC - c(0, \theta_1) & a_t = 1 \\ 39 | -c(x_t, \theta_1) & a_t = 0. 40 | \end{cases} 41 | \end{align} 42 | 43 | 44 | The objective of Harold Zurcher is to choose a strategy :math:`\pi` of all strategies 45 | :math:`\Pi` to maximize the utility over infinite horizon and therefore the current 46 | value in period :math:`t` and state :math:`s_t` is given by: 47 | 48 | .. math:: 49 | 50 | \begin{align} \tilde{v}^{\pi}(s_t) \equiv \max_{\pi\in\Pi} 51 | E^\pi\left[\sum^{\infty}_{i = t} \delta^{i - t} u(a_t, x_t, \theta_1, RC)) + 52 | \epsilon_t(a_t) \right]. \end{align} 53 | 54 | The discount factor :math:`\delta` weighs the utilities over all periods and therefore 55 | captures the preference of utility in current and future time periods. As the model 56 | assumes stationary utility, as well as stationary transition probabilities the future 57 | looks the same, whether the agent is at time :math:`t` in state :math:`s` or at any 58 | other time. Therefore the optimal decision in every period can be captures by the 59 | Bellman equation: 60 | 61 | .. math:: 62 | 63 | \begin{equation} 64 | v_\theta(x_t, \epsilon_t) = \max_{a_t \in \{0,1\}} \biggl[u(x_t, 65 | a_t, \theta_1, RC) + \epsilon_t(a_t) + \delta EV_\theta(x_t, \epsilon_t, 66 | a_t)\biggr], 67 | \end{equation} 68 | 69 | where 70 | 71 | .. math:: 72 | 73 | \begin{equation} EV_\theta(x_t, \epsilon_t, a_t) = 74 | \int \int v_\theta(\gamma, \eta) p(d\gamma, d\eta | x_t, \epsilon_t, a_t, \theta_2, 75 | \theta_3) 76 | \end{equation} 77 | 78 | and :math:`\theta` captures the parametrization of the model given by :math:`\{\delta, 79 | RC, \theta_1, \theta_2, \theta_3 \}`. Thus Harold Zurcher makes his decision in light of 80 | uncertainty about next month's state realization captured by the their conditional 81 | distribution :math:`p(x_{t+1}, \epsilon_{t+1} | x_t, \epsilon_t, a_t, \theta_2, 82 | \theta_3)`. 83 | 84 | Rust (1987) imposes conditional independence between the probability densities of the 85 | observable and unobservable state variables, i.e. 86 | 87 | .. math:: 88 | 89 | \begin{equation} 90 | p(x_{t+1}, \epsilon_{t+1}| x_t, a_t, \epsilon_t, \theta_2, \theta_3) = p(x_{t+1}| 91 | x_t, a_t, \theta_3) p(\epsilon_{t+1}|\epsilon_t, \theta_2) 92 | \end{equation} 93 | 94 | and furthermore assumes that the unobservables :math:`\epsilon_t(a_t)` are independent 95 | and identically distributed according to an extreme value distribution with mean zero 96 | and scale parameter one, i.e.: 97 | 98 | .. math:: 99 | 100 | \begin{equation} 101 | p(\epsilon_{t+1}| \theta_2) = \exp\{-\epsilon_{t+1} + \theta_2\} 102 | \exp\{-\exp\{-\epsilon_{t+1} + \theta_2 \}\} 103 | \end{equation} 104 | 105 | where :math:`\theta_2 = 0.577216`, i.e. the Euler-Mascheroni constant. 106 | 107 | Rust (1988) shows that these two assumptions, together with the additive separability 108 | between the observed and unobserved state variables in the immediate utilities, imply 109 | that :math:`EV_\theta` is a function independent of :math:`\epsilon_t` and the unique 110 | fixed point of a contraction mapping on the reduced space of all state action pairs 111 | :math:`(x,a)`. Furthermore, the regenerative property of the process yields for all 112 | states :math:`x`, that the expected value of replacement corresponds to the expected 113 | value of maintenance in state :math:`0`, i.e. :math:`EV_\theta(x, 1) = EV_\theta(0, 114 | 0)`. Thus :math:`EV_\theta` is the unique fixed point on the observed mileage state 115 | :math:`x` only. Therefore in the following :math:`EV_\theta(x)` refers to 116 | :math:`EV_\theta(x, 0)`. The contraction mapping is then given by: 117 | 118 | .. math:: 119 | 120 | \begin{equation} 121 | EV_\theta(x) = \sum_{x' \in X} p(x'|x, \theta_3) \log \sum_{a \in \{0, 1\}} \exp( 122 | u(x' , a, \theta_1, RC) + \delta EV_\theta(x')) 123 | \end{equation} 124 | 125 | This gives rise to the shorthand notation of the above formula: 126 | 127 | .. math:: 128 | \begin{equation} 129 | EV_\theta(x) = T_\theta(EV_\theta(x)) 130 | \end{equation} 131 | 132 | In addition, the conditional choice probabilities :math:`P(a| x, \theta)` have a 133 | closed-form solution given by the multinomial logit formula (McFadden, 1973): 134 | 135 | .. math:: 136 | 137 | \begin{equation} 138 | P(a|x, \theta) = \frac{\exp(u(a, x, RC, \theta_1) + \delta EV_\theta((a-1) \cdot 139 | x))}{\sum_{i \in \{0, 1\}} \exp(u(i, x, RC, \theta_1) + \delta EV_\theta((i - 1)x))} 140 | \end{equation} 141 | 142 | These closed form solutions allow to estimate the structural parameters driving 143 | Zurcher's decisions. Given the data :math:`\{a_0, ....a_T, x_0, ..., x_T\}` for a 144 | single bus, one can form the likelihood function :math:`l^f(a_1, ..., a_T, x_1, ...., 145 | x_T | a_0, x_0, \theta)` and estimate the parameter vector :math:`\theta` by maximum 146 | likelihood. Rust (1988) proofs that this function has due to the conditional 147 | independence assumption a simple form: 148 | 149 | .. math:: 150 | 151 | \begin{equation} 152 | l^f(a_1, ..., a_T, x_1, ...., x_T | a_0, x_0, \theta) = \prod_{t=1}^T P(a_t|x_t, 153 | \theta) p(x_t| x_{t-1}, a_{t-1}, \theta_3) 154 | \end{equation} 155 | 156 | 157 | Therefore the estimation can be split into two separate partial likelihood functions, 158 | given by: 159 | 160 | .. math:: 161 | 162 | \begin{equation} 163 | l^1(a_1, ..., a_T, x_1, ...., x_T | a_0, x_0, \theta_3) = \prod_{t=1}^T p(x_t| 164 | x_{t-1}, a_{t-1}, \theta_3) 165 | \end{equation} 166 | 167 | and 168 | 169 | .. math:: 170 | 171 | \begin{equation} 172 | l^2(a_1, ..., a_T, x_1, ...., x_T | \theta) = \prod_{t=1}^T P(a_t|x_t, \theta) 173 | \end{equation} 174 | 175 | 176 | Nested Fixed Point Algorithm 177 | ---------------------------- 178 | 179 | The calibration strategy employed by Rust (1987) involves handing the logarithm 180 | of the above :math:`l^f(a_1, ..., a_T, x_1, ...., x_T | a_0, x_0, \theta)` 181 | to an unconstrained optimization algorithm. 182 | Rust originally suggests a polyalgorithm of the BHHH and the BFGS for this purpose. 183 | This optimizer fixes a guess of the structural parameter vector :math:`\hat\theta` 184 | for which the unique fixed point of the economic model is found. 185 | Through this the conditional choice probabilities :math:`P(a|x, \hat\theta)` 186 | are obtained which in turn are used to evaluate the log likelihood function. 187 | On the basis of this, the optimization algorithm comes up with a new guess for 188 | the structural parameters and the procedure starts over until a certain 189 | convergence criteria is met. 190 | 191 | The algorithm consequently corresponds to solving the following optimization 192 | problem in an outer loop: 193 | 194 | .. math:: 195 | 196 | \begin{equation} 197 | \max_{\theta} \; log \; l^f(a_1, ..., a_T, x_1, ...., x_T | a_0, x_0, \theta) 198 | \end{equation} 199 | 200 | while finding the unique fixed point of :math:`EV_\theta(x) = T_\theta(EV_\theta(x))` 201 | in an inner loop for a given parameter guess produced in the outer loop. 202 | 203 | 204 | Mathematical Programming with Equilibrium Constraints 205 | ----------------------------------------------------- 206 | 207 | The approach developed by Su and Judd (2012) casts this unconstrained nested problem 208 | into a constrained optimization problem. For this they plug the conditional 209 | choice probabilities :math:`P(a|x, \theta)` into the likelihood function :math:`l^f(.)`: 210 | 211 | .. math:: 212 | 213 | \begin{equation} 214 | \begin{split} 215 | l^f_{aug}(. | a_0, x_0, \theta, EV) = & \prod_{t=1}^T \frac{ 216 | \exp(u(a, x, RC, \theta_1) + \delta EV((a-1) \cdot x))}{ 217 | \sum_{a \in \{0, 1\}} \exp(u(a, x, RC, \theta_1) + \delta EV((a - 1)x))} \\ 218 | \\ 219 | & \times p(x_t| x_{t-1}, a_{t-1}, \theta_3). 220 | \end{split} 221 | \end{equation} 222 | 223 | They coin the term augmented likelihood function for :math:`l^f_{aug}`. 224 | The particular feature now is that the likelihood depends explicitly on both the 225 | structural parameter vector :math:`\theta` as well as the choice of :math:`EV`. 226 | In order to ensure that guesses of both vectors are consistent 227 | in the spirit of the economic model, the contraction mapping of the expected value 228 | function is imposed as a constraint to the augmented likelihood function. 229 | Consequently, the calibration problem boils down to a constrained optimization 230 | looking like the following: 231 | 232 | .. math:: 233 | 234 | \begin{equation} 235 | \max_{(\theta, EV)} \; log \; l^f_{aug}(a_1, ..., a_T, x_1, ...., x_T | a_0, x_0, \theta, EV) \\ 236 | \text{ subject to } \; EV = T(EV, \theta). 237 | \end{equation} 238 | 239 | The constraints are generally nonlinear functions which restricts the use of 240 | optimization algorithms. An non-exhaustive list of optimizers that can handle 241 | the above problem are the commercial KNITRO (see Byrd et al. (2006)), as well 242 | as the open source IPOPT (see Wächter and Biegler (2006)) and the SLSQP (see 243 | Kraft (1994)) provided by NLOPT. 244 | 245 | 246 | The Implied Demand Function 247 | --------------------------- 248 | 249 | Rust (1987) shortly describes a way to uncover an implied demand function of engine 250 | replacement from his model and its estimated parameters. Theoretically, for Harold 251 | Zurcher the random annual implied demand function takes the following form: 252 | 253 | .. math:: 254 | 255 | \begin{equation*} 256 | \tilde{d}(RC) = \sum_{t=1}^{12} \sum_{m=1}^{M} \tilde{a}^m_t 257 | \end{equation*} 258 | 259 | where :math:`\tilde{a}^m_t` is the replacement decision for a certain bus :math:`m` 260 | in a certain month :math:`t` derived from the process {:math:`a^m_t, x^m_t`}. 261 | 262 | For convenience I will drop the index for the bus in the following. Its probability 263 | distribution is therefore the result of the process described by 264 | :math:`P(a_t|x_t; \theta)p(x_t|x_{t-1}, a_{t-1}; \theta_3)`. For simplification 265 | Rust actually derives the expected demand function :math:`d(RC)=E[\tilde{d}(RC)]`. 266 | Assuming that :math:`\pi` is the long-run stationary distribution of the process 267 | {:math:`a_t, x_t`} and that the observed initial state {:math:`a_0, x_0`} is in 268 | the long run equilibrium, :math:`\pi` can be described by the following functional 269 | equation: 270 | 271 | .. math:: 272 | 273 | \begin{equation} 274 | \pi(x, a; \theta) = \int_{y} \int_{j} P(a|x; \theta)p_3(x|y, j, \theta_3) 275 | \pi(dy, dj; \theta). 276 | \end{equation} 277 | 278 | Further assuming that the processes of {:math:`a_t, x_t`} are independent across 279 | buses the annual expected implied demand function boils down to: 280 | 281 | .. math:: 282 | 283 | \begin{equation} 284 | d(RC) = 12 M \int_{0}^{\infty} \pi(dx, 1; \theta). 285 | \end{equation} 286 | 287 | Given some estimated parameters :math:`\hat\theta` from calibrating the Rust Model 288 | and parametrically varying :math:`RC` results in different estimates of 289 | :math:`P(a_t|x_t; \theta)p(x_t|x_{t-1}, a_{t-1}; \theta_3)` which in turn affects 290 | the probability distribution :math:`\pi` which changes the implied demand. 291 | In the representation above it is clearly assumed that both the mileage state 292 | :math:`x` and the replacement decision :math:`a` are continuous. The replacement 293 | decision is actually discrete, though, and the mileage state has to be discretized 294 | again which in the end results in a sum representation of the function :math:`d(RC)` 295 | that is taken to calculate the expected annual demand. 296 | 297 | This demand function can be calculated in the ruspy package for a given 298 | parametrization of the model. A description how to do this can be found in 299 | :ref:`demand_function_calculation`. 300 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to ruspy's documentation! 2 | ================================= 3 | 4 | Ruspy is an open-source software package for estimating and simulating an infinite 5 | horizon single agent discrete choice model in the setting of Rust (1987). 6 | This package offers to choose whether to estimate the model using the nested fixed 7 | point algorithm suggested by Rust (1987) or by employing the mathematical programming 8 | with equilibrium constraints based on Su and Judd (2012). It serves 9 | as a foundation for teaching and research in this particular model and can be used 10 | freely by everyone. For a full understanding of the mechanisms in this package it is 11 | advisable to first read the two papers: 12 | 13 | Rust, J. (1987). `Optimal replacement of GMC bus engines: An empirical model of Harold 14 | Zurcher. `_ *Econometrica, 55* (5), 999-1033. 15 | 16 | Su, C. L., & Judd, K. L. (2012). `Constrained optimization approaches to estimation of 17 | structural models. `_ *Econometrica, 80* (5), 2213-2230. 18 | 19 | and the documentation provided by John Rust on his website: 20 | 21 | Rust, J. (2000). `Nested fixed point algorithm documentation manual. 22 | `_ *Unpublished Manuscript.* 23 | 24 | as well as the comment by Iskakhov et al. (2016) on Su and Judd (2012): 25 | 26 | Iskhakov, F., Lee, J., Rust, J., Schjerning, B., & Seo, K. (2016). `Comment on 27 | “constrained optimization approaches to estimation of structural models”. `_ 28 | *Econometrica, 84* (1), 365-370. 29 | 30 | So far, there has been only one research project based on this code. The promotional 31 | material for this project can be found 32 | `here. `_ 33 | 34 | ruspy can be installed via conda with: 35 | 36 | .. code-block:: bash 37 | 38 | $ conda config --add channels conda-forge 39 | $ conda install -c opensourceeconomics ruspy 40 | 41 | 42 | After installing ruspy, you can familiarize yourself with ruspy's tools and 43 | interface by exploring multiple tutorial notebooks, which can be found 44 | `here `_. Note that for a full 45 | comprehension, you should read the papers above or study at least the economics 46 | section of this documentation. 47 | 48 | 49 | .. toctree:: 50 | :maxdepth: 2 51 | :caption: Contents: 52 | 53 | economics 54 | model_code 55 | estimation 56 | simulation 57 | tutorials 58 | references 59 | credits 60 | api 61 | -------------------------------------------------------------------------------- /docs/source/model_code.rst: -------------------------------------------------------------------------------- 1 | Model code 2 | ========== 3 | This part documents the different functions for the calculation of the model objects 4 | determining the decision of Harold Zurcher. Following Rust (1987), the code does not 5 | estimate the discount factor and it needs to be externally set. 6 | 7 | .. _costs: 8 | 9 | Observed costs 10 | -------------- 11 | 12 | The observed costs are saved in :math:`num\_states \times 2` dimensional numpy array. 13 | The first column contains the maintenance and the second the replacement costs for each 14 | state. The function to calculate the observed costs is: 15 | 16 | .. currentmodule:: ruspy.model_code.cost_functions 17 | 18 | .. autosummary:: 19 | :toctree: _generated/ 20 | 21 | calc_obs_costs 22 | 23 | The inputs are besides the size of the state space, the type of the maintenance cost 24 | function as well as the cost parameters and the scale. The inputs will be explained in 25 | the following: 26 | 27 | .. _maint_func: 28 | 29 | ------------------------- 30 | Maintenance cost function 31 | ------------------------- 32 | 33 | So far the code allows for five functional forms. The following table reports the 34 | different functional forms for an arbitrary state :math:`x`. Afterwards I list the APIs 35 | of each function and their derivatives. :math:`states` is the size of the state space. 36 | 37 | +-------------+------------------------------------------------------------------------+ 38 | | Name | Functional form | 39 | +-------------+------------------------------------------------------------------------+ 40 | | linear | :math:`c(x,\theta_1) = \theta_{11} x` | 41 | +-------------+------------------------------------------------------------------------+ 42 | | square root | :math:`c(x,\theta_1) = \theta_{11} \sqrt{x}` | 43 | +-------------+------------------------------------------------------------------------+ 44 | | cubic | :math:`c(x,\theta_1) = \theta_{11}x+\theta_{12} x**2+\theta_{13} x**3` | 45 | +-------------+------------------------------------------------------------------------+ 46 | | hyperbolic | :math:`c(x,\theta_1) = (\theta_{11} / ((states + 1) - x))` | 47 | +-------------+------------------------------------------------------------------------+ 48 | | quadratic | :math:`c(x,\theta_1) = (\theta_{11} x +\theta_{12} x**2)` | 49 | +-------------+------------------------------------------------------------------------+ 50 | 51 | Linear cost function 52 | x 53 | .. currentmodule:: ruspy.model_code.cost_functions 54 | 55 | .. autosummary:: 56 | :toctree: _generated/ 57 | 58 | lin_cost 59 | lin_cost_dev 60 | 61 | Square root function 62 | 63 | .. currentmodule:: ruspy.model_code.cost_functions 64 | 65 | .. autosummary:: 66 | :toctree: _generated/ 67 | 68 | sqrt_costs 69 | sqrt_costs_dev 70 | 71 | 72 | Cubic cost function 73 | 74 | .. currentmodule:: ruspy.model_code.cost_functions 75 | 76 | .. autosummary:: 77 | :toctree: _generated/ 78 | 79 | cubic_costs 80 | cubic_costs_dev 81 | 82 | Quadratic cost function 83 | 84 | .. currentmodule:: ruspy.model_code.cost_functions 85 | 86 | .. autosummary:: 87 | :toctree: _generated/ 88 | 89 | quadratic_costs 90 | quadratic_costs_dev 91 | 92 | hyperbolic cost function 93 | 94 | .. currentmodule:: ruspy.model_code.cost_functions 95 | 96 | .. autosummary:: 97 | :toctree: _generated/ 98 | 99 | hyperbolic_costs 100 | hyperbolic_costs_dev 101 | 102 | 103 | .. _params: 104 | 105 | --------------- 106 | Cost parameters 107 | --------------- 108 | The second input are the cost parameters, which are stored as a one dimensional 109 | *numpy.array*. At the first position always the replacement cost :math:`RC` is stored. 110 | The next positions are subsequently filled with :math:`\theta_{11}, \theta_{12}, ...`. 111 | The exact number depends on the functional form. 112 | 113 | .. _scale: 114 | 115 | ----- 116 | Scale 117 | ----- 118 | 119 | The maintenance costs are, due to feasibility of the fixed point algorithm scaled. The 120 | scaling varies across functional forms. The following table contains an overview of the 121 | minimal scale needed for each form: 122 | 123 | +---------------+-----------------+ 124 | | Cost function | Scale | 125 | +---------------+-----------------+ 126 | | linear | :math:`10^{-3}` | 127 | +---------------+-----------------+ 128 | | square root | :math:`10^{-2}` | 129 | +---------------+-----------------+ 130 | | cubic | :math:`10^{-8}` | 131 | +---------------+-----------------+ 132 | | hyperbolic | :math:`10^{-1}` | 133 | +---------------+-----------------+ 134 | | quadratic | :math:`10^{-5}` | 135 | +---------------+-----------------+ 136 | 137 | 138 | 139 | Fixed Point Algorithm 140 | ---------------------------- 141 | 142 | This part documents the core contribution to research of the Rust (1987) paper, the 143 | Fixed Point Algorithm (FXP). It allows to consequently calculate the log-likelihood 144 | value for each cost parameter and thus, to estimate the model and hence builds 145 | the corner stone of the Nested Fixed Point Algorithm (NFXP). 146 | The computation of the fixed point is managed by: 147 | 148 | .. currentmodule:: ruspy.model_code.fix_point_alg 149 | 150 | .. autosummary:: 151 | :toctree: _generated/ 152 | 153 | calc_fixp 154 | 155 | 156 | The algorithm is set up as a polyalgorithm combining contraction and Newton-Kantorovich 157 | (Kantorovich, 1948) iterations. It starts by executing contraction iterations, until it 158 | reaches some convergence threshold and then switches to Newton-Kantorovich iterations. 159 | The exact mathematical deviation of the separate steps are very nicely illustrated in 160 | `Rust (2000) `_. The function of these two 161 | steps are the following in ruspy: 162 | 163 | .. currentmodule:: ruspy.model_code.fix_point_alg 164 | 165 | .. autosummary:: 166 | :toctree: _generated/ 167 | 168 | contraction_iteration 169 | kantorovich_step 170 | 171 | 172 | .. _alg_details: 173 | 174 | ------------------- 175 | Algorithmic details 176 | ------------------- 177 | 178 | In the following the variable keys are presented, which allow to specify the algorithmic 179 | behavior. The parameters can be grouped into two categories. Switching parameters, which 180 | allow to specify, when the algorithm switches from contraction to Newton-Kantorovich 181 | iterations and general parameters, which let the algorithm stop. So far, there is no 182 | switching back implemented. 183 | 184 | - **max_cont_steps :** *(int)* The maximum number of contraction iterations before 185 | switching to Newton-Kantorovich iterations. Default is 20. 186 | 187 | - **switch_tol :** *(float)* If this threshold is reached by contraction iterations, 188 | then the algorithm switches to Newton-Kantorovich iterations. Default is 189 | :math:`10^{-3}`. 190 | 191 | - **max_newt_kant_steps :** *(int)* The maximum number of Newton-Kantorovich iterations 192 | before the algorithm stops. Default is 20. 193 | 194 | - **threshold :** *(float)* If this threshold is reached by Newton-Kantorovich 195 | iterations, then the algorithm stops. Default is :math:`10^{-12}`. 196 | 197 | .. _ev: 198 | 199 | ----------------------------- 200 | Expected value of maintenance 201 | ----------------------------- 202 | 203 | In ruspy the expected value of maintenance is stored in a state space sized numpy array. 204 | Thus, the exected value of replacement can be found in the zero entry. It is generally 205 | denoted by *ev*, except in the simulation part of the package where it is denoted by 206 | *ev_known*. This illustrates that the expected value is created by the agent on his 207 | beliefs of the process. 208 | 209 | 210 | Common model objects 211 | -------------------- 212 | 213 | Here are some common objects with a short description documented. 214 | 215 | .. _trans_mat: 216 | 217 | ----------------- 218 | Transition matrix 219 | ----------------- 220 | 221 | The transition matrix for the Markov process are stored in a :math:`num\_states \times 222 | num\_states` dimensional numpy array. As transition in the case of the replacement 223 | corresponds to a transition from state 0, it exists only matrix. 224 | 225 | 226 | .. _pchoice: 227 | 228 | -------------------- 229 | Choice probabilities 230 | -------------------- 231 | 232 | The choice probabilities are stored in a :math:`num\_states \times 2` dimensional numpy 233 | array. So far only choice probabilities, resulting from an unobserved shock with i.i.d. 234 | gumbel distribution are implemented. The multinomial logit formula is herefore 235 | implemented in: 236 | 237 | .. currentmodule:: ruspy.model_code.choice_probabilities 238 | 239 | .. autosummary:: 240 | :toctree: _generated/ 241 | 242 | choice_prob_gumbel 243 | 244 | 245 | .. _disc_fac: 246 | 247 | --------------- 248 | Discount factor 249 | --------------- 250 | 251 | The discount factor, as described in the economic model section, is stored as a float in 252 | ruspy. It needs to be set externally for the simulation as well as for the estimation 253 | process. The key in the dictionary herefore is always *disc_fac*. 254 | 255 | 256 | .. _demand_function_calculation: 257 | 258 | 259 | Demand Function Calculation 260 | ----------------------------- 261 | 262 | The demand function can be derived using the following function ``get_demand``. 263 | 264 | .. currentmodule:: ruspy.model_code.demand_function 265 | 266 | .. autosummary:: 267 | :toctree: _generated/ 268 | 269 | get_demand 270 | 271 | Based on the estimated structural parameters :math:`\theta` obtained from the 272 | function ``estimate`` which is based on the model specification in :ref:`init_dict`, 273 | one can now derive the implied demand function. In order to do so one has to provide 274 | the following inputs: 275 | 276 | --------------------------- 277 | Initialization Dictionairy 278 | --------------------------- 279 | 280 | This is the :ref:`init_dict` needed for the function ``get_criterion_function``. 281 | The ``get_demand`` function draws the model specifications needed to calculate 282 | demand from this. 283 | 284 | 285 | .. _demand_dict: 286 | 287 | 288 | ------------------ 289 | Demand Dictionary 290 | ------------------ 291 | 292 | This dictionairy provides all the necessary information about how the demand 293 | function is supposed to look like and how precisely it is supposed to be calculated. 294 | It has to hold the following keys: 295 | 296 | - **RC_lower_bound :** *(float)* The lowest replacement cost for which the demand is 297 | supposed to be calculated. 298 | 299 | - **RC_upper_bound :** *(float)* The highest replacement cost for which the demand is 300 | supposed to be calculated. 301 | 302 | - **demand_evaluations :** *(int)* The grid size of the replacement cost between 303 | RC_lower_bound and RC_upper_bound for which the demand level shall be calculated. 304 | 305 | - **tolerance :** *(float)* The stopping tolerance for the fixed point calculation 306 | needed to obtain each demand level. 307 | 308 | - **num_periods :** *(int)* Number of months :math:`T` for which the expected demand 309 | is derived. Consequently, set it to 12 if you want to get the annual expected demand. 310 | 311 | - **num_buses :** *(int)* Number of buses :math:`M` for which the demand is calculated. 312 | 313 | 314 | .. _demand_params: 315 | 316 | ------------------- 317 | Demand Parameters 318 | ------------------- 319 | 320 | This numpy array contains the structural parameters of the model for which you want 321 | to derive the implied demand. This parametrization can come from an estimation 322 | procedure with the ``estimate`` function but is not limited to that. 323 | The first elements are the transition probabilities :math:`\theta_{30}, \theta_{31}, 324 | ...` and the elements after are :math:`RC, \theta_{11}, ...`. 325 | 326 | 327 | Based on those inputs, the ``get_demand`` function gives out the following DataFrame. 328 | 329 | .. _demand_results: 330 | 331 | 332 | ------------------ 333 | Demand Results 334 | ------------------ 335 | 336 | This pandas DataFrame has the grid of replacement costs defined by `RC_lower_bound`, 337 | `RC_upper_bound` and `demand_evaluations` as an index and gives out for each of them 338 | in the column *demand* the expected demand level over the specified time horizon 339 | and for the amount of buses in the fleet. It also contains a column *success* 340 | which indicates whether the fixed point algorithm converged successfully. 341 | 342 | 343 | 344 | The use of the ``get_demand`` function is shown in the following `replication 345 | `_ notebook. 346 | -------------------------------------------------------------------------------- /docs/source/references.rst: -------------------------------------------------------------------------------- 1 | References 2 | ========== 3 | 4 | Byrd, R. H., J. Nocedal, and R. A. Waltz (2006). `Knitro: An Integrated Package for 5 | Nonlinear Optimization `_, pp. 35–59. Boston, MA: Springer US. 6 | 7 | Iskhakov, F., Lee, J., Rust, J., Schjerning, B., & Seo, K. (2016). `Comment on 8 | “constrained optimization approaches to estimation of structural models”. `_ 9 | *Econometrica, 84* (1), 365-370. 10 | 11 | Kantorovich, L. (1948). On Newton’s method for functional equations. *Doklady Akademii 12 | Nauk SSSR, 71* (1), 1237– 1240. 13 | 14 | Kraft, D. (1994). `Algorithm 733: TOMP–Fortran modules for optimal control calculations 15 | `_ 16 | *ACM Transactions on Mathematical Software, vol. 20*, no. 3, pp. 262-281. 17 | 18 | McFadden, D. (1973). `Conditional logit analysis of qualitative choice behavior 19 | `_. In P. Zarembka (Ed.), 20 | *Frontiers in Econometrics* (pp. 105–142). New York City, NY: Academic Press. 21 | 22 | Rust, J. (1987). `Optimal replacement of GMC bus engines: An empirical model of Harold 23 | Zurcher. `_ *Econometrica, 55* (5), 999-1033. 24 | 25 | Rust, J. (1988). `Maximum likelihood estimation of discrete control processes. 26 | `_ *SIAM Journal on Control and 27 | Optimization, 26* (5), 1006–1024. 28 | 29 | Rust, J. (2000). `Nested fixed point algorithm documentation manual. 30 | `_ *Unpublished Manuscript.* 31 | 32 | Su, C. L., & Judd, K. L. (2012). `Constrained optimization approaches to estimation of 33 | structural models. `_ *Econometrica, 80* (5), 2213-2230. 34 | 35 | Wächter, A., & Biegler, L. (2006). `On the implementation of an interior-point filter line-search algorithm 36 | for large-scale nonlinear programming. `_ *Math. Program. 106*, 25–57. 37 | -------------------------------------------------------------------------------- /docs/source/simulation.rst: -------------------------------------------------------------------------------- 1 | Simulation 2 | ======================== 3 | 4 | 5 | The simulation package contains the functions to simulate a single agent in a dynamic 6 | discrete choice model. It is structured into two modules. The simulation module with 7 | the main function and the ``simulation_auxiliary`` with all supplementary functions. 8 | 9 | The simulate function 10 | --------------------- 11 | 12 | The simulate function can be found in ``ruspy.simulation.simulation`` and coordinates 13 | the whole simulation process. 14 | 15 | .. currentmodule:: ruspy.simulation.simulation 16 | 17 | .. autosummary:: 18 | :toctree: _generated/ 19 | 20 | simulate 21 | 22 | Besides the input :ref:`costs`, :ref:`ev` and :ref:`trans_mat`, there is more input 23 | specific to the simulation function. 24 | 25 | .. _sim_init_dict: 26 | 27 | Simulation initialization dictionary 28 | ------------------------------------ 29 | 30 | The initialization dictionary contains the following keys: 31 | 32 | - **seed :** *(int)* A positive integer setting the random seed for drawing random 33 | numbers. If none given, some random seed is drawn. 34 | 35 | - **discount_factor :** *(float)* See :ref:`disc_fac` for more details. 36 | 37 | - **buses :** *(int)* The number of buses to be simulated. 38 | 39 | - **periods :** *(int)* The number of periods to be simulated. 40 | 41 | 42 | 43 | The simulation process 44 | ---------------------- 45 | 46 | After all inputs are read in, the actual simulation starts. This is coordinated by: 47 | 48 | .. currentmodule:: ruspy.simulation.simulation_functions 49 | 50 | .. autosummary:: 51 | :toctree: _generated/ 52 | 53 | simulate_strategy 54 | 55 | The function calls in each period for each bus the following function, to choose the 56 | optimal decision: 57 | 58 | .. currentmodule:: ruspy.simulation.simulation_model 59 | 60 | .. autosummary:: 61 | :toctree: _generated/ 62 | 63 | decide 64 | 65 | Then the mileage state increase is drawn: 66 | 67 | .. currentmodule:: ruspy.simulation.simulation_model 68 | 69 | .. autosummary:: 70 | :toctree: _generated/ 71 | 72 | draw_increment 73 | 74 | .. _sim_results: 75 | 76 | The simulation 77 | -------------- 78 | After the simulation process the observed states, decisions and mileage uses are 79 | returned. Additionally the agent's utility. They are all stored in pandas.DataFrame with 80 | column names **states**, **decisions**, **utilities** and **usage**. Hence, the observed 81 | data is a subset of the returned Dataframe. 82 | 83 | Demonstration 84 | ------------- 85 | 86 | The `simulation notebook `_ allows to 87 | easily experiment with the estimation methods described here. The notebook can be 88 | downloaded from the `simulation folder `_ in the tutorials of 89 | ruspy. If you have have everything setup, then it should be easy to run it. 90 | -------------------------------------------------------------------------------- /docs/source/tutorials.rst: -------------------------------------------------------------------------------- 1 | Tutorials 2 | ========= 3 | 4 | We provide a `simulation `_ and 5 | `replication `_ tutorial. The first one 6 | showcases the simulation function of ruspy while the latter has a closer look at the 7 | estimation process. Lastly, for a combination of both you can further dive into the 8 | `replication of Iskhakov et al. (2016) 9 | `_ notebook which allows to 10 | replicate this paper using ruspy. All notebooks can be downloaded from the 11 | tutorials folder of the 12 | `repository `_. 13 | To execute the notebooks please install ruspy first. 14 | 15 | 16 | 17 | .. toctree:: 18 | :maxdepth: 1 19 | :caption: Tutorials: 20 | 21 | tutorials/replication/replication 22 | tutorials/replication/replication_iskhakov_et_al_2016 23 | tutorials/simulation/simulation_convergence 24 | -------------------------------------------------------------------------------- /docs/source/tutorials/replication/auxiliary_iskhakov.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | 4 | 5 | def check_simulated_data( 6 | simulated_data, 7 | discount_factor, 8 | number_runs, 9 | ): 10 | """ 11 | generates some key statistics of the simulated data set. 12 | 13 | Parameters 14 | ---------- 15 | simulated_data : pd.DataFrame 16 | The previously simulated data. 17 | discount_factor : list 18 | The discount used in the simulation. 19 | number_runs : int 20 | The number of simlation runs. 21 | 22 | Returns 23 | ------- 24 | results : pd.DataFrame 25 | The resulting key statistics of the data sets per discount factor. 26 | 27 | """ 28 | columns = [ 29 | "Average State at Replacement", 30 | "Average of all States", 31 | "Average Replacement", 32 | ] 33 | results = pd.DataFrame(index=discount_factor, columns=columns) 34 | results.index.name = "Discount Factor" 35 | for factor in discount_factor: 36 | temp = np.ones((number_runs, len(columns))) 37 | for run in np.arange(number_runs): 38 | temp[run, 0] = ( 39 | simulated_data[factor][run] 40 | .loc[simulated_data[factor][run]["decision"] == 1, "state"] 41 | .mean() 42 | ) 43 | temp[run, 1] = simulated_data[factor][run]["state"].mean() 44 | temp[run, 2] = simulated_data[factor][run]["decision"].mean() 45 | results.loc[factor, :] = temp.mean(axis=0) 46 | 47 | return results 48 | -------------------------------------------------------------------------------- /docs/source/tutorials/replication/check_simulated_data_iskhakov.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceEconomics/ruspy/414e9f98e3b1cf19544b1ca3b1e544ac5751c1e9/docs/source/tutorials/replication/check_simulated_data_iskhakov.pickle -------------------------------------------------------------------------------- /docs/source/tutorials/replication/group_4.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceEconomics/ruspy/414e9f98e3b1cf19544b1ca3b1e544ac5751c1e9/docs/source/tutorials/replication/group_4.pkl -------------------------------------------------------------------------------- /docs/source/tutorials/replication/results_iskhakov.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceEconomics/ruspy/414e9f98e3b1cf19544b1ca3b1e544ac5751c1e9/docs/source/tutorials/replication/results_iskhakov.pickle -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: ruspy 2 | channels: 3 | - defaults 4 | - conda-forge 5 | dependencies: 6 | - python=3.9 7 | - matplotlib 8 | - numpy 9 | - pandas 10 | - pre-commit 11 | - pytest 12 | - matplotlib 13 | - pytest-xdist 14 | - pytest-cov 15 | - scipy 16 | - jupyter 17 | - numba 18 | - pip 19 | - estimagic 20 | - black 21 | - flake8 22 | - pdbpp 23 | - nlopt 24 | - pip: 25 | - jupyter_contrib_nbextensions -------------------------------------------------------------------------------- /ex_promotion_nbs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """This module executes all notebooks. It serves the main purpose to ensure that all can be 3 | executed and work proper independently.""" 4 | import glob 5 | import os 6 | import subprocess as sp 7 | 8 | os.chdir(os.environ["PROJECT_ROOT"] + "/tutorials") 9 | promotion_folders = ["replication", "simulation"] 10 | 11 | 12 | for dir in promotion_folders: 13 | os.chdir(dir) 14 | for notebook in sorted(glob.glob("*.ipynb")): 15 | 16 | cmd = ( 17 | f" jupyter nbconvert --execute {notebook} --ExecutePreprocessor.timeout=-1" 18 | ) 19 | sp.check_call(cmd, shell=True) 20 | 21 | os.chdir("../") 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 88 3 | include = '\.pyi?$' 4 | exclude = '.git | .gitignore | documentation | ruspy.egg-info' 5 | -------------------------------------------------------------------------------- /ruspy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceEconomics/ruspy/414e9f98e3b1cf19544b1ca3b1e544ac5751c1e9/ruspy/__init__.py -------------------------------------------------------------------------------- /ruspy/config.py: -------------------------------------------------------------------------------- 1 | """This module provides some configuration for the package.""" 2 | import os 3 | import sys 4 | 5 | import numpy as np 6 | 7 | 8 | # We only support modern Python. 9 | np.testing.assert_equal(sys.version_info[:2] >= (3, 6), True) 10 | 11 | # We rely on relative paths throughout the package. 12 | PACKAGE_DIR = os.path.dirname(os.path.realpath(__file__)) 13 | TEST_RESOURCES_DIR = PACKAGE_DIR + "/test/resources/" 14 | -------------------------------------------------------------------------------- /ruspy/estimation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceEconomics/ruspy/414e9f98e3b1cf19544b1ca3b1e544ac5751c1e9/ruspy/estimation/__init__.py -------------------------------------------------------------------------------- /ruspy/estimation/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module is a global file to allow tracking the total number of contraction 3 | and Newton_Kantorovich steps needed per run of the estimate function with the 4 | Nested Fixed Point Algorithm. 5 | """ 6 | total_contr_count = 0 7 | total_newt_kant_count = 0 8 | -------------------------------------------------------------------------------- /ruspy/estimation/criterion_function.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module specifies the criterion function and its derivative. 3 | """ 4 | from functools import partial 5 | 6 | import numpy as np 7 | 8 | from ruspy.estimation.estimation_transitions import create_transition_matrix 9 | from ruspy.estimation.estimation_transitions import estimate_transitions 10 | from ruspy.estimation.mpec import mpec_constraint 11 | from ruspy.estimation.mpec import mpec_constraint_derivative 12 | from ruspy.estimation.mpec import mpec_loglike_cost_params 13 | from ruspy.estimation.mpec import mpec_loglike_cost_params_derivative 14 | from ruspy.estimation.nfxp import create_state_matrix 15 | from ruspy.estimation.nfxp import derivative_loglike_cost_params 16 | from ruspy.estimation.nfxp import derivative_loglike_cost_params_individual 17 | from ruspy.estimation.nfxp import loglike_cost_params 18 | from ruspy.estimation.nfxp import loglike_cost_params_individual 19 | from ruspy.estimation.pre_processing import select_model_parameters 20 | 21 | 22 | def get_criterion_function( 23 | init_dict, 24 | df, 25 | ): 26 | """ 27 | This function specifies the criterion function with its derivative, 28 | transition probabilites (for NXFP and MPEC) as well as the contraint 29 | function with its derivative (for MPEC). 30 | 31 | Parameters 32 | ---------- 33 | init_dict : dictionary 34 | see :ref:`init_dict` 35 | df : pandas.DataFrame 36 | see :ref:`df` 37 | 38 | Returns 39 | ------- 40 | func_dict : 41 | see :ref:`func_dict` 42 | transition_results : dictionary 43 | see :ref:`result_trans` 44 | """ 45 | 46 | transition_results = estimate_transitions(df) 47 | endog = df.loc[(slice(None), slice(1, None)), "decision"].to_numpy(int) 48 | states = df.loc[(slice(None), slice(1, None)), "state"].to_numpy(int) 49 | 50 | ( 51 | disc_fac, 52 | num_states, 53 | maint_func, 54 | maint_func_dev, 55 | num_params, 56 | scale, 57 | ) = select_model_parameters(init_dict) 58 | 59 | decision_mat = np.vstack(((1 - endog), endog)) 60 | 61 | trans_mat = create_transition_matrix(num_states, transition_results["x"]) 62 | state_mat = create_state_matrix(states, num_states) 63 | 64 | alg_details = {} if "alg_details" not in init_dict else init_dict["alg_details"] 65 | 66 | basic_kwargs = { 67 | "maint_func": maint_func, 68 | "maint_func_dev": maint_func_dev, 69 | "num_states": num_states, 70 | "disc_fac": disc_fac, 71 | "scale": scale, 72 | } 73 | 74 | if "method" in init_dict: 75 | method = init_dict["method"] 76 | else: 77 | raise ValueError("The key 'method' must be in init_dict") 78 | 79 | func_dict = {} 80 | nfxp_kwargs = { 81 | **basic_kwargs, 82 | "trans_mat": trans_mat, 83 | "state_mat": state_mat, 84 | "decision_mat": decision_mat, 85 | "alg_details": alg_details, 86 | } 87 | 88 | if method == "NFXP": 89 | func_dict["criterion_function"] = partial(loglike_cost_params, **nfxp_kwargs) 90 | func_dict["criterion_derivative"] = partial( 91 | derivative_loglike_cost_params, **nfxp_kwargs 92 | ) 93 | 94 | elif method == "NFXP_BHHH": 95 | func_dict["criterion_function"] = partial( 96 | loglike_cost_params_individual, **nfxp_kwargs 97 | ) 98 | func_dict["criterion_derivative"] = partial( 99 | derivative_loglike_cost_params_individual, **nfxp_kwargs 100 | ) 101 | 102 | elif method == "MPEC": 103 | mpec_crit_kwargs = { 104 | **basic_kwargs, 105 | "state_mat": state_mat, 106 | "decision_mat": decision_mat, 107 | } 108 | 109 | func_dict["criterion_function"] = partial( 110 | mpec_loglike_cost_params, **mpec_crit_kwargs 111 | ) 112 | func_dict["criterion_derivative"] = partial( 113 | mpec_loglike_cost_params_derivative, **mpec_crit_kwargs 114 | ) 115 | mpec_constr_kwargs = { 116 | **basic_kwargs, 117 | "trans_mat": trans_mat, 118 | } 119 | func_dict["constraint"] = partial(mpec_constraint, **mpec_constr_kwargs) 120 | func_dict["constraint_derivative"] = partial( 121 | mpec_constraint_derivative, **mpec_constr_kwargs 122 | ) 123 | else: 124 | raise ValueError( 125 | f"{method} is not implemented. Only MPEC or NFXP are valid choices" 126 | ) 127 | 128 | return func_dict, transition_results 129 | -------------------------------------------------------------------------------- /ruspy/estimation/estimation_transitions.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the functions necessary for the estimation process of transition 3 | probabilities. 4 | """ 5 | import numba 6 | import numpy as np 7 | 8 | 9 | def estimate_transitions(df): 10 | """Estimating the transition proabilities. 11 | 12 | The sub function for managing the estimation of the transition probabilities. 13 | 14 | Parameters 15 | ---------- 16 | df : pandas.DataFrame 17 | see :ref:`df` 18 | 19 | Returns 20 | ------- 21 | result_transitions : dictionary 22 | see :ref:`result_trans` 23 | 24 | """ 25 | result_transitions = {} 26 | usage = df["usage"].to_numpy(dtype=float) 27 | usage = usage[~np.isnan(usage)].astype(int) 28 | result_transitions["trans_count"] = transition_count = np.bincount(usage) 29 | result_transitions["x"] = tans_probs = transition_count / np.sum(transition_count) 30 | result_transitions["fun"] = loglike_trans(tans_probs, transition_count) 31 | 32 | return result_transitions 33 | 34 | 35 | def loglike_trans_individual(trans_dist, transition_count): 36 | """ 37 | Individual negative Log-likelihood function of transition probability estimation. 38 | 39 | Parameters 40 | ---------- 41 | trans_dist : numpy.ndarray 42 | The transition probabilities. 43 | transition_count : numpy.ndarray 44 | The pooled count of state increases per period in the data. 45 | 46 | Returns 47 | ------- 48 | log_like_individual : numpy.ndarray 49 | The individual negative log-likelihood contributions of the transition probabilities 50 | 51 | """ 52 | log_like_individual = -np.multiply(transition_count, np.log(trans_dist)) 53 | return log_like_individual 54 | 55 | 56 | def loglike_trans(trans_dist, transition_count): 57 | """ 58 | Sum the individual negative log-likelihood. 59 | 60 | Parameters 61 | ---------- 62 | trans_dist : np.ndarray 63 | parameters of the transition probabilities. 64 | transition_count : numpy.ndarray 65 | The pooled count of state increases per period in the data. 66 | 67 | Returns 68 | ------- 69 | log_like : float 70 | the negative log likelihood given some transition probability guess. 71 | 72 | """ 73 | log_like = loglike_trans_individual(trans_dist, transition_count).sum() 74 | return log_like 75 | 76 | 77 | def loglike_trans_individual_derivative(params, transition_count): 78 | """ 79 | generates the jacobian of the individual log likelihood function of the 80 | transition probabilities. This function is currently not used but is kept 81 | for further development of the package when estimagic can handle constrains 82 | with analytical derivatives. 83 | 84 | Parameters 85 | ---------- 86 | params : pd.DataFrame 87 | parameter guess of the transition probabilities. 88 | transition_count : numpy.ndarray 89 | The pooled count of state increases per period in the data. 90 | 91 | Returns 92 | ------- 93 | jacobian : numpy.ndarray 94 | a dim(params) x dim(params) matrix containing the Jacobian. 95 | 96 | """ 97 | p_raw = params.loc["trans_prob", "value"].to_numpy() 98 | diagonal = -np.multiply(transition_count, 1 / p_raw) 99 | jacobian = diagonal * np.eye(len(p_raw)) 100 | 101 | return jacobian 102 | 103 | 104 | def loglike_trans_derivative(params, transition_count): 105 | gradient = loglike_trans_individual_derivative(params, transition_count).sum(axis=1) 106 | return gradient 107 | 108 | 109 | @numba.jit(nopython=True) 110 | def create_transition_matrix(num_states, trans_prob): 111 | """ 112 | Creating the transition matrix with the assumption, that in every row the state 113 | increases have the same probability. 114 | 115 | Parameters 116 | ---------- 117 | num_states : int 118 | The size of the state space. 119 | trans_prob : numpy.ndarray 120 | The probabilities of an state increase. 121 | 122 | Returns 123 | ------- 124 | trans_mat : numpy.ndarray 125 | see :ref:`trans_mat` 126 | 127 | """ 128 | trans_mat = np.zeros((num_states, num_states)) 129 | for i in range(num_states): # Loop over all states. 130 | for j, p in enumerate(trans_prob): # Loop over the possible increases. 131 | if i + j < num_states - 1: 132 | trans_mat[i, i + j] = p 133 | elif i + j == num_states - 1: 134 | trans_mat[i, num_states - 1] = trans_prob[j:].sum() 135 | else: 136 | pass 137 | return trans_mat 138 | -------------------------------------------------------------------------------- /ruspy/estimation/mpec.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains all the key functions used to estimate the model using MPEC. 3 | """ 4 | import numpy as np 5 | 6 | from ruspy.estimation.nfxp import like_hood_data 7 | from ruspy.model_code.choice_probabilities import choice_prob_gumbel 8 | from ruspy.model_code.cost_functions import calc_obs_costs 9 | 10 | 11 | def mpec_loglike_cost_params( 12 | mpec_params, 13 | maint_func, 14 | maint_func_dev, 15 | num_states, 16 | disc_fac, 17 | scale, 18 | decision_mat, 19 | state_mat, 20 | ): 21 | """ 22 | Calculate the negative partial log likelihood for MPEC depending on cost parameters 23 | as well as the discretized expected values. 24 | 25 | Parameters 26 | ---------- 27 | mpec_params : numpy.ndarray 28 | see :ref:`mpec_params` 29 | maint_func: func 30 | see :ref:`maint_func` 31 | num_states : int 32 | The size of the state space. 33 | state_mat : numpy.ndarray 34 | see :ref:`state_mat` 35 | decision_mat : numpy.ndarray 36 | see :ref:`decision_mat` 37 | disc_fac : numpy.float 38 | see :ref:`disc_fac` 39 | scale : numpy.float 40 | see :ref:`scale` 41 | 42 | Returns 43 | ------- 44 | log_like: float 45 | Contains the negative partial log likelihood for the given parameters. 46 | """ 47 | 48 | costs = calc_obs_costs(num_states, maint_func, mpec_params[num_states:], scale) 49 | p_choice = choice_prob_gumbel(mpec_params[0:num_states], costs, disc_fac) 50 | log_like = like_hood_data(np.log(p_choice), decision_mat, state_mat) 51 | return float(log_like) 52 | 53 | 54 | def mpec_loglike_cost_params_derivative( 55 | mpec_params, 56 | maint_func, 57 | maint_func_dev, 58 | num_states, 59 | disc_fac, 60 | scale, 61 | decision_mat, 62 | state_mat, 63 | ): 64 | """ 65 | Computing the analytical gradient of the objective function for MPEC. 66 | 67 | Parameters 68 | ---------- 69 | mpec_params : numpy.ndarray 70 | see :ref:`mpec_params` 71 | maint_func: func 72 | see :ref:`maint_func` 73 | maint_func_dev: func 74 | see :ref:`maint_func` 75 | num_states : int 76 | The size of the state space. 77 | disc_fac : numpy.float 78 | see :ref:`disc_fac` 79 | scale : numpy.float 80 | see :ref:`scale` 81 | decision_mat : numpy.ndarray 82 | see :ref:`decision_mat` 83 | state_mat : numpy.ndarray 84 | see :ref:`state_mat` 85 | 86 | Returns 87 | ------- 88 | gradient : numpy.ndarray 89 | Vector that holds the derivative of the negative log likelihood function 90 | to the parameters. 91 | 92 | """ 93 | num_params = mpec_params[num_states:].shape[0] 94 | # Calculate choice probabilities 95 | costs = calc_obs_costs(num_states, maint_func, mpec_params[num_states:], scale) 96 | p_choice = choice_prob_gumbel(mpec_params[0:num_states], costs, disc_fac) 97 | 98 | # calculate the derivative based on the model 99 | derivative_both = mpec_loglike_cost_params_derivative_model( 100 | num_states, num_params, disc_fac, scale, maint_func_dev, p_choice 101 | ) 102 | 103 | # Calculate actual gradient depending on the given data 104 | # get decision matrix into the needed shape 105 | decision_mat_temp = np.vstack( 106 | ( 107 | np.tile(decision_mat[0], (num_states + num_params, 1)), 108 | np.tile(decision_mat[1], (num_states + num_params, 1)), 109 | ) 110 | ) 111 | 112 | # calculate the gradient 113 | gradient_temp = -np.sum( 114 | decision_mat_temp * np.dot(derivative_both, state_mat), axis=1 115 | ) 116 | # bring the calculated gradient into the correct shape 117 | gradient = np.reshape(gradient_temp, (num_states + num_params, 2), order="F").sum( 118 | axis=1 119 | ) 120 | 121 | return gradient 122 | 123 | 124 | def mpec_constraint( 125 | mpec_params, 126 | maint_func, 127 | maint_func_dev, 128 | num_states, 129 | disc_fac, 130 | scale, 131 | trans_mat, 132 | ): 133 | """ 134 | Calculate the constraint of MPEC. 135 | 136 | Parameters 137 | ---------- 138 | mpec_params : numpy.ndarray 139 | see :ref:`mpec_params` 140 | maint_func: func 141 | see :ref:`maint_func` 142 | maint_func_dev: func 143 | see :ref:`maint_func` 144 | num_states : int 145 | The size of the state space. 146 | disc_fac : numpy.float 147 | see :ref:`disc_fac` 148 | scale : numpy.float 149 | see :ref:`scale` 150 | trans_mat : numpy.ndarray 151 | see :ref:`trans_mat` 152 | 153 | Returns 154 | ------- 155 | None. 156 | 157 | """ 158 | ev = mpec_params[0:num_states] 159 | obs_costs = calc_obs_costs(num_states, maint_func, mpec_params[num_states:], scale) 160 | 161 | maint_value = disc_fac * ev - obs_costs[:, 0] 162 | repl_value = disc_fac * ev[0] - obs_costs[0, 1] - obs_costs[0, 0] 163 | 164 | # Select the minimal absolute value to rescale the value vector for the 165 | # exponential function. 166 | ev_max = np.max(np.array(maint_value, repl_value)) 167 | 168 | log_sum = ev_max + np.log( 169 | np.exp(maint_value - ev_max) + np.exp(repl_value - ev_max) 170 | ) 171 | 172 | ev_new = np.dot(trans_mat, log_sum) 173 | return ev_new - ev 174 | 175 | 176 | def mpec_constraint_derivative( 177 | mpec_params, 178 | maint_func, 179 | maint_func_dev, 180 | num_states, 181 | disc_fac, 182 | scale, 183 | trans_mat, 184 | ): 185 | """ 186 | Calculating the analytical Jacobian of the MPEC constraint. 187 | 188 | Parameters 189 | ---------- 190 | mpec_params : numpy.ndarray 191 | see :ref:`mpec_params` 192 | maint_func: func 193 | see :ref:`maint_func` 194 | maint_func_dev: func 195 | see :ref:`maint_func` 196 | num_states : int 197 | The size of the state space. 198 | disc_fac : numpy.float 199 | see :ref:`disc_fac` 200 | scale : numpy.float 201 | see :ref:`scale` 202 | trans_mat : numpy.ndarray 203 | see :ref:`trans_mat` 204 | 205 | Returns 206 | ------- 207 | jacobian : numpy.ndarray 208 | Jacobian of the MPEC constraint. 209 | 210 | """ 211 | # Calculate a vector representing 1 divided by the right hand side of the MPEC 212 | # constraint 213 | num_params = mpec_params[num_states:].shape[0] 214 | ev = mpec_params[0:num_states] 215 | obs_costs = calc_obs_costs(num_states, maint_func, mpec_params[num_states:], scale) 216 | 217 | maint_value = disc_fac * ev - obs_costs[:, 0] 218 | repl_value = disc_fac * ev[0] - obs_costs[0, 1] - obs_costs[0, 0] 219 | 220 | ev_max = np.max(np.array(maint_value, repl_value)) 221 | 222 | exp_centered_maint_value = np.exp(maint_value - ev_max) 223 | exp_centered_repl_value = np.exp(repl_value - ev_max) 224 | log_sum_denom = 1 / (exp_centered_maint_value + exp_centered_repl_value) 225 | 226 | jacobian = np.zeros((num_states, num_states + num_params)) 227 | 228 | # Calculate derivative to EV(0) 229 | jacobian[:, 0] = np.dot( 230 | disc_fac * exp_centered_repl_value * trans_mat, log_sum_denom 231 | ) 232 | jacobian[0, 0] = ( 233 | jacobian[0, 0] 234 | + (1 - log_sum_denom[0] * exp_centered_repl_value) * disc_fac * trans_mat[0, 0] 235 | ) 236 | # Calculate derivative to EV(1) until EV(num_states) 237 | jacobian[:, 1:num_states] = ( 238 | trans_mat[:, 1:] * log_sum_denom[1:] * disc_fac * exp_centered_maint_value[1:] 239 | ) 240 | # Calculate derivative to RC 241 | jacobian[:, num_states] = np.dot( 242 | trans_mat, -exp_centered_repl_value * log_sum_denom 243 | ) 244 | # Calculate derivative to maintenance cost parameters 245 | log_sum_denom_temp = np.reshape(log_sum_denom, (num_states, 1)) 246 | maint_cost_difference_dev = np.reshape( 247 | (-exp_centered_maint_value * maint_func_dev(num_states, scale).T).T 248 | - exp_centered_repl_value * maint_func_dev(num_states, scale)[0], 249 | (num_states, num_params - 1), 250 | ) 251 | 252 | jacobian[:, num_states + 1 :] = np.reshape( 253 | np.dot(trans_mat, log_sum_denom_temp * maint_cost_difference_dev), 254 | (num_states, num_params - 1), 255 | ) 256 | # Calculate the Jacobian of EV 257 | ev_jacobian = np.hstack((np.eye(num_states), np.zeros((num_states, num_params)))) 258 | 259 | jacobian = jacobian - ev_jacobian 260 | 261 | return jacobian 262 | 263 | 264 | def mpec_loglike_cost_params_derivative_model( 265 | num_states, num_params, disc_fac, scale, maint_func_dev, p_choice 266 | ): 267 | """ 268 | generates the derivative of the log likelihood function of mpec depending 269 | on the model characteristics. 270 | 271 | Parameters 272 | ---------- 273 | num_states : int 274 | The size of the state space. 275 | num_params : int 276 | Length of cost parameter vector. 277 | disc_fac : numpy.float 278 | see :ref:`disc_fac` 279 | scale : numpy.float 280 | see :ref:`scale` 281 | maint_func_dev : func 282 | see :ref:`maint_func` 283 | p_choice numpy.ndarray 284 | num_states x 2 matrix that contains the calculated conditional choice 285 | probabilities. 286 | 287 | Returns 288 | ------- 289 | derivative_both numpy.ndarray 290 | gives out the derivative of the log likelihood function depending 291 | on the model characteristics. 292 | 293 | """ 294 | # Create matrix that represents d[V(0)-V(x)]/ d[theta] (depending on x) 295 | payoff_difference_derivative = np.zeros((num_states + num_params, num_states)) 296 | payoff_difference_derivative[0, 1:] = disc_fac 297 | payoff_difference_derivative[1:num_states, 1:] = -disc_fac * np.eye(num_states - 1) 298 | payoff_difference_derivative[num_states, :] = -1 299 | payoff_difference_derivative[num_states + 1 :, :] = ( 300 | -maint_func_dev(num_states, scale)[0] + maint_func_dev(num_states, scale) 301 | ).T 302 | 303 | # Calculate derivative depending on whether d is 0 or 1 304 | derivative_d0 = -payoff_difference_derivative * p_choice[:, 1] 305 | derivative_d1 = payoff_difference_derivative * p_choice[:, 0] 306 | derivative_both = np.vstack((derivative_d0, derivative_d1)) 307 | 308 | return derivative_both 309 | -------------------------------------------------------------------------------- /ruspy/estimation/nfxp.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains functions for estimating the parameters shaping the cost 3 | function. Therefore it also contains the heart of this project. The python 4 | implementation of fix point algorithm developed by John Rust. 5 | """ 6 | import numba 7 | import numpy as np 8 | 9 | from ruspy.estimation import config 10 | from ruspy.model_code.choice_probabilities import choice_prob_gumbel 11 | from ruspy.model_code.cost_functions import calc_obs_costs 12 | from ruspy.model_code.fix_point_alg import calc_fixp 13 | from ruspy.model_code.fix_point_alg import contr_op_dev_wrt_params 14 | from ruspy.model_code.fix_point_alg import contr_op_dev_wrt_rc 15 | from ruspy.model_code.fix_point_alg import solve_equ_system_fixp 16 | 17 | ev_intermed = None 18 | current_params = None 19 | 20 | 21 | def loglike_cost_params_individual( 22 | params, 23 | maint_func, 24 | maint_func_dev, 25 | num_states, 26 | trans_mat, 27 | state_mat, 28 | decision_mat, 29 | disc_fac, 30 | scale, 31 | alg_details, 32 | ): 33 | """ 34 | This is the individual logliklihood function for the estimation of the cost parameters 35 | needed for the BHHH optimizer. 36 | 37 | 38 | Parameters 39 | ---------- 40 | params : numpy.ndarray 41 | see :ref:`params` 42 | maint_func: func 43 | see :ref:`maint_func` 44 | num_states : int 45 | The size of the state space. 46 | disc_fac : numpy.float 47 | see :ref:`disc_fac` 48 | trans_mat : numpy.ndarray 49 | see :ref:`trans_mat` 50 | state_mat : numpy.ndarray 51 | see :ref:`state_mat` 52 | decision_mat : numpy.ndarray 53 | see :ref:`decision_mat` 54 | 55 | Returns 56 | ------- 57 | log_like : numpy.ndarray 58 | A num_buses times num_periods dimensional array containing the negative 59 | log-likelihood contributions of the individuals. 60 | 61 | 62 | """ 63 | 64 | costs = calc_obs_costs(num_states, maint_func, params, scale) 65 | 66 | ev, contr_step_count, newt_kant_step_count = get_ev( 67 | params, trans_mat, costs, disc_fac, alg_details 68 | ) 69 | config.total_contr_count += contr_step_count 70 | config.total_newt_kant_count += newt_kant_step_count 71 | 72 | p_choice = choice_prob_gumbel(ev, costs, disc_fac) 73 | log_like = like_hood_data_individual(np.log(p_choice), decision_mat, state_mat) 74 | return log_like 75 | 76 | 77 | def loglike_cost_params( 78 | params, 79 | maint_func, 80 | maint_func_dev, 81 | num_states, 82 | trans_mat, 83 | state_mat, 84 | decision_mat, 85 | disc_fac, 86 | scale, 87 | alg_details, 88 | ): 89 | """ 90 | sums the individual negative log likelihood contributions for algorithms 91 | such as the L-BFGS-B. 92 | 93 | Parameters 94 | ---------- 95 | params : numpy.ndarray 96 | see :ref:`params` 97 | maint_func: func 98 | see :ref:`maint_func` 99 | maint_func_dev: func 100 | see :ref:`maint_func` 101 | num_states : int 102 | The size of the state space. 103 | trans_mat : numpy.ndarray 104 | see :ref:`trans_mat` 105 | state_mat : numpy.ndarray 106 | see :ref:`state_mat` 107 | decision_mat : numpy.ndarray 108 | see :ref:`decision_mat` 109 | disc_fac : numpy.float 110 | see :ref:`disc_fac` 111 | scale : numpy.float 112 | see :ref:`scale` 113 | alg_details : dict 114 | see :ref: `alg_details` 115 | 116 | Returns 117 | ------- 118 | log_like_sum : float 119 | The negative log likelihood based on the data. 120 | 121 | """ 122 | 123 | log_like_sum = loglike_cost_params_individual( 124 | params, 125 | maint_func, 126 | maint_func_dev, 127 | num_states, 128 | trans_mat, 129 | state_mat, 130 | decision_mat, 131 | disc_fac, 132 | scale, 133 | alg_details, 134 | ).sum() 135 | return log_like_sum 136 | 137 | 138 | def derivative_loglike_cost_params_individual( 139 | params, 140 | maint_func, 141 | maint_func_dev, 142 | num_states, 143 | trans_mat, 144 | state_mat, 145 | decision_mat, 146 | disc_fac, 147 | scale, 148 | alg_details, 149 | ): 150 | """ 151 | This is the Jacobian of the individual log likelihood function of the cost 152 | parameter estimation with respect to all cost parameters needed for the BHHH. 153 | 154 | 155 | Parameters 156 | ---------- 157 | params : numpy.ndarray 158 | see :ref:`params` 159 | maint_func: func 160 | see :ref:`maint_func` 161 | num_states : int 162 | The size of the state space. 163 | disc_fac : numpy.float 164 | see :ref:`disc_fac` 165 | trans_mat : numpy.ndarray 166 | see :ref:`trans_mat` 167 | state_mat : numpy.ndarray 168 | see :ref:`state_mat` 169 | decision_mat : numpy.ndarray 170 | see :ref:`decision_mat` 171 | 172 | Returns 173 | ------- 174 | dev : numpy.ndarray 175 | A num_buses + num_periods x dim(params) matrix in form of numpy array 176 | containing the derivative of the individual log-likelihood function for 177 | every cost parameter. 178 | 179 | 180 | """ 181 | 182 | # params = params["value"].to_numpy() 183 | dev = np.zeros((decision_mat.shape[1], len(params))) 184 | obs_costs = calc_obs_costs(num_states, maint_func, params, scale) 185 | 186 | ev = get_ev(params, trans_mat, obs_costs, disc_fac, alg_details)[0] 187 | 188 | p_choice = choice_prob_gumbel(ev, obs_costs, disc_fac) 189 | maint_cost_dev = maint_func_dev(num_states, scale) 190 | 191 | lh_values_rc = like_hood_vaules_rc(ev, obs_costs, p_choice, trans_mat, disc_fac) 192 | like_dev_rc = like_hood_data_individual(lh_values_rc, decision_mat, state_mat) 193 | dev[:, 0] = like_dev_rc 194 | 195 | for i in range(len(params) - 1): 196 | if len(params) == 2: 197 | cost_dev_param = maint_cost_dev 198 | else: 199 | cost_dev_param = maint_cost_dev[:, i] 200 | 201 | log_like_values_params = log_like_values_param( 202 | ev, obs_costs, p_choice, trans_mat, cost_dev_param, disc_fac 203 | ) 204 | dev[:, i + 1] = like_hood_data_individual( 205 | log_like_values_params, decision_mat, state_mat 206 | ) 207 | 208 | return dev 209 | 210 | 211 | def derivative_loglike_cost_params( 212 | params, 213 | maint_func, 214 | maint_func_dev, 215 | num_states, 216 | trans_mat, 217 | state_mat, 218 | decision_mat, 219 | disc_fac, 220 | scale, 221 | alg_details, 222 | ): 223 | """ 224 | sums up the Jacobian to obtain the gradient of the negative log likelihood 225 | function needed for algorithm such as the L-BFGS-B. 226 | 227 | Parameters 228 | ---------- 229 | params : numpy.ndarray 230 | see :ref:`params` 231 | maint_func: func 232 | see :ref:`maint_func` 233 | maint_func_dev: func 234 | see :ref:`maint_func` 235 | num_states : int 236 | The size of the state space. 237 | trans_mat : numpy.ndarray 238 | see :ref:`trans_mat` 239 | state_mat : numpy.ndarray 240 | see :ref:`state_mat` 241 | decision_mat : numpy.ndarray 242 | see :ref:`decision_mat` 243 | disc_fac : numpy.float 244 | see :ref:`disc_fac` 245 | scale : numpy.float 246 | see :ref:`scale` 247 | alg_details : dict 248 | see :ref: `alg_details` 249 | 250 | Returns 251 | ------- 252 | dev numpy.ndarray 253 | A dimension(params) sized vector containing the gradient of the negative 254 | likelihood function. 255 | 256 | """ 257 | dev = derivative_loglike_cost_params_individual( 258 | params, 259 | maint_func, 260 | maint_func_dev, 261 | num_states, 262 | trans_mat, 263 | state_mat, 264 | decision_mat, 265 | disc_fac, 266 | scale, 267 | alg_details, 268 | ).sum(axis=0) 269 | 270 | return dev 271 | 272 | 273 | def log_like_values_param(ev, costs, p_choice, trans_mat, cost_dev, disc_fac): 274 | dev_contr_op_params = contr_op_dev_wrt_params(trans_mat, p_choice[:, 0], cost_dev) 275 | dev_ev_params = solve_equ_system_fixp( 276 | dev_contr_op_params, ev, trans_mat, costs, disc_fac 277 | ) 278 | dev_value_maint_params = chain_rule_param(cost_dev, dev_ev_params, disc_fac) 279 | lh_values_param = like_hood_dev_values(p_choice, dev_value_maint_params) 280 | return lh_values_param 281 | 282 | 283 | def like_hood_vaules_rc(ev, costs, p_choice, trans_mat, disc_fac): 284 | dev_contr_op_rc = contr_op_dev_wrt_rc(trans_mat, p_choice[:, 0]) 285 | dev_ev_rc = solve_equ_system_fixp(dev_contr_op_rc, ev, trans_mat, costs, disc_fac) 286 | dev_value_maint_rc = 1 + disc_fac * dev_ev_rc - disc_fac * dev_ev_rc[0] 287 | lh_values_rc = like_hood_dev_values(p_choice, dev_value_maint_rc) 288 | return lh_values_rc 289 | 290 | 291 | def chain_rule_param(cost_dev, dev_ev_param, disc_fac): 292 | chain_value = ( 293 | cost_dev[0] - disc_fac * dev_ev_param[0] + disc_fac * dev_ev_param - cost_dev 294 | ) 295 | return chain_value 296 | 297 | 298 | def like_hood_data_individual(l_values, decision_mat, state_mat): 299 | """ 300 | generates the individual likelihood contribution based on the model. 301 | 302 | Parameters 303 | ---------- 304 | l_values numpy.ndarray 305 | the raw log likelihood per state. 306 | decision_mat : numpy.ndarray 307 | see :ref:`decision_mat` 308 | state_mat : numpy.ndarray 309 | see :ref:`state_mat` 310 | 311 | Returns 312 | ------- 313 | np.array 314 | the individual likelihood contribution depending on the exact model. 315 | 316 | """ 317 | return -(decision_mat * np.dot(l_values.T, state_mat)).sum(axis=0) 318 | 319 | 320 | def like_hood_data(l_values, decision_mat, state_mat): 321 | return -np.sum(decision_mat * np.dot(l_values.T, state_mat)) 322 | 323 | 324 | def like_hood_dev_values(p_choice, dev_values): 325 | l_values = np.empty_like(p_choice) 326 | l_values[:, 0] = np.multiply(1 - p_choice[:, 0], dev_values) 327 | l_values[:, 1] = np.multiply(1 - p_choice[:, 1], -dev_values) 328 | 329 | return l_values 330 | 331 | 332 | @numba.jit(nopython=True) 333 | def create_state_matrix(states, num_states): 334 | """ 335 | This function constructs a auxiliary matrix for the log-likelihood of the cost 336 | parameters. 337 | 338 | Parameters 339 | ---------- 340 | states : numpy.ndarray 341 | All mileage state observations. 342 | num_states : int 343 | The size of the state space. 344 | 345 | Returns 346 | ------- 347 | state_mat : numpy.ndarray 348 | see :ref:`state_mat` 349 | 350 | """ 351 | num_obs = states.shape[0] 352 | state_mat = np.full((num_states, num_obs), 0.0) 353 | for i, value in enumerate(states): 354 | state_mat[value, i] = 1.0 355 | return state_mat 356 | 357 | 358 | def get_ev(params, trans_mat, obs_costs, disc_fac, alg_details): 359 | """ 360 | A auxiliary function, which allows the log-likelihood function as well as its 361 | derivative to share the same fixed point and to avoid the need to execute the 362 | computation double. 363 | 364 | Parameters 365 | ---------- 366 | params : numpy.ndarray 367 | see :ref:`params` 368 | trans_mat : numpy.ndarray 369 | see :ref:`trans_mat` 370 | obs_costs : numpy.ndarray 371 | see :ref:`costs` 372 | disc_fac : numpy.float 373 | see :ref:`disc_fac` 374 | 375 | Returns 376 | ------- 377 | ev : numpy.ndarray 378 | see :ref:`ev` 379 | 380 | """ 381 | global ev_intermed 382 | global current_params 383 | if (ev_intermed is not None) & np.array_equal(current_params, params): 384 | ev = ev_intermed 385 | ev_intermed = None 386 | else: 387 | ev = calc_fixp(trans_mat, obs_costs, disc_fac, **alg_details) 388 | ev_intermed = ev 389 | current_params = params 390 | return ev 391 | -------------------------------------------------------------------------------- /ruspy/estimation/pre_processing.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module specifies the model specifications and the cost 3 | function from the initialisation dictionary init_dict. 4 | """ 5 | from ruspy.model_code.cost_functions import cubic_costs 6 | from ruspy.model_code.cost_functions import cubic_costs_dev 7 | from ruspy.model_code.cost_functions import hyperbolic_costs 8 | from ruspy.model_code.cost_functions import hyperbolic_costs_dev 9 | from ruspy.model_code.cost_functions import lin_cost 10 | from ruspy.model_code.cost_functions import lin_cost_dev 11 | from ruspy.model_code.cost_functions import quadratic_costs 12 | from ruspy.model_code.cost_functions import quadratic_costs_dev 13 | from ruspy.model_code.cost_functions import sqrt_costs 14 | from ruspy.model_code.cost_functions import sqrt_costs_dev 15 | 16 | 17 | def select_model_parameters(init_dict): 18 | """ 19 | Selecting the model specifications. 20 | 21 | Parameters 22 | ---------- 23 | init_dict : dictionary 24 | see :ref:`init_dict` 25 | Returns 26 | ------- 27 | The model sepcifications. 28 | """ 29 | if "model_specifications" not in init_dict: 30 | raise ValueError("Specify model parameters") 31 | model_specification = init_dict["model_specifications"] 32 | 33 | disc_fac = model_specification["discount_factor"] 34 | num_states = model_specification["num_states"] 35 | scale = model_specification["cost_scale"] 36 | 37 | maint_func, maint_func_dev, num_params = select_cost_function( 38 | model_specification["maint_cost_func"] 39 | ) 40 | 41 | return disc_fac, num_states, maint_func, maint_func_dev, num_params, scale 42 | 43 | 44 | def select_cost_function(maint_cost_func_name): 45 | """ 46 | Selecting the maintenance cost function. 47 | 48 | Parameters 49 | ---------- 50 | maint_cost_func_name : string 51 | The name of the maintenance cost function. 52 | 53 | Returns 54 | ------- 55 | The maintenance cost function, its derivative and the number of cost 56 | parameters in this model. 57 | """ 58 | if maint_cost_func_name == "cubic": 59 | maint_func = cubic_costs 60 | maint_func_dev = cubic_costs_dev 61 | num_params = 4 62 | elif maint_cost_func_name == "quadratic": 63 | maint_func = quadratic_costs 64 | maint_func_dev = quadratic_costs_dev 65 | num_params = 3 66 | elif maint_cost_func_name == "square_root": 67 | maint_func = sqrt_costs 68 | maint_func_dev = sqrt_costs_dev 69 | num_params = 2 70 | elif maint_cost_func_name == "hyperbolic": 71 | maint_func = hyperbolic_costs 72 | maint_func_dev = hyperbolic_costs_dev 73 | num_params = 2 74 | # Linear is the standard 75 | else: 76 | maint_func = lin_cost 77 | maint_func_dev = lin_cost_dev 78 | num_params = 2 79 | return maint_func, maint_func_dev, num_params 80 | -------------------------------------------------------------------------------- /ruspy/model_code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceEconomics/ruspy/414e9f98e3b1cf19544b1ca3b1e544ac5751c1e9/ruspy/model_code/__init__.py -------------------------------------------------------------------------------- /ruspy/model_code/choice_probabilities.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def choice_prob_gumbel(ev, obs_costs, disc_fac): 5 | """ 6 | This function calculates the choice probabilities to maintain or replace the 7 | bus engine for each state. 8 | 9 | Parameters 10 | ---------- 11 | ev : numpy.ndarray 12 | see :ref:`ev` 13 | obs_costs : numpy.ndarray 14 | see :ref:`costs` 15 | disc_fac : numpy.float 16 | see :ref:`disc_fac` 17 | 18 | Returns 19 | ------- 20 | pchoice : numpy.ndarray 21 | see :ref:`pchoice` 22 | 23 | 24 | """ 25 | s = ev.shape[0] 26 | util_main = disc_fac * ev - obs_costs[:, 0] # Utility to maintain the bus 27 | util_repl = np.full( 28 | util_main.shape, disc_fac * ev[0] - obs_costs[0, 0] - obs_costs[0, 1] 29 | ) 30 | util = np.vstack((util_main, util_repl)).T 31 | 32 | util = util - np.max(util) 33 | 34 | pchoice = np.exp(util) / (np.sum(np.exp(util), axis=1).reshape(s, -1)) 35 | return pchoice 36 | -------------------------------------------------------------------------------- /ruspy/model_code/cost_functions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def calc_obs_costs(num_states, maint_func, params, scale): 5 | """ 6 | Calculating the observed costs of maintenance and replacement for each state. 7 | 8 | Parameters 9 | ---------- 10 | num_states : int 11 | The size of the state space. 12 | maint_func : callable 13 | see :ref:`maint_func` 14 | params : numpy.ndarray 15 | see :ref:`params` 16 | scale : numpy.float 17 | see :ref:`scale` 18 | 19 | Returns 20 | ------- 21 | obs_costs : numpy.ndarray 22 | see :ref:`costs` 23 | 24 | 25 | """ 26 | rc = params[0] 27 | maint_cost = maint_func(num_states, params[1:], scale=scale) 28 | repl_cost = np.full(maint_cost.shape, rc + maint_cost[0]) 29 | obs_costs = np.vstack((maint_cost, repl_cost)).T 30 | return obs_costs 31 | 32 | 33 | def lin_cost(num_states, params, scale): 34 | """ 35 | Calculating for each state the observed costs of maintenance in the case of a 36 | linear cost function. 37 | 38 | Parameters 39 | ---------- 40 | num_states : int 41 | The size of the state space. 42 | params : numpy.ndarray 43 | see :ref:`params` 44 | scale : numpy.float 45 | see :ref:`scale` 46 | 47 | Returns 48 | ------- 49 | costs : numpy.ndarray 50 | A num_states sized one dimensional numpy array containing the maintenance 51 | costs for each state. 52 | 53 | """ 54 | states = np.arange(num_states) 55 | costs = states * params[0] * scale 56 | return costs 57 | 58 | 59 | def lin_cost_dev(num_states, scale): 60 | """ 61 | Calculating for each state the derivative of the linear maintenance cost function. 62 | 63 | Parameters 64 | ---------- 65 | num_states : int 66 | The size of the state space. 67 | scale : numpy.float 68 | see :ref:`scale` 69 | 70 | Returns 71 | ------- 72 | dev : numpy.ndarray 73 | A num_states sized one dimensional numpy array containing the derivative of the 74 | linear maintenance cost function for each state. 75 | 76 | """ 77 | dev = np.arange(num_states) * scale 78 | return dev 79 | 80 | 81 | def cubic_costs(num_states, params, scale): 82 | """ 83 | Calculating for each state the observed costs of maintenance in the case of a 84 | cubic cost function. 85 | 86 | Parameters 87 | ---------- 88 | num_states : int 89 | The size of the state space. 90 | params : numpy.ndarray 91 | see :ref:`params` 92 | scale : numpy.float 93 | see :ref:`scale` 94 | 95 | Returns 96 | ------- 97 | costs : numpy.ndarray 98 | A num_states sized one dimensional numpy array containing the maintenance 99 | costs for each state. 100 | 101 | """ 102 | states = np.arange(num_states) 103 | costs = ( 104 | params[0] * scale * states 105 | + params[1] * scale * (states**2) 106 | + params[2] * scale * (states**3) 107 | ) 108 | return costs 109 | 110 | 111 | def cubic_costs_dev(num_states, scale): 112 | """ 113 | Calculating for each state the derivative of the cubic maintenance cost function. 114 | 115 | Parameters 116 | ---------- 117 | num_states : int 118 | The size of the state space. 119 | scale : numpy.float 120 | see :ref:`scale` 121 | 122 | Returns 123 | ------- 124 | dev : numpy.ndarray 125 | A num_states x 3 dimensional numpy array containing the derivative of 126 | the cubic maintenance cost function for each state. 127 | 128 | """ 129 | states = np.arange(num_states) 130 | dev = np.array([states * scale, scale * (states**2), scale * (states**3)]).T 131 | return dev 132 | 133 | 134 | def quadratic_costs(num_states, params, scale): 135 | """ 136 | Calculating for each state the observed costs of maintenance in the case of a 137 | quadratic cost function. 138 | 139 | Parameters 140 | ---------- 141 | num_states : int 142 | The size of the state space. 143 | params : numpy.ndarray 144 | see :ref:`params` 145 | scale : numpy.float 146 | see :ref:`scale` 147 | 148 | Returns 149 | ------- 150 | costs : numpy.ndarray 151 | A num_states sized one dimensional numpy array containing the maintenance 152 | costs for each state. 153 | 154 | """ 155 | states = np.arange(num_states) 156 | costs = params[0] * scale * states + params[1] * scale * (states**2) 157 | return costs 158 | 159 | 160 | def quadratic_costs_dev(num_states, scale): 161 | """ 162 | Calculating for each state the derivative of the quadratic maintenance cost 163 | function. 164 | 165 | Parameters 166 | ---------- 167 | num_states : int 168 | The size of the state space. 169 | scale : numpy.float 170 | see :ref:`scale` 171 | 172 | Returns 173 | ------- 174 | dev : numpy.ndarray 175 | A num_states x 2 dimensional numpy array containing the derivative of 176 | the quadratic maintenance cost function for each state. 177 | 178 | """ 179 | states = np.arange(num_states) 180 | dev = np.array([scale * states, scale * (states**2)]).T 181 | return dev 182 | 183 | 184 | def sqrt_costs(num_states, params, scale): 185 | """ 186 | Calculating for each state the observed costs of maintenance in the case of a 187 | square root cost function. 188 | 189 | Parameters 190 | ---------- 191 | num_states : int 192 | The size of the state space. 193 | params : numpy.ndarray 194 | see :ref:`params` 195 | scale : numpy.float 196 | see :ref:`scale` 197 | 198 | Returns 199 | ------- 200 | costs : numpy.ndarray 201 | A num_states sized one dimensional numpy array containing the maintenance 202 | costs for each state. 203 | 204 | """ 205 | states = np.arange(num_states) 206 | costs = params[0] * scale * np.sqrt(states) 207 | return costs 208 | 209 | 210 | def sqrt_costs_dev(num_states, scale): 211 | """ 212 | Calculating for each state the derivative of the square root maintenance cost 213 | function. 214 | 215 | Parameters 216 | ---------- 217 | num_states : int 218 | The size of the state space. 219 | scale : numpy.float 220 | see :ref:`scale` 221 | 222 | Returns 223 | ------- 224 | dev : numpy.ndarray 225 | A num_states sized one dimensional numpy array containing the derivative of the 226 | square root maintenance cost function for each state. 227 | 228 | """ 229 | states = np.arange(num_states) 230 | dev = scale * np.sqrt(states) 231 | return dev 232 | 233 | 234 | def hyperbolic_costs(num_states, params, scale): 235 | """ 236 | Calculating for each state the observed costs of maintenance in the case of a 237 | hyperbolic cost function. 238 | 239 | Parameters 240 | ---------- 241 | num_states : int 242 | The size of the state space. 243 | params : numpy.ndarray 244 | see :ref:`params` 245 | scale : numpy.float 246 | see :ref:`scale` 247 | 248 | Returns 249 | ------- 250 | costs : numpy.ndarray 251 | A num_states sized one dimensional numpy array containing the maintenance 252 | costs for each state. 253 | 254 | """ 255 | states = np.arange(num_states) 256 | costs = params[0] * scale / ((num_states + 1) - states) 257 | return costs 258 | 259 | 260 | def hyperbolic_costs_dev(num_states, scale): 261 | """ 262 | Calculating for each state the derivative of the hyperbolic maintenance cost 263 | function. 264 | 265 | Parameters 266 | ---------- 267 | num_states : int 268 | The size of the state space. 269 | scale : numpy.float 270 | see :ref:`scale` 271 | 272 | Returns 273 | ------- 274 | dev : numpy.ndarray 275 | A num_states sized one dimensional numpy array containing the derivative of the 276 | hyperbolic maintenance cost function for each state. 277 | 278 | """ 279 | states = np.arange(num_states) 280 | dev = scale / ((num_states + 1) - states) 281 | return dev 282 | -------------------------------------------------------------------------------- /ruspy/model_code/demand_function.py: -------------------------------------------------------------------------------- 1 | """ 2 | Calculates the implied demand function as suggested in Rust (1987). 3 | """ 4 | import numpy as np 5 | import pandas as pd 6 | 7 | from ruspy.estimation.estimation_transitions import create_transition_matrix 8 | from ruspy.estimation.pre_processing import select_model_parameters 9 | from ruspy.model_code.choice_probabilities import choice_prob_gumbel 10 | from ruspy.model_code.cost_functions import calc_obs_costs 11 | from ruspy.model_code.fix_point_alg import calc_fixp 12 | 13 | 14 | def get_demand(init_dict, demand_dict, demand_params, max_num_iterations=2000): 15 | """ 16 | Calculates the implied demand for a range of replacement costs 17 | for a certain number of buses over a certain time period. 18 | 19 | Parameters 20 | ---------- 21 | init_dict : dict 22 | see :ref:`init_dict`. 23 | demand_dict : dict 24 | see :ref:`demand_dict`. 25 | demand_params : numpy.ndarray 26 | see :ref:`demand_params` 27 | 28 | Returns 29 | ------- 30 | demand_results : pd.DataFrame 31 | see :ref:`demand_results` 32 | 33 | """ 34 | params = demand_params.copy() 35 | ( 36 | disc_fac, 37 | num_states, 38 | maint_func, 39 | maint_func_dev, 40 | num_params, 41 | scale, 42 | ) = select_model_parameters(init_dict) 43 | 44 | # Initialize the loop over the replacement costs 45 | rc_range = np.linspace( 46 | demand_dict["RC_lower_bound"], 47 | demand_dict["RC_upper_bound"], 48 | demand_dict["demand_evaluations"], 49 | ) 50 | demand_results = pd.DataFrame(index=rc_range, columns=["demand", "success"]) 51 | demand_results.index.name = "RC" 52 | 53 | for rc in rc_range: 54 | params[-num_params] = rc 55 | demand_results.loc[(rc), "success"] = "No" 56 | 57 | # solve the model for the given paramaters 58 | trans_mat = create_transition_matrix(num_states, params[:-num_params]) 59 | 60 | obs_costs = calc_obs_costs(num_states, maint_func, params[-num_params:], scale) 61 | ev = calc_fixp(trans_mat, obs_costs, disc_fac)[0] 62 | p_choice = choice_prob_gumbel(ev, obs_costs, disc_fac) 63 | 64 | # calculate initial guess for pi and run contraction iterations 65 | pi_new = np.full((num_states, 2), 1 / (2 * num_states)) 66 | tol = 1 67 | iteration = 1 68 | while tol >= demand_dict["tolerance"]: 69 | pi = pi_new 70 | pi_new = p_choice * ( 71 | np.dot(trans_mat.T, pi[:, 0]) 72 | + np.dot(np.tile(trans_mat[0, :], (num_states, 1)).T, pi[:, 1]) 73 | ).reshape((num_states, 1)) 74 | tol = np.max(np.abs(pi_new - pi)) 75 | iteration += 1 76 | if iteration > max_num_iterations: 77 | break 78 | if tol < demand_dict["tolerance"]: 79 | demand_results.loc[(rc), "success"] = "Yes" 80 | 81 | demand_results.loc[(rc), "demand"] = ( 82 | demand_dict["num_buses"] * demand_dict["num_periods"] * pi_new[:, 1].sum() 83 | ) 84 | 85 | return demand_results 86 | -------------------------------------------------------------------------------- /ruspy/model_code/fix_point_alg.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from ruspy.model_code.choice_probabilities import choice_prob_gumbel 4 | 5 | 6 | def calc_fixp( 7 | trans_mat, 8 | obs_costs, 9 | disc_fac, 10 | threshold=1e-12, 11 | switch_tol=1e-3, 12 | max_contr_steps=20, 13 | max_newt_kant_steps=20, 14 | ): 15 | """ 16 | Calculating the expected value of maintenance fixed point with the polyalgorithm 17 | proposed by Rust (1987) and Rust (2000). 18 | 19 | Parameters 20 | ---------- 21 | trans_mat : numpy.ndarray 22 | see :ref:`trans_mat` 23 | obs_costs : numpy.ndarray 24 | see :ref:`costs` 25 | disc_fac : numpy.float 26 | see :ref:`disc_fac` 27 | threshold : numpy.float 28 | see :ref:`alg_details` 29 | switch_tol : numpy.float 30 | see :ref:`alg_details` 31 | max_contr_steps : int 32 | see :ref:`alg_details` 33 | max_newt_kant_steps : int 34 | see :ref:`alg_details` 35 | 36 | Returns 37 | ------- 38 | ev_new : numpy.ndarray 39 | see :ref:`ev` 40 | contr_step_count : int 41 | shows the amount of contraction iterations needed to find the fixed point. 42 | newt_kant_step_count : int 43 | shows the amount of Newton-Kantorovich iterations needed to find the fixed point. 44 | """ 45 | contr_step_count = 0 46 | newt_kant_step_count = 0 47 | ev_new = np.dot(trans_mat, np.log(np.sum(np.exp(-obs_costs), axis=1))) 48 | converge_crit = threshold + 1 # Make sure that the loop starts 49 | while converge_crit > threshold: 50 | while converge_crit > switch_tol and contr_step_count < max_contr_steps: 51 | ev = ev_new 52 | ev_new = contraction_iteration(ev, trans_mat, obs_costs, disc_fac) 53 | contr_step_count += 1 54 | converge_crit = np.max(np.abs(ev_new - ev)) 55 | ev = ev_new 56 | ev_new = kantorovich_step(ev, trans_mat, obs_costs, disc_fac) 57 | newt_kant_step_count += 1 58 | if newt_kant_step_count >= max_newt_kant_steps: 59 | break 60 | converge_crit = np.max( 61 | np.abs(ev - contraction_iteration(ev, trans_mat, obs_costs, disc_fac)) 62 | ) 63 | return ev_new, contr_step_count, newt_kant_step_count 64 | 65 | 66 | def contraction_iteration(ev, trans_mat, obs_costs, disc_fac): 67 | """ 68 | Calculating one iteration of the contraction mapping. 69 | 70 | Parameters 71 | ---------- 72 | ev : numpy.ndarray 73 | see :ref:`ev` 74 | trans_mat : numpy.ndarray 75 | see :ref:`trans_mat` 76 | obs_costs : numpy.ndarray 77 | see :ref:`costs` 78 | disc_fac : numpy.float 79 | see :ref:`disc_fac` 80 | 81 | Returns 82 | ------- 83 | ev_new : numpy.ndarray 84 | see :ref:`ev` 85 | 86 | 87 | """ 88 | maint_value = disc_fac * ev - obs_costs[:, 0] 89 | repl_value = disc_fac * ev[0] - obs_costs[0, 1] - obs_costs[0, 0] 90 | 91 | # Select the minimal absolute value to rescale the value vector for the 92 | # exponential function. 93 | 94 | ev_max = np.max(np.array(maint_value, repl_value)) 95 | 96 | log_sum = ev_max + np.log( 97 | np.exp(maint_value - ev_max) + np.exp(repl_value - ev_max) 98 | ) 99 | 100 | ev_new = np.dot(trans_mat, log_sum) 101 | return ev_new 102 | 103 | 104 | def kantorovich_step(ev, trans_mat, obs_costs, disc_fac): 105 | """ 106 | Calculating one Newton-Kantorovich step for approximating the fix-point. 107 | 108 | Parameters 109 | ---------- 110 | ev : numpy.ndarray 111 | see :ref:`ev` 112 | trans_mat : numpy.ndarray 113 | see :ref:`trans_mat` 114 | obs_costs : numpy.ndarray 115 | see :ref:`costs` 116 | disc_fac : numpy.float 117 | see :ref:`disc_fac` 118 | 119 | Returns 120 | ------- 121 | ev_new : numpy.ndarray 122 | see :ref:`ev` 123 | 124 | 125 | """ 126 | iteration_step = contraction_iteration(ev, trans_mat, obs_costs, disc_fac) 127 | ev_diff = solve_equ_system_fixp( 128 | ev - iteration_step, ev, trans_mat, obs_costs, disc_fac 129 | ) 130 | ev_new = ev - ev_diff 131 | return ev_new 132 | 133 | 134 | def solve_equ_system_fixp(fixp_vector, ev, trans_mat, obs_costs, disc_fac): 135 | """ 136 | Solving the multiple used equation system, deviated from the implicit 137 | function theorem 138 | 139 | 140 | Parameters 141 | ---------- 142 | fixp_vector: numpy.ndarray 143 | A state space sized containing the right hand side of the euqation. 144 | ev : numpy.ndarray 145 | see :ref:`ev` 146 | trans_mat : numpy.ndarray 147 | see :ref:`trans_mat` 148 | obs_costs : numpy.ndarray 149 | see :ref:`costs` 150 | disc_fac : numpy.float 151 | see :ref:`disc_fac` 152 | 153 | Returns 154 | ------- 155 | sol : numpy.ndarray 156 | The state space sized solution of the equation. 157 | 158 | """ 159 | num_states = ev.shape[0] 160 | t_prime = frechnet_dev(ev, trans_mat, obs_costs, disc_fac) 161 | sol = np.linalg.lstsq(np.eye(num_states) - t_prime, fixp_vector, rcond=None)[0] 162 | return sol 163 | 164 | 165 | def frechnet_dev(ev, trans_mat, obs_costs, disc_fac): 166 | """ 167 | Calculating the Frechnet derivative of the contraction mapping. 168 | 169 | Parameters 170 | ---------- 171 | ev : numpy.ndarray 172 | see :ref:`ev` 173 | trans_mat : numpy.ndarray 174 | see :ref:`trans_mat` 175 | obs_costs : numpy.ndarray 176 | see :ref:`costs` 177 | disc_fac : numpy.float 178 | see :ref:`disc_fac` 179 | 180 | Returns 181 | ------- 182 | t_prime : numpy.ndarray 183 | A num_states x num_states matrix containing the frechnet derivative of the 184 | contraction mapping. For details see Rust (2000). 185 | 186 | """ 187 | choice_probs = choice_prob_gumbel(ev, obs_costs, disc_fac) 188 | t_prime_pre = trans_mat[:, 1:] * choice_probs[1:, 0] 189 | t_prime = disc_fac * np.column_stack((1 - np.sum(t_prime_pre, axis=1), t_prime_pre)) 190 | return t_prime 191 | 192 | 193 | def contr_op_dev_wrt_params(trans_mat, maint_choice_prob, cost_dev): 194 | """ 195 | Calculating the derivative of the contraction mapping with respect to one 196 | particular maintenance cost parameter. 197 | 198 | Parameters 199 | ---------- 200 | trans_mat : numpy.ndarray 201 | see :ref:`trans_mat` 202 | maint_choice_prob : numpy.ndarray 203 | A num_states sized one dimensional numpy array containing the derivative of the 204 | maintenance cost function with respect to one particular parameter. 205 | cost_dev : numpy.ndarray 206 | A num_states sized one dimensional numpy array containing the derivative of the 207 | maintenance cost function with respect to one particular parameter. 208 | 209 | 210 | Returns 211 | ------- 212 | dev : numpy.ndarray 213 | A num_states sized one dimensional numpy array containing the derivative of the 214 | contraction mapping with respect to one particular maintenance cost parameter. 215 | 216 | """ 217 | dev = np.dot(trans_mat, np.multiply(-cost_dev, maint_choice_prob)) 218 | return dev 219 | 220 | 221 | def contr_op_dev_wrt_rc(trans_mat, maint_choice_prob): 222 | """ 223 | Calculating the derivative of the contraction mapping with respect to the 224 | replacement costs 225 | 226 | Parameters 227 | ---------- 228 | trans_mat : numpy.ndarray 229 | see :ref:`trans_mat` 230 | maint_choice_prob : numpy.ndarray 231 | A num_states sized one dimensional numpy array containing the derivative of the 232 | maintenance cost function for one particular parameter. 233 | 234 | 235 | Returns 236 | ------- 237 | dev : numpy.ndarray 238 | A num_states sized one dimensional numpy array containing the derivative of the 239 | contraction mapping with respect to the replacement cost parameter. 240 | 241 | """ 242 | dev = np.dot(trans_mat, maint_choice_prob - 1) 243 | return dev 244 | -------------------------------------------------------------------------------- /ruspy/simulation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceEconomics/ruspy/414e9f98e3b1cf19544b1ca3b1e544ac5751c1e9/ruspy/simulation/__init__.py -------------------------------------------------------------------------------- /ruspy/simulation/simulation.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the main function to manage the simulation process. To simulate 3 | a decision process in the model of John Rust's 1987 paper, it is sufficient to import 4 | the function from this module and feed it with a init dictionary containing the 5 | relevant variables. 6 | """ 7 | import warnings 8 | 9 | import numpy as np 10 | import pandas as pd 11 | 12 | from ruspy.simulation.simulation_functions import simulate_strategy 13 | from ruspy.simulation.simulation_functions import ( 14 | simulate_strategy_reduced_data_disc_utility, 15 | ) 16 | from ruspy.simulation.simulation_functions import ( 17 | simulate_strategy_reduced_data_utilities, 18 | ) 19 | 20 | 21 | def simulate(init_dict, ev_known, costs, trans_mat, reduced_data=None): 22 | """Simulating the decision process of Harold Zurcher. 23 | 24 | The main function to simulate a decision process in the theoretical framework of 25 | John Rust's 1987 paper. It reads the inputs from the initiation dictionary and 26 | then calls the main subfunction with all the relevant parameters. 27 | 28 | Parameters 29 | ---------- 30 | init_dict : dictionary 31 | See :ref:`sim_init_dict` 32 | ev_known : numpy.ndarray 33 | See :ref:`ev` 34 | costs : numpy.ndarray 35 | See ref:`costs` 36 | trans_mat : numpy.ndarray 37 | See ref:`trans_mat` 38 | reduced_data : string 39 | Keyword for simulation with reduced data usage. 40 | 41 | Returns 42 | ------- 43 | df : pandas.DataFrame 44 | See :ref:`sim_results` 45 | """ 46 | if "seed" in init_dict.keys(): 47 | seed = init_dict["seed"] 48 | else: 49 | seed = np.random.randint(1, 100000) 50 | 51 | num_buses, disc_fac, num_periods = read_init_dict(init_dict) 52 | 53 | if ev_known.shape[0] != trans_mat.shape[0]: 54 | raise ValueError( 55 | "The transition matrix and the expected value of the agent " 56 | "need to have the same size." 57 | ) 58 | 59 | out = create_output_by_keyword( 60 | num_periods, num_buses, costs, ev_known, trans_mat, disc_fac, seed, reduced_data 61 | ) 62 | return out 63 | 64 | 65 | def read_init_dict(init_dict): 66 | return init_dict["buses"], init_dict["discount_factor"], init_dict["periods"] 67 | 68 | 69 | def create_output_by_keyword( 70 | num_periods, num_buses, costs, ev_known, trans_mat, disc_fac, seed, reduced_data 71 | ): 72 | 73 | if not reduced_data: 74 | out, absorbing_state = create_standard_output( 75 | num_periods, num_buses, costs, ev_known, trans_mat, disc_fac, seed 76 | ) 77 | elif reduced_data == "utility": 78 | out, absorbing_state = simulate_strategy_reduced_data_utilities( 79 | num_periods, 80 | num_buses, 81 | costs, 82 | ev_known, 83 | trans_mat, 84 | disc_fac, 85 | seed, 86 | ) 87 | elif reduced_data == "discounted utility": 88 | out, absorbing_state = simulate_strategy_reduced_data_disc_utility( 89 | num_periods, 90 | num_buses, 91 | costs, 92 | ev_known, 93 | trans_mat, 94 | disc_fac, 95 | seed, 96 | ) 97 | 98 | else: 99 | raise ValueError( 100 | f'"utility" or "discounted utility" are the only valid keyword for ' 101 | f"reduced_data. You " 102 | f"provided {reduced_data} " 103 | ) 104 | 105 | warn_if_absorbing_state_reached(absorbing_state) 106 | return out 107 | 108 | 109 | def create_standard_output( 110 | num_periods, num_buses, costs, ev_known, trans_mat, disc_fac, seed 111 | ): 112 | states, decisions, utilities, usage, absorbing_state = simulate_strategy( 113 | num_periods, 114 | num_buses, 115 | costs, 116 | ev_known, 117 | trans_mat, 118 | disc_fac, 119 | seed, 120 | ) 121 | bus_ids = np.arange(num_buses) + 1 122 | periods = np.arange(num_periods) 123 | idx = pd.MultiIndex.from_product([bus_ids, periods], names=["Bus_ID", "period"]) 124 | df = pd.DataFrame( 125 | index=idx, 126 | data={ 127 | "state": states.flatten(), 128 | "decision": decisions.astype(np.uint8).flatten(), 129 | "utilities": utilities.flatten(), 130 | "usage": usage.flatten(), 131 | }, 132 | ) 133 | return df, absorbing_state 134 | 135 | 136 | def warn_if_absorbing_state_reached(absorbing_state): 137 | if absorbing_state == 1: 138 | warnings.warn( 139 | """ 140 | For at least one bus in at least one time period the state 141 | reached the highest possible grid point. This might confound 142 | your results. Please consider increasing the grid size 143 | until this messsage does not appear anymore. 144 | """ 145 | ) 146 | -------------------------------------------------------------------------------- /ruspy/simulation/simulation_functions.py: -------------------------------------------------------------------------------- 1 | import numba 2 | import numpy as np 3 | 4 | from ruspy.simulation.simulation_model import decide 5 | from ruspy.simulation.simulation_model import draw_increment 6 | 7 | 8 | @numba.jit(nopython=True) 9 | def simulate_strategy( 10 | num_periods, 11 | num_buses, 12 | costs, 13 | ev, 14 | trans_mat, 15 | disc_fac, 16 | seed, 17 | ): 18 | """ 19 | Simulating the decision process. 20 | 21 | This function simulates the decision strategy, as long as the current period is 22 | below the number of periods and the current highest state of a bus is in the 23 | first half of the state space. 24 | 25 | Parameters 26 | ---------- 27 | num_periods : int 28 | The number of periods to be simulated. 29 | num_buses : int 30 | The number of buses to be simulated. 31 | costs : numpy.ndarray 32 | see :ref:`costs` 33 | ev : numpy.ndarray 34 | see :ref:`ev` 35 | trans_mat : numpy.ndarray 36 | see :ref:`trans_mat` 37 | disc_fac : float 38 | see :ref:`disc_fac` 39 | seed : int 40 | A positive integer setting the random seed for drawing random numbers. 41 | 42 | Returns 43 | ------- 44 | states : numpy.ndarray 45 | A two dimensional numpy array containing for each bus in each period the 46 | state as an integer. 47 | 48 | decisions : numpy.ndarray 49 | A two dimensional numpy array containing for each bus in each period the 50 | decision as an integer. 51 | 52 | utilities : numpy.ndarray 53 | A two dimensional numpy array containing for each bus in each period the 54 | utility as a float. 55 | 56 | usage : numpy.ndarray 57 | A two dimensional numpy array containing for each bus in each period the 58 | mileage usage of last period as integer. 59 | """ 60 | np.random.seed(seed) 61 | num_states = ev.shape[0] 62 | states = np.zeros((num_buses, num_periods), dtype=numba.u2) 63 | decisions = np.zeros((num_buses, num_periods), dtype=numba.b1) 64 | utilities = np.zeros((num_buses, num_periods), dtype=numba.float32) 65 | usage = np.zeros((num_buses, num_periods), dtype=numba.u1) 66 | absorbing_state = 0 67 | for bus in range(num_buses): 68 | for period in range(num_periods): 69 | old_state = states[bus, period] 70 | 71 | intermediate_state, decision, utility = decide( 72 | old_state, 73 | costs, 74 | disc_fac, 75 | ev, 76 | ) 77 | 78 | state_increase = draw_increment(intermediate_state, trans_mat) 79 | decisions[bus, period] = decision 80 | utilities[bus, period] = utility 81 | new_state = intermediate_state + state_increase 82 | if new_state > num_states: 83 | new_state = num_states 84 | state_increase = num_states - intermediate_state 85 | usage[bus, period] = state_increase 86 | if period < num_periods - 1: 87 | states[bus, period + 1] = new_state 88 | if new_state == num_states: 89 | absorbing_state = 1 90 | 91 | return states, decisions, utilities, usage, absorbing_state 92 | 93 | 94 | @numba.jit(nopython=True) 95 | def simulate_strategy_reduced_data_utilities( 96 | num_periods, 97 | num_buses, 98 | costs, 99 | ev, 100 | trans_mat, 101 | disc_fac, 102 | seed, 103 | ): 104 | """ 105 | Simulating the decision process with reduced data usage. 106 | 107 | This function simulates the decision strategy, as long as the current period is 108 | below the number of periods and the current highest state of a bus is in the 109 | first half of the state space. 110 | 111 | Parameters 112 | ---------- 113 | num_periods : int 114 | The number of periods to be simulated. 115 | num_buses : int 116 | The number of buses to be simulated. 117 | costs : numpy.ndarray 118 | see :ref:`costs` 119 | ev : numpy.ndarray 120 | see :ref:`ev` 121 | trans_mat : numpy.ndarray 122 | see :ref:`trans_mat` 123 | disc_fac : float 124 | see :ref:`disc_fac` 125 | seed : int 126 | A positive integer setting the random seed for drawing random numbers. 127 | 128 | Returns 129 | ------- 130 | utilities : numpy.ndarray 131 | A two dimensional numpy array containing for each bus in each period the 132 | utility as a float. 133 | """ 134 | np.random.seed(seed) 135 | num_states = ev.shape[0] 136 | utilities = np.zeros((num_buses, num_periods), dtype=numba.float32) 137 | absorbing_state = 0 138 | for bus in range(num_buses): 139 | new_state = 0 140 | for period in range(num_periods): 141 | old_state = new_state 142 | 143 | intermediate_state, decision, utility = decide( 144 | old_state, 145 | costs, 146 | disc_fac, 147 | ev, 148 | ) 149 | 150 | state_increase = draw_increment(intermediate_state, trans_mat) 151 | utilities[bus, period] = utility 152 | 153 | new_state = intermediate_state + state_increase 154 | if new_state > num_states: 155 | new_state = num_states 156 | if new_state == num_states: 157 | absorbing_state = 1 158 | 159 | return utilities, absorbing_state 160 | 161 | 162 | @numba.jit(nopython=True) 163 | def simulate_strategy_reduced_data_disc_utility( 164 | num_periods, 165 | num_buses, 166 | costs, 167 | ev, 168 | trans_mat, 169 | disc_fac, 170 | seed, 171 | ): 172 | """ 173 | Simulating the decision process with reduced data usage. 174 | 175 | This function simulates the decision strategy, as long as the current period is 176 | below the number of periods and the current highest state of a bus is in the 177 | first half of the state space. 178 | 179 | Parameters 180 | ---------- 181 | num_periods : int 182 | The number of periods to be simulated. 183 | num_buses : int 184 | The number of buses to be simulated. 185 | costs : numpy.ndarray 186 | see :ref:`costs` 187 | ev : numpy.ndarray 188 | see :ref:`ev` 189 | trans_mat : numpy.ndarray 190 | see :ref:`trans_mat` 191 | disc_fac : float 192 | see :ref:`disc_fac` 193 | seed : int 194 | A positive integer setting the random seed for drawing random numbers. 195 | 196 | Returns 197 | ------- 198 | utilities : numpy.ndarray 199 | A two dimensional numpy array containing for each bus in each period the 200 | utility as a float. 201 | """ 202 | np.random.seed(seed) 203 | num_states = ev.shape[0] 204 | disc_utility = 0.0 205 | absorbing_state = 0 206 | for _ in range(num_buses): 207 | new_state = 0 208 | for period in range(num_periods): 209 | old_state = new_state 210 | 211 | intermediate_state, decision, utility = decide( 212 | old_state, 213 | costs, 214 | disc_fac, 215 | ev, 216 | ) 217 | 218 | state_increase = draw_increment(intermediate_state, trans_mat) 219 | disc_utility += disc_fac**period * utility 220 | 221 | new_state = intermediate_state + state_increase 222 | if new_state > num_states: 223 | new_state = num_states 224 | if new_state == num_states: 225 | absorbing_state = 1 226 | disc_utility /= num_buses 227 | return disc_utility, absorbing_state 228 | 229 | 230 | # This was an old attempt to implement more shocks than the standard gumbel. Would do 231 | # this much different now!!!! Just keep it for further work! 232 | # def get_unobs_data(shock): 233 | # # If no specification on the shocks is given. A right skewed gumbel distribution 234 | # # with mean 0 and scale pi^2/6 is assumed for each shock component. 235 | # shock = ( 236 | # ( 237 | # pd.Series(index=["loc"], data=[], name="gumbel"), 238 | # pd.Series(index=["loc"], data=[-np.euler_gamma], name="gumbel"), 239 | # ) 240 | # if shock is None 241 | # else shock 242 | # ) 243 | # 244 | # loc_scale = np.zeros((2, 2), dtype=float) 245 | # for i, params in enumerate(shock): 246 | # if "loc" in params.index: 247 | # loc_scale[i, 0] = params["loc"] 248 | # else: 249 | # loc_scale[i, 0] = 0 250 | # if "scale" in params.index: 251 | # loc_scale[i, 1] = params["scale"] 252 | # else: 253 | # loc_scale[i, 1] = 1 254 | # 255 | # return shock[0].name, shock[1].name, loc_scale 256 | # 257 | # 258 | # @numba.jit(nopython=True) 259 | # def draw_unob(dist_name, loc, scale): 260 | # if dist_name == "gumbel": 261 | # return np.random.gumbel(loc, scale) 262 | # elif dist_name == "normal": 263 | # return np.random.normal(loc, scale) 264 | # else: 265 | # raise ValueError 266 | -------------------------------------------------------------------------------- /ruspy/simulation/simulation_model.py: -------------------------------------------------------------------------------- 1 | import numba 2 | import numpy as np 3 | 4 | 5 | @numba.jit(nopython=True) 6 | def decide( 7 | old_state, 8 | costs, 9 | disc_fac, 10 | ev, 11 | ): 12 | """ 13 | Choosing action in current state. 14 | 15 | Parameters 16 | ---------- 17 | old_state: int 18 | Current state. 19 | costs : numpy.ndarray 20 | see :ref:`costs` 21 | disc_fac : float 22 | see :ref:`disc_fac` 23 | ev : numpy.ndarray 24 | see :ref:`ev` 25 | 26 | Returns 27 | ------- 28 | intermediate_state : int 29 | State before transition. 30 | decision : int 31 | Decision of this period. 32 | utility : float 33 | Utility of this period. 34 | """ 35 | unobs = np.random.gumbel(-np.euler_gamma, 1, size=2) 36 | 37 | value_replace = -costs[0, 0] - costs[0, 1] + unobs[1] + disc_fac * ev[0] 38 | value_maintain = -costs[old_state, 0] + unobs[0] + disc_fac * ev[old_state] 39 | if value_maintain > value_replace: 40 | decision = False 41 | utility = -costs[old_state, 0] + unobs[0] 42 | intermediate_state = old_state 43 | else: 44 | decision = True 45 | utility = -costs[0, 0] - costs[0, 1] + unobs[1] 46 | intermediate_state = 0 47 | return intermediate_state, decision, utility 48 | 49 | 50 | @numba.jit(nopython=True) 51 | def draw_increment(state, trans_mat): 52 | """ 53 | Drawing a random increase. 54 | 55 | Parameters 56 | ---------- 57 | state : int 58 | Current state. 59 | trans_mat : numpy.ndarray 60 | see :ref:`trans_mat` 61 | 62 | Returns 63 | ------- 64 | increase : int 65 | Number of state increase. 66 | """ 67 | max_state = np.max(np.nonzero(trans_mat[state, :])[0]) 68 | p = trans_mat[state, state : (max_state + 1)] # noqa: E203 69 | increase = np.argmax(np.random.multinomial(1, p)) 70 | return increase 71 | -------------------------------------------------------------------------------- /ruspy/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceEconomics/ruspy/414e9f98e3b1cf19544b1ca3b1e544ac5751c1e9/ruspy/test/__init__.py -------------------------------------------------------------------------------- /ruspy/test/conftest.py: -------------------------------------------------------------------------------- 1 | """This module provides the fixtures for the PYTEST runs.""" 2 | import numpy as np 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope="module", autouse=True) 7 | def set_seed(): 8 | """Each test is executed with the same random seed.""" 9 | np.random.seed(1223) 10 | 11 | 12 | @pytest.fixture(scope="session") 13 | def inputs(): 14 | constraints = {"PERIODS": 70000, "BUSES": 200, "discount_factor": 0.9999} 15 | return constraints 16 | -------------------------------------------------------------------------------- /ruspy/test/demand_test/test_get_demand.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import pytest 4 | from estimagic import minimize 5 | from numpy.testing import assert_array_almost_equal 6 | 7 | from ruspy.config import TEST_RESOURCES_DIR 8 | from ruspy.estimation.criterion_function import get_criterion_function 9 | from ruspy.model_code.demand_function import get_demand 10 | 11 | 12 | TEST_FOLDER = TEST_RESOURCES_DIR 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def inputs(): 17 | out = {} 18 | disc_fac = 0.9999 19 | num_states = 90 20 | scale = 1e-3 21 | init_dict = { 22 | "model_specifications": { 23 | "discount_factor": disc_fac, 24 | "num_states": num_states, 25 | "maint_cost_func": "linear", 26 | "cost_scale": scale, 27 | }, 28 | "method": "NFXP", 29 | } 30 | demand_dict = { 31 | "RC_lower_bound": 2, 32 | "RC_upper_bound": 13, 33 | "demand_evaluations": 100, 34 | "tolerance": 1e-10, 35 | "num_periods": 12, 36 | "num_buses": 1, 37 | } 38 | df = pd.read_pickle(TEST_FOLDER + "/replication_test/group_4.pkl") 39 | func_dict, result_trans = get_criterion_function(init_dict, df) 40 | criterion_func = func_dict["criterion_function"] 41 | criterion_dev = func_dict["criterion_derivative"] 42 | result_fixp = minimize( 43 | criterion=criterion_func, 44 | params=np.zeros(2, dtype=float), 45 | algorithm="scipy_lbfgsb", 46 | derivative=criterion_dev, 47 | ) 48 | demand_params = np.concatenate((result_trans["x"], result_fixp.params)) 49 | demand = get_demand(init_dict, demand_dict, demand_params) 50 | out["demand_estimate"] = demand["demand"].astype(float).to_numpy() 51 | return out 52 | 53 | 54 | @pytest.fixture(scope="module") 55 | def outputs(): 56 | out = {} 57 | out["demand_base"] = np.loadtxt(TEST_FOLDER + "/demand_test/get_demand.txt") 58 | return out 59 | 60 | 61 | def test_get_demand(inputs, outputs): 62 | assert_array_almost_equal( 63 | inputs["demand_estimate"], outputs["demand_base"], decimal=3 64 | ) 65 | -------------------------------------------------------------------------------- /ruspy/test/estimation_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceEconomics/ruspy/414e9f98e3b1cf19544b1ca3b1e544ac5751c1e9/ruspy/test/estimation_tests/__init__.py -------------------------------------------------------------------------------- /ruspy/test/estimation_tests/test_criterion_mpec.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains unit tests for the function get_criterion_function of 3 | ruspy.estimation.criterion_function for the MPEC method and different 4 | cost functions.The true values of the parameters and the likelihood are saved 5 | in resources/estimation_test. 6 | The criterion function is tested by calculating the true expected value, 7 | inserting the true expected value and the true parameters in the 8 | criterion function and comparing the result to the true likelihood. 9 | """ 10 | import numpy as np 11 | import pandas as pd 12 | import pytest 13 | from numpy.testing import assert_array_almost_equal 14 | 15 | from ruspy.config import TEST_RESOURCES_DIR 16 | from ruspy.estimation.criterion_function import get_criterion_function 17 | from ruspy.estimation.estimation_transitions import create_transition_matrix 18 | from ruspy.estimation.nfxp import get_ev 19 | from ruspy.model_code.cost_functions import calc_obs_costs 20 | from ruspy.model_code.cost_functions import cubic_costs 21 | from ruspy.model_code.cost_functions import hyperbolic_costs 22 | from ruspy.model_code.cost_functions import lin_cost 23 | from ruspy.model_code.cost_functions import quadratic_costs 24 | from ruspy.model_code.cost_functions import sqrt_costs 25 | 26 | TEST_FOLDER = TEST_RESOURCES_DIR + "replication_test/" 27 | 28 | 29 | @pytest.fixture(scope="module") 30 | def inputs(): 31 | out = {} 32 | disc_fac = 0.9999 33 | num_states = 90 34 | alg_details = {} 35 | init_dict = { 36 | "model_specifications": { 37 | "discount_factor": disc_fac, 38 | "num_states": num_states, 39 | }, 40 | "method": "MPEC", 41 | "alg_details": alg_details, 42 | } 43 | 44 | df = pd.read_pickle(TEST_FOLDER + "group_4.pkl") 45 | 46 | out["input data"] = df 47 | out["init_dict"] = init_dict 48 | out["num_states"] = num_states 49 | out["disc_fac"] = disc_fac 50 | out["alg_details"] = alg_details 51 | return out 52 | 53 | 54 | # true outputs 55 | @pytest.fixture(scope="module") 56 | def outputs(): 57 | out = {} 58 | out["cost_ll_linear"] = 163.584 59 | out["cost_ll_quad"] = 163.402 60 | out["cost_ll_cubic"] = 164.632939 61 | out["cost_ll_hyper"] = 165.11428 62 | out["cost_ll_sqrt"] = 163.390 63 | 64 | return out 65 | 66 | 67 | TEST_SPECIFICATIONS = [ 68 | ("linear", "linear", lin_cost, 1e-3, 2), 69 | ("quadratic", "quad", quadratic_costs, 1e-5, 3), 70 | ("cubic", "cubic", cubic_costs, 1e-8, 4), 71 | ("hyperbolic", "hyper", hyperbolic_costs, 1e-1, 2), 72 | ("square_root", "sqrt", sqrt_costs, 0.01, 2), 73 | ] 74 | 75 | 76 | @pytest.mark.parametrize("specification", TEST_SPECIFICATIONS) 77 | def test_criterion_function(inputs, outputs, specification): 78 | cost_func_name, cost_func_name_short, cost_func, scale, num_params = specification 79 | df = inputs["input data"] 80 | 81 | init_dict = inputs["init_dict"] 82 | init_dict["model_specifications"]["maint_cost_func"] = cost_func_name 83 | init_dict["model_specifications"]["cost_scale"] = scale 84 | num_states = init_dict["model_specifications"]["num_states"] 85 | 86 | func_dict, transition_results = get_criterion_function(init_dict, df) 87 | criterion_func = func_dict["criterion_function"] 88 | constraint = func_dict["constraint"] 89 | 90 | true_params = np.loadtxt(TEST_FOLDER + f"repl_params_{cost_func_name_short}.txt") 91 | trans_mat = create_transition_matrix(num_states, np.array(transition_results["x"])) 92 | obs_costs = calc_obs_costs( 93 | num_states=inputs["num_states"], 94 | maint_func=cost_func, 95 | params=true_params, 96 | scale=scale, 97 | ) 98 | ev = get_ev( 99 | true_params, trans_mat, obs_costs, inputs["disc_fac"], inputs["alg_details"] 100 | ) 101 | true_mpec_params = np.concatenate((ev[0], true_params)) 102 | assert_array_almost_equal( 103 | criterion_func(mpec_params=true_mpec_params), 104 | outputs["cost_ll_" + cost_func_name_short], 105 | decimal=3, 106 | ) 107 | assert_array_almost_equal( 108 | constraint(mpec_params=true_mpec_params), 109 | np.zeros(num_states, dtype=float), 110 | decimal=3, 111 | ) 112 | 113 | 114 | # @pytest.mark.parametrize("specification", TEST_SPECIFICATIONS) 115 | # def test_criterion_derivative(inputs, outputs, specification): 116 | # cost_func_name, cost_func_name_short, cost_func, scale, num_params = specification 117 | # df = inputs["input data"] 118 | # 119 | # init_dict = inputs["init_dict"] 120 | # init_dict["model_specifications"]["maint_cost_func"] = cost_func_name 121 | # init_dict["model_specifications"]["cost_scale"] = scale 122 | # 123 | # criterion_func, criterion_dev, transition_results = get_criterion_function( 124 | # init_dict, df 125 | # ) 126 | # true_params = np.loadtxt(TEST_FOLDER + f"repl_params_{cost_func_name_short}.txt") 127 | # trans_mat = create_transition_matrix( 128 | # inputs["num_states"], np.array(transition_results["x"]) 129 | # ) 130 | # obs_costs = calc_obs_costs( 131 | # num_states=inputs["num_states"], 132 | # maint_func=cost_func, 133 | # params=true_params, 134 | # scale=scale, 135 | # ) 136 | # ev = get_ev( 137 | # true_params, trans_mat, obs_costs, inputs["disc_fac"], inputs["alg_details"] 138 | # ) 139 | # true_mpec_params = np.concatenate((ev[0], true_params)) 140 | # assert_array_almost_equal( 141 | # criterion_dev(mpec_params=true_mpec_params)[90:], np.zeros(num_params), 142 | # decimal=2, 143 | # ) 144 | -------------------------------------------------------------------------------- /ruspy/test/estimation_tests/test_criterion_nfxp.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains unit tests for the function get_criterion_function of 3 | ruspy.estimation.criterion_function for the NFXP method and different 4 | cost functions.The true values of the parameters and the likelihood are saved 5 | in resources/estimation_test. 6 | The criterion function is tested by inserting the true parameters in the 7 | criterion function and comparing the result to the true likelihood. 8 | Its derivative is tested by inserting the true parameters in the derivative 9 | of the criterion function and comparing the result to zero. 10 | """ 11 | import numpy as np 12 | import pandas as pd 13 | import pytest 14 | from numpy.testing import assert_array_almost_equal 15 | 16 | from ruspy.config import TEST_RESOURCES_DIR 17 | from ruspy.estimation.criterion_function import get_criterion_function 18 | 19 | TEST_FOLDER = TEST_RESOURCES_DIR + "replication_test/" 20 | 21 | 22 | @pytest.fixture(scope="module") 23 | def inputs(): 24 | out = {} 25 | disc_fac = 0.9999 26 | num_states = 90 27 | init_dict = { 28 | "model_specifications": { 29 | "discount_factor": disc_fac, 30 | "num_states": num_states, 31 | }, 32 | "method": "NFXP", 33 | "alg_details": {}, 34 | } 35 | 36 | df = pd.read_pickle(TEST_FOLDER + "group_4.pkl") 37 | 38 | out["input data"] = df 39 | out["init_dict"] = init_dict 40 | return out 41 | 42 | 43 | # true outputs 44 | @pytest.fixture(scope="module") 45 | def outputs(): 46 | out = {} 47 | out["cost_ll_linear"] = 163.584 48 | out["cost_ll_quad"] = 163.402 49 | out["cost_ll_cubic"] = 164.632939 # 162.885 50 | out["cost_ll_hyper"] = 165.11428 # 165.423 51 | out["cost_ll_sqrt"] = 163.390 # 163.395. 52 | 53 | return out 54 | 55 | 56 | TEST_SPECIFICATIONS = [ 57 | ("linear", "linear", 1e-3, 2), 58 | ("quadratic", "quad", 1e-5, 3), 59 | ("cubic", "cubic", 1e-8, 4), 60 | ("hyperbolic", "hyper", 1e-1, 2), 61 | ("square_root", "sqrt", 0.01, 2), 62 | ] 63 | 64 | 65 | @pytest.mark.parametrize("specification", TEST_SPECIFICATIONS) 66 | def test_criterion_function(inputs, outputs, specification): 67 | cost_func_name, cost_func_name_short, scale, num_params = specification 68 | df = inputs["input data"] 69 | 70 | init_dict = inputs["init_dict"] 71 | init_dict["model_specifications"]["maint_cost_func"] = cost_func_name 72 | init_dict["model_specifications"]["cost_scale"] = scale 73 | 74 | crit_func_dict, transition_results = get_criterion_function(init_dict, df) 75 | 76 | assert_array_almost_equal( 77 | crit_func_dict["criterion_function"]( 78 | np.loadtxt(TEST_FOLDER + f"repl_params_{cost_func_name_short}.txt") 79 | ), 80 | outputs["cost_ll_" + cost_func_name_short], 81 | decimal=3, 82 | ) 83 | 84 | 85 | @pytest.mark.parametrize("specification", TEST_SPECIFICATIONS) 86 | def test_criterion_derivative(inputs, outputs, specification): 87 | cost_func_name, cost_func_name_short, scale, num_params = specification 88 | df = inputs["input data"] 89 | 90 | init_dict = inputs["init_dict"] 91 | init_dict["model_specifications"]["maint_cost_func"] = cost_func_name 92 | init_dict["model_specifications"]["cost_scale"] = scale 93 | 94 | crit_func_dict, transition_results = get_criterion_function(init_dict, df) 95 | 96 | assert_array_almost_equal( 97 | crit_func_dict["criterion_derivative"]( 98 | np.loadtxt(TEST_FOLDER + f"repl_params_{cost_func_name_short}.txt") 99 | ), 100 | np.zeros(num_params), 101 | decimal=2, 102 | ) 103 | -------------------------------------------------------------------------------- /ruspy/test/estimation_tests/test_estimation.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains unit tests, for the most important functions of 3 | ruspy.estimation.estimation_cost_parameters. The values to compare the results with 4 | are saved in resources/estimation_test. The setting of the test is documented in the 5 | inputs section in test module. 6 | """ 7 | import numpy as np 8 | import pytest 9 | from numpy.testing import assert_array_almost_equal 10 | 11 | from ruspy.config import TEST_RESOURCES_DIR 12 | from ruspy.estimation.estimation_transitions import create_transition_matrix 13 | from ruspy.model_code.choice_probabilities import choice_prob_gumbel 14 | from ruspy.model_code.cost_functions import calc_obs_costs 15 | from ruspy.model_code.cost_functions import lin_cost 16 | from ruspy.model_code.fix_point_alg import calc_fixp 17 | from ruspy.test.ranodm_init import random_init 18 | 19 | 20 | @pytest.fixture 21 | def inputs(): 22 | out = {} 23 | out["nstates"] = 90 24 | out["cost_fct"] = lin_cost 25 | out["params"] = np.array([10, 2]) 26 | out["trans_prob"] = np.array([0.2, 0.3, 0.15, 0.35]) 27 | out["disc_fac"] = 0.9999 28 | return out 29 | 30 | 31 | @pytest.fixture 32 | def outputs(): 33 | out = {} 34 | out["costs"] = np.loadtxt(TEST_RESOURCES_DIR + "estimation_test/myop_cost.txt") 35 | out["trans_mat"] = np.loadtxt(TEST_RESOURCES_DIR + "estimation_test/trans_mat.txt") 36 | out["fixp"] = np.loadtxt(TEST_RESOURCES_DIR + "estimation_test/fixp.txt") 37 | out["choice_probs"] = np.loadtxt( 38 | TEST_RESOURCES_DIR + "estimation_test/choice_prob.txt" 39 | ) 40 | return out 41 | 42 | 43 | def test_cost_func(inputs, outputs): 44 | assert_array_almost_equal( 45 | calc_obs_costs(inputs["nstates"], inputs["cost_fct"], inputs["params"], 0.001), 46 | outputs["costs"], 47 | ) 48 | 49 | 50 | def test_create_trans_mat(inputs, outputs): 51 | assert_array_almost_equal( 52 | create_transition_matrix(inputs["nstates"], inputs["trans_prob"]), 53 | outputs["trans_mat"], 54 | ) 55 | 56 | 57 | def test_fixp(inputs, outputs): 58 | assert_array_almost_equal( 59 | calc_fixp(outputs["trans_mat"], outputs["costs"], inputs["disc_fac"])[0], 60 | outputs["fixp"], 61 | ) 62 | 63 | 64 | def test_choice_probs(inputs, outputs): 65 | assert_array_almost_equal( 66 | choice_prob_gumbel(outputs["fixp"], outputs["costs"], inputs["disc_fac"]), 67 | outputs["choice_probs"], 68 | ) 69 | 70 | 71 | def test_trans_mat_rows_one(): 72 | rand_dict = random_init() 73 | control = np.ones(rand_dict["estimation"]["states"]) 74 | assert_array_almost_equal( 75 | create_transition_matrix( 76 | rand_dict["estimation"]["states"], 77 | np.array(rand_dict["simulation"]["known_trans"]), 78 | ).sum(axis=1), 79 | control, 80 | ) 81 | -------------------------------------------------------------------------------- /ruspy/test/estimation_tests/test_mpec_functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains unit tests, for the most important functions of 3 | ruspy.estimation.mpec. The values to compare the results with 4 | are saved in resources/estimation_test. The setting of the test is documented in the 5 | inputs section in test module. 6 | """ 7 | import numpy as np 8 | import pandas as pd 9 | import pytest 10 | from numpy.testing import assert_almost_equal 11 | from numpy.testing import assert_array_almost_equal 12 | 13 | from ruspy.config import TEST_RESOURCES_DIR 14 | from ruspy.estimation.estimation_transitions import create_transition_matrix 15 | from ruspy.estimation.estimation_transitions import estimate_transitions 16 | from ruspy.estimation.mpec import mpec_constraint 17 | from ruspy.estimation.mpec import mpec_constraint_derivative 18 | from ruspy.estimation.mpec import mpec_loglike_cost_params 19 | from ruspy.estimation.mpec import mpec_loglike_cost_params_derivative 20 | from ruspy.estimation.nfxp import create_state_matrix 21 | from ruspy.model_code.cost_functions import lin_cost 22 | from ruspy.model_code.cost_functions import lin_cost_dev 23 | 24 | TEST_FOLDER = TEST_RESOURCES_DIR + "replication_test/" 25 | 26 | 27 | @pytest.fixture 28 | def inputs(): 29 | out = {} 30 | df = pd.read_pickle(TEST_FOLDER + "group_4.pkl") 31 | transition_results = estimate_transitions(df) 32 | num_states = 90 33 | endog = df.loc[(slice(None), slice(1, None)), "decision"].to_numpy(int) 34 | states = df.loc[(slice(None), slice(1, None)), "state"].to_numpy(int) 35 | out["maint_func"] = lin_cost 36 | out["maint_func_dev"] = lin_cost_dev 37 | out["num_states"] = num_states 38 | out["trans_matrix"] = create_transition_matrix( 39 | num_states, np.array(transition_results["x"]) 40 | ) 41 | out["state_matrix"] = create_state_matrix(states, out["num_states"]) 42 | out["decision_matrix"] = np.vstack(((1 - endog), endog)) 43 | out["disc_fac"] = 0.9999 44 | out["scale"] = 0.001 45 | out["derivative"] = "Yes" 46 | out["params"] = np.ones(out["num_states"] + 2) 47 | return out 48 | 49 | 50 | @pytest.fixture 51 | def outputs(): 52 | out = {} 53 | out["mpec_likelihood"] = np.loadtxt( 54 | TEST_RESOURCES_DIR + "estimation_test/mpec_like.txt" 55 | ) 56 | out["mpec_constraint"] = np.loadtxt( 57 | TEST_RESOURCES_DIR + "estimation_test/mpec_constraint.txt" 58 | ) 59 | out["mpec_like_dev"] = np.loadtxt( 60 | TEST_RESOURCES_DIR + "estimation_test/mpec_like_dev.txt" 61 | ) 62 | out["mpec_constr_dev"] = np.loadtxt( 63 | TEST_RESOURCES_DIR + "estimation_test/mpec_constr_dev.txt" 64 | ) 65 | return out 66 | 67 | 68 | def test_mpec_likelihood(inputs, outputs): 69 | assert_almost_equal( 70 | mpec_loglike_cost_params( 71 | inputs["params"], 72 | inputs["maint_func"], 73 | inputs["maint_func_dev"], 74 | inputs["num_states"], 75 | inputs["disc_fac"], 76 | inputs["scale"], 77 | inputs["decision_matrix"], 78 | inputs["state_matrix"], 79 | ), 80 | outputs["mpec_likelihood"], 81 | ) 82 | 83 | 84 | def test_like_dev(inputs, outputs): 85 | assert_array_almost_equal( 86 | mpec_loglike_cost_params_derivative( 87 | inputs["params"], 88 | inputs["maint_func"], 89 | inputs["maint_func_dev"], 90 | inputs["num_states"], 91 | inputs["disc_fac"], 92 | inputs["scale"], 93 | inputs["decision_matrix"], 94 | inputs["state_matrix"], 95 | ), 96 | outputs["mpec_like_dev"], 97 | ) 98 | 99 | 100 | def test_mpec_constraint(inputs, outputs): 101 | assert_array_almost_equal( 102 | mpec_constraint( 103 | inputs["params"], 104 | inputs["maint_func"], 105 | inputs["maint_func_dev"], 106 | inputs["num_states"], 107 | inputs["disc_fac"], 108 | inputs["scale"], 109 | inputs["trans_matrix"], 110 | ), 111 | outputs["mpec_constraint"], 112 | ) 113 | 114 | 115 | def test_mpec_constraint_dev(inputs, outputs): 116 | assert_array_almost_equal( 117 | mpec_constraint_derivative( 118 | inputs["params"], 119 | inputs["maint_func"], 120 | inputs["maint_func_dev"], 121 | inputs["num_states"], 122 | inputs["disc_fac"], 123 | inputs["scale"], 124 | inputs["trans_matrix"], 125 | ), 126 | outputs["mpec_constr_dev"], 127 | ) 128 | -------------------------------------------------------------------------------- /ruspy/test/estimation_tests/test_replication.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains unit tests for the estimation process for different 3 | cost functions using the NFXP method. 4 | The parameters and likelihood are estimated by minimizing the criterion 5 | function using the minimize function from estimagic with scipy_lbgfsb algorithm. 6 | The estimated parameters and likelihood are compared to the true parameters and 7 | the true likelihood saved in resources/estimation_test. 8 | Moreover, the convergence of the algorithm is tested. 9 | """ 10 | import numpy as np 11 | import pandas as pd 12 | import pytest 13 | from estimagic import minimize 14 | from numpy.testing import assert_allclose 15 | 16 | from ruspy.config import TEST_RESOURCES_DIR 17 | from ruspy.estimation.criterion_function import get_criterion_function 18 | 19 | TEST_FOLDER = TEST_RESOURCES_DIR + "replication_test/" 20 | 21 | 22 | @pytest.fixture(scope="module") 23 | def inputs(): 24 | out = {} 25 | disc_fac = 0.9999 26 | num_states = 90 27 | init_dict = { 28 | "model_specifications": { 29 | "discount_factor": disc_fac, 30 | "num_states": num_states, 31 | }, 32 | "alg_details": {}, 33 | } 34 | 35 | df = pd.read_pickle(TEST_FOLDER + "group_4.pkl") 36 | 37 | out["input data"] = df 38 | out["init_dict"] = init_dict 39 | return out 40 | 41 | 42 | # true outputs 43 | @pytest.fixture(scope="module") 44 | def outputs(): 45 | out = {} 46 | out["params_linear"] = np.loadtxt( 47 | TEST_FOLDER + "repl_params_linear.txt" 48 | ) # 10.0750, 2.2930 49 | out["params_quadratic"] = np.loadtxt( 50 | TEST_FOLDER + "repl_params_quad.txt" 51 | ) # 11.48129539, 476.30640207, -2.31414426 52 | out["params_cubic"] = np.loadtxt( 53 | TEST_FOLDER + "repl_params_cubic.txt" 54 | ) # 8.26859, 1.00489, 0.494566, 26.3475 55 | out["params_hyperbolic"] = np.loadtxt( 56 | TEST_FOLDER + "repl_params_hyper.txt" 57 | ) # 8.05929601, 22.96685649 58 | out["params_square_root"] = np.loadtxt( 59 | TEST_FOLDER + "repl_params_sqrt.txt" 60 | ) # 11.42995702, 3.2308913 61 | 62 | out["cost_ll_linear"] = 163.584 63 | out["cost_ll_quadratic"] = 163.402 64 | out["cost_ll_cubic"] = 164.632939 # 162.885 65 | out["cost_ll_hyperbolic"] = 165.11428 # 165.423 66 | out["cost_ll_square_root"] = 163.390 # 163.395. 67 | 68 | return out 69 | 70 | 71 | TEST_SPECIFICATIONS = [ 72 | ("linear", np.array([2, 10]), 1e-3), 73 | # ("quadratic", np.array([11, 476.3, -2.3]), 1e-5), 74 | # ("cubic", np.array([8.3, 1, 0.5, 26]), 1e-8), 75 | ("hyperbolic", np.array([8, 23]), 1e-1), 76 | ("square_root", np.array([11, 3]), 0.01), 77 | ] 78 | 79 | 80 | # test parameters 81 | @pytest.mark.parametrize("specification", TEST_SPECIFICATIONS) 82 | def test_nfxp(inputs, outputs, specification): 83 | cost_func_name, init_params, scale = specification 84 | # specify init_dict with cost function and cost scale as well as input data 85 | df = inputs["input data"] 86 | init_dict = inputs["init_dict"] 87 | init_dict["model_specifications"]["maint_cost_func"] = cost_func_name 88 | init_dict["model_specifications"]["cost_scale"] = scale 89 | init_dict["method"] = "NFXP" 90 | # specify criterion function 91 | func_dict, result_trans = get_criterion_function(init_dict, df) 92 | criterion_func = func_dict["criterion_function"] 93 | criterion_dev = func_dict["criterion_derivative"] 94 | # minimize criterion function 95 | result_fixp = minimize( 96 | criterion=criterion_func, 97 | params=init_params, 98 | algorithm="scipy_lbfgsb", 99 | derivative=criterion_dev, 100 | ) 101 | # compare estimated cost parameters to true parameters 102 | assert_allclose(result_fixp.params, outputs["params_" + cost_func_name], atol=1e-1) 103 | # compare computed minimum neg log-likelihood to true minimum neg log-likelihood 104 | assert_allclose( 105 | result_fixp.criterion, outputs["cost_ll_" + cost_func_name], atol=1e-3 106 | ) 107 | 108 | # test success of algorithm 109 | assert result_fixp.success 110 | 111 | 112 | @pytest.mark.parametrize("specification", TEST_SPECIFICATIONS) 113 | def test_bhhh(inputs, outputs, specification): 114 | cost_func_name, init_params, scale = specification 115 | # specify init_dict with cost function and cost scale as well as input data 116 | df = inputs["input data"] 117 | init_dict = inputs["init_dict"] 118 | init_dict["model_specifications"]["maint_cost_func"] = cost_func_name 119 | init_dict["model_specifications"]["cost_scale"] = scale 120 | init_dict["method"] = "NFXP_BHHH" 121 | # specify criterion function 122 | func_dict, result_trans = get_criterion_function(init_dict, df) 123 | criterion_func = func_dict["criterion_function"] 124 | criterion_dev = func_dict["criterion_derivative"] 125 | # minimize criterion function 126 | result_fixp = minimize( 127 | criterion=criterion_func, 128 | params=outputs["params_" + cost_func_name], 129 | algorithm="bhhh", 130 | derivative=criterion_dev, 131 | ) 132 | # compare estimated cost parameters to true parameters 133 | assert_allclose(result_fixp.params, outputs["params_" + cost_func_name], atol=1e-1) 134 | # compare computed minimum neg log-likelihood to true minimum neg log-likelihood 135 | assert_allclose( 136 | result_fixp.criterion, outputs["cost_ll_" + cost_func_name], atol=1e-3 137 | ) 138 | # 139 | # # test success of algorithm 140 | # assert result_fixp.success 141 | 142 | 143 | @pytest.mark.parametrize("specification", TEST_SPECIFICATIONS) 144 | def test_mpec(inputs, outputs, specification): 145 | cost_func_name, init_params, scale = specification 146 | # specify init_dict with cost function and cost scale as well as input data 147 | df = inputs["input data"] 148 | init_dict = inputs["init_dict"] 149 | init_dict["model_specifications"]["maint_cost_func"] = cost_func_name 150 | init_dict["model_specifications"]["cost_scale"] = scale 151 | num_states = init_dict["model_specifications"]["num_states"] 152 | init_dict["method"] = "MPEC" 153 | # specify criterion function 154 | func_dict, result_trans = get_criterion_function(init_dict, df) 155 | criterion_func = func_dict["criterion_function"] 156 | criterion_dev = func_dict["criterion_derivative"] 157 | constraint = func_dict["constraint"] 158 | constraint_dev = func_dict["constraint_derivative"] 159 | x0 = np.zeros(num_states + init_params.shape[0], dtype=float) 160 | x0[num_states:] = init_params 161 | result_mpec = minimize( 162 | criterion=criterion_func, 163 | params=x0, 164 | algorithm="nlopt_slsqp", 165 | derivative=criterion_dev, 166 | constraints={ 167 | "type": "nonlinear", 168 | "func": constraint, 169 | "derivative": constraint_dev, 170 | "value": np.zeros(num_states, dtype=float), 171 | }, 172 | ) 173 | # compare computed minimum neg log-likelihood to true minimum neg log-likelihood 174 | assert_allclose( 175 | result_mpec.criterion, outputs["cost_ll_" + cost_func_name], atol=1e-3 176 | ) 177 | 178 | # compare estimated cost parameters to true parameters 179 | assert_allclose( 180 | result_mpec.params[90:], outputs["params_" + cost_func_name], atol=1e-1 181 | ) 182 | 183 | # test success of algorithm 184 | assert result_mpec.success 185 | -------------------------------------------------------------------------------- /ruspy/test/estimation_tests/test_transition_estimate.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains unit tests for the function estimate_transitions from 3 | ruspy.estimation.estimation_transitions. The values to compare the results with 4 | are saved in resources/estimation_test. The setting of the test is documented in the 5 | inputs section in test module. 6 | """ 7 | import numpy as np 8 | import pandas as pd 9 | import pytest 10 | from numpy.testing import assert_allclose 11 | from numpy.testing import assert_array_almost_equal 12 | 13 | from ruspy.config import TEST_RESOURCES_DIR 14 | from ruspy.estimation.estimation_transitions import estimate_transitions 15 | 16 | TEST_FOLDER = TEST_RESOURCES_DIR + "replication_test/" 17 | 18 | 19 | @pytest.fixture(scope="module") 20 | def inputs(): 21 | df = pd.read_pickle(TEST_FOLDER + "group_4.pkl") 22 | 23 | transition_results = estimate_transitions(df) 24 | out = { 25 | "params_est": transition_results["x"], 26 | "trans_count": transition_results["trans_count"], 27 | "fun": transition_results["fun"], 28 | } 29 | return out 30 | 31 | 32 | @pytest.fixture(scope="module") 33 | def outputs(): 34 | out = {} 35 | out["trans_base"] = np.loadtxt(TEST_FOLDER + "repl_test_trans.txt") 36 | out["transition_count"] = np.loadtxt(TEST_FOLDER + "transition_count.txt") 37 | out["trans_ll"] = 3140.570557 38 | return out 39 | 40 | 41 | def test_repl_trans(inputs, outputs): 42 | assert_array_almost_equal(inputs["params_est"], outputs["trans_base"]) 43 | 44 | 45 | def test_trans_ll(inputs, outputs): 46 | assert_allclose(inputs["fun"], outputs["trans_ll"]) 47 | 48 | 49 | def test_transcount(inputs, outputs): 50 | assert_allclose(inputs["trans_count"], outputs["transition_count"]) 51 | -------------------------------------------------------------------------------- /ruspy/test/old_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceEconomics/ruspy/414e9f98e3b1cf19544b1ca3b1e544ac5751c1e9/ruspy/test/old_tests/__init__.py -------------------------------------------------------------------------------- /ruspy/test/old_tests/test_mpec_cubic_repl.py: -------------------------------------------------------------------------------- 1 | # import numpy as np 2 | # import pandas as pd 3 | # import pytest 4 | # from numpy.testing import assert_almost_equal 5 | # from numpy.testing import assert_array_almost_equal 6 | # 7 | # from ruspy.config import TEST_RESOURCES_DIR 8 | # from ruspy.estimation.estimation import estimate 9 | # 10 | # 11 | # TEST_FOLDER = TEST_RESOURCES_DIR + "replication_test/" 12 | # 13 | # 14 | # @pytest.fixture(scope="module") 15 | # def inputs(): 16 | # out = {} 17 | # disc_fac = 0.9999 18 | # num_states = 90 19 | # scale = 1e-8 20 | # num_params = 4 21 | # lb = np.concatenate((np.full(num_states, -np.inf), np.full(num_params, 0.0))) 22 | # ub = np.concatenate((np.full(num_states, 50.0), np.full(num_params, np.inf))) 23 | # init_dict = { 24 | # "model_specifications": { 25 | # "discount_factor": disc_fac, 26 | # "num_states": num_states, 27 | # "maint_cost_func": "cubic", 28 | # "cost_scale": scale, 29 | # }, 30 | # "optimizer": { 31 | # "approach": "MPEC", 32 | # "algorithm": "LD_SLSQP", 33 | # "derivative": "Yes", 34 | # "params": np.concatenate( 35 | # (np.full(num_states, 0.0), np.array([4.0]), np.ones(num_params - 1)) 36 | # ), 37 | # "set_ftol_abs": 1e-15, 38 | # "set_xtol_rel": 1e-15, 39 | # "set_xtol_abs": 1e-3, 40 | # "set_lower_bounds": lb, 41 | # "set_upper_bounds": ub, 42 | # }, 43 | # } 44 | # df = pd.read_pickle(TEST_FOLDER + "group_4.pkl") 45 | # result_trans, result_fixp = estimate(init_dict, df) 46 | # out["params_est"] = result_fixp["x"][num_states:].round(8) 47 | # out["cost_ll"] = result_fixp["fun"] 48 | # out["status"] = result_fixp["status"] 49 | # return out 50 | # 51 | # 52 | # @pytest.fixture(scope="module") 53 | # def outputs(): 54 | # out = {} 55 | # out["params_base"] = np.array([10.07494318, 229309.298, 0.0, 0.0]) 56 | # out["cost_ll"] = 163.584283 # 162.885 57 | # return out 58 | # 59 | # 60 | # def test_repl_params(inputs, outputs): 61 | # assert_array_almost_equal(inputs["params_est"], outputs["params_base"], decimal=3) 62 | # 63 | # 64 | # def test_cost_ll(inputs, outputs): 65 | # assert_almost_equal(inputs["cost_ll"], outputs["cost_ll"], decimal=3) 66 | # 67 | # 68 | # def test_success(inputs): 69 | # assert inputs["status"] is True 70 | -------------------------------------------------------------------------------- /ruspy/test/old_tests/test_mpec_hyperbolic_repl.py: -------------------------------------------------------------------------------- 1 | # import numpy as np 2 | # import pandas as pd 3 | # import pytest 4 | # from numpy.testing import assert_almost_equal 5 | # from numpy.testing import assert_array_almost_equal 6 | # 7 | # from ruspy.config import TEST_RESOURCES_DIR 8 | # from ruspy.estimation.estimation import estimate 9 | # 10 | # 11 | # TEST_FOLDER = TEST_RESOURCES_DIR + "replication_test/" 12 | # 13 | # 14 | # @pytest.fixture(scope="module") 15 | # def inputs(): 16 | # out = {} 17 | # disc_fac = 0.9999 18 | # num_states = 90 19 | # scale = 1e-1 20 | # num_params = 2 21 | # lb = np.concatenate((np.full(num_states, -np.inf), np.full(num_params, 0.0))) 22 | # ub = np.concatenate((np.full(num_states, 50.0), np.full(num_params, np.inf))) 23 | # init_dict = { 24 | # "model_specifications": { 25 | # "discount_factor": disc_fac, 26 | # "num_states": num_states, 27 | # "maint_cost_func": "hyperbolic", 28 | # "cost_scale": scale, 29 | # }, 30 | # "optimizer": { 31 | # "approach": "MPEC", 32 | # "algorithm": "LD_SLSQP", 33 | # "derivative": "Yes", 34 | # "params": np.concatenate( 35 | # (np.full(num_states, 0.0), np.array([4.0]), np.ones(num_params - 1)) 36 | # ), 37 | # "set_ftol_abs": 1e-15, 38 | # "set_xtol_rel": 1e-15, 39 | # "set_xtol_abs": 1e-3, 40 | # "set_lower_bounds": lb, 41 | # "set_upper_bounds": ub, 42 | # }, 43 | # } 44 | # df = pd.read_pickle(TEST_FOLDER + "group_4.pkl") 45 | # result_trans, result_fixp = estimate(init_dict, df) 46 | # out["params_est"] = result_fixp["x"][num_states:] 47 | # out["cost_ll"] = result_fixp["fun"] 48 | # out["status"] = result_fixp["status"] 49 | # return out 50 | # 51 | # 52 | # @pytest.fixture(scope="module") 53 | # def outputs(): 54 | # out = {} 55 | # out["params_base"] = np.array([8.05712, 22.9398]) 56 | # out["cost_ll"] = 165.11428 # 165.423, 57 | # return out 58 | # 59 | # 60 | # def test_repl_params(inputs, outputs): 61 | # assert_array_almost_equal(inputs["params_est"], outputs["params_base"], decimal=3) 62 | # 63 | # 64 | # def test_cost_ll(inputs, outputs): 65 | # assert_almost_equal(inputs["cost_ll"], outputs["cost_ll"], decimal=3) 66 | # 67 | # 68 | # def test_success(inputs): 69 | # assert inputs["status"] is True 70 | -------------------------------------------------------------------------------- /ruspy/test/old_tests/test_mpec_linear_repl.py: -------------------------------------------------------------------------------- 1 | # import numpy as np 2 | # import pandas as pd 3 | # import pytest 4 | # from numpy.testing import assert_almost_equal 5 | # from numpy.testing import assert_array_almost_equal 6 | # 7 | # from ruspy.config import TEST_RESOURCES_DIR 8 | # from ruspy.estimation.estimation import estimate 9 | # 10 | # 11 | # TEST_FOLDER = TEST_RESOURCES_DIR + "replication_test/" 12 | # 13 | # 14 | # @pytest.fixture(scope="module") 15 | # def inputs(): 16 | # out = {} 17 | # disc_fac = 0.9999 18 | # num_states = 90 19 | # num_params = 2 20 | # scale = 1e-3 21 | # lb = np.concatenate((np.full(num_states, -np.inf), np.full(num_params, 0.0))) 22 | # ub = np.concatenate((np.full(num_states, 50.0), np.full(num_params, np.inf))) 23 | # init_dict = { 24 | # "model_specifications": { 25 | # "discount_factor": disc_fac, 26 | # "num_states": num_states, 27 | # "maint_cost_func": "linear", 28 | # "cost_scale": scale, 29 | # }, 30 | # "optimizer": { 31 | # "approach": "MPEC", 32 | # "algorithm": "LD_SLSQP", 33 | # "derivative": "Yes", 34 | # "params": np.concatenate( 35 | # (np.full(num_states, 0.0), np.array([4.0]), np.ones(num_params - 1)) 36 | # ), 37 | # "set_ftol_abs": 1e-15, 38 | # "set_xtol_rel": 1e-15, 39 | # "set_xtol_abs": 1e-3, 40 | # "set_lower_bounds": lb, 41 | # "set_upper_bounds": ub, 42 | # }, 43 | # } 44 | # df = pd.read_pickle(TEST_FOLDER + "group_4.pkl") 45 | # result_trans, result_fixp = estimate(init_dict, df) 46 | # out["params_est"] = result_fixp["x"][num_states:] 47 | # out["cost_ll"] = result_fixp["fun"] 48 | # out["status"] = result_fixp["status"] 49 | # return out 50 | # 51 | # 52 | # @pytest.fixture(scope="module") 53 | # def outputs(): 54 | # out = {} 55 | # out["params_base"] = np.loadtxt(TEST_FOLDER + "repl_params_linear.txt") 56 | # out["cost_ll"] = 163.584 57 | # return out 58 | # 59 | # 60 | # def test_repl_params(inputs, outputs): 61 | # assert_array_almost_equal(inputs["params_est"], outputs["params_base"], decimal=3) 62 | # 63 | # 64 | # def test_cost_ll(inputs, outputs): 65 | # assert_almost_equal(inputs["cost_ll"], outputs["cost_ll"], decimal=3) 66 | # 67 | # 68 | # def test_success(inputs): 69 | # assert inputs["status"] is True 70 | -------------------------------------------------------------------------------- /ruspy/test/old_tests/test_mpec_quadratic_repl.py: -------------------------------------------------------------------------------- 1 | # import numpy as np 2 | # import pandas as pd 3 | # import pytest 4 | # from numpy.testing import assert_almost_equal 5 | # from numpy.testing import assert_array_almost_equal 6 | # 7 | # from ruspy.config import TEST_RESOURCES_DIR 8 | # from ruspy.estimation.estimation import estimate 9 | # 10 | # 11 | # TEST_FOLDER = TEST_RESOURCES_DIR + "replication_test/" 12 | # 13 | # 14 | # @pytest.fixture(scope="module") 15 | # def inputs(): 16 | # out = {} 17 | # disc_fac = 0.9999 18 | # num_states = 90 19 | # scale = 1e-5 20 | # num_params = 3 21 | # lb = np.concatenate((np.full(num_states, -np.inf), np.full(num_params, 0.0))) 22 | # ub = np.concatenate((np.full(num_states, 50.0), np.full(num_params, np.inf))) 23 | # init_dict = { 24 | # "model_specifications": { 25 | # "discount_factor": disc_fac, 26 | # "num_states": num_states, 27 | # "maint_cost_func": "quadratic", 28 | # "cost_scale": scale, 29 | # }, 30 | # "optimizer": { 31 | # "approach": "MPEC", 32 | # "algorithm": "LD_SLSQP", 33 | # "derivative": "Yes", 34 | # "params": np.concatenate( 35 | # (np.full(num_states, 0.0), np.array([4.0]), np.ones(num_params - 1)) 36 | # ), 37 | # "set_ftol_abs": 1e-15, 38 | # "set_xtol_rel": 1e-15, 39 | # "set_xtol_abs": 1e-3, 40 | # "set_lower_bounds": lb, 41 | # "set_upper_bounds": ub, 42 | # }, 43 | # } 44 | # df = pd.read_pickle(TEST_FOLDER + "group_4.pkl") 45 | # result_trans, result_fixp = estimate(init_dict, df) 46 | # out["params_est"] = result_fixp["x"][num_states:] 47 | # out["cost_ll"] = result_fixp["fun"] 48 | # out["status"] = result_fixp["status"] 49 | # return out 50 | # 51 | # 52 | # @pytest.fixture(scope="module") 53 | # def outputs(): 54 | # out = {} 55 | # out["params_base"] = np.array([10.0749, 229.309, 5.62966e-16]) 56 | # out["cost_ll"] = 163.58428 57 | # return out 58 | # 59 | # 60 | # def test_repl_params(inputs, outputs): 61 | # assert_array_almost_equal(inputs["params_est"], outputs["params_base"], decimal=3) 62 | # 63 | # 64 | # def test_cost_ll(inputs, outputs): 65 | # assert_almost_equal(inputs["cost_ll"], outputs["cost_ll"], decimal=3) 66 | # 67 | # 68 | # def test_success(inputs): 69 | # assert inputs["status"] is True 70 | -------------------------------------------------------------------------------- /ruspy/test/old_tests/test_mpec_sqrt_repl.py: -------------------------------------------------------------------------------- 1 | # import numpy as np 2 | # import pandas as pd 3 | # import pytest 4 | # from numpy.testing import assert_almost_equal 5 | # from numpy.testing import assert_array_almost_equal 6 | # 7 | # from ruspy.config import TEST_RESOURCES_DIR 8 | # from ruspy.estimation.estimation import estimate 9 | # 10 | # 11 | # TEST_FOLDER = TEST_RESOURCES_DIR + "replication_test/" 12 | # 13 | # 14 | # @pytest.fixture(scope="module") 15 | # def inputs(): 16 | # out = {} 17 | # disc_fac = 0.9999 18 | # num_states = 90 19 | # scale = 0.01 20 | # num_params = 2 21 | # lb = np.concatenate((np.full(num_states, -np.inf), np.full(num_params, 0.0))) 22 | # ub = np.concatenate((np.full(num_states, 50.0), np.full(num_params, np.inf))) 23 | # init_dict = { 24 | # "model_specifications": { 25 | # "discount_factor": disc_fac, 26 | # "num_states": num_states, 27 | # "maint_cost_func": "square_root", 28 | # "cost_scale": scale, 29 | # }, 30 | # "optimizer": { 31 | # "approach": "MPEC", 32 | # "algorithm": "LD_SLSQP", 33 | # "derivative": "Yes", 34 | # "params": np.concatenate( 35 | # (np.full(num_states, 0.0), np.array([4.0]), np.ones(num_params - 1)) 36 | # ), 37 | # "set_ftol_abs": 1e-15, 38 | # "set_xtol_rel": 1e-15, 39 | # "set_xtol_abs": 1e-3, 40 | # "set_lower_bounds": lb, 41 | # "set_upper_bounds": ub, 42 | # }, 43 | # } 44 | # df = pd.read_pickle(TEST_FOLDER + "group_4.pkl") 45 | # result_trans, result_fixp = estimate(init_dict, df) 46 | # out["params_est"] = result_fixp["x"][num_states:] 47 | # out["cost_ll"] = result_fixp["fun"] 48 | # out["status"] = result_fixp["status"] 49 | # return out 50 | # 51 | # 52 | # @pytest.fixture(scope="module") 53 | # def outputs(): 54 | # out = {} 55 | # out["params_base"] = np.array([4.94556, 0.105939]) 56 | # out["cost_ll"] = 190.012779 57 | # return out 58 | # 59 | # 60 | # def test_repl_params(inputs, outputs): 61 | # assert_array_almost_equal(inputs["params_est"], outputs["params_base"], decimal=3) 62 | # 63 | # 64 | # def test_cost_ll(inputs, outputs): 65 | # assert_almost_equal(inputs["cost_ll"], outputs["cost_ll"], decimal=3) 66 | # 67 | # 68 | # def test_success(inputs): 69 | # assert inputs["status"] is True 70 | -------------------------------------------------------------------------------- /ruspy/test/param_sim_test/test_param_linear.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | from estimagic import minimize 4 | from numpy.testing import assert_allclose 5 | 6 | from ruspy.config import TEST_RESOURCES_DIR 7 | from ruspy.estimation.criterion_function import get_criterion_function 8 | from ruspy.estimation.estimation_transitions import create_transition_matrix 9 | from ruspy.model_code.cost_functions import calc_obs_costs 10 | from ruspy.model_code.cost_functions import lin_cost 11 | from ruspy.model_code.fix_point_alg import calc_fixp 12 | from ruspy.simulation.simulation import simulate 13 | 14 | TEST_FOLDER = TEST_RESOURCES_DIR + "replication_test/" 15 | 16 | 17 | @pytest.fixture(scope="module") 18 | def inputs(): 19 | out = {} 20 | disc_fac = 0.9999 21 | num_states = 300 22 | num_buses = 200 23 | num_periods = 1000 24 | scale = 0.001 25 | init_dict = { 26 | "model_specifications": { 27 | "discount_factor": disc_fac, 28 | "num_states": num_states, 29 | "maint_cost_func": "linear", 30 | "cost_scale": scale, 31 | }, 32 | "method": "NFXP", 33 | "simulation": { 34 | "discount_factor": disc_fac, 35 | "seed": 123, 36 | "buses": num_buses, 37 | "periods": num_periods, 38 | }, 39 | } 40 | out["trans_base"] = np.loadtxt(TEST_FOLDER + "repl_test_trans.txt") 41 | out["params_base"] = np.loadtxt(TEST_FOLDER + "repl_params_linear.txt") 42 | trans_mat = create_transition_matrix(num_states, out["trans_base"]) 43 | costs = calc_obs_costs(num_states, lin_cost, out["params_base"], scale) 44 | ev_known = calc_fixp(trans_mat, costs, disc_fac)[0] 45 | df = simulate(init_dict["simulation"], ev_known, costs, trans_mat) 46 | func_dict, result_trans = get_criterion_function(init_dict, df) 47 | criterion_func = func_dict["criterion_function"] 48 | criterion_dev = func_dict["criterion_derivative"] 49 | result_fixp = minimize( 50 | criterion=criterion_func, 51 | params=np.zeros(2, dtype=float), 52 | algorithm="scipy_lbfgsb", 53 | derivative=criterion_dev, 54 | ) 55 | out["trans_est"] = result_trans["x"] 56 | out["params_est"] = result_fixp.params 57 | return out 58 | 59 | 60 | def test_repl_rc(inputs): 61 | # This is as precise as the paper gets 62 | assert_allclose(inputs["params_est"][0], inputs["params_base"][0], atol=0.5) 63 | 64 | 65 | def test_repl_params(inputs): 66 | # This is as precise as the paper gets 67 | assert_allclose(inputs["params_est"][1], inputs["params_base"][1], atol=0.25) 68 | 69 | 70 | def test_repl_trans(inputs): 71 | assert_allclose(inputs["trans_est"], inputs["trans_base"], atol=1e-2) 72 | -------------------------------------------------------------------------------- /ruspy/test/ranodm_init.py: -------------------------------------------------------------------------------- 1 | """This function provides an random init file generating process.""" 2 | import collections 3 | 4 | import numpy as np 5 | import yaml 6 | 7 | 8 | def random_init(constr=None): 9 | """ 10 | The module provides a random dictionary generating process for test purposes. 11 | 12 | """ 13 | if constr is not None: 14 | pass 15 | else: 16 | constr = {} 17 | 18 | keys = constr.keys() 19 | if "BUSES" in keys: 20 | agents = constr["BUSES"] 21 | else: 22 | agents = np.random.randint(20, 100) 23 | 24 | if "discount_factor" in keys: 25 | disc_fac = constr["discount_factor"] 26 | else: 27 | disc_fac = np.random.uniform(0.9, 0.999) 28 | 29 | if "PERIODS" in keys: 30 | periods = constr["PERIODS"] 31 | else: 32 | periods = np.random.randint(1000, 10000) 33 | 34 | if "SEED" in keys: 35 | seed = constr["SEED"] 36 | else: 37 | seed = np.random.randint(1000, 9999) 38 | 39 | if "MAINT_FUNC" in keys: 40 | maint_func = constr["MAINT_FUNC"] 41 | else: 42 | maint_func = "linear" 43 | 44 | init_dict = {} 45 | 46 | for key_ in ["simulation", "estimation"]: 47 | init_dict[key_] = {} 48 | 49 | init_dict["simulation"]["periods"] = periods 50 | init_dict["simulation"]["buses"] = agents 51 | init_dict["simulation"]["discount_factor"] = disc_fac 52 | init_dict["simulation"]["seed"] = seed 53 | init_dict["simulation"]["maint_func"] = maint_func 54 | 55 | init_dict["estimation"]["states"] = np.random.randint(100, 150) 56 | init_dict["estimation"]["disc_fac"] = disc_fac 57 | init_dict["estimation"]["maint_func"] = maint_func 58 | 59 | # Generate random parameterization 60 | 61 | # Draw probabilities: 62 | p1 = np.random.uniform(0.37, 0.42) 63 | p2 = np.random.uniform(0.55, 0.58) 64 | p3 = 1 - p1 - p2 65 | init_dict["simulation"]["known_trans"] = [p1, p2, p3] 66 | 67 | return init_dict 68 | 69 | 70 | def print_dict(init_dict, file_name="test"): 71 | """ 72 | This function prints the initialization dict to a yaml file. 73 | 74 | """ 75 | ordered_dict = collections.OrderedDict() 76 | order = ["simulation", "estimation"] 77 | for key_ in order: 78 | ordered_dict[key_] = init_dict[key_] 79 | 80 | with open(f"{file_name}.ruspy.yml", "w") as outfile: 81 | yaml.dump(ordered_dict, outfile, explicit_start=True, indent=4) 82 | -------------------------------------------------------------------------------- /ruspy/test/regression_sim_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceEconomics/ruspy/414e9f98e3b1cf19544b1ca3b1e544ac5751c1e9/ruspy/test/regression_sim_tests/__init__.py -------------------------------------------------------------------------------- /ruspy/test/regression_sim_tests/regression_aux.py: -------------------------------------------------------------------------------- 1 | import numba 2 | import numpy as np 3 | 4 | 5 | def discount_utility(df, disc_fac): 6 | v = 0.0 7 | for i in df.index.levels[0]: 8 | v += np.sum( 9 | np.multiply( 10 | disc_fac ** df.loc[(i, slice(None))].index, 11 | df.loc[(i, slice(None)), "utilities"], 12 | ) 13 | ) 14 | return v / len(df.index.levels[0]) 15 | 16 | 17 | @numba.jit(nopython=True) 18 | def disc_ut_loop(utilities, disc_fac): 19 | num_buses, num_periods = utilities.shape 20 | v = 0.0 21 | for i in range(num_periods): 22 | v += (disc_fac**i) * np.sum(utilities[:, i]) 23 | return v / num_buses 24 | -------------------------------------------------------------------------------- /ruspy/test/regression_sim_tests/test_cubic_regression.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a regression test for the simulation, discounting and some 3 | estimation functions. The test first draws a random dictionary with some constraints 4 | to ensure enough observations. It then simulates a dataset and the compares the 5 | discounted utility average over all buses, with the theoretical expected value 6 | calculated by the NFXP. 7 | """ 8 | import numpy as np 9 | from numpy.testing import assert_allclose 10 | 11 | from ruspy.estimation.estimation_transitions import create_transition_matrix 12 | from ruspy.model_code.cost_functions import calc_obs_costs 13 | from ruspy.model_code.cost_functions import cubic_costs 14 | from ruspy.model_code.fix_point_alg import calc_fixp 15 | from ruspy.simulation.simulation import simulate 16 | from ruspy.test.ranodm_init import random_init 17 | from ruspy.test.regression_sim_tests.regression_aux import discount_utility 18 | 19 | 20 | def test_regression_simulation(inputs): 21 | init_dict = random_init(inputs) 22 | 23 | # Draw parameter 24 | param_1 = np.random.normal(17.5, 2) 25 | param_2 = np.random.normal(21, 2) 26 | param_3 = np.random.normal(-2, 0.1) 27 | param_4 = np.random.normal(0.2, 0.1) 28 | params = np.array([param_1, param_2, param_3, param_4]) 29 | 30 | disc_fac = init_dict["simulation"]["discount_factor"] 31 | probs = np.array(init_dict["simulation"]["known_trans"]) 32 | num_states = 800 33 | 34 | trans_mat = create_transition_matrix(num_states, probs) 35 | 36 | costs = calc_obs_costs(num_states, cubic_costs, params, 0.00001) 37 | 38 | ev = calc_fixp(trans_mat, costs, disc_fac)[0] 39 | 40 | df = simulate(init_dict["simulation"], ev, costs, trans_mat) 41 | 42 | v_disc = discount_utility(df, disc_fac) 43 | 44 | assert_allclose(v_disc / ev[0], 1, rtol=1e-02) 45 | -------------------------------------------------------------------------------- /ruspy/test/regression_sim_tests/test_hyper_regression.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a regression test for the simulation, discounting and some 3 | estimation functions. The test first draws a random dictionary with some constraints 4 | to ensure enough observations. It then simulates a dataset and the compares the 5 | discounted utility average over all buses, with the theoretical expected value 6 | calculated by the NFXP. 7 | """ 8 | import numpy as np 9 | from numpy.testing import assert_allclose 10 | 11 | from ruspy.estimation.estimation_transitions import create_transition_matrix 12 | from ruspy.model_code.cost_functions import calc_obs_costs 13 | from ruspy.model_code.cost_functions import hyperbolic_costs 14 | from ruspy.model_code.fix_point_alg import calc_fixp 15 | from ruspy.simulation.simulation import simulate 16 | from ruspy.test.ranodm_init import random_init 17 | from ruspy.test.regression_sim_tests.regression_aux import discount_utility 18 | 19 | 20 | def test_regression_simulation(inputs): 21 | init_dict = random_init(inputs) 22 | 23 | # Draw parameter 24 | param_1 = np.random.normal(8, 1) 25 | param_2 = np.random.normal(23, 2) 26 | params = np.array([param_1, param_2]) 27 | 28 | disc_fac = init_dict["simulation"]["discount_factor"] 29 | probs = np.array(init_dict["simulation"]["known_trans"]) 30 | num_states = 600 31 | 32 | trans_mat = create_transition_matrix(num_states, probs) 33 | 34 | costs = calc_obs_costs(num_states, hyperbolic_costs, params, 0.1) 35 | 36 | ev = calc_fixp(trans_mat, costs, disc_fac)[0] 37 | 38 | df = simulate(init_dict["simulation"], ev, costs, trans_mat) 39 | 40 | v_disc = discount_utility(df, disc_fac) 41 | 42 | assert_allclose(v_disc / ev[0], 1, rtol=1e-01) 43 | -------------------------------------------------------------------------------- /ruspy/test/regression_sim_tests/test_linear_regression.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a regression test for the simulation, discounting and some 3 | estimation functions. The test first draws a random dictionary with some constraints 4 | to ensure enough observations. It then simulates a dataset and the compares the 5 | discounted utility average over all buses, with the theoretical expected value 6 | calculated by the NFXP. 7 | """ 8 | import numpy as np 9 | import pytest 10 | from numpy.testing import assert_allclose 11 | 12 | from ruspy.estimation.estimation_transitions import create_transition_matrix 13 | from ruspy.model_code.cost_functions import calc_obs_costs 14 | from ruspy.model_code.cost_functions import lin_cost 15 | from ruspy.model_code.fix_point_alg import calc_fixp 16 | from ruspy.simulation.simulation import simulate 17 | from ruspy.test.ranodm_init import random_init 18 | from ruspy.test.regression_sim_tests.regression_aux import discount_utility 19 | 20 | 21 | @pytest.fixture(scope="module") 22 | def inputs_sim(inputs): 23 | out = {} 24 | out["init_dict"] = init_dict = random_init(inputs)["simulation"] 25 | 26 | # Draw parameter 27 | param1 = np.random.normal(10.0, 2) 28 | param2 = np.random.normal(2.3, 0.5) 29 | params = np.array([param1, param2]) 30 | 31 | disc_fac = init_dict["discount_factor"] 32 | probs = np.array(init_dict["known_trans"]) 33 | num_states = 300 34 | 35 | out["trans_mat"] = trans_mat = create_transition_matrix(num_states, probs) 36 | out["costs"] = costs = calc_obs_costs(num_states, lin_cost, params, 0.001) 37 | out["ev"] = ev = calc_fixp(trans_mat, costs, disc_fac)[0] 38 | out["df"] = simulate( 39 | init_dict, 40 | ev, 41 | costs, 42 | trans_mat, 43 | ) 44 | return out 45 | 46 | 47 | def test_regression_simulation(inputs_sim): 48 | 49 | v_disc = discount_utility( 50 | inputs_sim["df"], inputs_sim["init_dict"]["discount_factor"] 51 | ) 52 | 53 | assert_allclose(v_disc / inputs_sim["ev"][0], 1, rtol=1e-02) 54 | -------------------------------------------------------------------------------- /ruspy/test/regression_sim_tests/test_quadratic_regression.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a regression test for the simulation, discounting and some 3 | estimation functions. The test first draws a random dictionary with some constraints 4 | to ensure enough observations. It then simulates a dataset and the compares the 5 | discounted utility average over all buses, with the theoretical expected value 6 | calculated by the NFXP. 7 | """ 8 | import numpy as np 9 | from numpy.testing import assert_allclose 10 | 11 | from ruspy.estimation.estimation_transitions import create_transition_matrix 12 | from ruspy.model_code.cost_functions import calc_obs_costs 13 | from ruspy.model_code.cost_functions import quadratic_costs 14 | from ruspy.model_code.fix_point_alg import calc_fixp 15 | from ruspy.simulation.simulation import simulate 16 | from ruspy.test.ranodm_init import random_init 17 | from ruspy.test.regression_sim_tests.regression_aux import discount_utility 18 | 19 | 20 | def test_regression_simulation(inputs): 21 | init_dict = random_init(inputs) 22 | 23 | # Draw parameter 24 | param_1 = np.random.normal(11.5, 0.2) 25 | param_2 = np.random.normal(47, 0.5) 26 | param_3 = np.random.normal(0.2, 0.02) 27 | params = np.array([param_1, param_2, param_3]) 28 | 29 | disc_fac = init_dict["simulation"]["discount_factor"] 30 | probs = np.array(init_dict["simulation"]["known_trans"]) 31 | num_states = 500 32 | 33 | trans_mat = create_transition_matrix(num_states, probs) 34 | 35 | costs = calc_obs_costs(num_states, quadratic_costs, params, 0.0001) 36 | 37 | ev = calc_fixp(trans_mat, costs, disc_fac)[0] 38 | 39 | df = simulate(init_dict["simulation"], ev, costs, trans_mat) 40 | 41 | v_disc = discount_utility(df, disc_fac) 42 | 43 | assert_allclose(v_disc / ev[0], 1, rtol=1e-02) 44 | -------------------------------------------------------------------------------- /ruspy/test/regression_sim_tests/test_sqrt_regression.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a regression test for the simulation, discounting and some 3 | estimation functions. The test first draws a random dictionary with some constraints 4 | to ensure enough observations. It then simulates a dataset and the compares the 5 | discounted utility average over all buses, with the theoretical expected value 6 | calculated by the NFXP. 7 | """ 8 | import numpy as np 9 | from numpy.testing import assert_allclose 10 | 11 | from ruspy.estimation.estimation_transitions import create_transition_matrix 12 | from ruspy.model_code.cost_functions import calc_obs_costs 13 | from ruspy.model_code.cost_functions import sqrt_costs 14 | from ruspy.model_code.fix_point_alg import calc_fixp 15 | from ruspy.simulation.simulation import simulate 16 | from ruspy.test.ranodm_init import random_init 17 | from ruspy.test.regression_sim_tests.regression_aux import discount_utility 18 | 19 | 20 | def test_regression_simulation(inputs): 21 | init_dict = random_init(inputs) 22 | 23 | # Draw parameter 24 | param_1 = np.random.normal(11.5, 0.2) 25 | param_2 = np.random.normal(3.2, 0.5) 26 | params = np.array([param_1, param_2]) 27 | 28 | disc_fac = init_dict["simulation"]["discount_factor"] 29 | probs = np.array(init_dict["simulation"]["known_trans"]) 30 | num_states = 500 31 | 32 | trans_mat = create_transition_matrix(num_states, probs) 33 | 34 | costs = calc_obs_costs(num_states, sqrt_costs, params, 0.01) 35 | 36 | ev = calc_fixp(trans_mat, costs, disc_fac)[0] 37 | 38 | df = simulate(init_dict["simulation"], ev, costs, trans_mat) 39 | 40 | v_disc = discount_utility(df, disc_fac) 41 | 42 | assert_allclose(v_disc / ev[0], 1, rtol=1e-02) 43 | -------------------------------------------------------------------------------- /ruspy/test/resources/demand_test/get_demand.txt: -------------------------------------------------------------------------------- 1 | 1.533931365 2 | 1.4081459 3 | 1.293713281 4 | 1.189867499 5 | 1.095828888 6 | 1.010819577 7 | 0.9340768126 8 | 0.8648639805 9 | 0.8024792809 10 | 0.7462620796 11 | 0.695597092 12 | 0.6499166215 13 | 0.6087011479 14 | 0.5714785795 15 | 0.5378224882 16 | 0.5073496226 17 | 0.4797169547 18 | 0.4546184754 19 | 0.4317819074 20 | 0.4109654564 21 | 0.3919546971 22 | 0.3745596419 23 | 0.3586120319 24 | 0.3439628653 25 | 0.3304801593 26 | 0.3180469486 27 | 0.3065595003 28 | 0.2959257308 29 | 0.286063813 30 | 0.2769009442 31 | 0.268372268 32 | 0.260419926 33 | 0.2529922256 34 | 0.2460429163 35 | 0.2395305489 36 | 0.2334179233 37 | 0.2276716004 38 | 0.2222614719 39 | 0.2171603909 40 | 0.2123438354 41 | 0.207789622 42 | 0.2034776525 43 | 0.1993896847 44 | 0.1955091443 45 | 0.191820944 46 | 0.1883113344 47 | 0.184967764 48 | 0.1817787553 49 | 0.1787338036 50 | 0.1758232704 51 | 0.1730383125 52 | 0.1703708003 53 | 0.1678132475 54 | 0.165358762 55 | 0.1630009741 56 | 0.160734 57 | 0.1585523906 58 | 0.1564510958 59 | 0.1544254353 60 | 0.152471057 61 | 0.1505839226 62 | 0.1487602655 63 | 0.1469965721 64 | 0.1452895675 65 | 0.1436361776 66 | 0.1420335368 67 | 0.1404789579 68 | 0.1389699175 69 | 0.1375040572 70 | 0.1360791474 71 | 0.1346930911 72 | 0.1333439129 73 | 0.1320297381 74 | 0.130748805 75 | 0.1294994452 76 | 0.1282800696 77 | 0.127089189 78 | 0.1259253721 79 | 0.1247872592 80 | 0.12367356 81 | 0.1225830307 82 | 0.1215144891 83 | 0.1204668054 84 | 0.1194388894 85 | 0.1184297116 86 | 0.1174382612 87 | 0.116463575 88 | 0.1155047269 89 | 0.1145607998 90 | 0.1136309246 91 | 0.112714252 92 | 0.1118099554 93 | 0.1109172312 94 | 0.110035282 95 | 0.1091633498 96 | 0.1083006735 97 | 0.1074465094 98 | 0.1066001248 99 | 0.1057607963 100 | 0.1049278067 101 | -------------------------------------------------------------------------------- /ruspy/test/resources/estimation_test/choice_prob.txt: -------------------------------------------------------------------------------- 1 | 9.999546021312974986e-01 4.539786870243438780e-05 2 | 9.999495601838638281e-01 5.043981613624756862e-05 3 | 9.999440256464610099e-01 5.597435353905519173e-05 4 | 9.999379584941774368e-01 6.204150582264734222e-05 5 | 9.999313163563542384e-01 6.868364364576132913e-05 6 | 9.999240544754056659e-01 7.594552459435588931e-05 7 | 9.999161256759879768e-01 8.387432401208557103e-05 8 | 9.999074803458125604e-01 9.251965418754790949e-05 9 | 9.998980664294244702e-01 1.019335705754468088e-04 10 | 9.998878294362908425e-01 1.121705637090948568e-04 11 | 9.998767124645459026e-01 1.232875354540060588e-04 12 | 9.998646562417428107e-01 1.353437582571242798e-04 13 | 9.998515991839352912e-01 1.484008160646697536e-04 14 | 9.998374774743846727e-01 1.625225256152382778e-04 15 | 9.998222251631361246e-01 1.777748368637446796e-04 16 | 9.998057742886420352e-01 1.942257113579219131e-04 17 | 9.997880550225283214e-01 2.119449774716990838e-04 18 | 9.997689958384990971e-01 2.310041615009319441e-04 19 | 9.997485237062544439e-01 2.514762937456127296e-04 20 | 9.997265643111605815e-01 2.734356888394070509e-04 21 | 9.997030423002574162e-01 2.969576997426231482e-04 22 | 9.996778815550130259e-01 3.221184449870343434e-04 23 | 9.996510054910496823e-01 3.489945089502872284e-04 24 | 9.996223373848563964e-01 3.776626151435823566e-04 25 | 9.995918007272834860e-01 4.081992727165140709e-04 26 | 9.995593196033830674e-01 4.406803966169541118e-04 27 | 9.995248190979100222e-01 4.751809020899823714e-04 28 | 9.994882257255444102e-01 5.117742744556455980e-04 29 | 9.994494678846314040e-01 5.505321153686218352e-04 30 | 9.994084763329674770e-01 5.915236670325062099e-04 31 | 9.993651846838862429e-01 6.348153161137794123e-04 32 | 9.993195299206284465e-01 6.804700793715698316e-04 33 | 9.992714529267089363e-01 7.285470732911222651e-04 34 | 9.992208990297272164e-01 7.791009702728638082e-04 35 | 9.991678185558161562e-01 8.321814441838508132e-04 36 | 9.991121673916738555e-01 8.878326083261723874e-04 37 | 9.990539075508927391e-01 9.460924491072763729e-04 38 | 9.989930077410905662e-01 1.006992258909491459e-03 39 | 9.989294439281469762e-01 1.070556071853125298e-03 40 | 9.988631998936751133e-01 1.136800106324925520e-03 41 | 9.987942677817198689e-01 1.205732218280225867e-03 42 | 9.987226486305429418e-01 1.277351369457101335e-03 43 | 9.986483528852447833e-01 1.351647114755301924e-03 44 | 9.985714008869530645e-01 1.428599113047025098e-03 45 | 9.984918233342524596e-01 1.508176665747522393e-03 46 | 9.984096617124418316e-01 1.590338287558099679e-03 47 | 9.983249686864276162e-01 1.675031313572356885e-03 48 | 9.982378084530526552e-01 1.762191546947267399e-03 49 | 9.981482570483838357e-01 1.851742951616135472e-03 50 | 9.980564026063613037e-01 1.943597393638640813e-03 51 | 9.979623455651892572e-01 2.037654434810638304e-03 52 | 9.978661988164231333e-01 2.133801183576858849e-03 53 | 9.977680877948498228e-01 2.231912205150120374e-03 54 | 9.976681505071910427e-01 2.331849492808932105e-03 55 | 9.975665374911081296e-01 2.433462508891835741e-03 56 | 9.974634117073754025e-01 2.536588292624568450e-03 57 | 9.973589483688913848e-01 2.641051631108564009e-03 58 | 9.972533346822397560e-01 2.746665317760157727e-03 59 | 9.971467695191328362e-01 2.853230480867053644e-03 60 | 9.970394630441551387e-01 2.960536955844766775e-03 61 | 9.969316362105012930e-01 3.068363789498708763e-03 62 | 9.968235201862337691e-01 3.176479813766299419e-03 63 | 9.967153558339810759e-01 3.284644166018940107e-03 64 | 9.966073929080448979e-01 3.392607091955190599e-03 65 | 9.964998891695180383e-01 3.500110830481949556e-03 66 | 9.963931099499580002e-01 3.606890050041998506e-03 67 | 9.962873268946710326e-01 3.712673105328965703e-03 68 | 9.961828164819064302e-01 3.817183518093656119e-03 69 | 9.960798605356667723e-01 3.920139464333251104e-03 70 | 9.959787440228951017e-01 4.021255977104991157e-03 71 | 9.958797517339983418e-01 4.120248266001665105e-03 72 | 9.957831728898747237e-01 4.216827110125356132e-03 73 | 9.956892965228913983e-01 4.310703477108669343e-03 74 | 9.955984011395102584e-01 4.401598860489804900e-03 75 | 9.955107747121477724e-01 4.489225287852316101e-03 76 | 9.954267037063525558e-01 4.573296293647528300e-03 77 | 9.953464349214653506e-01 4.653565078534691903e-03 78 | 9.952702507969064527e-01 4.729749203093585458e-03 79 | 9.951984434152417736e-01 4.801556584758264606e-03 80 | 9.951311673007494640e-01 4.868832699250627040e-03 81 | 9.950687087616015836e-01 4.931291238398476280e-03 82 | 9.950114345671139393e-01 4.988565432886103192e-03 83 | 9.949592261841506691e-01 5.040773815849332665e-03 84 | 9.949124156848291323e-01 5.087584315170958806e-03 85 | 9.948717476110359170e-01 5.128252388964125476e-03 86 | 9.948362407429913734e-01 5.163759257008654346e-03 87 | 9.948063662359291071e-01 5.193633764070986619e-03 88 | 9.947844230520817010e-01 5.215576947918324976e-03 89 | 9.947666139208877212e-01 5.233386079112326526e-03 90 | 9.947536002076516892e-01 5.246399792348369756e-03 91 | -------------------------------------------------------------------------------- /ruspy/test/resources/estimation_test/fixp.txt: -------------------------------------------------------------------------------- 1 | -1.722820390492623346e+03 2 | -1.722923721574748470e+03 3 | -1.723025850069854187e+03 4 | -1.723126776226479706e+03 5 | -1.723226500318767421e+03 6 | -1.723325022647906280e+03 7 | -1.723422343543597435e+03 8 | -1.723518463365533080e+03 9 | -1.723613382504875744e+03 10 | -1.723707101385738042e+03 11 | -1.723799620466645365e+03 12 | -1.723890940241976523e+03 13 | -1.723981061243370277e+03 14 | -1.724069984041082762e+03 15 | -1.724157709245289880e+03 16 | -1.724244237507313073e+03 17 | -1.724329569520762107e+03 18 | -1.724413706022575752e+03 19 | -1.724496647793946067e+03 20 | -1.724578395661113518e+03 21 | -1.724658950496011585e+03 22 | -1.724738313216751294e+03 23 | -1.724816484787923855e+03 24 | -1.724893466220708433e+03 25 | -1.724969258572767785e+03 26 | -1.725043862947914249e+03 27 | -1.725117280495534033e+03 28 | -1.725189512409752524e+03 29 | -1.725260559928324028e+03 30 | -1.725330424331238873e+03 31 | -1.725399106939027661e+03 32 | -1.725466609110758327e+03 33 | -1.725532932241711706e+03 34 | -1.725598077760728302e+03 35 | -1.725662047127221285e+03 36 | -1.725724841827846603e+03 37 | -1.725786463372833168e+03 38 | -1.725846913291967894e+03 39 | -1.725906193130234897e+03 40 | -1.725964304443123865e+03 41 | -1.726021248791595781e+03 42 | -1.726077027736725995e+03 43 | -1.726131642834047398e+03 44 | -1.726185095627575265e+03 45 | -1.726237387643544707e+03 46 | -1.726288520383923924e+03 47 | -1.726338495319645972e+03 48 | -1.726387313883580418e+03 49 | -1.726434977463455652e+03 50 | -1.726481487394516307e+03 51 | -1.726526844951853946e+03 52 | -1.726571051343165436e+03 53 | -1.726614107701133889e+03 54 | -1.726656015075034020e+03 55 | -1.726696774424151954e+03 56 | -1.726736386610399677e+03 57 | -1.726774852388151885e+03 58 | -1.726812172400306508e+03 59 | -1.726848347172426202e+03 60 | -1.726883377096205777e+03 61 | -1.726917262433640190e+03 62 | -1.726950003317300570e+03 63 | -1.726981599710241653e+03 64 | -1.727012051434268642e+03 65 | -1.727041358194811210e+03 66 | -1.727069519456177204e+03 67 | -1.727096534540972698e+03 68 | -1.727122402754180030e+03 69 | -1.727147122961049490e+03 70 | -1.727170693892903728e+03 71 | -1.727193114666224574e+03 72 | -1.727214383331542422e+03 73 | -1.727234497756262726e+03 74 | -1.727253457687084619e+03 75 | -1.727271259777879550e+03 76 | -1.727287899984941987e+03 77 | -1.727303381548777452e+03 78 | -1.727317698092763976e+03 79 | -1.727330839533476137e+03 80 | -1.727342822397133887e+03 81 | -1.727353632896306181e+03 82 | -1.727363238928383225e+03 83 | -1.727371703471505725e+03 84 | -1.727378994767847189e+03 85 | -1.727384998057631719e+03 86 | -1.727389934157967900e+03 87 | -1.727393733314897872e+03 88 | -1.727395971712040136e+03 89 | -1.727397398545108899e+03 90 | -1.727397895262125530e+03 91 | -------------------------------------------------------------------------------- /ruspy/test/resources/estimation_test/mpec_constraint.txt: -------------------------------------------------------------------------------- 1 | 3.127078207561009293e-01 2 | 3.119769826368141707e-01 3 | 3.112463412766801607e-01 4 | 3.105158967664807257e-01 5 | 3.097856491969634973e-01 6 | 3.090555986588388038e-01 7 | 3.083257452427825562e-01 8 | 3.075960890394333624e-01 9 | 3.068666301393945250e-01 10 | 3.061373686332329314e-01 11 | 3.054083046114790534e-01 12 | 3.046794381646262817e-01 13 | 3.039507693831322577e-01 14 | 3.032222983574168751e-01 15 | 3.024940251778642786e-01 16 | 3.017659499348206431e-01 17 | 3.010380727185957284e-01 18 | 3.003103936194611023e-01 19 | 2.995829127276523618e-01 20 | 2.988556301333662457e-01 21 | 2.981285459267621896e-01 22 | 2.974016601979625474e-01 23 | 2.966749730370510374e-01 24 | 2.959484845340736303e-01 25 | 2.952221947790378831e-01 26 | 2.944961038619142712e-01 27 | 2.937702118726328582e-01 28 | 2.930445189010872920e-01 29 | 2.923190250371312526e-01 30 | 2.915937303705802286e-01 31 | 2.908686349912099622e-01 32 | 2.901437389887586704e-01 33 | 2.894190424529246020e-01 34 | 2.886945454733664818e-01 35 | 2.879702481397037328e-01 36 | 2.872461505415171423e-01 37 | 2.865222527683468634e-01 38 | 2.857985549096939693e-01 39 | 2.850750570550188989e-01 40 | 2.843517592937425675e-01 41 | 2.836286617152461442e-01 42 | 2.829057644088699419e-01 43 | 2.821830674639143055e-01 44 | 2.814605709696382796e-01 45 | 2.807382750152613848e-01 46 | 2.800161796899613975e-01 47 | 2.792942850828759038e-01 48 | 2.785725912831011897e-01 49 | 2.778510983796922407e-01 50 | 2.771298064616634083e-01 51 | 2.764087156179870775e-01 52 | 2.756878259375943330e-01 53 | 2.749671375093745151e-01 54 | 2.742466504221749979e-01 55 | 2.735263647648020768e-01 56 | 2.728062806260189710e-01 57 | 2.720863980945473770e-01 58 | 2.713667172590670251e-01 59 | 2.706472382082143469e-01 60 | 2.699279610305842514e-01 61 | 2.692088858147283492e-01 62 | 2.684900126491551742e-01 63 | 2.677713416223315157e-01 64 | 2.670528728226799764e-01 65 | 2.663346063385807483e-01 66 | 2.656165422583702806e-01 67 | 2.648986806703421681e-01 68 | 2.641810216627464847e-01 69 | 2.634635653237882291e-01 70 | 2.627463117416304339e-01 71 | 2.620292610043912784e-01 72 | 2.613124132001456434e-01 73 | 2.605957684169231126e-01 74 | 2.598793267427099707e-01 75 | 2.591630882654474277e-01 76 | 2.584470530730325066e-01 77 | 2.577312212533180436e-01 78 | 2.570155928941111334e-01 79 | 2.563001680831744622e-01 80 | 2.555849469082258629e-01 81 | 2.548699294569369833e-01 82 | 2.541551158169359503e-01 83 | 2.534405060758035955e-01 84 | 2.527261003210765633e-01 85 | 2.520118986402450911e-01 86 | 2.512979011207538971e-01 87 | 2.505841078500019581e-01 88 | 2.498705189153413997e-01 89 | 2.491662722812835185e-01 90 | 2.488867327674844088e-01 91 | -------------------------------------------------------------------------------- /ruspy/test/resources/estimation_test/mpec_like.txt: -------------------------------------------------------------------------------- 1 | 1405.8385077255232 2 | -------------------------------------------------------------------------------- /ruspy/test/resources/estimation_test/mpec_like_dev.txt: -------------------------------------------------------------------------------- 1 | 1.115833334908453935e+03 2 | -2.825667231642623278e+01 3 | -3.097040774354147885e+01 4 | -3.314907966254465066e+01 5 | -3.263389634999842315e+01 6 | -3.265773343833079423e+01 7 | -2.781985863035293249e+01 8 | -2.621841099860221291e+01 9 | -2.650803661356621177e+01 10 | -2.679806573944675208e+01 11 | -2.600495869852113273e+01 12 | -2.656608800403520831e+01 13 | -2.522905537443812563e+01 14 | -2.769074482034491780e+01 15 | -2.282075929169812767e+01 16 | -2.501237227212911307e+01 17 | -2.503058791378247605e+01 18 | -2.477654216592922864e+01 19 | -2.261483327091923456e+01 20 | -2.154062486484180283e+01 21 | -1.855478562467431303e+01 22 | -2.020665767461206030e+01 23 | -1.994808794485911108e+01 24 | -1.968912762380507786e+01 25 | -1.678792350014430212e+01 26 | -1.789627648637694080e+01 27 | -1.699150469399071284e+01 28 | -1.755235291493809058e+01 29 | -1.509500251155412620e+01 30 | -1.483130291868733153e+01 31 | -1.494157482127744352e+01 32 | -1.347756565468237611e+01 33 | -1.348733801053714743e+01 34 | -1.414992268921179175e+01 35 | -1.416090151993790336e+01 36 | -1.306848271171918263e+01 37 | -1.435462265238382784e+01 38 | -1.408876628613242588e+01 39 | -1.382251462529446151e+01 40 | -1.410916810972552149e+01 41 | -1.578047890219899330e+01 42 | -1.562314490136289002e+01 43 | -1.158550286865151513e+01 44 | -1.442747793779269472e+01 45 | -1.260504838564652097e+01 46 | -1.278121988111647411e+01 47 | -1.251239820684810411e+01 48 | -1.291280349761026081e+01 49 | -1.308738196709883184e+01 50 | -1.237558271991462000e+01 51 | -1.154866200727226477e+01 52 | -1.223216136186135117e+01 53 | -9.729090063590124515e+00 54 | -1.157582688217447675e+01 55 | -7.627455860803622478e+00 56 | -9.795221941826602219e+00 57 | -9.362507237955671968e+00 58 | -6.688468493413697757e+00 59 | -8.816496776757279719e+00 60 | -6.578198685051828143e+00 61 | -6.583650818832659901e+00 62 | -7.307931070322666756e+00 63 | -6.469356735034256900e+00 64 | -6.474007249200335323e+00 65 | -5.478759796022490747e+00 66 | -5.201531140831708200e+00 67 | -2.949299729179925667e+00 68 | -4.234323666823311960e+00 69 | -4.802344996905781649e+00 70 | -4.523097728732586198e+00 71 | -1.263271376284614611e+00 72 | -1.415496620481435652e+00 73 | -1.333092567351181756e-01 74 | -5.670107845075789132e-01 75 | -5.674171165238518499e-01 76 | -5.678236243273435679e-01 77 | -5.682303078287663789e-01 78 | 4.312628330614206029e-01 79 | 0.000000000000000000e+00 80 | 0.000000000000000000e+00 81 | 0.000000000000000000e+00 82 | 0.000000000000000000e+00 83 | 0.000000000000000000e+00 84 | 0.000000000000000000e+00 85 | 0.000000000000000000e+00 86 | 0.000000000000000000e+00 87 | 0.000000000000000000e+00 88 | 0.000000000000000000e+00 89 | 0.000000000000000000e+00 90 | 0.000000000000000000e+00 91 | -1.143108012959763528e+03 92 | 2.875260773464651365e+01 93 | -------------------------------------------------------------------------------- /ruspy/test/resources/estimation_test/myop_cost.txt: -------------------------------------------------------------------------------- 1 | 0.000000000000000000e+00 1.000000000000000000e+01 2 | 2.000000000000000042e-03 1.000000000000000000e+01 3 | 4.000000000000000083e-03 1.000000000000000000e+01 4 | 6.000000000000000125e-03 1.000000000000000000e+01 5 | 8.000000000000000167e-03 1.000000000000000000e+01 6 | 1.000000000000000021e-02 1.000000000000000000e+01 7 | 1.200000000000000025e-02 1.000000000000000000e+01 8 | 1.400000000000000029e-02 1.000000000000000000e+01 9 | 1.600000000000000033e-02 1.000000000000000000e+01 10 | 1.800000000000000211e-02 1.000000000000000000e+01 11 | 2.000000000000000042e-02 1.000000000000000000e+01 12 | 2.199999999999999872e-02 1.000000000000000000e+01 13 | 2.400000000000000050e-02 1.000000000000000000e+01 14 | 2.600000000000000228e-02 1.000000000000000000e+01 15 | 2.800000000000000058e-02 1.000000000000000000e+01 16 | 2.999999999999999889e-02 1.000000000000000000e+01 17 | 3.200000000000000067e-02 1.000000000000000000e+01 18 | 3.400000000000000244e-02 1.000000000000000000e+01 19 | 3.600000000000000422e-02 1.000000000000000000e+01 20 | 3.799999999999999906e-02 1.000000000000000000e+01 21 | 4.000000000000000083e-02 1.000000000000000000e+01 22 | 4.200000000000000261e-02 1.000000000000000000e+01 23 | 4.399999999999999745e-02 1.000000000000000000e+01 24 | 4.599999999999999922e-02 1.000000000000000000e+01 25 | 4.800000000000000100e-02 1.000000000000000000e+01 26 | 5.000000000000000278e-02 1.000000000000000000e+01 27 | 5.200000000000000455e-02 1.000000000000000000e+01 28 | 5.399999999999999939e-02 1.000000000000000000e+01 29 | 5.600000000000000117e-02 1.000000000000000000e+01 30 | 5.800000000000000294e-02 1.000000000000000000e+01 31 | 5.999999999999999778e-02 1.000000000000000000e+01 32 | 6.199999999999999956e-02 1.000000000000000000e+01 33 | 6.400000000000000133e-02 1.000000000000000000e+01 34 | 6.600000000000000311e-02 1.000000000000000000e+01 35 | 6.800000000000000488e-02 1.000000000000000000e+01 36 | 7.000000000000000666e-02 1.000000000000000000e+01 37 | 7.200000000000000844e-02 1.000000000000000000e+01 38 | 7.399999999999999634e-02 1.000000000000000000e+01 39 | 7.599999999999999811e-02 1.000000000000000000e+01 40 | 7.799999999999999989e-02 1.000000000000000000e+01 41 | 8.000000000000000167e-02 1.000000000000000000e+01 42 | 8.200000000000000344e-02 1.000000000000000000e+01 43 | 8.400000000000000522e-02 1.000000000000000000e+01 44 | 8.600000000000000699e-02 1.000000000000000000e+01 45 | 8.799999999999999489e-02 1.000000000000000000e+01 46 | 8.999999999999999667e-02 1.000000000000000000e+01 47 | 9.199999999999999845e-02 1.000000000000000000e+01 48 | 9.400000000000000022e-02 1.000000000000000000e+01 49 | 9.600000000000000200e-02 1.000000000000000000e+01 50 | 9.800000000000000377e-02 1.000000000000000000e+01 51 | 1.000000000000000056e-01 1.000000000000000000e+01 52 | 1.020000000000000073e-01 1.000000000000000000e+01 53 | 1.040000000000000091e-01 1.000000000000000000e+01 54 | 1.059999999999999970e-01 1.000000000000000000e+01 55 | 1.079999999999999988e-01 1.000000000000000000e+01 56 | 1.100000000000000006e-01 1.000000000000000000e+01 57 | 1.120000000000000023e-01 1.000000000000000000e+01 58 | 1.140000000000000041e-01 1.000000000000000000e+01 59 | 1.160000000000000059e-01 1.000000000000000000e+01 60 | 1.180000000000000077e-01 1.000000000000000000e+01 61 | 1.199999999999999956e-01 1.000000000000000000e+01 62 | 1.219999999999999973e-01 1.000000000000000000e+01 63 | 1.239999999999999991e-01 1.000000000000000000e+01 64 | 1.260000000000000009e-01 1.000000000000000000e+01 65 | 1.280000000000000027e-01 1.000000000000000000e+01 66 | 1.300000000000000044e-01 1.000000000000000000e+01 67 | 1.320000000000000062e-01 1.000000000000000000e+01 68 | 1.340000000000000080e-01 1.000000000000000000e+01 69 | 1.360000000000000098e-01 1.000000000000000000e+01 70 | 1.380000000000000115e-01 1.000000000000000000e+01 71 | 1.400000000000000133e-01 1.000000000000000000e+01 72 | 1.420000000000000151e-01 1.000000000000000000e+01 73 | 1.440000000000000169e-01 1.000000000000000000e+01 74 | 1.459999999999999909e-01 1.000000000000000000e+01 75 | 1.479999999999999927e-01 1.000000000000000000e+01 76 | 1.499999999999999944e-01 1.000000000000000000e+01 77 | 1.519999999999999962e-01 1.000000000000000000e+01 78 | 1.539999999999999980e-01 1.000000000000000000e+01 79 | 1.559999999999999998e-01 1.000000000000000000e+01 80 | 1.580000000000000016e-01 1.000000000000000000e+01 81 | 1.600000000000000033e-01 1.000000000000000000e+01 82 | 1.620000000000000051e-01 1.000000000000000000e+01 83 | 1.640000000000000069e-01 1.000000000000000000e+01 84 | 1.660000000000000087e-01 1.000000000000000000e+01 85 | 1.680000000000000104e-01 1.000000000000000000e+01 86 | 1.700000000000000122e-01 1.000000000000000000e+01 87 | 1.720000000000000140e-01 1.000000000000000000e+01 88 | 1.740000000000000158e-01 1.000000000000000000e+01 89 | 1.759999999999999898e-01 1.000000000000000000e+01 90 | 1.779999999999999916e-01 1.000000000000000000e+01 91 | -------------------------------------------------------------------------------- /ruspy/test/resources/replication_test/group_4.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceEconomics/ruspy/414e9f98e3b1cf19544b1ca3b1e544ac5751c1e9/ruspy/test/resources/replication_test/group_4.pkl -------------------------------------------------------------------------------- /ruspy/test/resources/replication_test/repl_params_cubic.txt: -------------------------------------------------------------------------------- 1 | 8.26859 2 | 1.00489 3 | 0.494566 4 | 26.3475 5 | -------------------------------------------------------------------------------- /ruspy/test/resources/replication_test/repl_params_hyper.txt: -------------------------------------------------------------------------------- 1 | 8.05929601 2 | 22.96685649 3 | -------------------------------------------------------------------------------- /ruspy/test/resources/replication_test/repl_params_linear.txt: -------------------------------------------------------------------------------- 1 | 10.0750 2 | 2.2930 3 | -------------------------------------------------------------------------------- /ruspy/test/resources/replication_test/repl_params_quad.txt: -------------------------------------------------------------------------------- 1 | 11.48129539 2 | 476.30640207 3 | -2.31414426 -------------------------------------------------------------------------------- /ruspy/test/resources/replication_test/repl_params_sqrt.txt: -------------------------------------------------------------------------------- 1 | 11.42995702 2 | 3.2308913 -------------------------------------------------------------------------------- /ruspy/test/resources/replication_test/repl_test_trans.txt: -------------------------------------------------------------------------------- 1 | 3.918918193332567301e-01 2 | 5.952937063630258097e-01 3 | 1.281447430371752960e-02 4 | -------------------------------------------------------------------------------- /ruspy/test/resources/replication_test/transition_count.txt: -------------------------------------------------------------------------------- 1 | 1.682000000000000000e+03 2 | 2.555000000000000000e+03 3 | 5.500000000000000000e+01 4 | -------------------------------------------------------------------------------- /ruspy/test/simulation_output_test/test_utility.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | from numpy.testing import assert_almost_equal 4 | from numpy.testing import assert_array_equal 5 | 6 | from ruspy.estimation.estimation_transitions import create_transition_matrix 7 | from ruspy.model_code.cost_functions import calc_obs_costs 8 | from ruspy.model_code.cost_functions import lin_cost 9 | from ruspy.model_code.fix_point_alg import calc_fixp 10 | from ruspy.simulation.simulation import simulate 11 | from ruspy.test.ranodm_init import random_init 12 | from ruspy.test.regression_sim_tests.regression_aux import discount_utility 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def inputs_sim(inputs): 17 | out = {} 18 | out["init_dict"] = init_dict = random_init(inputs)["simulation"] 19 | 20 | # Draw parameter 21 | param1 = np.random.normal(10.0, 2) 22 | param2 = np.random.normal(2.3, 0.5) 23 | params = np.array([param1, param2]) 24 | 25 | disc_fac = init_dict["discount_factor"] 26 | probs = np.array(init_dict["known_trans"]) 27 | num_states = 300 28 | 29 | out["trans_mat"] = trans_mat = create_transition_matrix(num_states, probs) 30 | out["costs"] = costs = calc_obs_costs(num_states, lin_cost, params, 0.001) 31 | out["ev"] = ev = calc_fixp(trans_mat, costs, disc_fac)[0] 32 | out["df"] = df = simulate( 33 | init_dict, 34 | ev, 35 | costs, 36 | trans_mat, 37 | ) 38 | out["v_disc"] = discount_utility(df, disc_fac) 39 | return out 40 | 41 | 42 | def test_regression_simulation_reduced_data_discounted_utility(inputs_sim): 43 | 44 | utility = simulate( 45 | inputs_sim["init_dict"], 46 | inputs_sim["ev"], 47 | inputs_sim["costs"], 48 | inputs_sim["trans_mat"], 49 | reduced_data="discounted utility", 50 | ) 51 | 52 | assert_almost_equal(utility, inputs_sim["v_disc"], decimal=4) 53 | 54 | 55 | def test_regression_simulation_reduced_data_utility(inputs_sim): 56 | 57 | utilities = simulate( 58 | inputs_sim["init_dict"], 59 | inputs_sim["ev"], 60 | inputs_sim["costs"], 61 | inputs_sim["trans_mat"], 62 | reduced_data="utility", 63 | ) 64 | 65 | assert_array_equal( 66 | utilities, 67 | inputs_sim["df"]["utilities"] 68 | .to_numpy() 69 | .reshape( 70 | (inputs_sim["init_dict"]["buses"], inputs_sim["init_dict"]["periods"]) 71 | ), 72 | ) 73 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages 2 | from setuptools import setup 3 | 4 | # Package meta-data. 5 | NAME = "ruspy" 6 | DESCRIPTION = ( 7 | "An open-source package for the simulation and estimation of a prototypical" 8 | " infinite-horizon dynamic discrete choice model based on Rust (1987)." 9 | ) 10 | URL = "" 11 | EMAIL = "maximilian.blesch@hu-berlin.de" 12 | AUTHOR = "Maximilian Blesch" 13 | 14 | 15 | setup( 16 | name=NAME, 17 | version="2.0", 18 | description=DESCRIPTION, 19 | author=AUTHOR, 20 | author_email=EMAIL, 21 | url=URL, 22 | packages=find_packages(exclude=("tests",)), 23 | license="MIT", 24 | include_package_data=True, 25 | ) 26 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [doc8] 2 | max-line-length = 88 3 | ignore = D002, D004 4 | 5 | [flake8] 6 | docstring-convention = numpy 7 | ignore = 8 | D ; ignore missing docstrings. 9 | E203 ; ignore whitespace around : which is enforced by Black. 10 | W503 ; ignore linebreak before binary operator which is enforced by Black. 11 | PT006 ; ignore that parametrizing tests with tuple argument names is preferred. 12 | max-line-length = 88 13 | pytest-mark-no-parentheses = true 14 | warn-symbols = 15 | pytest.mark.wip = Remove 'wip' mark for tests. 16 | per-file-ignores = 17 | docs/source/conf.py:E501,E800,A001,D --------------------------------------------------------------------------------