├── 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 |
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 |
--------------------------------------------------------------------------------