├── source_ddc ├── src │ ├── __init__.py │ └── source_ddc │ │ ├── __init__.py │ │ ├── fixed_point.py │ │ ├── probability_tools.py │ │ ├── simulation_tools.py │ │ └── algorithms.py ├── test │ ├── __init__.py │ ├── unit │ │ ├── __init__.py │ │ ├── test_simulation.py │ │ ├── test_ddc.py │ │ └── test_state_manager.py │ ├── utils │ │ ├── __init__.py │ │ └── functional_tools.py │ ├── benchmark │ │ ├── __init__.py │ │ ├── conftest.py │ │ └── benchmark_test.py │ ├── performance │ │ ├── __init__.py │ │ └── profile_test.py │ └── conftest.py ├── .flake8 ├── requirements.txt ├── tox.ini ├── setup.py ├── .gitignore ├── README.md └── LICENSE ├── logo.jpg ├── .github └── ISSUE_TEMPLATE │ ├── other-issues.md │ ├── feature_request.md │ └── bug_report.md ├── .gitignore └── README.md /source_ddc/src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source_ddc/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source_ddc/test/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source_ddc/test/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source_ddc/src/source_ddc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source_ddc/test/benchmark/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source_ddc/test/performance/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source_ddc/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 -------------------------------------------------------------------------------- /logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sansan-inc/econ-source/HEAD/logo.jpg -------------------------------------------------------------------------------- /source_ddc/test/benchmark/conftest.py: -------------------------------------------------------------------------------- 1 | 2 | pytest_plugins = ['pytest_benchmark'] 3 | -------------------------------------------------------------------------------- /source_ddc/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==6.0.1 2 | pytest-benchmark==3.2.3 3 | pytest-profiling==1.7.0 -------------------------------------------------------------------------------- /source_ddc/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36,py37 3 | 4 | [testenv] 5 | deps = pytest 6 | commands = 7 | pytest test/unit/ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other issues 3 | about: Not a bug nor a feature request? Write it here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Library name** 11 | Which library is it for? 12 | 13 | **Is your feature request related to a problem? Please describe.** 14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 15 | 16 | **Describe the solution you'd like** 17 | A clear and concise description of what you want to happen. 18 | 19 | **Describe alternatives you've considered** 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /source_ddc/test/utils/functional_tools.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import numpy as np 3 | 4 | 5 | def average_out(n_repetitions): 6 | """Takes a function that returns a flat array of parameters, runs it `n_repetitions` times and returns the mean 7 | parameter values. Useful for testing using Monte Carlo simulations to prevent flaky tests due to sampling error. 8 | :param n_repetitions: how many times to sample from the parameter space. 9 | :return: a flat numpy array of float values representing the mean parameters. 10 | """ 11 | def decorator(fn): 12 | @functools.wraps(fn) 13 | def wrapper(*args, **kwargs): 14 | return np.vstack([fn(*args, **kwargs).params for _ in range(n_repetitions)]).mean(axis=0) 15 | return wrapper 16 | return decorator 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Name of the library** 11 | What library has the bug you want to report? 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Environment (please complete the following information):** 30 | - OS: [e.g. iOS] 31 | - Version [e.g. 22] 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /source_ddc/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | packages = find_packages(where='src') 4 | 5 | setup( 6 | name='source-ddc', 7 | version='0.0.2.dev1', 8 | author='DSOC', 9 | description='Algorithms for the estimation of Dynamic Discrete Choice models.', 10 | long_description='Algorithms for the estimation of Dynamic Discrete Choice models.', 11 | url='https://github.com/sansan-inc', 12 | packages=find_packages(where='src'), 13 | package_dir={'': 'src'}, 14 | classifiers=[ 15 | 'Programming Language :: Python :: 3.6', 16 | 'Programming Language :: Python :: 3.7', 17 | 'Development Status :: 3 - Alpha', 18 | 'Intended Audience :: Developers', 19 | 'Intended Audience :: Education', 20 | 'License :: OSI Approved :: Apache Software License', 21 | 'Topic :: Education', 22 | 'Topic :: Scientific/Engineering :: Mathematics' 23 | ], 24 | license='Apache Software License (http://www.apache.org/licenses/LICENSE-2.0)', 25 | keywords='economics econometrics structural estimation', 26 | python_requires='>=3', 27 | install_requires=[ 28 | 'numpy==1.18.4', 29 | 'pandas==1.0.3', 30 | 'scipy==1.4.1', 31 | 'statsmodels==0.12.0rc0', 32 | 'bidict==0.20.0' 33 | ], 34 | test_requires=[ 35 | 'pytest==6.0.1', 36 | 'pytest-benchmark==3.2.3', 37 | 'pytest-profiling==1.7.0' 38 | ] 39 | ) 40 | -------------------------------------------------------------------------------- /source_ddc/test/unit/test_simulation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from source_ddc.simulation_tools import simulate 3 | from source_ddc.algorithms import CCP 4 | from source_ddc.probability_tools import StateManager 5 | 6 | 7 | def test_simulate_forward(simple_transition_matrix): 8 | def utility_fn(theta, choices, states): 9 | m_states, m_actions = np.meshgrid(states, choices) 10 | return (theta[0] * np.log(m_states + 1) - theta[1] * m_actions).reshape((len(choices), -1, 1)) 11 | 12 | true_params = [0.5, 3] 13 | discount_factor = 0.95 14 | n_choices = 2 15 | n_states = 5 16 | n_simulation_draws = 10 17 | 18 | state_manager = StateManager(miles=n_states) 19 | 20 | df, ccp = simulate(100, 21 | 100, 22 | n_choices, 23 | state_manager, 24 | true_params, 25 | utility_fn, 26 | discount_factor, 27 | simple_transition_matrix) 28 | 29 | solver = CCP( 30 | df['action'].values, 31 | df['state'].values, 32 | simple_transition_matrix, 33 | utility_fn, 34 | discount_factor, 35 | initial_p=ccp, 36 | parameter_names=['variable_cost', 'replacement_cost'] 37 | ) 38 | 39 | solver.estimate(start_params=[1, 1], method='bfgs') 40 | 41 | expected_shape = (n_simulation_draws + 1, df['action'].values.shape[0], 2) 42 | 43 | history = solver.simulate_forward(10) 44 | assert history.shape == expected_shape 45 | -------------------------------------------------------------------------------- /source_ddc/test/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | 4 | 5 | @pytest.fixture 6 | def simple_transition_matrix(): 7 | return np.array( 8 | [ 9 | [ 10 | [1., 0., 0., 0., 0.], 11 | [0.1, 0.9, 0., 0., 0.], 12 | [0., 0.1, 0.9, 0., 0.], 13 | [0., 0., 0.1, 0.9, 0.], 14 | [0., 0., 0., 0.1, 0.9] 15 | ], 16 | [ 17 | [0.4, 0.6, 0., 0., 0.], 18 | [0.1, 0.3, 0.6, 0., 0.], 19 | [0., 0.1, 0.3, 0.6, 0.], 20 | [0., 0., 0.1, 0.3, 0.6], 21 | [0., 0., 0., 0.1, 0.9] 22 | ] 23 | ] 24 | ) 25 | 26 | 27 | @pytest.fixture 28 | def large_transition_matrix(): 29 | return np.array( 30 | [ 31 | [ 32 | [1., 0, 0, 0, 0, 0, 0, 0, 0, 0], 33 | [1., 0, 0, 0, 0, 0, 0, 0, 0, 0], 34 | [1., 0, 0, 0, 0, 0, 0, 0, 0, 0], 35 | [1., 0, 0, 0, 0, 0, 0, 0, 0, 0], 36 | [1., 0, 0, 0, 0, 0, 0, 0, 0, 0], 37 | [1., 0, 0, 0, 0, 0, 0, 0, 0, 0], 38 | [1., 0, 0, 0, 0, 0, 0, 0, 0, 0], 39 | [1., 0, 0, 0, 0, 0, 0, 0, 0, 0], 40 | [1., 0, 0, 0, 0, 0, 0, 0, 0, 0], 41 | [1., 0, 0, 0, 0, 0, 0, 0, 0, 0], 42 | ], 43 | [ 44 | [0.25, 0.25, 0.25, 0.25, 0, 0, 0, 0, 0, 0], 45 | [0, 0.25, 0.25, 0.25, 0.25, 0, 0, 0, 0, 0], 46 | [0, 0, 0.25, 0.25, 0.25, 0.25, 0, 0, 0, 0], 47 | [0, 0, 0, 0.25, 0.25, 0.25, 0.25, 0, 0, 0], 48 | [0, 0, 0, 0, 0.25, 0.25, 0.25, 0.25, 0, 0], 49 | [0, 0, 0, 0, 0, 0.25, 0.25, 0.25, 0.25, 0], 50 | [0, 0, 0, 0, 0, 0, 0.25, 0.25, 0.25, 0.25], 51 | [0, 0, 0, 0, 0, 0, 0, 0.33, 0.33, 0.34], 52 | [0, 0, 0, 0, 0, 0, 0, 0, 0.5, 0.5], 53 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 54 | ], 55 | [ 56 | [0.2, 0.2, 0.2, 0.2, 0.2, 0, 0, 0, 0, 0], 57 | [0, 0.2, 0.2, 0.2, 0.2, 0.2, 0, 0, 0, 0], 58 | [0, 0, 0.2, 0.2, 0.2, 0.2, 0.2, 0, 0, 0], 59 | [0, 0, 0, 0.2, 0.2, 0.2, 0.2, 0.2, 0, 0], 60 | [0, 0, 0, 0, 0.2, 0.2, 0.2, 0.2, 0.2, 0], 61 | [0, 0, 0, 0, 0, 0.2, 0.2, 0.2, 0.2, 0.2], 62 | [0, 0, 0, 0, 0, 0, 0.25, 0.25, 0.25, 0.25], 63 | [0, 0, 0, 0, 0, 0, 0, 0.33, 0.33, 0.34], 64 | [0, 0, 0, 0, 0, 0, 0, 0, 0.5, 0.5], 65 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 66 | ] 67 | ] 68 | ) 69 | -------------------------------------------------------------------------------- /source_ddc/test/performance/profile_test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from source_ddc.simulation_tools import simulate 3 | from source_ddc.algorithms import NFXP, CCP, NPL 4 | from source_ddc.probability_tools import StateManager, random_ccp 5 | 6 | n_agents = 1000 7 | n_periods = 100 8 | 9 | true_params = [0.5, 3] 10 | discount_factor = 0.95 11 | n_choices = 2 12 | n_states = 5 13 | 14 | state_manager = StateManager(miles=n_states) 15 | 16 | 17 | def utility_fn(theta, choices, states): 18 | m_states, m_actions = np.meshgrid(states, choices) 19 | return (theta[0] * np.log(m_states + 1) - theta[1] * m_actions).reshape((len(choices), -1, 1)) 20 | 21 | 22 | def test_profile_nfxp(simple_transition_matrix): 23 | 24 | df, _ = simulate(n_periods, 25 | n_agents, 26 | n_choices, 27 | state_manager, 28 | true_params, 29 | utility_fn, 30 | discount_factor, 31 | simple_transition_matrix) 32 | 33 | solver = NFXP( 34 | df['action'].values, 35 | df['state'].values, 36 | simple_transition_matrix, 37 | utility_fn, 38 | discount_factor, 39 | parameter_names=['variable_cost', 'replacement_cost'] 40 | ) 41 | solver.estimate(start_params=[1, 1], method='bfgs') 42 | 43 | 44 | def test_profile_ccp(simple_transition_matrix): 45 | 46 | df, ccp = simulate(n_periods, 47 | n_agents, 48 | n_choices, 49 | state_manager, 50 | true_params, 51 | utility_fn, 52 | discount_factor, 53 | simple_transition_matrix) 54 | 55 | solver = CCP( 56 | df['action'].values, 57 | df['state'].values, 58 | simple_transition_matrix, 59 | utility_fn, 60 | discount_factor, 61 | initial_p=ccp, 62 | parameter_names=['variable_cost', 'replacement_cost'] 63 | ) 64 | solver.estimate(start_params=[1, 1], method='bfgs') 65 | 66 | 67 | def test_profile_npl(simple_transition_matrix): 68 | 69 | df, _ = simulate(n_periods, 70 | n_agents, 71 | n_choices, 72 | state_manager, 73 | true_params, 74 | utility_fn, 75 | discount_factor, 76 | simple_transition_matrix) 77 | 78 | ccp = random_ccp(n_states, n_choices) 79 | 80 | solver = NPL( 81 | df['action'].values, 82 | df['state'].values, 83 | simple_transition_matrix, 84 | utility_fn, 85 | discount_factor, 86 | initial_p=ccp, 87 | parameter_names=['variable_cost', 'replacement_cost'], 88 | ) 89 | solver.estimate(start_params=[1, 1], method='bfgs') 90 | -------------------------------------------------------------------------------- /source_ddc/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | .idea/ 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # pytype static type analyzer 138 | .pytype/ 139 | 140 | # Cython debug symbols 141 | cython_debug/ 142 | 143 | test/performance/prof/* 144 | *.prof -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | .idea/ 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # pytype static type analyzer 138 | .pytype/ 139 | 140 | # Cython debug symbols 141 | cython_debug/ 142 | 143 | test/performance/prof/* 144 | *.prof 145 | 146 | .DS_Store 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Econ Source 3 |

