diff --git a/poetry.lock b/poetry.lock
index 37b1adf..f7dcf55 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,33 @@
-# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
+
+[[package]]
+name = "appnope"
+version = "0.1.4"
+description = "Disable App Nap on macOS >= 10.9"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"},
+ {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"},
+]
+
+[[package]]
+name = "asttokens"
+version = "2.4.1"
+description = "Annotate AST trees with source code positions"
+optional = false
+python-versions = "*"
+files = [
+ {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"},
+ {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"},
+]
+
+[package.dependencies]
+six = ">=1.12.0"
+
+[package.extras]
+astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"]
+test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"]
[[package]]
name = "astunparse"
@@ -15,6 +44,17 @@ files = [
six = ">=1.6.1,<2.0"
wheel = ">=0.23.0,<1.0"
+[[package]]
+name = "backcall"
+version = "0.2.0"
+description = "Specifications for callback functions passed in to an API"
+optional = false
+python-versions = "*"
+files = [
+ {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
+ {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
+]
+
[[package]]
name = "build"
version = "1.0.3"
@@ -274,6 +314,23 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
+[[package]]
+name = "comm"
+version = "0.2.2"
+description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"},
+ {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"},
+]
+
+[package.dependencies]
+traitlets = ">=4"
+
+[package.extras]
+test = ["pytest"]
+
[[package]]
name = "crashtest"
version = "0.4.1"
@@ -339,6 +396,48 @@ ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
+[[package]]
+name = "debugpy"
+version = "1.8.1"
+description = "An implementation of the Debug Adapter Protocol for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "debugpy-1.8.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:3bda0f1e943d386cc7a0e71bfa59f4137909e2ed947fb3946c506e113000f741"},
+ {file = "debugpy-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda73bf69ea479c8577a0448f8c707691152e6c4de7f0c4dec5a4bc11dee516e"},
+ {file = "debugpy-1.8.1-cp310-cp310-win32.whl", hash = "sha256:3a79c6f62adef994b2dbe9fc2cc9cc3864a23575b6e387339ab739873bea53d0"},
+ {file = "debugpy-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:7eb7bd2b56ea3bedb009616d9e2f64aab8fc7000d481faec3cd26c98a964bcdd"},
+ {file = "debugpy-1.8.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:016a9fcfc2c6b57f939673c874310d8581d51a0fe0858e7fac4e240c5eb743cb"},
+ {file = "debugpy-1.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd97ed11a4c7f6d042d320ce03d83b20c3fb40da892f994bc041bbc415d7a099"},
+ {file = "debugpy-1.8.1-cp311-cp311-win32.whl", hash = "sha256:0de56aba8249c28a300bdb0672a9b94785074eb82eb672db66c8144fff673146"},
+ {file = "debugpy-1.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a9fe0829c2b854757b4fd0a338d93bc17249a3bf69ecf765c61d4c522bb92a8"},
+ {file = "debugpy-1.8.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3ebb70ba1a6524d19fa7bb122f44b74170c447d5746a503e36adc244a20ac539"},
+ {file = "debugpy-1.8.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2e658a9630f27534e63922ebf655a6ab60c370f4d2fc5c02a5b19baf4410ace"},
+ {file = "debugpy-1.8.1-cp312-cp312-win32.whl", hash = "sha256:caad2846e21188797a1f17fc09c31b84c7c3c23baf2516fed5b40b378515bbf0"},
+ {file = "debugpy-1.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:edcc9f58ec0fd121a25bc950d4578df47428d72e1a0d66c07403b04eb93bcf98"},
+ {file = "debugpy-1.8.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:7a3afa222f6fd3d9dfecd52729bc2e12c93e22a7491405a0ecbf9e1d32d45b39"},
+ {file = "debugpy-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d915a18f0597ef685e88bb35e5d7ab968964b7befefe1aaea1eb5b2640b586c7"},
+ {file = "debugpy-1.8.1-cp38-cp38-win32.whl", hash = "sha256:92116039b5500633cc8d44ecc187abe2dfa9b90f7a82bbf81d079fcdd506bae9"},
+ {file = "debugpy-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:e38beb7992b5afd9d5244e96ad5fa9135e94993b0c551ceebf3fe1a5d9beb234"},
+ {file = "debugpy-1.8.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:bfb20cb57486c8e4793d41996652e5a6a885b4d9175dd369045dad59eaacea42"},
+ {file = "debugpy-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd3fdd3f67a7e576dd869c184c5dd71d9aaa36ded271939da352880c012e703"},
+ {file = "debugpy-1.8.1-cp39-cp39-win32.whl", hash = "sha256:58911e8521ca0c785ac7a0539f1e77e0ce2df753f786188f382229278b4cdf23"},
+ {file = "debugpy-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:6df9aa9599eb05ca179fb0b810282255202a66835c6efb1d112d21ecb830ddd3"},
+ {file = "debugpy-1.8.1-py2.py3-none-any.whl", hash = "sha256:28acbe2241222b87e255260c76741e1fbf04fdc3b6d094fcf57b6c6f75ce1242"},
+ {file = "debugpy-1.8.1.zip", hash = "sha256:f696d6be15be87aef621917585f9bb94b1dc9e8aced570db1b8a6fc14e8f9b42"},
+]
+
+[[package]]
+name = "decorator"
+version = "5.1.1"
+description = "Decorators for Humans"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
+ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
+]
+
[[package]]
name = "distlib"
version = "0.3.7"
@@ -452,6 +551,20 @@ files = [
[package.extras]
test = ["pytest (>=6)"]
+[[package]]
+name = "executing"
+version = "2.0.1"
+description = "Get the currently executing AST node of a frame, and other information"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"},
+ {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"},
+]
+
+[package.extras]
+tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"]
+
[[package]]
name = "fastjsonschema"
version = "2.18.1"
@@ -583,6 +696,78 @@ files = [
{file = "installer-0.7.0.tar.gz", hash = "sha256:a26d3e3116289bb08216e0d0f7d925fcef0b0194eedfa0c944bcaaa106c4b631"},
]
+[[package]]
+name = "ipykernel"
+version = "6.29.4"
+description = "IPython Kernel for Jupyter"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "ipykernel-6.29.4-py3-none-any.whl", hash = "sha256:1181e653d95c6808039c509ef8e67c4126b3b3af7781496c7cbfb5ed938a27da"},
+ {file = "ipykernel-6.29.4.tar.gz", hash = "sha256:3d44070060f9475ac2092b760123fadf105d2e2493c24848b6691a7c4f42af5c"},
+]
+
+[package.dependencies]
+appnope = {version = "*", markers = "platform_system == \"Darwin\""}
+comm = ">=0.1.1"
+debugpy = ">=1.6.5"
+ipython = ">=7.23.1"
+jupyter-client = ">=6.1.12"
+jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
+matplotlib-inline = ">=0.1"
+nest-asyncio = "*"
+packaging = "*"
+psutil = "*"
+pyzmq = ">=24"
+tornado = ">=6.1"
+traitlets = ">=5.4.0"
+
+[package.extras]
+cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"]
+pyqt5 = ["pyqt5"]
+pyside6 = ["pyside6"]
+test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"]
+
+[[package]]
+name = "ipython"
+version = "8.12.3"
+description = "IPython: Productive Interactive Computing"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "ipython-8.12.3-py3-none-any.whl", hash = "sha256:b0340d46a933d27c657b211a329d0be23793c36595acf9e6ef4164bc01a1804c"},
+ {file = "ipython-8.12.3.tar.gz", hash = "sha256:3910c4b54543c2ad73d06579aa771041b7d5707b033bd488669b4cf544e3b363"},
+]
+
+[package.dependencies]
+appnope = {version = "*", markers = "sys_platform == \"darwin\""}
+backcall = "*"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+decorator = "*"
+jedi = ">=0.16"
+matplotlib-inline = "*"
+pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
+pickleshare = "*"
+prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0"
+pygments = ">=2.4.0"
+stack-data = "*"
+traitlets = ">=5"
+typing-extensions = {version = "*", markers = "python_version < \"3.10\""}
+
+[package.extras]
+all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"]
+black = ["black"]
+doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"]
+kernel = ["ipykernel"]
+nbconvert = ["nbconvert"]
+nbformat = ["nbformat"]
+notebook = ["ipywidgets", "notebook"]
+parallel = ["ipyparallel"]
+qtconsole = ["qtconsole"]
+test = ["pytest (<7.1)", "pytest-asyncio", "testpath"]
+test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"]
+
[[package]]
name = "jaraco-classes"
version = "3.3.0"
@@ -601,6 +786,25 @@ more-itertools = "*"
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
+[[package]]
+name = "jedi"
+version = "0.19.1"
+description = "An autocompletion tool for Python that can be used for text editors."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"},
+ {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"},
+]
+
+[package.dependencies]
+parso = ">=0.8.3,<0.9.0"
+
+[package.extras]
+docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"]
+qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
+testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
+
[[package]]
name = "jeepney"
version = "0.8.0"
@@ -633,6 +837,60 @@ MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
+[[package]]
+name = "joblib"
+version = "1.4.2"
+description = "Lightweight pipelining with Python functions"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6"},
+ {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"},
+]
+
+[[package]]
+name = "jupyter-client"
+version = "8.6.2"
+description = "Jupyter protocol implementation and client libraries"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "jupyter_client-8.6.2-py3-none-any.whl", hash = "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f"},
+ {file = "jupyter_client-8.6.2.tar.gz", hash = "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df"},
+]
+
+[package.dependencies]
+importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""}
+jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
+python-dateutil = ">=2.8.2"
+pyzmq = ">=23.0"
+tornado = ">=6.2"
+traitlets = ">=5.3"
+
+[package.extras]
+docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"]
+test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"]
+
+[[package]]
+name = "jupyter-core"
+version = "5.7.2"
+description = "Jupyter core package. A base package on which Jupyter projects rely."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"},
+ {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"},
+]
+
+[package.dependencies]
+platformdirs = ">=2.5"
+pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""}
+traitlets = ">=5.3"
+
+[package.extras]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"]
+test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"]
+
[[package]]
name = "keyring"
version = "24.2.0"
@@ -777,6 +1035,20 @@ files = [
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
]
+[[package]]
+name = "matplotlib-inline"
+version = "0.1.7"
+description = "Inline Matplotlib backend for Jupyter"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"},
+ {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"},
+]
+
+[package.dependencies]
+traitlets = "*"
+
[[package]]
name = "mergedeep"
version = "1.3.4"
@@ -996,6 +1268,17 @@ files = [
{file = "msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87"},
]
+[[package]]
+name = "nest-asyncio"
+version = "1.6.0"
+description = "Patch asyncio to allow nested event loops"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"},
+ {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"},
+]
+
[[package]]
name = "numba"
version = "0.58.1"
@@ -1146,6 +1429,21 @@ sql-other = ["SQLAlchemy (>=1.4.16)"]
test = ["hypothesis (>=6.34.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"]
xml = ["lxml (>=4.6.3)"]
+[[package]]
+name = "parso"
+version = "0.8.4"
+description = "A Python Parser"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"},
+ {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"},
+]
+
+[package.extras]
+qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
+testing = ["docopt", "pytest"]
+
[[package]]
name = "pathspec"
version = "0.11.2"
@@ -1189,6 +1487,17 @@ files = [
[package.dependencies]
ptyprocess = ">=0.5"
+[[package]]
+name = "pickleshare"
+version = "0.7.5"
+description = "Tiny 'shelve'-like database with concurrency support"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
+ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
+]
+
[[package]]
name = "pkginfo"
version = "1.9.6"
@@ -1315,6 +1624,48 @@ files = [
poetry = ">=1.6.0,<2.0.0"
poetry-core = ">=1.7.0,<2.0.0"
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.43"
+description = "Library for building powerful interactive command lines in Python"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"},
+ {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"},
+]
+
+[package.dependencies]
+wcwidth = "*"
+
+[[package]]
+name = "psutil"
+version = "5.9.8"
+description = "Cross-platform lib for process and system monitoring in Python."
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+files = [
+ {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"},
+ {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"},
+ {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"},
+ {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"},
+ {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"},
+ {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"},
+ {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"},
+ {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"},
+ {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"},
+ {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"},
+ {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"},
+ {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"},
+ {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"},
+ {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"},
+ {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"},
+ {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"},
+]
+
+[package.extras]
+test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
+
[[package]]
name = "ptyprocess"
version = "0.7.0"
@@ -1326,6 +1677,20 @@ files = [
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
]
+[[package]]
+name = "pure-eval"
+version = "0.2.2"
+description = "Safely evaluate AST nodes without side effects"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
+ {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
+]
+
+[package.extras]
+tests = ["pytest"]
+
[[package]]
name = "pycparser"
version = "2.21"
@@ -1447,6 +1812,29 @@ files = [
{file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"},
]
+[[package]]
+name = "pywin32"
+version = "306"
+description = "Python for Window Extensions"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"},
+ {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"},
+ {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"},
+ {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"},
+ {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"},
+ {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"},
+ {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"},
+ {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"},
+ {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"},
+ {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"},
+ {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"},
+ {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"},
+ {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"},
+ {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"},
+]
+
[[package]]
name = "pywin32-ctypes"
version = "0.2.2"
@@ -1532,6 +1920,106 @@ files = [
[package.dependencies]
pyyaml = "*"
+[[package]]
+name = "pyzmq"
+version = "26.0.3"
+description = "Python bindings for 0MQ"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:44dd6fc3034f1eaa72ece33588867df9e006a7303725a12d64c3dff92330f625"},
+ {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:acb704195a71ac5ea5ecf2811c9ee19ecdc62b91878528302dd0be1b9451cc90"},
+ {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbb9c997932473a27afa93954bb77a9f9b786b4ccf718d903f35da3232317de"},
+ {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bcb34f869d431799c3ee7d516554797f7760cb2198ecaa89c3f176f72d062be"},
+ {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ece17ec5f20d7d9b442e5174ae9f020365d01ba7c112205a4d59cf19dc38ee"},
+ {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ba6e5e6588e49139a0979d03a7deb9c734bde647b9a8808f26acf9c547cab1bf"},
+ {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3bf8b000a4e2967e6dfdd8656cd0757d18c7e5ce3d16339e550bd462f4857e59"},
+ {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2136f64fbb86451dbbf70223635a468272dd20075f988a102bf8a3f194a411dc"},
+ {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e8918973fbd34e7814f59143c5f600ecd38b8038161239fd1a3d33d5817a38b8"},
+ {file = "pyzmq-26.0.3-cp310-cp310-win32.whl", hash = "sha256:0aaf982e68a7ac284377d051c742610220fd06d330dcd4c4dbb4cdd77c22a537"},
+ {file = "pyzmq-26.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:f1a9b7d00fdf60b4039f4455afd031fe85ee8305b019334b72dcf73c567edc47"},
+ {file = "pyzmq-26.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:80b12f25d805a919d53efc0a5ad7c0c0326f13b4eae981a5d7b7cc343318ebb7"},
+ {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:a72a84570f84c374b4c287183debc776dc319d3e8ce6b6a0041ce2e400de3f32"},
+ {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ca684ee649b55fd8f378127ac8462fb6c85f251c2fb027eb3c887e8ee347bcd"},
+ {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e222562dc0f38571c8b1ffdae9d7adb866363134299264a1958d077800b193b7"},
+ {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f17cde1db0754c35a91ac00b22b25c11da6eec5746431d6e5092f0cd31a3fea9"},
+ {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7c0c0b3244bb2275abe255d4a30c050d541c6cb18b870975553f1fb6f37527"},
+ {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac97a21de3712afe6a6c071abfad40a6224fd14fa6ff0ff8d0c6e6cd4e2f807a"},
+ {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:88b88282e55fa39dd556d7fc04160bcf39dea015f78e0cecec8ff4f06c1fc2b5"},
+ {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:72b67f966b57dbd18dcc7efbc1c7fc9f5f983e572db1877081f075004614fcdd"},
+ {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4b6cecbbf3b7380f3b61de3a7b93cb721125dc125c854c14ddc91225ba52f83"},
+ {file = "pyzmq-26.0.3-cp311-cp311-win32.whl", hash = "sha256:eed56b6a39216d31ff8cd2f1d048b5bf1700e4b32a01b14379c3b6dde9ce3aa3"},
+ {file = "pyzmq-26.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:3191d312c73e3cfd0f0afdf51df8405aafeb0bad71e7ed8f68b24b63c4f36500"},
+ {file = "pyzmq-26.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:b6907da3017ef55139cf0e417c5123a84c7332520e73a6902ff1f79046cd3b94"},
+ {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:068ca17214038ae986d68f4a7021f97e187ed278ab6dccb79f837d765a54d753"},
+ {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7821d44fe07335bea256b9f1f41474a642ca55fa671dfd9f00af8d68a920c2d4"},
+ {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb438a26d87c123bb318e5f2b3d86a36060b01f22fbdffd8cf247d52f7c9a2b"},
+ {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69ea9d6d9baa25a4dc9cef5e2b77b8537827b122214f210dd925132e34ae9b12"},
+ {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7daa3e1369355766dea11f1d8ef829905c3b9da886ea3152788dc25ee6079e02"},
+ {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6ca7a9a06b52d0e38ccf6bca1aeff7be178917893f3883f37b75589d42c4ac20"},
+ {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1b7d0e124948daa4d9686d421ef5087c0516bc6179fdcf8828b8444f8e461a77"},
+ {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e746524418b70f38550f2190eeee834db8850088c834d4c8406fbb9bc1ae10b2"},
+ {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6b3146f9ae6af82c47a5282ac8803523d381b3b21caeae0327ed2f7ecb718798"},
+ {file = "pyzmq-26.0.3-cp312-cp312-win32.whl", hash = "sha256:2b291d1230845871c00c8462c50565a9cd6026fe1228e77ca934470bb7d70ea0"},
+ {file = "pyzmq-26.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:926838a535c2c1ea21c903f909a9a54e675c2126728c21381a94ddf37c3cbddf"},
+ {file = "pyzmq-26.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:5bf6c237f8c681dfb91b17f8435b2735951f0d1fad10cc5dfd96db110243370b"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c0991f5a96a8e620f7691e61178cd8f457b49e17b7d9cfa2067e2a0a89fc1d5"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dbf012d8fcb9f2cf0643b65df3b355fdd74fc0035d70bb5c845e9e30a3a4654b"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:01fbfbeb8249a68d257f601deb50c70c929dc2dfe683b754659569e502fbd3aa"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c8eb19abe87029c18f226d42b8a2c9efdd139d08f8bf6e085dd9075446db450"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5344b896e79800af86ad643408ca9aa303a017f6ebff8cee5a3163c1e9aec987"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:204e0f176fd1d067671157d049466869b3ae1fc51e354708b0dc41cf94e23a3a"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a42db008d58530efa3b881eeee4991146de0b790e095f7ae43ba5cc612decbc5"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-win32.whl", hash = "sha256:8d7a498671ca87e32b54cb47c82a92b40130a26c5197d392720a1bce1b3c77cf"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:3b4032a96410bdc760061b14ed6a33613ffb7f702181ba999df5d16fb96ba16a"},
+ {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2cc4e280098c1b192c42a849de8de2c8e0f3a84086a76ec5b07bfee29bda7d18"},
+ {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5bde86a2ed3ce587fa2b207424ce15b9a83a9fa14422dcc1c5356a13aed3df9d"},
+ {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:34106f68e20e6ff253c9f596ea50397dbd8699828d55e8fa18bd4323d8d966e6"},
+ {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ebbbd0e728af5db9b04e56389e2299a57ea8b9dd15c9759153ee2455b32be6ad"},
+ {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b1d1c631e5940cac5a0b22c5379c86e8df6a4ec277c7a856b714021ab6cfad"},
+ {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e891ce81edd463b3b4c3b885c5603c00141151dd9c6936d98a680c8c72fe5c67"},
+ {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9b273ecfbc590a1b98f014ae41e5cf723932f3b53ba9367cfb676f838038b32c"},
+ {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b32bff85fb02a75ea0b68f21e2412255b5731f3f389ed9aecc13a6752f58ac97"},
+ {file = "pyzmq-26.0.3-cp38-cp38-win32.whl", hash = "sha256:f6c21c00478a7bea93caaaef9e7629145d4153b15a8653e8bb4609d4bc70dbfc"},
+ {file = "pyzmq-26.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:3401613148d93ef0fd9aabdbddb212de3db7a4475367f49f590c837355343972"},
+ {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:2ed8357f4c6e0daa4f3baf31832df8a33334e0fe5b020a61bc8b345a3db7a606"},
+ {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1c8f2a2ca45292084c75bb6d3a25545cff0ed931ed228d3a1810ae3758f975f"},
+ {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b63731993cdddcc8e087c64e9cf003f909262b359110070183d7f3025d1c56b5"},
+ {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b3cd31f859b662ac5d7f4226ec7d8bd60384fa037fc02aee6ff0b53ba29a3ba8"},
+ {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115f8359402fa527cf47708d6f8a0f8234f0e9ca0cab7c18c9c189c194dbf620"},
+ {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:715bdf952b9533ba13dfcf1f431a8f49e63cecc31d91d007bc1deb914f47d0e4"},
+ {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e1258c639e00bf5e8a522fec6c3eaa3e30cf1c23a2f21a586be7e04d50c9acab"},
+ {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15c59e780be8f30a60816a9adab900c12a58d79c1ac742b4a8df044ab2a6d920"},
+ {file = "pyzmq-26.0.3-cp39-cp39-win32.whl", hash = "sha256:d0cdde3c78d8ab5b46595054e5def32a755fc028685add5ddc7403e9f6de9879"},
+ {file = "pyzmq-26.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:ce828058d482ef860746bf532822842e0ff484e27f540ef5c813d516dd8896d2"},
+ {file = "pyzmq-26.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:788f15721c64109cf720791714dc14afd0f449d63f3a5487724f024345067381"},
+ {file = "pyzmq-26.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c18645ef6294d99b256806e34653e86236eb266278c8ec8112622b61db255de"},
+ {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e6bc96ebe49604df3ec2c6389cc3876cabe475e6bfc84ced1bf4e630662cb35"},
+ {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:971e8990c5cc4ddcff26e149398fc7b0f6a042306e82500f5e8db3b10ce69f84"},
+ {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8416c23161abd94cc7da80c734ad7c9f5dbebdadfdaa77dad78244457448223"},
+ {file = "pyzmq-26.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:082a2988364b60bb5de809373098361cf1dbb239623e39e46cb18bc035ed9c0c"},
+ {file = "pyzmq-26.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d57dfbf9737763b3a60d26e6800e02e04284926329aee8fb01049635e957fe81"},
+ {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:77a85dca4c2430ac04dc2a2185c2deb3858a34fe7f403d0a946fa56970cf60a1"},
+ {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4c82a6d952a1d555bf4be42b6532927d2a5686dd3c3e280e5f63225ab47ac1f5"},
+ {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4496b1282c70c442809fc1b151977c3d967bfb33e4e17cedbf226d97de18f709"},
+ {file = "pyzmq-26.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:e4946d6bdb7ba972dfda282f9127e5756d4f299028b1566d1245fa0d438847e6"},
+ {file = "pyzmq-26.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:03c0ae165e700364b266876d712acb1ac02693acd920afa67da2ebb91a0b3c09"},
+ {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3e3070e680f79887d60feeda051a58d0ac36622e1759f305a41059eff62c6da7"},
+ {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6ca08b840fe95d1c2bd9ab92dac5685f949fc6f9ae820ec16193e5ddf603c3b2"},
+ {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e76654e9dbfb835b3518f9938e565c7806976c07b37c33526b574cc1a1050480"},
+ {file = "pyzmq-26.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:871587bdadd1075b112e697173e946a07d722459d20716ceb3d1bd6c64bd08ce"},
+ {file = "pyzmq-26.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d0a2d1bd63a4ad79483049b26514e70fa618ce6115220da9efdff63688808b17"},
+ {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0270b49b6847f0d106d64b5086e9ad5dc8a902413b5dbbb15d12b60f9c1747a4"},
+ {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:703c60b9910488d3d0954ca585c34f541e506a091a41930e663a098d3b794c67"},
+ {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74423631b6be371edfbf7eabb02ab995c2563fee60a80a30829176842e71722a"},
+ {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4adfbb5451196842a88fda3612e2c0414134874bffb1c2ce83ab4242ec9e027d"},
+ {file = "pyzmq-26.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3516119f4f9b8671083a70b6afaa0a070f5683e431ab3dc26e9215620d7ca1ad"},
+ {file = "pyzmq-26.0.3.tar.gz", hash = "sha256:dba7d9f2e047dfa2bca3b01f4f84aa5246725203d6284e3790f2ca15fba6b40a"},
+]
+
+[package.dependencies]
+cffi = {version = "*", markers = "implementation_name == \"pypy\""}
+
[[package]]
name = "rapidfuzz"
version = "3.5.2"
@@ -1744,6 +2232,25 @@ files = [
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
+[[package]]
+name = "stack-data"
+version = "0.6.3"
+description = "Extract data from python stack frames and tracebacks for informative displays"
+optional = false
+python-versions = "*"
+files = [
+ {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
+ {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
+]
+
+[package.dependencies]
+asttokens = ">=2.1.0"
+executing = ">=1.2.0"
+pure-eval = "*"
+
+[package.extras]
+tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
+
[[package]]
name = "statsmodels"
version = "0.14.0"
@@ -1833,6 +2340,41 @@ files = [
{file = "tomlkit-0.12.2.tar.gz", hash = "sha256:df32fab589a81f0d7dc525a4267b6d7a64ee99619cbd1eeb0fae32c1dd426977"},
]
+[[package]]
+name = "tornado"
+version = "6.4"
+description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
+optional = false
+python-versions = ">= 3.8"
+files = [
+ {file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"},
+ {file = "tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263"},
+ {file = "tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e"},
+ {file = "tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579"},
+ {file = "tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212"},
+ {file = "tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2"},
+ {file = "tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78"},
+ {file = "tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f"},
+ {file = "tornado-6.4-cp38-abi3-win32.whl", hash = "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052"},
+ {file = "tornado-6.4-cp38-abi3-win_amd64.whl", hash = "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63"},
+ {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"},
+]
+
+[[package]]
+name = "traitlets"
+version = "5.14.3"
+description = "Traitlets Python configuration system"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"},
+ {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"},
+]
+
+[package.extras]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
+test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"]
+
[[package]]
name = "trove-classifiers"
version = "2023.10.18"
@@ -1844,6 +2386,17 @@ files = [
{file = "trove_classifiers-2023.10.18-py3-none-any.whl", hash = "sha256:20a3da8e3cb65587cc9f5d5b837bf74edeb480bba9bd8cd4f03ab056d6b06c4c"},
]
+[[package]]
+name = "typing-extensions"
+version = "4.11.0"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
+ {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
+]
+
[[package]]
name = "tzdata"
version = "2023.3"
@@ -1931,6 +2484,17 @@ files = [
[package.extras]
watchmedo = ["PyYAML (>=3.10)"]
+[[package]]
+name = "wcwidth"
+version = "0.2.13"
+description = "Measures the displayed width of unicode strings in a terminal"
+optional = false
+python-versions = "*"
+files = [
+ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
+ {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
+]
+
[[package]]
name = "wheel"
version = "0.41.3"
@@ -2046,5 +2610,5 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
-python-versions = ">=3.8,<4.0"
-content-hash = "c1bdd7ba1dbf4d249e6aa0d336ebb5a70c8e35ff81235a94cc57055cb45bf884"
+python-versions = ">=3.8,<3.12"
+content-hash = "15bc27046ea2dae655b38615735a15b44dd8703685e9dd8974130b149a0e18eb"
diff --git a/pyproject.toml b/pyproject.toml
index dfec8b6..05ad9d3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,7 +22,7 @@ repository="https://github.com/s3alfisc/wildboottest"
[tool.poetry.dependencies]
-python=">=3.8,<4.0"
+python=">=3.8,<3.12"
numba=">=0.57"
numpy=">=1.18"
pandas=">=1.4"
@@ -30,6 +30,7 @@ pytest="^7.2.0"
statsmodels=">=0.13"
tabulate = "^0.9.0"
poetry = "^1.4.2"
+joblib = "^1.4.2"
[tool.poetry.dev-dependencies]
pytest = "^7.2.0"
@@ -48,6 +49,7 @@ pymdown-extensions = ">=10.0"
mkdocstrings-python-legacy = "^0.2.3"
mkdocstrings = {version = "^0.19.0", extras = ["python"], optional = true }
pymdown-extensions = ">=10.0"
+ipykernel = "^6.29.4"
[build-system]
diff --git a/tests/sim_sample.dta b/tests/sim_sample.dta
new file mode 100644
index 0000000..7cf9a8e
Binary files /dev/null and b/tests/sim_sample.dta differ
diff --git a/tests/test_mle.ipynb b/tests/test_mle.ipynb
new file mode 100644
index 0000000..9c740a1
--- /dev/null
+++ b/tests/test_mle.ipynb
@@ -0,0 +1,1178 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Score Bootstrap based on Kline and Santos (2012)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ " ___ ____ ____ ____ ____ ®\n",
+ " /__ / ____/ / ____/ Stata 18.0\n",
+ "___/ / /___/ / /___/ MP—Parallel Edition\n",
+ "\n",
+ " Statistics and Data Science Copyright 1985-2023 StataCorp LLC\n",
+ " StataCorp\n",
+ " 4905 Lakeway Drive\n",
+ " College Station, Texas 77845 USA\n",
+ " 800-782-8272 https://www.stata.com\n",
+ " 979-696-4600 service@stata.com\n",
+ "\n",
+ "Stata license: Single-user 2-core perpetual\n",
+ "Serial number: 501806367191\n",
+ " Licensed to: Aleksandr Michuda\n",
+ " Cornell University\n",
+ "\n",
+ "Notes:\n",
+ " 1. Unicode is supported; see help unicode_advice.\n",
+ " 2. More than 2 billion observations are allowed; see help obs_advice.\n",
+ " 3. Maximum number of variables is set to 5,000 but can be increased;\n",
+ " see help set_maxvar.\n"
+ ]
+ }
+ ],
+ "source": [
+ "import numpy as np\n",
+ "from wildboottest.wildboottest import wildboottest\n",
+ "from wildboottest.weights import draw_weights\n",
+ "from statsmodels.discrete.discrete_model import Probit\n",
+ "from numba import prange, jit\n",
+ "import statsmodels.api as sm\n",
+ "import pandas as pd\n",
+ "import stata_setup\n",
+ "stata_setup.config(path = \"/Applications/Stata\", \n",
+ " edition = \"mp\")\n",
+ "from pystata import stata\n",
+ "import sfi\n",
+ "from joblib import Parallel, delayed"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Stata Code from Paper"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ ". \n",
+ ". ****************************************************\n",
+ ". * Code for Table 2 of Kline and Santos (2011) *\n",
+ ". * This file computes Analytical Wald and LM tests *\n",
+ ". * along with Score bootstrapped tests. *\n",
+ ". * Some other results not reported in the paper *\n",
+ ". * are also included. * \n",
+ ". ****************************************************\n",
+ ". \n",
+ ". version 11.1\n",
+ "\n",
+ ". set seed 12345\n",
+ "\n",
+ ". \n",
+ ". **mata subroutine to compute recentered clustered outer products**\n",
+ ". **inputs a matrix S of scores and a cluster summing matrix L**\n",
+ ". **outputs clustered outer product matrix**\n",
+ ". **note this program assumes a balanced design**\n",
+ ". **and proper sort order**\n",
+ ". cap mata: mata drop clustOPG()\n",
+ "\n",
+ ". mata: \n",
+ "------------------------------------------------- mata (type end to exit) -----\n",
+ ": real matrix clustOPG(real matrix S,real matrix L){\n",
+ "> real scalar N\n",
+ "> real matrix Scent, Scentsum, OPGcent\n",
+ "> \n",
+ "> N=rows(S)\n",
+ "> Scent=S - J(N,1,1)*mean(S)\n",
+ "> Scentsum=L'*Scent\n",
+ "> OPGcent=Scentsum'*Scentsum\n",
+ "> return(OPGcent)\n",
+ "> }\n",
+ "\n",
+ ": end\n",
+ "-------------------------------------------------------------------------------\n",
+ "\n",
+ ". \n",
+ ". \n",
+ ". \n",
+ ". /* cap prog drop sim_probit\n",
+ "> prog def sim_probit, rclass\n",
+ "> syntax [, c(integer 5) m(integer 50) f(integer 0) r(integer 199)] */\n",
+ ". *Notes on syntax: \n",
+ ". *c - # of clusters\n",
+ ". *m - # of obs per cluster\n",
+ ". *f - controls whether to use mixture regressor\n",
+ ". *r - # of bootstrap reps per simulation\n",
+ ". \n",
+ ". local c 10\n",
+ "\n",
+ ". local m 100\n",
+ "\n",
+ ". local f 0\n",
+ "\n",
+ ". local r 1000\n",
+ "\n",
+ ". \n",
+ ". **Generate data**\n",
+ ". drop _all\n",
+ "\n",
+ ". set obs `c' //generate c clusters\n",
+ "Number of observations (_N) was 0, now 10.\n",
+ "\n",
+ ". \n",
+ ". gen x=invnorm(uniform()) //covariate\n",
+ "\n",
+ ". gen mix=uniform()>.1 //contamination probability\n",
+ "\n",
+ ". \n",
+ ". if `f'==1{\n",
+ ". gen D=(mix*invnorm(uniform()) + (1-mix)*(2+3*invnorm(uniform())))/4 /\n",
+ "> /regressor of interest\n",
+ ". }\n",
+ "\n",
+ ". else{\n",
+ ". gen D=(invnorm(uniform()))/4 //regressor of interest\n",
+ ". }\n",
+ "\n",
+ ". \n",
+ ". gen v=invnorm(uniform())\n",
+ "\n",
+ ". \n",
+ ". \n",
+ ". gen c=_n //generate cluster id\n",
+ "\n",
+ ". \n",
+ ". expand `m' //make m obs per cluster\n",
+ "(990 observations created)\n",
+ "\n",
+ ". \n",
+ ". replace x=(x+invnorm(uniform()))/4 //add some within cluster variation in x\n",
+ "(1,000 real changes made)\n",
+ "\n",
+ ". \n",
+ ". gen y=x+D+v/sqrt(2)+invnorm(uniform())/sqrt(2) //form outcome\n",
+ "\n",
+ ". replace y=y>0\n",
+ "(1,000 real changes made)\n",
+ "\n",
+ ". sort c\n",
+ "\n",
+ ". \n",
+ ". **Restricted**\n",
+ ". constraint 1 D=1\n",
+ "\n",
+ ". glm y x D, constraint(1) fam(bin) link(probit)\n",
+ "\n",
+ "Iteration 0: Log likelihood = -685.70942 \n",
+ "Iteration 1: Log likelihood = -685.44941 \n",
+ "Iteration 2: Log likelihood = -685.4494 \n",
+ "\n",
+ "Generalized linear models Number of obs = 1,000\n",
+ "Optimization : ML Residual df = 998\n",
+ " Scale parameter = 1\n",
+ "Deviance = 1370.898802 (1/df) Deviance = 1.373646\n",
+ "Pearson = 1097.020519 (1/df) Pearson = 1.099219\n",
+ "\n",
+ "Variance function: V(u) = u*(1-u) [Bernoulli]\n",
+ "Link function : g(u) = invnorm(u) [Probit]\n",
+ "\n",
+ " AIC = 1.374899\n",
+ "Log likelihood = -685.4494008 BIC = -5523.041\n",
+ "\n",
+ " ( 1) [y]D = 1\n",
+ "------------------------------------------------------------------------------\n",
+ " | OIM\n",
+ " y | Coefficient std. err. z P>|z| [95% conf. interval]\n",
+ "-------------+----------------------------------------------------------------\n",
+ " x | .9846371 .1474927 6.68 0.000 .6955568 1.273717\n",
+ " D | 1 (constrained)\n",
+ " _cons | .1066109 .040653 2.62 0.009 .0269325 .1862893\n",
+ "------------------------------------------------------------------------------\n",
+ "\n",
+ ". predict pr\n",
+ "(option mu assumed; predicted mean y)\n",
+ "\n",
+ ". predict xbr, xb\n",
+ "\n",
+ ". gen phir=normalden(xbr)\n",
+ "\n",
+ ". gen er=(y-pr)*phir/(pr*(1-pr)) //quasi-residual\n",
+ "\n",
+ ". gen wr=phir^2/(pr*(1-pr)) //weights for hessian\n",
+ "\n",
+ ". gen one=1\n",
+ "\n",
+ ". \n",
+ ". mat accum Hr=one x D [iw=wr], nocons //form hessian */\n",
+ "(obs=598.5860322)\n",
+ "\n",
+ ". \n",
+ ". \n",
+ ". **Unrestricted**\n",
+ ". probit y x D, cluster(c)\n",
+ "\n",
+ "Iteration 0: Log pseudolikelihood = -693.12918 \n",
+ "Iteration 1: Log pseudolikelihood = -665.3805 \n",
+ "Iteration 2: Log pseudolikelihood = -665.33497 \n",
+ "Iteration 3: Log pseudolikelihood = -665.33497 \n",
+ "\n",
+ "Probit regression Number of obs = 1,000\n",
+ " Wald chi2(2) = 7.41\n",
+ " Prob > chi2 = 0.0247\n",
+ "Log pseudolikelihood = -665.33497 Pseudo R2 = 0.0401\n",
+ "\n",
+ " (Std. err. adjusted for 10 clusters in c)\n",
+ "------------------------------------------------------------------------------\n",
+ " | Robust\n",
+ " y | Coefficient std. err. z P>|z| [95% conf. interval]\n",
+ "-------------+----------------------------------------------------------------\n",
+ " x | 1.057877 .3958503 2.67 0.008 .2820252 1.83373\n",
+ " D | .0942995 1.041973 0.09 0.928 -1.947931 2.13653\n",
+ " _cons | .0357662 .3079585 0.12 0.908 -.5678212 .6393537\n",
+ "------------------------------------------------------------------------------\n",
+ "\n",
+ ". global b=_b[D]\n",
+ "\n",
+ ". local stdof=sqrt((`m'*`c'-3)/(`m'*`c'-1)) //inverse stata DOF correction\n",
+ "\n",
+ ". global W=((_b[D]-1)/(_se[D]*`stdof'))^2 //save analytical Wald\n",
+ "\n",
+ ". \n",
+ ". predict pu\n",
+ "(option pr assumed; Pr(y))\n",
+ "\n",
+ ". predict xbu, xb\n",
+ "\n",
+ ". gen phiu=normalden(xbu)\n",
+ "\n",
+ ". gen eu=(y-pu)*phiu/(pu*(1-pu)) //quasi-residual\n",
+ "\n",
+ ". gen wu=phiu^2/(pu*(1-pu)) //weights for hessian\n",
+ "\n",
+ ". \n",
+ ". local dfk=(`c'/(`c'-1))\n",
+ "\n",
+ ". mata: L=I(`c')#J(`m',1,1) //matrix for summing across clusters\n",
+ "\n",
+ ". mata: X=st_data(.,(\"one\", \"x\", \"D\"))\n",
+ "\n",
+ ". mata: C=(0\\0\\1) //constraint matrix -- third coefficient is restricte\n",
+ "> d\n",
+ "\n",
+ ". \n",
+ ". mata: Hr=st_matrix(\"Hr\")\n",
+ "\n",
+ ". mata: Hrinv=invsym(Hr)\n",
+ "\n",
+ ". mata: Ar=C'*Hrinv //form A_n\n",
+ "\n",
+ ". \n",
+ ". \n",
+ ". **Prepare influence function wald\n",
+ ". mat accum Hu=one x D [iw=wu], nocons //form hessian\n",
+ "(obs=615.9930729)\n",
+ "\n",
+ ". mata: Hu=st_matrix(\"Hu\")\n",
+ "\n",
+ ". mata: Huinv=invsym(Hu)\n",
+ "\n",
+ ". mata: Au=C'*Huinv\n",
+ "\n",
+ ". mata: eu=st_data(.,\"eu\")\n",
+ "\n",
+ ". mata: OPGucent=clustOPG(X:*eu,L)\n",
+ "\n",
+ ". mata: Vinvu=invsym(Au*OPGucent*`dfk'*Au') //compute inverse of variance of in\n",
+ "> fluence function\n",
+ "\n",
+ ". \n",
+ ". \n",
+ ". save \"sim_sample.dta\", replace\n",
+ "file sim_sample.dta saved\n",
+ "\n",
+ ". \n",
+ ". mata: S = X:*eu\n",
+ "\n",
+ ". \n",
+ ". **Bootstrap**\n",
+ ". gen u=.\n",
+ "(1,000 missing values generated)\n",
+ "\n",
+ ". gen ystar=.\n",
+ "(1,000 missing values generated)\n",
+ "\n",
+ ". gen ernew=.\n",
+ "(1,000 missing values generated)\n",
+ "\n",
+ ". gen eunew=.\n",
+ "(1,000 missing values generated)\n",
+ "\n",
+ ". gen pos=.\n",
+ "(1,000 missing values generated)\n",
+ "\n",
+ ". \n",
+ ". \n",
+ ". mat Ts=J(1,6,.) //store bootstrap statistics\n",
+ "\n",
+ ". \n",
+ ". by c: replace u=uniform()\n",
+ "(1,000 real changes made)\n",
+ "\n",
+ ". by c: replace pos=u[1]<.5 //cluster level rademacher indicator\n",
+ "(1000 real changes made)\n",
+ "\n",
+ ". gen radem = 2*pos - 1\n",
+ "\n",
+ ". \n",
+ ". qui replace ernew=(2*pos-1)*er //weighted residual\n",
+ "\n",
+ ". qui replace eunew=(2*pos-1)*eu //weighted residual\n",
+ "\n",
+ ". \n",
+ ". *Score bootstrap LM -- Rademacher\n",
+ ". mata: er=st_data(.,\"ernew\")\n",
+ "\n",
+ ". mata: Sr=colsum(X:*er)\n",
+ "\n",
+ ". mata: OPGrcent=clustOPG(X:*er,L)\n",
+ "\n",
+ ". mata: lm=Sr*Ar'*invsym(Ar*(OPGrcent)*`dfk'*Ar')*Ar*Sr'\n",
+ "\n",
+ ". mata: st_numscalar(\"lm\",lm)\n",
+ "\n",
+ ". mat Ts[1,1]=lm //save bootstrap LM \n",
+ "\n",
+ ". \n",
+ ". \n",
+ ". *Score Wald -- Rademacher\n",
+ ". mata: eu=st_data(.,\"eunew\")\n",
+ "\n",
+ ". mata: Su=colsum(X:*eu)\n",
+ "\n",
+ ". mata: OPGucent=clustOPG(X:*eu,L)\n",
+ "\n",
+ ". mata: wald=Su*Au'*invsym(Au*OPGucent*`dfk'*Au')*Au*Su'\n",
+ "\n",
+ ". mata: st_numscalar(\"wald\",wald)\n",
+ "\n",
+ ". mat Ts[1,2]=wald //save bootstrap Wald\n",
+ "\n",
+ ". \n",
+ ". *Score bootstrap Wald combining restricted and unrestricted scores -- Rademac\n",
+ "> her\n",
+ ". mata: wald_r=Sr*Ar'*invsym(Au*OPGucent*`dfk'*Au')*Ar*Sr'\n",
+ "\n",
+ ". mata: st_numscalar(\"wald_r\",wald)\n",
+ "\n",
+ ". mat Ts[1,3]=wald //save bootstrap Wald\n",
+ "\n",
+ ". \n",
+ ". \n",
+ ". \n"
+ ]
+ }
+ ],
+ "source": [
+ "%%stata\n",
+ "\n",
+ "****************************************************\n",
+ "* Code for Table 2 of Kline and Santos (2011) *\n",
+ "* This file computes Analytical Wald and LM tests *\n",
+ "* along with Score bootstrapped tests. \t *\n",
+ "* Some other results not reported in the paper *\n",
+ "* are also included.\t\t\t\t *\t\n",
+ "****************************************************\n",
+ "\n",
+ "version 11.1\n",
+ "set seed 12345\n",
+ "\n",
+ "**mata subroutine to compute recentered clustered outer products**\n",
+ "**inputs a matrix S of scores and a cluster summing matrix L**\n",
+ "**outputs clustered outer product matrix**\n",
+ "**note this program assumes a balanced design**\n",
+ "**and proper sort order**\n",
+ "cap mata: mata drop clustOPG()\n",
+ "mata: \n",
+ "real matrix clustOPG(real matrix S,real matrix L){\n",
+ "real scalar N\n",
+ "real matrix Scent, Scentsum, OPGcent\n",
+ "\n",
+ "N=rows(S)\n",
+ "Scent=S - J(N,1,1)*mean(S)\n",
+ "Scentsum=L'*Scent\n",
+ "OPGcent=Scentsum'*Scentsum\n",
+ "return(OPGcent)\n",
+ "}\n",
+ "end\n",
+ "\n",
+ "\n",
+ "\n",
+ "/* cap prog drop sim_probit\n",
+ "prog def sim_probit, rclass\n",
+ "syntax [, c(integer 5) m(integer 50) f(integer 0) r(integer 199)] */\n",
+ "*Notes on syntax: \n",
+ "*c - # of clusters\n",
+ "*m - # of obs per cluster\n",
+ "*f - controls whether to use mixture regressor\n",
+ "*r - # of bootstrap reps per simulation\n",
+ "\n",
+ "local c 10\n",
+ "local m 100\n",
+ "local f 0\n",
+ "local r 1000\n",
+ "\n",
+ "**Generate data**\n",
+ "drop _all\n",
+ "set obs `c' //generate c clusters\n",
+ "\n",
+ "gen x=invnorm(uniform()) //covariate\n",
+ "gen mix=uniform()>.1 //contamination probability\n",
+ "\n",
+ "if `f'==1{\n",
+ "\tgen D=(mix*invnorm(uniform()) + (1-mix)*(2+3*invnorm(uniform())))/4 //regressor of interest\n",
+ "}\n",
+ "else{\n",
+ "\tgen D=(invnorm(uniform()))/4 //regressor of interest\n",
+ "}\n",
+ "\n",
+ "gen v=invnorm(uniform())\n",
+ "\n",
+ "\n",
+ "gen c=_n\t//generate cluster id\n",
+ "\n",
+ "expand `m'\t//make m obs per cluster\n",
+ "\n",
+ "replace x=(x+invnorm(uniform()))/4 //add some within cluster variation in x\n",
+ "\n",
+ "gen y=x+D+v/sqrt(2)+invnorm(uniform())/sqrt(2) //form outcome\n",
+ "replace y=y>0\n",
+ "sort c\n",
+ "\n",
+ "**Restricted**\n",
+ "constraint 1 D=1\n",
+ "glm y x D, constraint(1) fam(bin) link(probit)\n",
+ "predict pr\n",
+ "predict xbr, xb\n",
+ "gen phir=normalden(xbr)\n",
+ "gen er=(y-pr)*phir/(pr*(1-pr)) //quasi-residual\n",
+ "gen wr=phir^2/(pr*(1-pr)) //weights for hessian\n",
+ "gen one=1\n",
+ "\n",
+ "mat accum Hr=one x D [iw=wr], nocons //form hessian */\n",
+ "\n",
+ "\n",
+ "**Unrestricted**\n",
+ "probit y x D, cluster(c)\n",
+ "global b=_b[D]\n",
+ "local stdof=sqrt((`m'*`c'-3)/(`m'*`c'-1)) //inverse stata DOF correction\n",
+ "global W=((_b[D]-1)/(_se[D]*`stdof'))^2 //save analytical Wald\n",
+ "\n",
+ "predict pu\n",
+ "predict xbu, xb\n",
+ "gen phiu=normalden(xbu)\n",
+ "gen eu=(y-pu)*phiu/(pu*(1-pu)) //quasi-residual\n",
+ "gen wu=phiu^2/(pu*(1-pu)) //weights for hessian\n",
+ "\n",
+ "local dfk=(`c'/(`c'-1))\n",
+ "mata: L=I(`c')#J(`m',1,1) //matrix for summing across clusters\n",
+ "mata: X=st_data(.,(\"one\", \"x\", \"D\"))\n",
+ "mata: C=(0\\0\\1)\t\t//constraint matrix -- third coefficient is restricted\n",
+ "\n",
+ "mata: Hr=st_matrix(\"Hr\")\n",
+ "mata: Hrinv=invsym(Hr)\n",
+ "mata: Ar=C'*Hrinv\t\t//form A_n\n",
+ "\n",
+ "\n",
+ "**Prepare influence function wald\n",
+ "mat accum Hu=one x D [iw=wu], nocons //form hessian\n",
+ "mata: Hu=st_matrix(\"Hu\")\n",
+ "mata: Huinv=invsym(Hu)\n",
+ "mata: Au=C'*Huinv\n",
+ "mata: eu=st_data(.,\"eu\")\n",
+ "mata: OPGucent=clustOPG(X:*eu,L)\n",
+ "mata: Vinvu=invsym(Au*OPGucent*`dfk'*Au') //compute inverse of variance of influence function\n",
+ "\n",
+ "\n",
+ "save \"sim_sample.dta\", replace\n",
+ "\n",
+ "mata: S = X:*eu\n",
+ "\n",
+ "**Bootstrap**\n",
+ "gen u=.\n",
+ "gen ystar=.\n",
+ "gen ernew=.\n",
+ "gen eunew=.\n",
+ "gen pos=.\n",
+ "\n",
+ "\n",
+ "mat Ts=J(1,6,.) //store bootstrap statistics\n",
+ "\n",
+ "by c: replace u=uniform()\n",
+ "by c: replace pos=u[1]<.5 //cluster level rademacher indicator\n",
+ "gen radem = 2*pos - 1\n",
+ "\n",
+ "qui replace ernew=(2*pos-1)*er //weighted residual\n",
+ "qui replace eunew=(2*pos-1)*eu //weighted residual\n",
+ "\n",
+ "*Score bootstrap LM -- Rademacher\n",
+ "mata: er=st_data(.,\"ernew\")\n",
+ "mata: Sr=colsum(X:*er)\n",
+ "mata: OPGrcent=clustOPG(X:*er,L)\n",
+ "mata: lm=Sr*Ar'*invsym(Ar*(OPGrcent)*`dfk'*Ar')*Ar*Sr'\n",
+ "mata: st_numscalar(\"lm\",lm)\n",
+ "mat Ts[1,1]=lm\t //save bootstrap LM \n",
+ "\n",
+ "\n",
+ "*Score Wald -- Rademacher\n",
+ "mata: eu=st_data(.,\"eunew\")\n",
+ "mata: Su=colsum(X:*eu)\n",
+ "mata: OPGucent=clustOPG(X:*eu,L)\n",
+ "mata: wald=Su*Au'*invsym(Au*OPGucent*`dfk'*Au')*Au*Su'\n",
+ "mata: st_numscalar(\"wald\",wald)\n",
+ "mat Ts[1,2]=wald\t //save bootstrap Wald\n",
+ "\n",
+ "*Score bootstrap Wald combining restricted and unrestricted scores -- Rademacher\n",
+ "mata: wald_r=Sr*Ar'*invsym(Au*OPGucent*`dfk'*Au')*Ar*Sr'\n",
+ "mata: st_numscalar(\"wald_r\",wald)\n",
+ "mat Ts[1,3]=wald\t //save bootstrap Wald\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "N = 1000\n",
+ "rng = np.random.default_rng(seed=1)\n",
+ "num_X = 2\n",
+ "\n",
+ "\n",
+ "# generates fake probit data for testing (Not used in favor of data generated from paper)\n",
+ "def probit_data(N, num_X):\n",
+ " # generate X and error\n",
+ " epsilon = rng.normal(size=N)\n",
+ " X = sm.add_constant(rng.normal(size=(N, num_X)))\n",
+ " beta = np.arange(1,num_X+2)\n",
+ " \n",
+ " true_y = X @ beta + epsilon\n",
+ " \n",
+ " y_star = np.where(true_y >0, 1,0)\n",
+ " \n",
+ " return y_star, true_y, X\n",
+ "\n",
+ "y_star, true_y, X = probit_data(1000, 3)\n",
+ "\n",
+ "clusters = rng.choice(range(100), size=1000)\n",
+ "\n",
+ "df = stata.pdataframe_from_data()\n",
+ "\n",
+ "p = Probit.from_formula(\"y ~ x + D\", data=df)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Optimization terminated successfully.\n",
+ " Current function value: 0.665335\n",
+ " Iterations 4\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "Probit Regression Results\n",
+ "\n",
+ " Dep. Variable: | y | No. Observations: | 1000 | \n",
+ "
\n",
+ "\n",
+ " Model: | Probit | Df Residuals: | 997 | \n",
+ "
\n",
+ "\n",
+ " Method: | MLE | Df Model: | 2 | \n",
+ "
\n",
+ "\n",
+ " Date: | Thu, 06 Jun 2024 | Pseudo R-squ.: | 0.04010 | \n",
+ "
\n",
+ "\n",
+ " Time: | 19:23:11 | Log-Likelihood: | -665.33 | \n",
+ "
\n",
+ "\n",
+ " converged: | True | LL-Null: | -693.13 | \n",
+ "
\n",
+ "\n",
+ " Covariance Type: | cluster | LLR p-value: | 8.494e-13 | \n",
+ "
\n",
+ "
\n",
+ "\n",
+ "\n",
+ " | coef | std err | z | P>|z| | [0.025 | 0.975] | \n",
+ "
\n",
+ "\n",
+ " Intercept | 0.0358 | 0.308 | 0.116 | 0.908 | -0.568 | 0.640 | \n",
+ "
\n",
+ "\n",
+ " x | 1.0579 | 0.396 | 2.670 | 0.008 | 0.281 | 1.835 | \n",
+ "
\n",
+ "\n",
+ " D | 0.0943 | 1.043 | 0.090 | 0.928 | -1.950 | 2.139 | \n",
+ "
\n",
+ "
"
+ ],
+ "text/latex": [
+ "\\begin{center}\n",
+ "\\begin{tabular}{lclc}\n",
+ "\\toprule\n",
+ "\\textbf{Dep. Variable:} & y & \\textbf{ No. Observations: } & 1000 \\\\\n",
+ "\\textbf{Model:} & Probit & \\textbf{ Df Residuals: } & 997 \\\\\n",
+ "\\textbf{Method:} & MLE & \\textbf{ Df Model: } & 2 \\\\\n",
+ "\\textbf{Date:} & Thu, 06 Jun 2024 & \\textbf{ Pseudo R-squ.: } & 0.04010 \\\\\n",
+ "\\textbf{Time:} & 19:23:11 & \\textbf{ Log-Likelihood: } & -665.33 \\\\\n",
+ "\\textbf{converged:} & True & \\textbf{ LL-Null: } & -693.13 \\\\\n",
+ "\\textbf{Covariance Type:} & cluster & \\textbf{ LLR p-value: } & 8.494e-13 \\\\\n",
+ "\\bottomrule\n",
+ "\\end{tabular}\n",
+ "\\begin{tabular}{lcccccc}\n",
+ " & \\textbf{coef} & \\textbf{std err} & \\textbf{z} & \\textbf{P$> |$z$|$} & \\textbf{[0.025} & \\textbf{0.975]} \\\\\n",
+ "\\midrule\n",
+ "\\textbf{Intercept} & 0.0358 & 0.308 & 0.116 & 0.908 & -0.568 & 0.640 \\\\\n",
+ "\\textbf{x} & 1.0579 & 0.396 & 2.670 & 0.008 & 0.281 & 1.835 \\\\\n",
+ "\\textbf{D} & 0.0943 & 1.043 & 0.090 & 0.928 & -1.950 & 2.139 \\\\\n",
+ "\\bottomrule\n",
+ "\\end{tabular}\n",
+ "%\\caption{Probit Regression Results}\n",
+ "\\end{center}"
+ ],
+ "text/plain": [
+ "\n",
+ "\"\"\"\n",
+ " Probit Regression Results \n",
+ "==============================================================================\n",
+ "Dep. Variable: y No. Observations: 1000\n",
+ "Model: Probit Df Residuals: 997\n",
+ "Method: MLE Df Model: 2\n",
+ "Date: Thu, 06 Jun 2024 Pseudo R-squ.: 0.04010\n",
+ "Time: 19:23:11 Log-Likelihood: -665.33\n",
+ "converged: True LL-Null: -693.13\n",
+ "Covariance Type: cluster LLR p-value: 8.494e-13\n",
+ "==============================================================================\n",
+ " coef std err z P>|z| [0.025 0.975]\n",
+ "------------------------------------------------------------------------------\n",
+ "Intercept 0.0358 0.308 0.116 0.908 -0.568 0.640\n",
+ "x 1.0579 0.396 2.670 0.008 0.281 1.835\n",
+ "D 0.0943 1.043 0.090 0.928 -1.950 2.139\n",
+ "==============================================================================\n",
+ "\"\"\""
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "p.fit(cov_type='cluster', cov_kwds={'groups' : df['c']}).summary()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "L = np.array(sfi.Mata.get(\"L\"))\n",
+ "S = np.array(sfi.Mata.get(\"S\"))\n",
+ "OPGucent = np.array(sfi.Mata.get(\"OPGucent\"))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Optimization terminated successfully.\n",
+ " Current function value: 0.665335\n",
+ " Iterations 4\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "Intercept 0.856059\n",
+ "x 7.202267\n",
+ "D 0.662808\n",
+ "dtype: float64"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "p.fit().tvalues"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from typing import Union, TypeVar\n",
+ "import numpy.typing as npt\n",
+ "from statsmodels.base.model import Model\n",
+ "\n",
+ "ArrayLike = Union[npt.ArrayLike, pd.DataFrame, pd.Series]\n",
+ "\n",
+ "class IncorrectModelException(Exception):\n",
+ " pass\n",
+ "\n",
+ "class ScoreWildBootstrap:\n",
+ " \n",
+ " def __init__(self,\n",
+ " mod: Model, \n",
+ " clusters=Union[None, ArrayLike]) -> None:\n",
+ " \"\"\"Score wild bootstrap as in Kline and Santos (2012). Perturbs the score of using wild bootstrap weights and calculates wald statistics.\n",
+ " Optionally with restricted score or unrestricted scores.\n",
+ "\n",
+ " Args:\n",
+ " mod (Model): A statsmodels model that has a `score_obs` and `hessian` method.\n",
+ " clusters (ArrayLike|None, optional): A dataframe or array of shape Nx1 that gives the cluster membership of each of the N observations. Defaults to Union[None, ArrayLike].\n",
+ " \"\"\" \n",
+ " \n",
+ " if not hasattr(mod, \"score_obs\"):\n",
+ " raise IncorrectModelException(f\"{mod} does not implement `score_obs`.\")\n",
+ " if not hasattr(mod, \"hessian\"):\n",
+ " raise IncorrectModelException(\"{mod} does not implement `hessian`.\")\n",
+ " \n",
+ " self.mod = mod\n",
+ " \n",
+ " self.fitted_mod = self.mod.fit(cov_type='cluster', cov_kwds = {'groups' : clusters})\n",
+ " \n",
+ " self.beta_hat = self.fitted_mod.params\n",
+ " \n",
+ " if clusters is not None:\n",
+ " self.cluster_ids, self.cluster_obs = np.unique(clusters, return_counts=True)\n",
+ " self.clusters = clusters\n",
+ " self.num_clusters = len(self.cluster_ids)\n",
+ " else:\n",
+ " self.clusters = np.ones(self.mod.endog.shape[0])\n",
+ " self.cluster_ids = np.array([1])\n",
+ " self.num_clusters = 1\n",
+ " \n",
+ " def get_analytical_wald(self) -> pd.Series:\n",
+ " \"\"\"Calculates cluster-robust analytical wald test for each parameter.\n",
+ " \"\"\" \n",
+ " return self.fitted_mod.tvalues**2\n",
+ "\n",
+ " def wald_test(self, W: ArrayLike, restricted_residuals: bool =False, \n",
+ " R: ArrayLike =None, r: Union[None, int, float]=None) -> Union[ArrayLike, float]:\n",
+ " \"\"\"Calculates Wald statistic, given perturbation weights, scores and hessians from the model.\n",
+ "\n",
+ " Args:\n",
+ " W (ArrayLike): Weight vector used to perturb observation score\n",
+ " restricted_residuals (bool, optional): Whether to construct the test with the restricted score. Defaults to False.\n",
+ " \\begin{aligned}\n",
+ " & T_{n, 1}^{\\star s} \\equiv\\left(R H_n^{-1} S_n^{\\star}\\left(\\hat{\\beta}_u\\right)\\right)^{\\prime}\\left(R H_n^{-1} \\sum_n^{\\star s}\\left(\\hat{\\beta}_u\\right) H_n^{-1} R^{\\prime}\\right)^{-1}\\left(R H_n^{-1} S_n^{\\star}\\left(\\hat{\\beta}_u\\right)\\right) \\\\\n",
+ " & T_{n, 2}^{\\star s} \\equiv\\left(R H_n^{-1} S_n^{\\star}\\left(\\hat{\\beta}_r\\right)\\right)^{\\prime}\\left(R H_n^{-1} \\sum_n^{\\star s}\\left(\\hat{\\beta}_u\\right) H_n^{-1} R^{\\prime}\\right)^{-1}\\left(R H_n^{-1} S_n^{\\star}\\left(\\hat{\\beta}_r\\right)\\right)\n",
+ " \\end{aligned}\n",
+ " See Equations 50 and 51 from Kline and Santos (2012).\n",
+ " R (ArrayLike, optional): Restriction matrix for test. Defaults to None.\n",
+ " r (Union[None, int, float], optional): test restriction. Defaults to None.\n",
+ "\n",
+ " Returns:\n",
+ " Union[ArrayLike, float]: A Wald Statistic based on the perturbed weights.\n",
+ " \"\"\" \n",
+ " \n",
+ " \n",
+ " hessian_inv = -np.linalg.inv(self.mod.hessian(self.beta_hat))\n",
+ " \n",
+ " if restricted_residuals: # if we use the restricted score in the test\n",
+ " if r is None:\n",
+ " raise ValueError(\"Must specify coefficient constraint to restrict residuals\")\n",
+ " score_beta = self.beta_hat.copy()\n",
+ " score_beta[np.where(R==1)[0]] = 1 \n",
+ " else:\n",
+ " score_beta = self.beta_hat\n",
+ " \n",
+ " # make array of weights divided by cluster membership\n",
+ " W_Jn = np.select([self.clusters == c for c in self.cluster_ids], W.flatten())[:,np.newaxis]\n",
+ " # divide weights by observations per clusters \n",
+ " perturbed_score_obs = self.mod.score_obs(self.beta_hat) * W_Jn\n",
+ " \n",
+ " # multiply by cluster membership\n",
+ " L = pd.get_dummies(self.clusters).astype(int).values\n",
+ " \n",
+ " perturbed_score_obs_mean = (((L/(L.sum(axis=0)-1)).T @ perturbed_score_obs)).mean(axis=0)\n",
+ " \n",
+ " demeaned_perturbed_score_obs = perturbed_score_obs - perturbed_score_obs_mean \n",
+ " \n",
+ " demeaned_perturbed_score_obs_L = L.T @ demeaned_perturbed_score_obs\n",
+ " \n",
+ " perturbed_score = (self.mod.score_obs(score_beta) * W_Jn).sum(axis=0)\n",
+ " \n",
+ " # equation 50 from the paper\n",
+ " bread = R @ hessian_inv @ perturbed_score\n",
+ " \n",
+ " opg_centered = demeaned_perturbed_score_obs_L.T @ demeaned_perturbed_score_obs_L\n",
+ " \n",
+ " meat = R @ hessian_inv @ opg_centered @ hessian_inv @ R.T\n",
+ " \n",
+ " if isinstance(bread, np.ndarray):\n",
+ " meat_inv = np.linalg.inv(meat)\n",
+ " return bread.T @ meat_inv @ bread\n",
+ " else:\n",
+ " return (bread)**2/meat \n",
+ " \n",
+ " def bootstrap(self, R: ArrayLike, \n",
+ " W: Union[ArrayLike,None]=None, \n",
+ " r: Union[int, float, None]=None, \n",
+ " n_jobs: int = -1, \n",
+ " rng: Union[np.random.Generator, None] = None, \n",
+ " seed: Union[int, None] = None, \n",
+ " bootstrap_num: int =100, \n",
+ " restricted_residuals: bool=False, \n",
+ " full_enumeration: bool=False) -> ArrayLike:\n",
+ " \"\"\"Bootstraps Wald Statistics by generating weights and continuously perturbing the score\n",
+ "\n",
+ " Args:\n",
+ " R (ArrayLike, optional): Restriction Matrix. Defaults to None.\n",
+ " r (Union[int, float], optional): restriction. Defaults to None.\n",
+ " W (ArrayLike, optional): Weights vector. If not specified, Rademacher weights will be generated. Defaults to None.\n",
+ " n_jobs (int, optional): Number of jobs to dispath for parallel processing. Defaults to -1, or all cores.\n",
+ " rng (Union[np.random.Generator, None], optional): random generator that can be used for reproducibility. Defaults to None.\n",
+ " seed (Union[int, None], optional): seed for random generator. Defaults to None.\n",
+ " bootstrap_num (int, optional): number of bootstraps to perform. Defaults to 100.\n",
+ " restricted_residuals (bool, optional): Whether to use restricted score in constructing Wald. Defaults to False.\n",
+ " full_enumeration (bool, optional): Whether to generate the full set of random weight possibilities. Defaults to False.\n",
+ "\n",
+ " Returns:\n",
+ " ArrayLike: A `bootstrap_num` x 1 matrix.\n",
+ " \"\"\" \n",
+ " \n",
+ " def bootstrapper(restricted_residuals, R, r, full_enumeration, rng, W):\n",
+ " if rng is None:\n",
+ " rng=np.random.RandomState(seed)\n",
+ "\n",
+ " if W is None:\n",
+ " W = draw_weights('rademacher',\n",
+ " full_enumeration=full_enumeration,\n",
+ " N_G_bootcluster=self.num_clusters,\n",
+ " boot_iter=1,\n",
+ " rng=rng)[0]\n",
+ " \n",
+ " return self.wald_test(W = W, restricted_residuals=restricted_residuals, R=R, r=r)\n",
+ " \n",
+ " walds = Parallel(n_jobs=n_jobs, backend='loky')(delayed(bootstrapper)(restricted_residuals=restricted_residuals, \n",
+ " R=R, r=r, full_enumeration=full_enumeration, rng=rng, W=W) for b in range(bootstrap_num))\n",
+ " \n",
+ " return np.array(walds)\n",
+ " \n",
+ " \n",
+ " def get_wald_boot(self, n_jobs: int =-1, \n",
+ " bootstrap_num: int =100, \n",
+ " restricted_residuals: bool=False, \n",
+ " full_enumeration: bool=False, \n",
+ " rng: Union[np.random.Generator, None] =None, \n",
+ " seed: Union[int, None]=None,\n",
+ " W= Union[ArrayLike,None]) -> ArrayLike:\n",
+ " \"\"\"Generates Wald Statistics for each parameter separately.\n",
+ "\n",
+ " Args:\n",
+ " n_jobs (int): Number of jobs to dispath for parallel processing. Defaults to -1, or all cores.\n",
+ " bootstrap_num (int, optional): number of bootstraps to perform. Defaults to 100.\n",
+ " restricted_residuals (bool, optional): Whether to use restricted score in constructing Wald. Defaults to False.\n",
+ " full_enumeration (bool, optional): Whether to generate the full set of random weight possibilities. Defaults to False.\n",
+ " rng (Union[np.random.Generator, None], optional): random generator that can be used for reproducibility. Defaults to None.\n",
+ " seed (Union[int, None], optional): seed for random generator. Defaults to None.\n",
+ " W ( Union[ArrayLike,None], optional): Weights vector. If not specified, Rademacher weights will be generated. Defaults to None.\n",
+ "\n",
+ " Returns:\n",
+ " ArrayLike: A `bootstrap_num` x k matrix where k is the number of parameters.\n",
+ " \"\"\" \n",
+ " \n",
+ " # generate restriction vectors for each variable for beta=0\n",
+ " R = np.identity(self.beta_hat.shape[0])\n",
+ " \n",
+ " wald_boot = np.zeros(shape=(bootstrap_num, self.beta_hat.shape[0]))\n",
+ " \n",
+ " for i, R_vec in enumerate(R):\n",
+ " wald_boot[:, i] = self.bootstrap(R = R_vec, r = 0, n_jobs=n_jobs,\n",
+ " rng=rng, seed=seed, bootstrap_num=bootstrap_num,\n",
+ " restricted_residuals=restricted_residuals,\n",
+ " full_enumeration=full_enumeration,\n",
+ " W=None)\n",
+ " \n",
+ " return wald_boot\n",
+ " \n",
+ " \n",
+ " def get_pvalue(self, pval_type: str='two-tailed', **kwargs) -> ArrayLike:\n",
+ " \"\"\"Generates p-values by comparing analytical wald statistics with boostrapped, perturbed Wald statistics\n",
+ "\n",
+ " Args:\n",
+ " pval_type (str, optional): The type of p-value to generate. Defaults to 'two-tailed'.\n",
+ " kwargs can are used for specifying other options for `get_wald_boot`.\n",
+ "\n",
+ " Returns:\n",
+ " ArrayLike: An array of p-values for each parameter\n",
+ " \"\"\" \n",
+ " \n",
+ " wald_boot = self.get_wald_boot(**kwargs)\n",
+ " \n",
+ " analytical_wald = self.get_analytical_wald()\n",
+ " \n",
+ " if pval_type == \"two-tailed\":\n",
+ " pvalue = np.where(wald_boot > analytical_wald.values, 1, 0).mean(axis=0)\n",
+ " elif pval_type == \"equal-tailed\":\n",
+ " pl = np.where(analytical_wald.values < wald_boot, 1,0).mean(axis=0)\n",
+ " ph = np.where(analytical_wald.values > wald_boot, 1,0).mean(axis=0)\n",
+ " pvalue = 2 * min(pl, ph)\n",
+ " elif pval_type == \">\":\n",
+ " pvalue = np.where(analytical_wald.values < wald_boot,1,0).mean(axis=0)\n",
+ " else:\n",
+ " pvalue = np.where(analytical_wald.values > wald_boot,1,0).mean(axis=0)\n",
+ " \n",
+ " return pvalue\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Optimization terminated successfully.\n",
+ " Current function value: 0.665335\n",
+ " Iterations 4\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "array([0.909, 0.028, 0.931])"
+ ]
+ },
+ "execution_count": 17,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "sb = ScoreWildBootstrap(p, clusters=df['c'])\n",
+ "\n",
+ "ts = sb.get_pvalue(n_jobs=1, bootstrap_num=1000)\n",
+ "\n",
+ "ts"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def score_obs(self, params):\n",
+ " X= self.exog\n",
+ " y = self.endog\n",
+ " return (X.T*(y - X @ params)).T\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "sm.OLS.score_obs = score_obs"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# linear_mod = sm.OLS(true_y, np.column_stack([X, rng.normal(size=1000)]))\n",
+ "\n",
+ "linear_mod = sm.OLS.from_formula(\"y ~ x +D\", data=df)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "| param | statistic | p-value |\n",
+ "|:----------|------------:|----------:|\n",
+ "| Intercept | 32.953 | 0.000 |\n",
+ "| x | 7.820 | 0.000 |\n",
+ "| D | 0.632 | 0.526 |\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/lordflaron/Documents/wildboottest/.venv/lib/python3.9/site-packages/tabulate/__init__.py:107: FutureWarning: elementwise comparison failed; returning scalar instead, but in the future will perform elementwise comparison\n",
+ " (len(row) >= 1 and row[0] == SEPARATING_LINE)\n",
+ "/Users/lordflaron/Documents/wildboottest/.venv/lib/python3.9/site-packages/tabulate/__init__.py:108: FutureWarning: elementwise comparison failed; returning scalar instead, but in the future will perform elementwise comparison\n",
+ " or (len(row) >= 2 and row[1] == SEPARATING_LINE)\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " statistic | \n",
+ " p-value | \n",
+ "
\n",
+ " \n",
+ " param | \n",
+ " | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " Intercept | \n",
+ " [32.95279531027586] | \n",
+ " 0.000 | \n",
+ "
\n",
+ " \n",
+ " x | \n",
+ " [7.820060460734793] | \n",
+ " 0.000 | \n",
+ "
\n",
+ " \n",
+ " D | \n",
+ " [0.6320615764783465] | \n",
+ " 0.526 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " statistic p-value\n",
+ "param \n",
+ "Intercept [32.95279531027586] 0.000\n",
+ "x [7.820060460734793] 0.000\n",
+ "D [0.6320615764783465] 0.526"
+ ]
+ },
+ "execution_count": 21,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "wildboottest(linear_mod, B=1000, weights_type='rademacher',\n",
+ " impose_null=True, bootstrap_type='11')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([0. , 0.001, 0.868])"
+ ]
+ },
+ "execution_count": 22,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "sb = ScoreWildBootstrap(linear_mod, clusters=df['c'])\n",
+ "\n",
+ "ts = sb.get_pvalue(n_jobs=1, bootstrap_num=1000)\n",
+ "\n",
+ "ts"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": ".venv",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.9.18"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/tests/test_score.do b/tests/test_score.do
new file mode 100644
index 0000000..aec619f
--- /dev/null
+++ b/tests/test_score.do
@@ -0,0 +1,163 @@
+
+****************************************************
+* Code for Table 2 of Kline and Santos (2011) *
+* This file computes Analytical Wald and LM tests *
+* along with Score bootstrapped tests. *
+* Some other results not reported in the paper *
+* are also included. *
+****************************************************
+
+version 11.1
+set seed 12345
+
+**mata subroutine to compute recentered clustered outer products**
+**inputs a matrix S of scores and a cluster summing matrix L**
+**outputs clustered outer product matrix**
+**note this program assumes a balanced design**
+**and proper sort order**
+cap mata: mata drop clustOPG()
+mata:
+real matrix clustOPG(real matrix S,real matrix L){
+real scalar N
+real matrix Scent, Scentsum, OPGcent
+
+N=rows(S)
+Scent=S - J(N,1,1)*mean(S)
+Scentsum=L'*Scent
+OPGcent=Scentsum'*Scentsum
+return(OPGcent)
+}
+end
+
+
+
+/* cap prog drop sim_probit
+prog def sim_probit, rclass
+syntax [, c(integer 5) m(integer 50) f(integer 0) r(integer 199)] */
+*Notes on syntax:
+*c - # of clusters
+*m - # of obs per cluster
+*f - controls whether to use mixture regressor
+*r - # of bootstrap reps per simulation
+
+local c 10
+local m 100
+local f 0
+local r 1000
+
+**Generate data**
+drop _all
+set obs `c' //generate c clusters
+
+gen x=invnorm(uniform()) //covariate
+gen mix=uniform()>.1 //contamination probability
+
+if `f'==1{
+ gen D=(mix*invnorm(uniform()) + (1-mix)*(2+3*invnorm(uniform())))/4 //regressor of interest
+}
+else{
+ gen D=(invnorm(uniform()))/4 //regressor of interest
+}
+
+gen v=invnorm(uniform())
+
+
+gen c=_n //generate cluster id
+
+expand `m' //make m obs per cluster
+
+replace x=(x+invnorm(uniform()))/4 //add some within cluster variation in x
+
+gen y=x+D+v/sqrt(2)+invnorm(uniform())/sqrt(2) //form outcome
+replace y=y>0
+sort c
+
+**Restricted**
+constraint 1 D=1
+glm y x D, constraint(1) fam(bin) link(probit)
+predict pr
+predict xbr, xb
+gen phir=normalden(xbr)
+gen er=(y-pr)*phir/(pr*(1-pr)) //quasi-residual
+gen wr=phir^2/(pr*(1-pr)) //weights for hessian
+gen one=1
+
+mat accum Hr=one x D [iw=wr], nocons //form hessian */
+
+
+**Unrestricted**
+probit y x D, cluster(c)
+global b=_b[D]
+local stdof=sqrt((`m'*`c'-3)/(`m'*`c'-1)) //inverse stata DOF correction
+global W=((_b[D]-1)/(_se[D]*`stdof'))^2 //save analytical Wald
+
+predict pu
+predict xbu, xb
+gen phiu=normalden(xbu)
+gen eu=(y-pu)*phiu/(pu*(1-pu)) //quasi-residual
+gen wu=phiu^2/(pu*(1-pu)) //weights for hessian
+
+global dfk=(`c'/(`c'-1))
+mata: L=I(`c')#J(`m',1,1) //matrix for summing across clusters
+mata: X=st_data(.,("one", "x", "D"))
+mata: C=(0\0\1) //constraint matrix -- third coefficient is restricted
+
+mata: Hr=st_matrix("Hr")
+mata: Hrinv=invsym(Hr)
+mata: Ar=C'*Hrinv //form A_n
+
+
+**Prepare influence function wald
+mat accum Hu=one x D [iw=wu], nocons //form hessian
+mata: Hu=st_matrix("Hu")
+mata: Huinv=invsym(Hu)
+mata: Au=C'*Huinv
+mata: eu=st_data(.,"eu")
+mata: OPGucent=clustOPG(X:*eu,L)
+mata: Vinvu=invsym(Au*OPGucent*$dfk*Au') //compute inverse of variance of influence function
+
+
+save "sim_sample.dta", replace
+
+mata: S = X:*eu
+
+**Bootstrap**
+gen u=.
+gen ystar=.
+gen ernew=.
+gen eunew=.
+gen pos=.
+
+
+mat Ts=J(1,6,.) //store bootstrap statistics
+
+by c: replace u=uniform()
+by c: replace pos=u[1]<.5 //cluster level rademacher indicator
+gen radem = 2*pos - 1
+
+qui replace ernew=(2*pos-1)*er //weighted residual
+qui replace eunew=(2*pos-1)*eu //weighted residual
+
+*Score bootstrap LM -- Rademacher
+mata: er=st_data(.,"ernew")
+mata: Sr=colsum(X:*er)
+mata: OPGrcent=clustOPG(X:*er,L)
+mata: lm=Sr*Ar'*invsym(Ar*(OPGrcent)*$dfk*Ar')*Ar*Sr'
+mata: st_numscalar("lm",lm)
+mat Ts[1,1]=lm //save bootstrap LM
+
+
+*Score Wald -- Rademacher
+mata: eu=st_data(.,"eunew")
+mata: Su=colsum(X:*eu)
+mata: OPGucent=clustOPG(X:*eu,L)
+mata: wald=Su*Au'*invsym(Au*OPGucent*$dfk*Au')*Au*Su'
+mata: st_numscalar("wald",wald)
+mat Ts[1,2]=wald //save bootstrap Wald
+
+*Score bootstrap Wald combining restricted and unrestricted scores -- Rademacher
+mata: wald_r=Sr*Ar'*invsym(Au*OPGucent*$dfk*Au')*Ar*Sr'
+mata: st_numscalar("wald_r",wald)
+mat Ts[1,3]=wald //save bootstrap Wald
+
+