4 | 5 | 6 | # Concept 7 | Econ Source is a project that combines the free spirit of Open Source Software, the rigurosity of Economics and the practicality of Data Science. 8 | We believe that many interesting real-life challenges can be solved through the methods of Economics but many obstacles still remain for it to become mainstream. 9 | This project is about working together for removing those obstacles, and to expand the boundaries of Economics beyond academia and into the real world for the benefit of everyone. 10 | It is based on four principles: 11 | 12 | ### Share 13 | We share our code for everyone to use it freely under the Apache 2.0 license. 14 | We welcome contributions, and we are open to bug reports, feature requests, and look forward to hearing about your use cases. 15 | Feel free to use the libraries here for your personal research, study or work. 16 | 17 | 18 | ### Build 19 | Done is better than perfect, and what you'll find here is just done. 20 | We hope for active participation from researchers, data scientists, students, people in the academia, the industry, and specially anyone who shares this vision. 21 | We aim for high-quality software that is at the same time easy to understand and to contribute to. 22 | We encourage test-oriented development, and the usage of best practices in software development. 23 | 24 | ### Discuss 25 | We appreciate and encourage polite and constructive discussion. 26 | 27 | ### Learn 28 | One of the best things about Open Source is that everyone makes each other better. 29 | We aim to maintain easy-to-read code from which anyone willing to learn can find a hint. 30 | Papers in Empirical Economics can sometimes be very shallow regarding the implementation details of algorithms. 31 | Did you ever want to see a code implementation of an algorithm, but the paper is not very detailed about it? Check the unit tests or the source code here, maybe you'll find a clue. 32 | Have you thought of contributing to an open source project but don't know where to start? Well, what about here? 33 | 34 | # Working together 35 | 36 | In DSOC we see great value in each encounter. 37 | We hope that *Econ Source* can become the starting point for collaboration, be it software development, empirical research or business. 38 | There are many opportunities to collaborate with us: 39 | 40 | ### Through Code 41 | If you have a bug report or feature request, fill an Issue stating the name of the library. We appreciate your code contributions as well. 42 | See the README file on each library for details about details on developing, testing, code conventions, etc. 43 | 44 | ### Collaborative Research 45 | *Sansan Data Discovery* is our collaborative research platform. Learn more about it [here](https://en.sansan-dsoc.com/research/datadiscovery/). 46 | 47 | To know more about the kind of research we perform at DSOC, check our [Data Science Reports](https://sansan-dsoc.com/research/report/) (only in Japanese). 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /source_ddc/src/source_ddc/fixed_point.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.special import softmax 3 | 4 | 5 | def get_discounted_value(current_utility, discount_factor, transition_matrix, v): 6 | """ 7 | 8 | :param current_utility: an array of shape (n_choices, n_states, 1) representing the result of evaluating the utility 9 | function at some parameter values. 10 | :param discount_factor: a float scalar in the range [0, 1) representing the discount factor of the agent. 11 | :param transition_matrix: an array of transition matrices with shape (n_choices, n_states, n_states). 12 | :param v: an array of shape (n_states, 1) representing the discounted expectation of the future value at each state. 13 | :return: a numpy array of shape (n_choices, n_states, 1) 14 | """ 15 | n_choices, n_states, _ = transition_matrix.shape 16 | discounted_value = current_utility + discount_factor * np.array( 17 | [transition_matrix[i].dot(v) for i in range(n_choices)]) 18 | return discounted_value 19 | 20 | 21 | def phi_map(p, transition_matrix, parameters, utility_function, discount_factor, state_manager): 22 | """Mapping from the probability space to the value space. Assumes a Type I Extreme Value distribution for the 23 | unobservable component of the utility. 24 | :param p: the conditional choice probability numpy array with shape (n_choices, n_states, 1) 25 | :param transition_matrix: an array of transition matrices with shape (n_choices, n_states, n_states) 26 | :param parameters: the structural parameter values. 27 | :param utility_function: a function that takes as arguments an array of structural parameters, a set of choices and 28 | a mesh of state variables, and returns a numpy array of shape (n_choices, n_states, 1) that represents the utility 29 | value at each state and choice combination. 30 | :param discount_factor: a float scalar in the range [0, 1) representing the discount factor of the agent. 31 | :param state_manager: an instance of `StateManager`. 32 | :return: 33 | """ 34 | n_choices, n_states, _ = p.shape 35 | current_utility = utility_function( 36 | parameters, 37 | np.arange(n_choices).reshape(-1, 1, 1), 38 | state_manager.get_states_mesh() 39 | ) 40 | denominator = np.identity(n_states) - discount_factor*((p*transition_matrix).sum(axis=0)) 41 | denominator = np.linalg.solve(denominator, np.identity(n_states)) 42 | numerator = (p*(current_utility + np.euler_gamma - np.nan_to_num(np.log(p), 0))).sum(axis=0) 43 | v = denominator.dot(numerator) 44 | v = v - v.min() 45 | discounted_value = get_discounted_value(current_utility, discount_factor, transition_matrix, v) 46 | return discounted_value 47 | 48 | 49 | def lambda_map(v): 50 | """Mapping from the value space to the probability space. Assumes a Type I Extreme Value distribution for the 51 | unobservable component of the utility. 52 | :param v: 53 | :return: a numpy array of shape (n_choices, n_states, 1) representing a conditional choice probability consistent 54 | with v and the distributional parametric assumption. 55 | """ 56 | return softmax(v, axis=0) 57 | -------------------------------------------------------------------------------- /source_ddc/test/benchmark/benchmark_test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from source_ddc.simulation_tools import simulate 3 | from source_ddc.algorithms import NFXP, CCP, NPL 4 | from source_ddc.probability_tools import StateManager, random_ccp 5 | 6 | n_agents = 1000 7 | n_periods = 100 8 | 9 | 10 | def test_profile_nfxp(simple_transition_matrix, benchmark): 11 | 12 | def utility_fn(theta, choices, states): 13 | m_states, m_actions = np.meshgrid(states, choices) 14 | return (theta[0] * np.log(m_states + 1) - theta[1] * m_actions).reshape((len(choices), -1, 1)) 15 | 16 | true_params = [0.5, 3] 17 | discount_factor = 0.95 18 | n_choices = 2 19 | n_states = 5 20 | 21 | state_manager = StateManager(miles=n_states) 22 | 23 | df, _ = simulate(n_periods, 24 | n_agents, 25 | n_choices, 26 | state_manager, 27 | true_params, 28 | utility_fn, 29 | discount_factor, 30 | simple_transition_matrix) 31 | 32 | def estimate(): 33 | solver = NFXP( 34 | df['action'].values, 35 | df['state'].values, 36 | simple_transition_matrix, 37 | utility_fn, 38 | discount_factor, 39 | parameter_names=['variable_cost', 'replacement_cost'] 40 | ) 41 | solver.estimate(start_params=[1, 1], method='bfgs') 42 | 43 | benchmark(estimate) 44 | 45 | 46 | def test_profile_ccp(simple_transition_matrix, benchmark): 47 | 48 | def utility_fn(theta, choices, states): 49 | m_states, m_actions = np.meshgrid(states, choices) 50 | return (theta[0] * np.log(m_states + 1) - theta[1] * m_actions).reshape((len(choices), -1, 1)) 51 | 52 | true_params = [0.5, 3] 53 | discount_factor = 0.95 54 | n_choices = 2 55 | n_states = 5 56 | 57 | state_manager = StateManager(miles=n_states) 58 | 59 | df, ccp = simulate(n_periods, 60 | n_agents, 61 | n_choices, 62 | state_manager, 63 | true_params, 64 | utility_fn, 65 | discount_factor, 66 | simple_transition_matrix) 67 | 68 | def estimate(): 69 | solver = CCP( 70 | df['action'].values, 71 | df['state'].values, 72 | simple_transition_matrix, 73 | utility_fn, 74 | discount_factor, 75 | initial_p=ccp, 76 | parameter_names=['variable_cost', 'replacement_cost'] 77 | ) 78 | solver.estimate(start_params=[1, 1], method='bfgs') 79 | 80 | benchmark(estimate) 81 | 82 | 83 | def test_profile_npl(simple_transition_matrix, benchmark): 84 | 85 | def utility_fn(theta, choices, states): 86 | m_states, m_actions = np.meshgrid(states, choices) 87 | return (theta[0] * np.log(m_states + 1) - theta[1] * m_actions).reshape((len(choices), -1, 1)) 88 | 89 | true_params = [0.5, 3] 90 | discount_factor = 0.95 91 | n_choices = 2 92 | n_states = 5 93 | 94 | state_manager = StateManager(miles=n_states) 95 | 96 | df, _ = simulate(n_periods, 97 | n_agents, 98 | n_choices, 99 | state_manager, 100 | true_params, 101 | utility_fn, 102 | discount_factor, 103 | simple_transition_matrix) 104 | 105 | ccp = random_ccp(n_states, n_choices) 106 | 107 | def estimate(): 108 | solver = NPL( 109 | df['action'].values, 110 | df['state'].values, 111 | simple_transition_matrix, 112 | utility_fn, 113 | discount_factor, 114 | initial_p=ccp, 115 | parameter_names=['variable_cost', 'replacement_cost'] 116 | ) 117 | solver.estimate(start_params=[1, 1], method='bfgs') 118 | 119 | benchmark(estimate) 120 | -------------------------------------------------------------------------------- /source_ddc/src/source_ddc/probability_tools.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | import numpy as np 3 | import pandas as pd 4 | 5 | from bidict import frozenbidict 6 | 7 | 8 | def random_ccp(n_states, n_choices): 9 | """ 10 | Obtain an array of random choice probabilities. 11 | :param n_states: 12 | :param n_choices: 13 | :return: an array of shape (n_choices, n_states, 1), where the values across choices add up to 1. 14 | """ 15 | p = np.random.uniform(size=(n_choices, n_states, 1)) 16 | p = (p / p.sum(axis=0)) 17 | return p 18 | 19 | 20 | class StateManager(object): 21 | 22 | """A convenience class for managing complex state-spaces. The class keeps an internal biyective mapping of state IDs 23 | and combinations of values for the corresponding state variables. 24 | 25 | :param **kwargs: the keys represent the name of the state variable and the values are the number of possible states. 26 | """ 27 | 28 | def __init__(self, **n_states): 29 | self.state_names = list(n_states.keys()) 30 | state_sizes = list(n_states.values()) 31 | 32 | self.n_states = state_sizes 33 | self.n_dimensions = len(self.n_states) 34 | self.total_states = np.prod(self.n_states) 35 | 36 | if self.n_dimensions > 1: 37 | mesh = self.get_states_mesh().squeeze().T 38 | mesh = list(map(tuple, mesh)) 39 | index = np.arange(len(mesh)) 40 | self.values_dict = frozenbidict(zip(mesh, index)) 41 | 42 | def get_states_mesh(self): 43 | """ 44 | 45 | :return: 46 | """ 47 | state_dimension = len(self.n_states) 48 | states_mesh = np.stack(np.meshgrid(*[np.arange(0, n) for n in self.n_states], indexing='ij'), -1) 49 | return states_mesh.reshape(-1, state_dimension).T.reshape(state_dimension, -1, 1) 50 | 51 | def state_id_for(self, others): 52 | """Get the unique state ID for a given combination of state variables. 53 | 54 | :param others: can be a list of tuples, a pandas Series or pandas DataFrame, where the values represent the 55 | value of the state for each of the dimensions used to instantiate the State Manager. Values must be in the same 56 | as the one used to create the State Manager. 57 | :return: an array, Series or pandas DataFrame containing the state ID for the values passed. 58 | """ 59 | if self.n_dimensions == 1: 60 | return others 61 | else: 62 | if type(others) == pd.core.series.Series: 63 | return others.apply(lambda v: self[v]) 64 | elif type(others) == pd.core.frame.DataFrame: 65 | df = others.apply(lambda v: self.values_dict[tuple(v)], axis=1) 66 | return df 67 | else: 68 | search_values = map(tuple, others) 69 | return np.array([self.values_dict[v] for v in search_values]) 70 | 71 | def state_variables_for(self, others): 72 | """Pass a state ID to obtain the original state variables. 73 | 74 | :param others: can be an iterable of `int`, a pandas Series or a pandas DataFrame. 75 | :return: an array of tuples, Series or pandas DataFrame containing the state ID for the values passed. 76 | """ 77 | if self.n_dimensions == 1: 78 | return others 79 | else: 80 | if type(others) == pd.core.series.Series: 81 | return others.apply(lambda v: self.values_dict.inv[v]) 82 | elif type(others) == pd.core.frame.DataFrame: 83 | if len(others.columns) > 1: 84 | raise Exception('The dataframe must have exactly one column.') 85 | df = others.apply(lambda v: pd.Series(list(self.values_dict.inv[v.values.item()])), axis=1) 86 | df.columns = self.state_names 87 | return df 88 | else: 89 | return np.array([self.values_dict.inv[v] for v in others]) 90 | 91 | def __getitem__(self, x): 92 | if isinstance(x, int): 93 | return self.values_dict.inv[x] 94 | if isinstance(x, tuple): 95 | return self.values_dict[x] 96 | 97 | @classmethod 98 | def merge_matrices(cls, *args): 99 | """Takes several squared matrices and applies recursively the Kronecker product to all of them to obtain a single 100 | matrix. Useful when you have several transition matrices that you wish to merge into a single one. 101 | 102 | :param args: a list of squared numpy arrays. 103 | :return: A numpy array with dimensions equal to the multiplication of all the dimensions of the argument 104 | matrices.s 105 | """ 106 | return reduce(lambda x, y: np.kron(x, y), args) 107 | -------------------------------------------------------------------------------- /source_ddc/src/source_ddc/simulation_tools.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from .probability_tools import random_ccp 4 | from .fixed_point import phi_map, lambda_map 5 | 6 | 7 | def simulate(n_periods, 8 | n_agents, 9 | n_choices, 10 | state_manager, 11 | parameters, 12 | utility_function, 13 | discount_factor, 14 | transition_matrix, 15 | convergence_criterion=10e-6, 16 | seed=None): 17 | """A helper function that creates a simulated dataset for a Dynamic Discrete Choice model compatible with the 18 | parameters in the arguments. 19 | 20 | :param n_periods: an `int` representing the number of simulated periods for each agent. 21 | :param n_agents: an `int` representing the number of agents to be simulated. 22 | :param n_choices: an `int` representing the number of choices available to the agent. 23 | :param state_manager: an instance of `StateManager`. 24 | :param parameters: a list or numpy array containing the structural parameters 25 | :param utility_function: a function that takes as arguments an array of structural parameters, a set of choices and 26 | a mesh of state variables, and returns a numpy array of shape (n_choices, n_states, 1) that represents the utility 27 | value at each state and choice combination. 28 | :param discount_factor: a float scalar in the range [0, 1) representing the discount factor of the agent. 29 | :param transition_matrix: an array of transition matrices with shape (n_choices, n_states, n_states) 30 | :param parameters: the structural parameter values 31 | :param convergence_criterion: a tolerance level to determine the convergence of the iterations of the conditional 32 | choice probability array. 33 | :param seed: the seed for random number generation. 34 | :return: a tuple of pandas `DataFrame` and numpy array. The dataframe contains the simulated states and choices and 35 | the numpy array has shape (n_choices, n_states, 1) and represents the conditional choice probabilities. 36 | """ 37 | 38 | if seed is not None: 39 | np.random.seed(seed) 40 | 41 | n_states = state_manager.total_states 42 | p = random_ccp(n_states, n_choices) 43 | converged = False 44 | 45 | # Obtain the conditional choice probabilities by iterating until the fixed point is reach in probability space 46 | while not converged: 47 | p_0 = p 48 | v = phi_map(p, transition_matrix, parameters, utility_function, discount_factor, state_manager) 49 | p = lambda_map(v) 50 | delta = np.abs(np.max((p - p_0))) 51 | if delta <= convergence_criterion: 52 | converged = True 53 | 54 | errors = np.random.gumbel(size=(n_periods, n_agents, n_choices)) 55 | agents, periods = [i.T.ravel() for i in np.meshgrid(np.arange(n_agents), np.arange(n_periods))] 56 | 57 | states = [] 58 | actions = [] 59 | 60 | for agent in range(n_agents): 61 | # Draw some random initial state 62 | s = np.random.choice(np.arange(n_states)) 63 | # v = phi_map(p, transition_matrix, parameters, utility_function, discount_factor, state_manager) 64 | for t in range(n_periods): 65 | states.append(s) 66 | action = (errors[t, agent, :] + v[:, s, :].ravel()).argmax() 67 | actions.append(action) 68 | if t != n_periods: 69 | s = np.random.choice(list(range(n_states)), p=transition_matrix[action, s]) 70 | 71 | df = pd.DataFrame({ 72 | 'agent_id': agents, 73 | 't': periods, 74 | 'state': states, 75 | 'action': actions 76 | }) 77 | 78 | return df, p 79 | 80 | 81 | def simulate_state_draw(current_action_state, transition_matrix): 82 | """Convenience function for obtaining the following state given a departure point n the state space and a 83 | transition matrix. 84 | 85 | :param current_action_state: an array of current statess. 86 | :param transition_matrix: a numpy array of shape (n_choices, n_states_ 1) representing transition probabilities/ 87 | :return: an array of future states. 88 | """ 89 | next_states = np.empty(current_action_state.shape[1]).astype(np.int32) 90 | for i in range(current_action_state[0].shape[0]): 91 | s = current_action_state[0][i] 92 | transition_probs = transition_matrix[s[0], s[1]] 93 | next_states[i] = np.searchsorted(np.cumsum(transition_probs), np.random.random(), side="right") 94 | return next_states 95 | 96 | 97 | def simulate_action_draw(ccp, states): 98 | """Convenience function for simulating an action draw given a decision policy in the form of conditional choice 99 | probabilities. 100 | 101 | :param ccp: the conditional choice probabilities as a numpy array of shape (n_choices, n_states, 1). 102 | :param states: an array of current states. 103 | :return: 104 | """ 105 | return np.array([ 106 | np.searchsorted(np.cumsum(ccp.reshape(ccp.shape[0], -1).T[s]), np.random.random(), side="right") for s in states 107 | ]) 108 | -------------------------------------------------------------------------------- /source_ddc/test/unit/test_ddc.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from source_ddc.simulation_tools import simulate 3 | from source_ddc.algorithms import NFXP, NPL, CCP 4 | from source_ddc.probability_tools import StateManager, random_ccp 5 | from test.utils.functional_tools import average_out 6 | 7 | n_repetitions = 10 8 | 9 | 10 | def test_nfxp(simple_transition_matrix): 11 | 12 | def utility_fn(theta, choices, states): 13 | m_states, m_actions = np.meshgrid(states, choices) 14 | return (theta[0] * np.log(m_states + 1) - theta[1] * m_actions).reshape((len(choices), -1, 1)) 15 | 16 | true_params = [0.5, 3] 17 | discount_factor = 0.95 18 | n_choices = 2 19 | n_states = 5 20 | 21 | state_manager = StateManager(miles=n_states) 22 | 23 | @average_out(n_repetitions) 24 | def test(): 25 | df, _ = simulate( 26 | 500, 27 | 100, 28 | n_choices, 29 | state_manager, 30 | true_params, 31 | utility_fn, 32 | discount_factor, 33 | simple_transition_matrix 34 | ) 35 | 36 | algorithm = NFXP( 37 | df['action'].values, 38 | df['state'].values, 39 | simple_transition_matrix, 40 | utility_fn, 41 | discount_factor, 42 | parameter_names=['variable_cost', 'replacement_cost'] 43 | ) 44 | 45 | return algorithm.estimate(start_params=[-1, -1], method='bfgs') 46 | 47 | mean_params = test() 48 | tolerance_levels = np.array([0.05, 0.05]) 49 | assert np.all(np.abs(mean_params - true_params) < tolerance_levels) 50 | 51 | 52 | def test_ccp(simple_transition_matrix): 53 | 54 | def utility_fn(theta, choices, states): 55 | m_states, m_actions = np.meshgrid(states, choices) 56 | return (theta[0] * np.log(m_states + 1) - theta[1] * m_actions).reshape((len(choices), -1, 1)) 57 | 58 | true_params = [0.5, 3] 59 | discount_factor = 0.95 60 | n_choices = 2 61 | n_states = 5 62 | 63 | state_manager = StateManager(miles=n_states) 64 | 65 | @average_out(n_repetitions) 66 | def test(): 67 | df, ccp = simulate( 68 | 500, 69 | 100, 70 | n_choices, 71 | state_manager, 72 | true_params, 73 | utility_fn, 74 | discount_factor, 75 | simple_transition_matrix 76 | ) 77 | 78 | algorithm = CCP( 79 | df['action'].values, 80 | df['state'].values, 81 | simple_transition_matrix, 82 | utility_fn, 83 | discount_factor, 84 | initial_p=ccp, 85 | parameter_names=['variable_cost', 'replacement_cost'] 86 | ) 87 | 88 | return algorithm.estimate(start_params=[1, 1], method='bfgs') 89 | 90 | mean_params = test() 91 | tolerance_levels = np.array([0.05, 0.05]) 92 | assert np.all(np.abs(mean_params - true_params) < tolerance_levels) 93 | 94 | 95 | def test_npl(simple_transition_matrix): 96 | 97 | def utility_fn(theta, choices, states): 98 | m_states, m_actions = np.meshgrid(states, choices) 99 | return (theta[0] * np.log(m_states + 1) - theta[1] * m_actions).reshape((len(choices), -1, 1)) 100 | 101 | true_params = [0.5, 3] 102 | discount_factor = 0.95 103 | n_choices = 2 104 | n_states = 5 105 | 106 | state_manager = StateManager(miles=n_states) 107 | 108 | @average_out(n_repetitions) 109 | def test(): 110 | df, _ = simulate( 111 | 500, 112 | 100, 113 | n_choices, 114 | state_manager, 115 | true_params, 116 | utility_fn, 117 | discount_factor, 118 | simple_transition_matrix) 119 | 120 | ccp = random_ccp(n_states, n_choices) 121 | 122 | algorithm = NPL( 123 | df['action'].values, 124 | df['state'].values, 125 | simple_transition_matrix, 126 | utility_fn, 127 | discount_factor, 128 | initial_p=ccp, 129 | parameter_names=['variable_cost', 'replacement_cost'] 130 | ) 131 | return algorithm.estimate(start_params=[1, 1], method='bfgs') 132 | 133 | mean_params = test() 134 | tolerance_levels = np.array([0.05, 0.05]) 135 | assert np.all(np.abs(mean_params - true_params) < tolerance_levels) 136 | 137 | 138 | def test_npl_relaxation_param(simple_transition_matrix): 139 | 140 | def utility_fn(theta, choices, states): 141 | m_states, m_actions = np.meshgrid(states, choices) 142 | return (theta[0] * np.log(m_states + 1) - theta[1] * m_actions).reshape((len(choices), -1, 1)) 143 | 144 | true_params = [0.5, 3] 145 | discount_factor = 0.95 146 | n_choices = 2 147 | n_states = 5 148 | 149 | state_manager = StateManager(miles=n_states) 150 | 151 | @average_out(n_repetitions) 152 | def test(): 153 | df, _ = simulate(500, 154 | 100, 155 | n_choices, 156 | state_manager, 157 | true_params, 158 | utility_fn, 159 | discount_factor, 160 | simple_transition_matrix) 161 | 162 | ccp = random_ccp(n_states, n_choices) 163 | 164 | algorithm = NPL( 165 | df['action'].values, 166 | df['state'].values, 167 | simple_transition_matrix, 168 | utility_fn, 169 | discount_factor, 170 | initial_p=ccp, 171 | relaxation_param=0.9, 172 | parameter_names=['variable_cost', 'replacement_cost'], 173 | npl_maxiter=50 174 | ) 175 | 176 | return algorithm.estimate(start_params=[1, 1], method='bfgs') 177 | 178 | mean_params = test() 179 | tolerance_levels = np.array([0.05, 0.05]) 180 | assert np.all(np.abs(mean_params - true_params) < tolerance_levels) 181 | -------------------------------------------------------------------------------- /source_ddc/README.md: -------------------------------------------------------------------------------- 1 | # Source DDC 2 | 3 | ### About the library 4 | Source DDC is a library for the simulation and estimation of Dynamic Discrete Choice (DDC) Models. 5 | 6 | ### Currently Implemented Models 7 | Source DDC provides tools for constructing single-agent DDC models with a multivariate state-space and any number of choices. 8 | 9 | Currently implemented estimation algorithms rely on Maximum Likelihood: 10 | 11 | - Nested Fixed Point Algorithm (Rust, 1987) 12 | - CCP Estimator (Hotz & Miller, 1993) 13 | - Nested Pseudo-Likelihood Estimator (Aguirregabiria & Mira, 2002) 14 | 15 | Areas of future development include the implementation of simulation-based algorithms, dynamic games and improvements that allow the distributed estimation of large-scale models. 16 | 17 | ## Installation 18 | 19 | The current version has been tested on Python 3.6 and 3.7. 20 | To install, clone or download the repository to your local environment and run `pip install .` from the root directory where the `setup.py` file is. 21 | This installs the necessary dependencies. 22 | 23 | 24 | ## Basic Components of a DDC model 25 | 26 | A DDC model is made of the following components: 27 | 28 | - Agents 29 | - A utility function to be maximized by the agent 30 | - A set of available choices 31 | - The state-space 32 | - The beliefs about the evolution of the state, given in the form of transition matrices 33 | - A discount factor, representing the degree of preference of short-term utility over the long-term 34 | 35 | Currently, Source DDC does not support dynamic games. See the **Future Areas of Work** for more. 36 | 37 | The model variables are defined in the following way: 38 | 39 | ```python 40 | n_choices = 2 41 | n_states = 5 42 | discount_factor = 0.95 43 | ``` 44 | 45 | ### Defining the state-space using a State Manager 46 | 47 | The state space is defined by employing a *State Manager*, which performs operations on the state-space. 48 | When the state-space is multi-dimensional, since it reduces the complexity of managing several state variables and transition matrices. 49 | 50 | A State Manager can be instantiated in the following way: 51 | 52 | ```python 53 | from source_ddc.probability_tools import StateManager 54 | state_manager = StateManager(miles=n_states) 55 | ``` 56 | 57 | ### Transition Matrices 58 | 59 | The beliefs of the agent about the evolution of the state-space is defined as a numpy array with dimensions n_choices * n_states * n_states. 60 | In other words, you need to specify a number of squared transition matrices equal to the number of choices. See the example below: 61 | 62 | ```python 63 | import numpy as np 64 | transition_matrix = np.array( 65 | [ 66 | [ 67 | [1., 0., 0., 0., 0.], 68 | [0.1, 0.9, 0., 0., 0.], 69 | [0., 0.1, 0.9, 0., 0.], 70 | [0., 0., 0.1, 0.9, 0.], 71 | [0., 0., 0., 0.1, 0.9] 72 | ], 73 | [ 74 | [0.4, 0.6, 0., 0., 0.], 75 | [0.1, 0.3, 0.6, 0., 0.], 76 | [0., 0.1, 0.3, 0.6, 0.], 77 | [0., 0., 0.1, 0.3, 0.6], 78 | [0., 0., 0., 0.1, 0.9] 79 | ] 80 | ] 81 | ) 82 | ``` 83 | 84 | ### The Utility Function 85 | 86 | It is expressed as a python function that receives three arguments: the parameter values, an array of choices and an array of states. 87 | For example: 88 | 89 | ```python 90 | def utility_fn(parameters, choices, states): 91 | [variable_cost, fixed_cost] = parameters 92 | m_states, m_actions = np.meshgrid(states, choices) 93 | return (variable_cost * np.log(m_states + 1) - fixed_cost * m_actions).reshape((len(choices), -1, 1)) 94 | ``` 95 | 96 | ## Simulating data 97 | 98 | Source DDC includes an easy way of performing Monte Carlo simulations. 99 | 100 | You can simulate new data in the following way: 101 | 102 | 103 | ```python 104 | from source_ddc.simulation_tools import simulate 105 | 106 | df, ccp = simulate(100, 10, n_choices, state_manager, true_params, utility_fn, discount_factor, transition_matrix) 107 | ``` 108 | 109 | This function returns a tuple containing the simulated data and the conditional choice probabilities. 110 | 111 | 112 | ### Estimating a model 113 | 114 | The algorithms are included in the `dynamic` module. 115 | 116 | You need to create an instance of the algorithm with the necessary data and use the `estimate` function to perform the estimation: 117 | 118 | ```python 119 | from source_ddc.algorithms import NFXP 120 | algorithm = NFXP( 121 | df['action'], 122 | df['state'], 123 | transition_matrix, 124 | utility_fn, 125 | discount_factor, 126 | parameter_names=['variable_cost', 'replacement_cost'] 127 | ) 128 | 129 | result = algorithm.estimate(start_params=[-1, -1], method='bfgs') 130 | ``` 131 | 132 | The `result` object inherits from the Statsmodels [GenericLikelihoodModelResults](https://www.statsmodels.org/stable/dev/generated/statsmodels.base.model.GenericLikelihoodModelResults.html). 133 | 134 | A summary of the results can be obtained with `result.summary()`. 135 | 136 | ## Testing 137 | 138 | Tests run against the installed version. To run them, install the library (in a virtual environment if necessary). 139 | 140 | Then, install the necessary dependencies by running `pip install -r requirements.txt` from the root directory. 141 | 142 | Tests are found inside the `/test/` directory and are of three kinds: 143 | 144 | - Unit tests: employ Monte Carlo simulations to verify that the results returned by the algorithms are as expected. 145 | Run them using `pytest .` from the `/test/unit/` directory. 146 | 147 | - Benchmarks: compare the speed of the `estimate` method of each algorithm. 148 | Run them using `pytest .` from the `/test/benchmark/` directory. 149 | 150 | - Profiling: measure the time that key parts of the algorithms take when estimating. Helps spot bottlenecks. 151 | Run them using `pytest . --profile` from the `/test/performance/` directory. This will generate profile files which you can visualize using tools like [snakeviz](https://jiffyclub.github.io/snakeviz/) 152 | 153 | ## Contributing 154 | 155 | This project is managed under the principles of: Share, Build, Discuss, Learn. 156 | 157 | Feel free to open new Issues, submit Pull Requests, share your use cases and discuss of others. 158 | 159 | For the good of the community, remember to keep the conversation polite and productive. 160 | 161 | 162 | ## Maintainers 163 | 164 | This project is maintained by researchers at DSOC. 165 | 166 | Contact: https://sansan-dsoc.com/contact/ 167 | 168 | ## References 169 | 170 | - Aguirregabiria, Victor and Pedro Mira (2002) "Swapping the Nested Fixed Point Algorithm: A Class of Estimators for Discrete Markov Decision Models." Econometrica 70(4):1519-543. 171 | - Hotz, V. Joseph, and Robert A. Miller. "Conditional Choice Probabilities and the Estimation of Dynamic Models." The Review of Economic Studies 60, no. 3 (1993): 497-529. 172 | - Kasahara, Hiroyuki, and Katsumi Shimotsu. "Sequential Estimation of Structural Models With a Fixed Point Constraint." Econometrica 80, no. 5 (2012): 2303-319. Accessed August 21, 2020. http://www.jstor.org/stable/23271448. 173 | - Rust, John (1987) "Optimal Replacement of GMC Bus Engines: An Empirical Model of Harold Zurcher." Econometrica, 55: 999–1033. 174 | -------------------------------------------------------------------------------- /source_ddc/test/unit/test_state_manager.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from source_ddc.simulation_tools import simulate 3 | from source_ddc.algorithms import NPL, CCP 4 | from source_ddc.probability_tools import StateManager, random_ccp 5 | from test.utils.functional_tools import average_out 6 | 7 | n_repetitions = 10 8 | 9 | 10 | def test_state_manager(simple_transition_matrix): 11 | 12 | def utility_fn(theta, choices, states): 13 | m_states, m_actions = np.meshgrid(states, choices) 14 | return (theta[0] * np.log(m_states + 1) - theta[1] * m_actions).reshape((len(choices), -1, 1)) 15 | 16 | true_params = [0.5, 3] 17 | discount_factor = 0.95 18 | n_choices = 2 19 | n_states = 5 20 | 21 | state_manager = StateManager(miles=n_states) 22 | 23 | @average_out(n_repetitions) 24 | def test(): 25 | df, ccp = simulate(100, 26 | 100, 27 | n_choices, 28 | state_manager, 29 | true_params, 30 | utility_fn, 31 | discount_factor, 32 | simple_transition_matrix) 33 | 34 | solver = NPL( 35 | df['action'].values, 36 | df['state'].values, 37 | simple_transition_matrix, 38 | utility_fn, 39 | discount_factor, 40 | initial_p=ccp, 41 | parameter_names=['variable_cost', 'replacement_cost'], 42 | state_manager=state_manager) 43 | return solver.estimate(start_params=[1, 1], method='bfgs') 44 | 45 | mean_params = test() 46 | tolerance_levels = np.array([0.1, 0.1]) 47 | assert np.all(np.abs(mean_params - true_params) < tolerance_levels) 48 | assert state_manager.get_states_mesh().shape == (1, n_states, 1) 49 | 50 | 51 | def test_multivariate_utility_fn_estimation(large_transition_matrix): 52 | 53 | unit_value_real = 4 54 | exit_unit_cost = -3 55 | exit_unit_value = unit_value_real + exit_unit_cost 56 | plan_a_unit_cost = -2.5 57 | plan_a_fixed_cost = -4 58 | plan_b_unit_cost = -2 59 | plan_b_unit_value = unit_value_real + plan_b_unit_cost 60 | plan_b_user_cost = -5 61 | 62 | real_params = [ 63 | unit_value_real, 64 | exit_unit_cost, 65 | plan_a_unit_cost, 66 | plan_a_fixed_cost, 67 | plan_b_unit_cost, 68 | plan_b_user_cost 69 | ] 70 | discount_factor = 0.9 71 | n_choices = 3 72 | n_periods = 100 73 | n_agents = 1000 74 | 75 | def real_utility_fn(theta, _, states): 76 | [ 77 | unit_value, 78 | unit_exit_cost, 79 | unit_plan_a_cost, 80 | fixed_plan_a_cost, 81 | unit_plan_b_cost, 82 | user_plan_b_cost 83 | ] = theta 84 | 85 | [cards, users] = states 86 | u_exit = (unit_value + unit_exit_cost) * cards 87 | u_plan_a = (unit_value + unit_plan_a_cost) * cards + fixed_plan_a_cost 88 | u_plan_b = (unit_value + unit_plan_b_cost) * cards + user_plan_b_cost * users 89 | u = np.array([u_exit, u_plan_a, u_plan_b]) 90 | return u 91 | 92 | def model_utility_fn(theta, _, states): 93 | [exit_net_unit_value, unit_value, plan_b_net_unit_value] = theta 94 | [units, users] = states 95 | u_exit = exit_net_unit_value * units 96 | u_plan_a = (unit_value + plan_a_unit_cost) * units + plan_a_fixed_cost 97 | u_plan_b = plan_b_net_unit_value * units + plan_b_user_cost * users 98 | u = np.array([u_exit, u_plan_a, u_plan_b]) 99 | return u 100 | 101 | state_manager = StateManager(units=5, users=2) 102 | 103 | @average_out(n_repetitions) 104 | def test(): 105 | df, ccp = simulate( 106 | n_periods, 107 | n_agents, 108 | n_choices, 109 | state_manager, 110 | real_params, 111 | real_utility_fn, 112 | discount_factor, 113 | large_transition_matrix, 114 | convergence_criterion=10e-6) 115 | 116 | parameter_names = ['exit_net_unit_value', 'unit_value', 'plan_b_net_unit_value'] 117 | 118 | algo = NPL(df['action'].values, 119 | df['state'].values, 120 | large_transition_matrix, 121 | model_utility_fn, 122 | discount_factor, 123 | initial_p=random_ccp(state_manager.total_states, 3), 124 | state_manager=state_manager, 125 | parameter_names=parameter_names 126 | ) 127 | return algo.estimate(start_params=np.random.uniform(size=len(parameter_names)), method='bfgs') 128 | 129 | mean_params = test() 130 | 131 | tolerance_levels = np.array([0.1, 0.1, 0.1]) 132 | assert np.all(np.abs(mean_params - [exit_unit_value, unit_value_real, plan_b_unit_value]) < tolerance_levels) 133 | 134 | 135 | def test_multivariate_ddc(): 136 | n_age_states = 30 137 | n_health_states = 2 138 | n_retirement_states = 2 139 | n_pension_states = 3 140 | n_medical_exp_states = 3 141 | discount_factor = 0.9 142 | 143 | def utility_fn(theta, _, states): 144 | [age, health, retirement, pension, medical_exp] = states 145 | u_retires = pension - medical_exp 146 | 147 | wage = np.exp(theta[0] + theta[1] * health + theta[2] * (age / (1 + age))) 148 | u_works = wage - medical_exp 149 | 150 | u_works = np.where(retirement == 1, u_works - theta[3], u_works) 151 | 152 | u = np.array([u_retires, u_works]) 153 | 154 | return u 155 | 156 | true_params = [1, 0.8, -1, 2] 157 | 158 | # Transition functions retired: 159 | health_transition_retired = np.array( 160 | [ 161 | [0.2, 0.8], 162 | [0.1, 0.9] 163 | ] 164 | ) 165 | 166 | is_retired_transition_retired = np.array( 167 | [ 168 | [0.0, 1.0], 169 | [0.0, 1.0] 170 | ] 171 | ) 172 | 173 | pension_transition_retired = np.eye(3) 174 | 175 | medical_exp_transition_retired = np.array( 176 | [ 177 | [0.7, 0.2, 0.1], 178 | [0.1, 0.7, 0.2], 179 | [0.2, 0.1, 0.7] 180 | ] 181 | ) 182 | 183 | age_transition_retired = np.diag(np.ones(shape=29), k=1) 184 | age_transition_retired[-1, -1] = 1 185 | 186 | # Transition matrices working: 187 | health_transition_working = np.array( 188 | [ 189 | [0.4, 0.6], 190 | [0.1, 0.9] 191 | ] 192 | ) 193 | 194 | is_retired_transition_working = np.array( 195 | [ 196 | [1.0, 0.0], 197 | [0.9, 0.1] 198 | ] 199 | ) 200 | 201 | pension_transition_working = np.array( 202 | [ 203 | [0.7, 0.2, 0.1], 204 | [0.1, 0.7, 0.2], 205 | [0.1, 0.2, 0.7] 206 | ] 207 | ) 208 | 209 | medical_exp_transition_working = np.array( 210 | [ 211 | [0.5, 0.4, 0.1], 212 | [0.4, 0.5, 0.1], 213 | [0.1, 0.5, 0.4] 214 | ] 215 | ) 216 | 217 | age_transition_working = np.diag(np.ones(shape=29), k=1) 218 | age_transition_working[-1, -1] = 1 219 | 220 | state_manager = StateManager( 221 | age=n_age_states, 222 | health=n_health_states, 223 | retirement=n_retirement_states, 224 | pension=n_pension_states, 225 | medical_exp=n_medical_exp_states, 226 | ) 227 | 228 | transition_matrix_working = StateManager.merge_matrices(age_transition_working, 229 | health_transition_working, 230 | is_retired_transition_working, 231 | pension_transition_working, 232 | medical_exp_transition_working) 233 | 234 | transition_matrix_retired = StateManager.merge_matrices(age_transition_retired, 235 | health_transition_retired, 236 | is_retired_transition_retired, 237 | pension_transition_retired, 238 | medical_exp_transition_retired) 239 | 240 | transition_matrix = np.array([transition_matrix_retired, transition_matrix_working]) 241 | 242 | # Flaky test 243 | @average_out(5) 244 | def test(): 245 | df, ccp = simulate(100, 100, 2, state_manager, true_params, utility_fn, discount_factor, transition_matrix) 246 | 247 | parameter_names = ['const', 'health', 'age', 'work_disutility'] 248 | 249 | algorithm = CCP(df['action'].values, 250 | df['state'].values, 251 | transition_matrix, 252 | utility_fn, 253 | discount_factor, 254 | initial_p=ccp, 255 | state_manager=state_manager, 256 | parameter_names=parameter_names 257 | ) 258 | 259 | return algorithm.estimate(start_params=np.random.uniform(size=len(parameter_names)), method='bfgs') 260 | 261 | mean_params = test() 262 | 263 | assert np.abs((mean_params - true_params)/true_params).max() < 0.4 264 | -------------------------------------------------------------------------------- /source_ddc/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 © Sansan, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /source_ddc/src/source_ddc/algorithms.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from statsmodels.base.model import GenericLikelihoodModel 3 | from .fixed_point import phi_map, lambda_map 4 | from .probability_tools import random_ccp 5 | from .probability_tools import StateManager 6 | from .simulation_tools import simulate_action_draw, simulate_state_draw 7 | 8 | 9 | class MaximumLikelihoodDDCModel(GenericLikelihoodModel): 10 | """ 11 | Abstract interface for all Dynamic Discrete Choice Models that are estimated by Maximum Likelihood. 12 | The basic components of a DDC model estimated by Maximum Likelihood include: 13 | 14 | - A set of structural parameters of interest: DDCs usually include in the definition of structural parameters 15 | the conditional choice probabilities, the utility function parameters, the discount factor, the transition 16 | probabilities and the distribution of the unobservables. This implementation provides tools for estimating the 17 | utility function parameters and the conditional choice probabilities. 18 | - The number of choices and states. 19 | - A transition matrix, defined as a numpy array of shape (n_choices, n_states, n_states). This array represents the 20 | transition probabilities at each state and its elements are therefore normalized to 1 at the second axis (i.e. the 21 | operation `transition_matrix.sum(axis=2)` should return an array of ones with shape (n_choices, n_states). Although 22 | the transition probabilities can be considered structural parameters of interest, the current implementation does 23 | not estimate them, and therefore must be provided. 24 | - A utility function: this is a custom function that takes as arguments a list of float structural parameters, 25 | an array of shape (n_choices, 1, 1) representing the unique values of the choices available to the agent, and a mesh 26 | of state variables of shape (n_state_dimensions, n_states, 1) representing all the possible combinations of state 27 | variables. 28 | - A discount factor: this is a float in the range [0, 1) representing the valuation the agent has of the future. 29 | Currently this value must be provided, and is therefore not estimated along with the other parameters. 30 | - Conditional choice probabilities: these are represented as a numpy array of shape (n_choices, n_states, 1), 31 | representing the agent's probability of making a given choice from the choice set at some state. For some models, 32 | these probabilities are estimated at the same time as the utility parameters, while in others they must be 33 | consistently estimated in a first step. This implementation support both cases. 34 | 35 | 36 | :param decisions: a single-column vector of decisions data, where choices are represented by consecutive integers 37 | beginning at zero. 38 | :param state: an NTx1 vector of state data, where states are represented by consecutive integers beginning at zero 39 | :param transition_matrix: a numpy array of shape (n_choices, n_states, n_states) containing the transition 40 | probabilities. 41 | :param utility_function: a function that takes parameter values, an array of available choices and a mesh of state 42 | categories as inputs, and returns a real-valued numpy array of shape (n_choices, n_states, 1). 43 | :param initial_p: the initial value of the conditional choice probabilities as a numpy array of shape 44 | (n_choices, n_states, 1). 45 | """ 46 | 47 | def __init__(self, 48 | decisions, 49 | state, 50 | transition_matrix, 51 | utility_function, 52 | discount_factor, 53 | initial_p=np.empty(shape=()), 54 | parameter_names=None, 55 | state_manager=None, 56 | **kwds): 57 | super(MaximumLikelihoodDDCModel, self).__init__(decisions, state, **kwds) 58 | self.transition_matrix = transition_matrix 59 | self.n_choices, self.n_states, _ = transition_matrix.shape 60 | if state_manager: 61 | self.state_manager = state_manager 62 | else: 63 | self.state_manager = StateManager(state=self.n_states) 64 | self.utility_function = utility_function 65 | self.discount_factor = discount_factor 66 | if initial_p.shape == (): 67 | self.p = random_ccp(self.n_states, self.n_choices) 68 | else: 69 | initial_p[initial_p < 0] = 0 70 | self.p = initial_p 71 | self.v = np.random.uniform(size=self.p.shape) 72 | self.p_convergence_criterion = 10e-10 73 | self.v_convergence_criterion = 10e-10 74 | if parameter_names: 75 | self.data.xnames = parameter_names 76 | 77 | def probability_iteration(self, parameters): 78 | """Iterates until achieving the fixed point in the probability space. 79 | 80 | :param parameters: the utility function parameters. 81 | :return: None 82 | """ 83 | 84 | converged = False 85 | while not converged: 86 | p_0 = self.p 87 | self.v = phi_map(self.p, 88 | self.transition_matrix, 89 | parameters, 90 | self.utility_function, 91 | self.discount_factor, 92 | self.state_manager) 93 | self.p = lambda_map(self.v) 94 | delta = np.abs(np.max((self.p - p_0))) 95 | if delta <= self.p_convergence_criterion: 96 | converged = True 97 | 98 | def value_iteration(self, parameters): 99 | """Iterates until achieving the fixed point in the value space. 100 | 101 | :param parameters: the utility function parameters. 102 | :return: None 103 | """ 104 | converged = False 105 | while not converged: 106 | v_0 = self.v 107 | self.p = lambda_map(self.v) 108 | self.v = phi_map(self.p, 109 | self.transition_matrix, 110 | parameters, 111 | self.utility_function, 112 | self.discount_factor, 113 | self.state_manager) 114 | delta = np.abs(np.max((self.v - v_0))) 115 | if delta <= self.v_convergence_criterion: 116 | converged = True 117 | 118 | def estimate(self, **kwargs): 119 | """Wraps the fit method from statsmodels `GenericLikelihoodModel`. 120 | 121 | :param kwargs: parameters passed by keyword arguments to the `fit` method of `GenericLikelihoodModel`. 122 | :return: an instance of `statsmodels.base.model.GenericLikelihoodModelResults` with the estimation results. 123 | """ 124 | return self.fit(**kwargs) 125 | 126 | def simulate_forward(self, n_draws): 127 | """This method uses the estimated parameters to simulate the actions of the agent `n_draws` into the future. 128 | 129 | :param n_draws: the number of time steps to simulate forward. 130 | :return: a numpy array of shape (n_draws + 1, n_obsevations, 2), where the first element in the first axis 131 | represents the last observed value for the agent, and the rest contain the state and choice values for the 132 | rest of the time steps. 133 | """ 134 | history = np.vstack([self.endog.ravel(), self.exog.ravel()]).T.reshape(1, self.endog.shape[0], 2) 135 | current_history = history 136 | for _ in range(n_draws): 137 | next_states = simulate_state_draw(current_history, self.transition_matrix) 138 | next_choices = simulate_action_draw(self.p, next_states) 139 | next_history = np.vstack([next_choices, next_states]).T.reshape(1, -1, 2) 140 | history = np.vstack([history, next_history]) 141 | current_history = next_history 142 | return history 143 | 144 | 145 | class NFXP(MaximumLikelihoodDDCModel): 146 | """ 147 | This class implements the Nested Fixed Point algorithm by Rust (1987). It obtains the fixed point for the 148 | conditional choice probabilities at each iteration of the maximum likelihood estimation procedure. 149 | 150 | :param decisions: a single-column vector of decisions data, where choices are represented by consecutive integers 151 | beginning at zero. 152 | :param state: an NTx1 vector of state data, where states are represented by consecutive integers beginning at zero 153 | :param transition_matrix: a numpy array of shape (n_choices, n_states, n_states) containing the transition 154 | probabilities. 155 | :param utility_function: a function that takes parameter values, an array of available choices and a mesh of state 156 | categories as inputs, and returns a real-valued numpy array of shape (n_choices, n_states, 1). 157 | :param initial_p: the initial value of the conditional choice probabilities as a numpy array of shape 158 | (n_choices, n_states, 1). 159 | :param parameter_names: a list of `str` containing the names of the target parameters. 160 | """ 161 | 162 | def __init__(self, 163 | decisions, 164 | state, 165 | transition_matrix, 166 | utility_function, 167 | discount_factor, 168 | initial_p=np.empty(shape=()), 169 | parameter_names=None, 170 | **kwds): 171 | super(NFXP, self).__init__(decisions, 172 | state, 173 | transition_matrix, 174 | utility_function, 175 | discount_factor, 176 | initial_p, 177 | parameter_names, 178 | **kwds) 179 | 180 | def nloglikeobs(self, parameters): 181 | """Obtains the likelihood value for the current parameter values by first finding the fixed point for the 182 | conditional choice probabilities. 183 | 184 | :param parameters: a list of float values. 185 | :return: a float. 186 | """ 187 | self.probability_iteration(parameters) 188 | pr = self.p[self.endog.ravel(), self.exog.ravel()] 189 | ll = -np.log(pr).sum() 190 | return ll 191 | 192 | 193 | class CCP(MaximumLikelihoodDDCModel): 194 | """ 195 | Implements the Hotz & Miller (1993) Conditional Choice Probability algorithm. The conditional choice probabilities 196 | must be consistently estimated in a separate step and passed as the `initial_p` argument. This algorithm finds the 197 | value at each state that is consistent with the passed probabilities. 198 | 199 | :param decisions: a single-column vector of decisions data, where choices are represented by consecutive integers 200 | beginning at zero. 201 | :param state: an NTx1 vector of state data, where states are represented by consecutive integers beginning at zero 202 | :param transition_matrix: a numpy array of shape (n_choices, n_states, n_states) containing the transition 203 | probabilities. 204 | :param utility_function: a function that takes parameter values, an array of available choices and a mesh of state 205 | categories as inputs, and returns a real-valued numpy array of shape (n_choices, n_states, 1). 206 | :param initial_p: the initial value of the conditional choice probabilities as a numpy array of shape 207 | (n_choices, n_states, 1). This value is taken as the definitive value of the conditional choice probabilities and 208 | is therefore not updated any further during the estimation. 209 | :param parameter_names: a list of `str` containing the names of the target parameters. 210 | """ 211 | 212 | def __init__(self, 213 | decisions, 214 | state, 215 | transition_matrix, 216 | utility_function, 217 | discount_factor, 218 | initial_p, 219 | parameter_names=None, 220 | **kwds): 221 | super(CCP, self).__init__(decisions, 222 | state, 223 | transition_matrix, 224 | utility_function, 225 | discount_factor, 226 | initial_p, 227 | parameter_names, 228 | **kwds) 229 | 230 | def nloglikeobs(self, parameters): 231 | """Updates the value function values and obtains the log-likelihood value consistent with the new value function 232 | without updating the conditional choice probabilities. 233 | 234 | :param parameters: a list of float values. 235 | :return: a float. 236 | """ 237 | self.v = phi_map(self.p, 238 | self.transition_matrix, 239 | parameters, 240 | self.utility_function, 241 | self.discount_factor, 242 | self.state_manager) 243 | p = lambda_map(self.v) 244 | pr = p[self.endog.ravel(), self.exog.ravel()] 245 | ll = -np.log(pr).sum() 246 | return ll 247 | 248 | 249 | class NPL(MaximumLikelihoodDDCModel): 250 | """Implements the Nested Pseudo-Likelihood algorithm by Aguirregabiria & Mira (2002). Similar to the CCP algorithm, 251 | but updates both the conditional choice probabilities and the value function. Better performance can be obtained 252 | if the passed conditional choice probabilities are close to the true parameters. 253 | 254 | :param decisions: a single-column vector of decisions data, where choices are represented by consecutive integers 255 | beginning at zero. 256 | :param state: an NTx1 vector of state data, where states are represented by consecutive integers beginning at zero 257 | :param transition_matrix: a numpy array of shape (n_choices, n_states, n_states) containing the transition 258 | probabilities. 259 | :param utility_function: a function that takes parameter values, an array of available choices and a mesh of state 260 | categories as inputs, and returns a real-valued numpy array of shape (n_choices, n_states, 1). 261 | :param initial_p: the initial value of the conditional choice probabilities as a numpy array of shape 262 | (n_choices, n_states, 1). 263 | :param parameter_names: a list of `str` containing the names of the target parameters. 264 | :param relaxation_parameter: this is a float value in the range (0, 1] that acts as a learning rate to solve 265 | convergence issues as proposed in Kasahara & Shimotsu (2008). Values closer to 1 give more preference to new values 266 | of the conditional choice probabilities, while values closer to 0 give more weight to the values obtained in the 267 | previous iteration. 268 | :param npl_maxiter: the maximum allowed number of iterations for the NPL algorithm. 269 | """ 270 | 271 | def __init__(self, 272 | decisions, 273 | state, 274 | transition_matrix, 275 | utility_function, 276 | discount_factor, 277 | initial_p, 278 | parameter_names=None, 279 | relaxation_param=1., 280 | npl_maxiter=None, 281 | **kwds): 282 | super(NPL, self).__init__(decisions, 283 | state, 284 | transition_matrix, 285 | utility_function, 286 | discount_factor, 287 | initial_p, 288 | parameter_names, 289 | **kwds) 290 | self.__fit = self.fit 291 | if 0. <= relaxation_param <= 1.: 292 | self.relaxation_param = relaxation_param 293 | else: 294 | raise Exception('Relaxation param must be a float between 0 and 1.') 295 | 296 | if npl_maxiter is None: 297 | self.npl_maxiter = npl_maxiter 298 | elif npl_maxiter > 0: 299 | self.npl_maxiter = int(npl_maxiter) 300 | else: 301 | raise Exception('Max Iterations must be a positive integer') 302 | 303 | def nloglikeobs(self, parameters): 304 | """Updates the value function and the conditional choice probabilities for each iteration and calculates the 305 | log-likelihood at the given parameters. 306 | 307 | :param parameters: a list of float values. 308 | :return: a float. 309 | """ 310 | self.v = phi_map(self.p, 311 | self.transition_matrix, 312 | parameters, 313 | self.utility_function, 314 | self.discount_factor, 315 | self.state_manager) 316 | p = (lambda_map(self.v)**self.relaxation_param)*(self.p**(1 - self.relaxation_param)) 317 | pr = p[self.endog.ravel(), self.exog.ravel()] 318 | ll = -np.log(pr).sum() 319 | return ll 320 | 321 | def estimate(self, **kwargs): 322 | """Estimate using the NPL algorithm. The procedure performs several CCP updates until convergence or a maximum 323 | number of iterations. 324 | 325 | :param kwargs: parameters passed by keyword arguments to the `fit` method of `GenericLikelihoodModel`. 326 | :return: an instance of `statsmodels.base.model.GenericLikelihoodModelResults` with the estimation results. 327 | """ 328 | results = self.fit(**kwargs) 329 | self.p = lambda_map(self.v) 330 | converged = False 331 | n_iterations = 0 332 | while not converged: 333 | p_0 = self.p 334 | results = self.fit(**{**kwargs, 'start_params': results.params}) 335 | delta = np.abs(np.max((self.p - p_0))) 336 | n_iterations += 1 337 | if self.npl_maxiter is not None: 338 | if delta <= self.p_convergence_criterion or n_iterations >= self.npl_maxiter: 339 | converged = True 340 | elif delta <= self.p_convergence_criterion: 341 | converged = True 342 | 343 | return results 344 | --------------------------------------------------------------------------------