Repository: aeturrell/specification_curve (language: python)

Issue:
Switch plot test to save and delete instead of show

Ran github actions for MacOS and Windows, which hung on nox testing. Suspect this was due to figures not automatically being closed in testing suite. 

Need to switch to create fig, save fig, delete fig testing (rather than show plot).




Discussion / hints:

Fix patch:
diff --git a/.cookiecutter.json b/.cookiecutter.json
new file mode 100644
index 0000000..be59daf
--- /dev/null
+++ b/.cookiecutter.json
@@ -0,0 +1,12 @@
+{
+  "_template": "gh:cjolowicz/cookiecutter-hypermodern-python",
+  "author": "aeturrell",
+  "development_status": "Development Status :: 3 - Alpha",
+  "email": "mail@mail.com",
+  "friendly_name": "Specification_Curve",
+  "github_user": "aeturrell",
+  "license": "MIT",
+  "package_name": "specification_curve",
+  "project_name": "specification_curve",
+  "version": "0.2.6"
+}
diff --git a/.darglint b/.darglint
new file mode 100644
index 0000000..72ccc6c
--- /dev/null
+++ b/.darglint
@@ -0,0 +1,2 @@
+[darglint]
+strictness = long
diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index d4a2c44..0000000
--- a/.editorconfig
+++ /dev/null
@@ -1,21 +0,0 @@
-# http://editorconfig.org
-
-root = true
-
-[*]
-indent_style = space
-indent_size = 4
-trim_trailing_whitespace = true
-insert_final_newline = true
-charset = utf-8
-end_of_line = lf
-
-[*.bat]
-indent_style = tab
-end_of_line = crlf
-
-[LICENSE]
-insert_final_newline = false
-
-[Makefile]
-indent_style = tab
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..0f46431
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,9 @@
+[flake8]
+select = B,B9,C,D,DAR,E,F,N,RST,S,W
+ignore = E203,E501,RST201,RST203,RST301,W503
+max-line-length = 80
+max-complexity = 10
+docstring-convention = google
+per-file-ignores = tests/*:S101
+rst-roles = class,const,func,meth,mod,ref
+rst-directives = deprecated
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6313b56
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,1 @@
+* text=auto eol=lf
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 5d5d180..45ec3cd 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,6 +1,6 @@
-* Specification Curve version:
-* Python version:
-* Operating System:
+- Specification Curve version:
+- Python version:
+- Operating System:
 
 ### Description
 
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..a0a5c73
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,18 @@
+version: 2
+updates:
+  - package-ecosystem: github-actions
+    directory: "/"
+    schedule:
+      interval: daily
+  - package-ecosystem: pip
+    directory: "/.github/workflows"
+    schedule:
+      interval: daily
+  - package-ecosystem: pip
+    directory: "/docs"
+    schedule:
+      interval: daily
+  - package-ecosystem: pip
+    directory: "/"
+    schedule:
+      interval: daily
diff --git a/.github/labels.yml b/.github/labels.yml
new file mode 100644
index 0000000..f7f83aa
--- /dev/null
+++ b/.github/labels.yml
@@ -0,0 +1,66 @@
+---
+# Labels names are important as they are used by Release Drafter to decide
+# regarding where to record them in changelog or if to skip them.
+#
+# The repository labels will be automatically configured using this file and
+# the GitHub Action https://github.com/marketplace/actions/github-labeler.
+- name: breaking
+  description: Breaking Changes
+  color: bfd4f2
+- name: bug
+  description: Something isn't working
+  color: d73a4a
+- name: build
+  description: Build System and Dependencies
+  color: bfdadc
+- name: ci
+  description: Continuous Integration
+  color: 4a97d6
+- name: dependencies
+  description: Pull requests that update a dependency file
+  color: 0366d6
+- name: documentation
+  description: Improvements or additions to documentation
+  color: 0075ca
+- name: duplicate
+  description: This issue or pull request already exists
+  color: cfd3d7
+- name: enhancement
+  description: New feature or request
+  color: a2eeef
+- name: github_actions
+  description: Pull requests that update Github_actions code
+  color: "000000"
+- name: good first issue
+  description: Good for newcomers
+  color: 7057ff
+- name: help wanted
+  description: Extra attention is needed
+  color: 008672
+- name: invalid
+  description: This doesn't seem right
+  color: e4e669
+- name: performance
+  description: Performance
+  color: "016175"
+- name: python
+  description: Pull requests that update Python code
+  color: 2b67c6
+- name: question
+  description: Further information is requested
+  color: d876e3
+- name: refactoring
+  description: Refactoring
+  color: ef67c4
+- name: removal
+  description: Removals and Deprecations
+  color: 9ae7ea
+- name: style
+  description: Style
+  color: c120e5
+- name: testing
+  description: Testing
+  color: b1fc6f
+- name: wontfix
+  description: This will not be worked on
+  color: ffffff
diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
new file mode 100644
index 0000000..7a04410
--- /dev/null
+++ b/.github/release-drafter.yml
@@ -0,0 +1,29 @@
+categories:
+  - title: ":boom: Breaking Changes"
+    label: "breaking"
+  - title: ":rocket: Features"
+    label: "enhancement"
+  - title: ":fire: Removals and Deprecations"
+    label: "removal"
+  - title: ":beetle: Fixes"
+    label: "bug"
+  - title: ":racehorse: Performance"
+    label: "performance"
+  - title: ":rotating_light: Testing"
+    label: "testing"
+  - title: ":construction_worker: Continuous Integration"
+    label: "ci"
+  - title: ":books: Documentation"
+    label: "documentation"
+  - title: ":hammer: Refactoring"
+    label: "refactoring"
+  - title: ":lipstick: Style"
+    label: "style"
+  - title: ":package: Dependencies"
+    labels:
+      - "dependencies"
+      - "build"
+template: |
+  ## Changes
+
+  $CHANGES
diff --git a/.github/workflows/constraints.txt b/.github/workflows/constraints.txt
new file mode 100644
index 0000000..50b25e4
--- /dev/null
+++ b/.github/workflows/constraints.txt
@@ -0,0 +1,5 @@
+pip==22.3
+nox==2022.8.7
+nox-poetry==1.0.1
+poetry==1.2.2
+virtualenv==20.16.5
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
new file mode 100644
index 0000000..074c3a3
--- /dev/null
+++ b/.github/workflows/labeler.yml
@@ -0,0 +1,19 @@
+name: Labeler
+
+on:
+  push:
+    branches:
+      - main
+      - master
+
+jobs:
+  labeler:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check out the repository
+        uses: actions/checkout@v3
+
+      - name: Run Labeler
+        uses: crazy-max/ghaction-github-labeler@v4.1.0
+        with:
+          skip-delete: true
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..18a413f
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,73 @@
+name: Release
+
+on:
+  push:
+    branches:
+      - main
+      - master
+
+jobs:
+  release:
+    name: Release
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check out the repository
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 2
+
+      - name: Set up Python
+        uses: actions/setup-python@v4.3.0
+        with:
+          python-version: "3.9"
+
+      - name: Upgrade pip
+        run: |
+          pip install --constraint=.github/workflows/constraints.txt pip
+          pip --version
+      - name: Install Poetry
+        run: |
+          pip install --constraint=.github/workflows/constraints.txt poetry
+          poetry --version
+      - name: Check if there is a parent commit
+        id: check-parent-commit
+        run: |
+          echo "::set-output name=sha::$(git rev-parse --verify --quiet HEAD^)"
+      - name: Detect and tag new version
+        id: check-version
+        if: steps.check-parent-commit.outputs.sha
+        uses: salsify/action-detect-and-tag-new-version@v2.0.1
+        with:
+          version-command: |
+            bash -o pipefail -c "poetry version | awk '{ print \$2 }'"
+      - name: Bump version for developmental release
+        if: "! steps.check-version.outputs.tag"
+        run: |
+          poetry version patch &&
+          version=$(poetry version | awk '{ print $2 }') &&
+          poetry version $version.dev.$(date +%s)
+      - name: Build package
+        run: |
+          poetry build --ansi
+      - name: Publish package on PyPI
+        if: steps.check-version.outputs.tag
+        uses: pypa/gh-action-pypi-publish@v1.5.1
+        with:
+          user: __token__
+          password: ${{ secrets.PYPI_TOKEN }}
+
+      - name: Publish package on TestPyPI
+        if: "! steps.check-version.outputs.tag"
+        uses: pypa/gh-action-pypi-publish@v1.5.1
+        with:
+          user: __token__
+          password: ${{ secrets.TEST_PYPI_TOKEN }}
+          repository_url: https://test.pypi.org/legacy/
+
+      - name: Publish the release notes
+        uses: release-drafter/release-drafter@v5.21.1
+        with:
+          publish: ${{ steps.check-version.outputs.tag != '' }}
+          tag: ${{ steps.check-version.outputs.tag }}
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index c94d402..458d0ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -104,4 +104,4 @@ ENV/
 .mypy_cache/
 
 # IDE settings
-.vscode/
\ No newline at end of file
+.vscode/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..a86d957
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,62 @@
+repos:
+  - repo: https://github.com/psf/black
+    rev: 22.10.0
+    hooks:
+      - id: black
+        name: black
+        description: "applies black formatter to .py files"
+        entry: black
+        language: python
+        types: [python]
+  - repo: local
+    hooks:
+      - id: check-added-large-files
+        name: Check for added large files
+        entry: check-added-large-files
+        language: system
+      - id: check-toml
+        name: Check Toml
+        entry: check-toml
+        language: system
+        types: [toml]
+      - id: check-yaml
+        exclude: docs/
+        name: Check Yaml
+        entry: check-yaml
+        language: system
+        types: [yaml]
+      - id: end-of-file-fixer
+        exclude: docs/
+        name: Fix End of Files
+        entry: end-of-file-fixer
+        language: system
+        types: [text]
+        stages: [commit, push, manual]
+      - id: reorder-python-imports
+        exclude: docs/
+        name: Reorder python imports
+        entry: reorder-python-imports
+        language: system
+        types: [python]
+        args: [--application-directories=src]
+      - id: trailing-whitespace
+        exclude: docs/
+        name: Trim Trailing Whitespace
+        entry: trailing-whitespace-fixer
+        language: system
+        types: [text]
+        stages: [commit, push, manual]
+  - repo: https://github.com/pre-commit/mirrors-prettier
+    rev: v2.3.0
+    hooks:
+      - id: prettier
+        exclude: docs/
+  - repo: https://github.com/kynan/nbstripout
+    rev: 0.4.0
+    hooks:
+      - id: nbstripout
+        name: nbstripout
+        description: "nbstripout: strip output from Jupyter and IPython notebooks"
+        entry: nbstripout
+        language: python
+        types: [jupyter]
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index cb54b0e..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-language: python
-python:
-- 3.8
-- 3.7
-- 3.6
-install: pip install -U tox-travis
-script: tox
-deploy:
-  provider: pypi
-  distributions: sdist bdist_wheel
-  user: __token__
-  password:
-    secure: H3XjvbJKup/QCT72PtcfROSwJ7QQh+GHd2ogIIDjczLD/d4d6/EF+FPs8z2atV2ON3u8zdIki4HqfS1RzOf5saZ666Go32GHHid8ieJLGD3kEqlqdSpfRqvxW0GAwT3bvzcZtkCd6m19Sm7GWozHbWwyQyGpr31Fs0R0gWRgdJD2wThKFRFmdUlw+0ijuhLBmzPXmUsFRqDY8oMdadAYy6vzrnGlHyZ9rok2UkmkeA4qSMpTVeGuL845zoq/zXwOTKjVkKpk85hARgU/g0SKT1MRRtW8BGFnNEOPVetIkJDd95+VIkxJKciImGrGqQ4mZodWg53X8eR1HeV2aldxqBWWMB9GzsrmidC9GLRu62BtX44zabel3UiKDTWAlSIu4OB4BpLaFh6rfY+XsoZhij9fmBXo4JOx7+DIH4xS+kwDGm+RvvxqroU9s8MpIrJhyZmm9Rs47uufyy5x1lSx1aFxOsST0Jmo99IM0pY4moCrjfNLzDfWDbIaQ6+uY3rzI40G4o080ifBuspH538lG+dwqsoYOz/bNtKGL3uNXnd0qQEyat01xM7Fo1fdveviPQVAkv740PTMRK39Fnn7dCTmQqwV4O29DVvyMsjmcPCIWPOYU9lg/uVWoNW66jZubG03uEEYX0BsS/Of3m6z+HqX5k9vv1GQVLhZHLLT1Ak=
-  on:
-    tags: true
-    repo: aeturrell/specification_curve
-    python: 3.8
diff --git a/AUTHORS.rst b/AUTHORS.rst
deleted file mode 100644
index fbe8fa6..0000000
--- a/AUTHORS.rst
+++ /dev/null
@@ -1,13 +0,0 @@
-=======
-Credits
-=======
-
-Development Lead
-----------------
-
-* Arthur Turrell (http://www.aeturrell.com)
-
-Contributors
-------------
-
-None yet. Why not be the first?
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
deleted file mode 100644
index 95e27b9..0000000
--- a/CONTRIBUTING.rst
+++ /dev/null
@@ -1,128 +0,0 @@
-.. highlight:: shell
-
-============
-Contributing
-============
-
-Contributions are welcome, and they are greatly appreciated! Every little bit
-helps, and credit will always be given.
-
-You can contribute in many ways:
-
-Types of Contributions
-----------------------
-
-Report Bugs
-~~~~~~~~~~~
-
-Report bugs at https://github.com/aeturrell/specification_curve/issues.
-
-If you are reporting a bug, please include:
-
-* Your operating system name and version.
-* Any details about your local setup that might be helpful in troubleshooting.
-* Detailed steps to reproduce the bug.
-
-Fix Bugs
-~~~~~~~~
-
-Look through the GitHub issues for bugs. Anything tagged with "bug" and "help
-wanted" is open to whoever wants to implement it.
-
-Implement Features
-~~~~~~~~~~~~~~~~~~
-
-Look through the GitHub issues for features. Anything tagged with "enhancement"
-and "help wanted" is open to whoever wants to implement it.
-
-Write Documentation
-~~~~~~~~~~~~~~~~~~~
-
-Specification Curve could always use more documentation, whether as part of the
-official Specification Curve docs, in docstrings, or even on the web in blog posts,
-articles, and such.
-
-Submit Feedback
-~~~~~~~~~~~~~~~
-
-The best way to send feedback is to file an issue at https://github.com/aeturrell/specification_curve/issues.
-
-If you are proposing a feature:
-
-* Explain in detail how it would work.
-* Keep the scope as narrow as possible, to make it easier to implement.
-* Remember that this is a volunteer-driven project, and that contributions
-  are welcome :)
-
-Get Started!
-------------
-
-Ready to contribute? Here's how to set up `specification_curve` for local development.
-
-1. Fork the `specification_curve` repo on GitHub.
-2. Clone your fork locally::
-
-    $ git clone git@github.com:your_name_here/specification_curve.git
-
-3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development::
-
-    $ mkvirtualenv specification_curve
-    $ cd specification_curve/
-    $ python setup.py develop
-
-4. Create a branch for local development::
-
-    $ git checkout -b name-of-your-bugfix-or-feature
-
-   Now you can make your changes locally.
-
-5. When you're done making changes, check that your changes pass flake8 and the
-   tests, including testing other Python versions with tox::
-
-    $ flake8 specification_curve tests
-    $ python setup.py test or pytest
-    $ tox
-
-   To get flake8 and tox, just pip install them into your virtualenv.
-
-6. Commit your changes and push your branch to GitHub::
-
-    $ git add .
-    $ git commit -m "Your detailed description of your changes."
-    $ git push origin name-of-your-bugfix-or-feature
-
-7. Submit a pull request through the GitHub website.
-
-Pull Request Guidelines
------------------------
-
-Before you submit a pull request, check that it meets these guidelines:
-
-1. The pull request should include tests.
-2. If the pull request adds functionality, the docs should be updated. Put
-   your new functionality into a function with a docstring, and add the
-   feature to the list in README.rst.
-3. The pull request should work for Python 3.5, 3.6, 3.7 and 3.8, and for PyPy. Check
-   https://travis-ci.com/aeturrell/specification_curve/pull_requests
-   and make sure that the tests pass for all supported Python versions.
-
-Tips
-----
-
-To run a subset of tests::
-
-
-    $ python -m unittest tests.test_specification_curve
-
-Deploying
----------
-
-A reminder for the maintainers on how to deploy.
-Make sure all your changes are committed (including an entry in HISTORY.rst).
-Then run::
-
-$ bump2version patch # possible: major / minor / patch
-$ git push
-$ git push --tags
-
-Travis will then deploy to PyPI if tests pass.
diff --git a/HISTORY.rst b/HISTORY.rst
deleted file mode 100644
index b896b2a..0000000
--- a/HISTORY.rst
+++ /dev/null
@@ -1,51 +0,0 @@
-=======
-History
-=======
-
-0.2.6 (2021-05-05)
-------------------
-
-* Updates of some dev packages
-* Release on Zenodo to create DOI
-
-0.2.5 (2020-11-22)
-------------------
-
-* Added option to always include some regressors
-* Added option to label preferred specification in plots
-
-0.2.4 (2020-09-15)
-------------------
-
-* Further fix to pick up example data
-
-0.2.3 (2020-09-14)
-------------------
-
-* Fixed examples
-* Bug fix for including csv data in pypi distribution
-
-0.2.2 (2020-09-02)
-------------------
-
-* More badges in readme
-* Dropped support for python 3.5
-
-0.2.1 (2020-09-02)
-------------------
-
-* Switched to object oriented design
-* Now supports range of statsmodels estimators!
-* Example showing how to save plots to file in docs
-* Example showing where estimation results are stored in docs
-* Docs example of very large number of specifications
-
-0.1.1 (2020-08-01)
-------------------
-
-* Multiple independent, dependent, and control variables implemented as lists. Mutually exclusive control variables implemented. Expansions of categorical variables into mutually exclusive fixed effects implemented.
-
-0.1.0 (2020-07-27)
-------------------
-
-* First release on PyPI.
diff --git a/LICENSE b/LICENSE
index 24b6ebf..f52ebf2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
-
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 2fa23ce..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,12 +0,0 @@
-include AUTHORS.rst
-include CONTRIBUTING.rst
-include HISTORY.rst
-include LICENSE
-include README.rst
-include specification_curve/data/example_data.csv
-
-recursive-include tests *
-recursive-exclude * __pycache__
-recursive-exclude * *.py[co]
-
-recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif
diff --git a/Makefile b/Makefile
index eeca4bc..5ba4e2a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,85 +1,12 @@
-.PHONY: clean clean-test clean-pyc clean-build docs help
-.DEFAULT_GOAL := help
+# This makes the documentation and readme for specification_curve
 
-define BROWSER_PYSCRIPT
-import os, webbrowser, sys
+.PHONY: all clean site
 
-from urllib.request import pathname2url
+all: site
 
-webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
-endef
-export BROWSER_PYSCRIPT
+# Build the github pages site
+site:
+	poetry run jupyter-book build docs/
 
-define PRINT_HELP_PYSCRIPT
-import re, sys
-
-for line in sys.stdin:
-	match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
-	if match:
-		target, help = match.groups()
-		print("%-20s %s" % (target, help))
-endef
-export PRINT_HELP_PYSCRIPT
-
-BROWSER := python -c "$$BROWSER_PYSCRIPT"
-
-help:
-	@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
-
-clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
-
-clean-build: ## remove build artifacts
-	rm -fr build/
-	rm -fr dist/
-	rm -fr .eggs/
-	find . -name '*.egg-info' -exec rm -fr {} +
-	find . -name '*.egg' -exec rm -f {} +
-
-clean-pyc: ## remove Python file artifacts
-	find . -name '*.pyc' -exec rm -f {} +
-	find . -name '*.pyo' -exec rm -f {} +
-	find . -name '*~' -exec rm -f {} +
-	find . -name '__pycache__' -exec rm -fr {} +
-
-clean-test: ## remove test and coverage artifacts
-	rm -fr .tox/
-	rm -f .coverage
-	rm -fr htmlcov/
-	rm -fr .pytest_cache
-
-lint: ## check style with flake8
-	flake8 specification_curve tests
-
-test: ## run tests quickly with the default Python
-	python setup.py test
-
-test-all: ## run tests on every Python version with tox
-	tox
-
-coverage: ## check code coverage quickly with the default Python
-	coverage run --source specification_curve setup.py test
-	coverage report -m
-	coverage html
-	$(BROWSER) htmlcov/index.html
-
-docs: ## generate Sphinx HTML documentation, including API docs
-	rm -f docs/specification_curve.rst
-	rm -f docs/modules.rst
-	sphinx-apidoc -o docs/ specification_curve
-	$(MAKE) -C docs clean
-	$(MAKE) -C docs html
-	$(BROWSER) docs/_build/html/index.html
-
-servedocs: docs ## compile the docs watching for changes
-	watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D .
-
-release: dist ## package and upload a release
-	twine upload dist/*
-
-dist: clean ## builds source and wheel package
-	python setup.py sdist
-	python setup.py bdist_wheel
-	ls -l dist
-
-install: clean ## install the package to the active Python's site-packages
-	python setup.py install
+clean:
+	rm -rf docs/_build
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0abbe69
--- /dev/null
+++ b/README.md
@@ -0,0 +1,97 @@
+# Specification Curve
+
+Specification Curve is a Python package that performs specification curve analysis; it helps with the problem of the "garden of forking paths" (many defensible choices) when doing analysis by running many regressions and summarising the effects in an easy to digest chart.
+
+[![PyPI](https://img.shields.io/pypi/v/specification_curve.svg)](https://pypi.org/project/specification_curve/)
+[![Status](https://img.shields.io/pypi/status/specification_curve.svg)](https://pypi.org/project/specification_curve/)
+[![Python Version](https://img.shields.io/pypi/pyversions/specification_curve)](https://pypi.org/project/specification_curve)
+[![License](https://img.shields.io/pypi/l/specification_curve)](https://opensource.org/licenses/MIT)
+[![Tests](https://github.com/aeturrell/specification_curve/workflows/Tests/badge.svg)](https://github.com/aeturrell/specification_curve/actions?workflow=Tests)
+[![Codecov](https://codecov.io/gh/aeturrell/specification_curve/branch/main/graph/badge.svg)](https://codecov.io/gh/aeturrell/specification_curve)
+[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
+[![Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
+[![Google Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/gist/aeturrell/438fb066e4471312667268669cef2c11/specification_curve-examples.ipynb)
+[![DOI](https://zenodo.org/badge/282989537.svg)](https://zenodo.org/badge/latestdoi/282989537)
+[![Downloads](https://static.pepy.tech/badge/specification-curve)](https://pepy.tech/project/Specification_curve)
+
+[![Linux](https://svgshare.com/i/Zhy.svg)](https://svgshare.com/i/Zhy.svg)
+[![macOS](https://svgshare.com/i/ZjP.svg)](https://svgshare.com/i/ZjP.svg)
+[![Windows](https://svgshare.com/i/ZhY.svg)](https://svgshare.com/i/ZhY.svg)
+
+[![Source](https://img.shields.io/badge/source%20code-github-lightgrey?style=for-the-badge)](https://github.com/aeturrell/specification_curve)
+
+[Go to the full documentation for **Specification Curve**](https://aeturrell.github.io/specification_curve/).
+
+## Quickstart
+
+You can try out specification curve right now in [Google Colab](https://colab.research.google.com/gist/aeturrell/438fb066e4471312667268669cef2c11/specification_curve-examples.ipynb).
+
+Here's an example of using **Specification Curve**.
+
+```python
+# import specification curve
+import specification_curve as specy
+
+
+# Generate some fake data
+# ------------------------
+import numpy as np
+import pandas as pd
+# Set seed for random numbers
+seed_for_prng = 78557
+# prng=probabilistic random number generator
+prng = np.random.default_rng(seed_for_prng)
+n_samples = 400
+# Number of dimensions
+n_dim = 4
+c_rnd_vars = prng.random(size=(n_dim, n_samples))
+y_1 = (0.4*c_rnd_vars[0, :] -  # THIS IS THE TRUE VALUE OF THE COEFFICIENT
+       0.2*c_rnd_vars[1, :] +
+       0.3*prng.standard_normal(n_samples))
+# Next line causes y_2 ests to be much more noisy
+y_2 = y_1 - 0.5*np.abs(prng.standard_normal(n_samples))
+# Put data into dataframe
+df = pd.DataFrame([y_1, y_2], ['y1', 'y2']).T
+df["x_1"] = c_rnd_vars[0, :]
+df["c_1"] = c_rnd_vars[1, :]
+df["c_2"] = c_rnd_vars[2, :]
+df["c_3"] = c_rnd_vars[3, :]
+
+# Specification Curve Analysis
+#-----------------------------
+sc = specy.SpecificationCurve(
+    df,
+    y_endog=['y1', 'y2'],
+    x_exog="x_1",
+    controls=["c_1", "c_2", "c_3"])
+sc.fit()
+sc.plot()
+```
+
+Grey squares (black lines when there are many specifications) show whether a variable is included in a specification or not. Blue or red markers and error bars show whether the coefficient is positive and significant (at the 0.05 level) or red and significant, respectively.
+
+## Installation
+
+You can install **Specification Curve** via pip:
+
+```bash
+$ pip install specification-curve
+```
+
+To install the development version from git, use:
+
+```bash
+$ pip install git+https://github.com/aeturrell/specification_curve.git
+```
+
+## License
+
+Distributed under the terms of the [MIT license](https://opensource.org/licenses/MIT).
+
+## Credits
+
+The package is built with [poetry](https://python-poetry.org/), while the documentation is built with [Jupyter Book](https://jupyterbook.org). Tests are run with [nox](https://nox.thea.codes/en/stable/).
+
+## Similar Packages
+
+In RStats, there is [specr](https://github.com/masurp/specr) (which inspired many design choices in this package) and [spec_chart](https://github.com/ArielOrtizBobea/spec_chart). Some of the example data in this package is the same as in specr, but please note that results have not been cross-checked across packages.
diff --git a/README.rst b/README.rst
deleted file mode 100644
index 32c3547..0000000
--- a/README.rst
+++ /dev/null
@@ -1,237 +0,0 @@
-===================
-Specification Curve
-===================
-
-
-.. image:: https://img.shields.io/pypi/v/specification_curve.svg
-        :target: https://pypi.python.org/pypi/specification_curve
-
-.. image:: https://img.shields.io/travis/aeturrell/specification_curve.svg
-        :target: https://travis-ci.com/aeturrell/specification_curve
-
-.. image:: https://readthedocs.org/projects/specification-curve/badge/?version=latest
-        :target: https://specification-curve.readthedocs.io/en/latest/?badge=latest
-        :alt: Documentation Status
-
-.. image:: https://static.pepy.tech/badge/specification-curve
-        :target: https://pepy.tech/project/Specification_curve
-        :alt: Downloads
-
-.. image:: https://img.shields.io/pypi/pyversions/specification_curve.svg
-        :target: https://pypi.python.org/pypi/specification_curve/
-        :alt: Support Python versions
-
-.. image:: https://colab.research.google.com/assets/colab-badge.svg
-        :target: https://colab.research.google.com/gist/aeturrell/438fb066e4471312667268669cef2c11/specification_curve-examples.ipynb
-
-
-.. image:: https://zenodo.org/badge/282989537.svg
-        :target: https://zenodo.org/badge/latestdoi/282989537
-
-
-Specification Curve is a Python (3.6+) package that performs specification curve analysis.
-
-
-* Free software: MIT license
-* Documentation: https://specification-curve.readthedocs.io.
-
-You can cite Specification Curve as:
-    Arthur Turrell. aeturrell/specification_curve: Specification Curve 0.2.6: Biosphere Mansion (Version v0.2.6). Zenodo. http://doi.org/10.5281/zenodo.4739532, (2021, May 5).
-
-
-Quickstart
-----------
-
-You can run this package right now in `Google Colab`_
-
-.. _`Google Colab`: https://colab.research.google.com/gist/aeturrell/438fb066e4471312667268669cef2c11/specification_curve-examples.ipynb
-
-Running
-
-.. code-block:: python
-
-   from specification_curve import specification_curve as specy
-   from specification_curve import example as scdata
-   df = scdata.load_example_data1()
-   y_endog = 'y1'
-   x_exog = 'x1'
-   controls = ['c1', 'c2', 'group1', 'group2']
-   sc = specy.SpecificationCurve(df, y_endog, x_exog, controls,
-                                 cat_expand=['group2'])
-   sc.fit()
-   sc.plot()
-
-produces
-
-.. image:: https://raw.githubusercontent.com/aeturrell/specification_curve/master/docs/images/example.png
-   :width: 600
-
-Grey squares (black lines when there are many specifications) show whether
-a variable is included in a specification or not. Blue markers and error bars
-show whether the coefficient is significant (0.05).
-
-Here's another example:
-
-.. code-block:: python
-
-   from specification_curve import specification_curve as specy
-   import numpy as np
-   import pandas as pd
-   n_samples = 300
-   np.random.seed(1332)
-   x_1 = np.random.random(size=n_samples)
-   x_2 = np.random.random(size=n_samples)
-   x_3 = np.random.random(size=n_samples)
-   x_4 = np.random.randint(2, size=n_samples)
-   y = (0.8*x_1 + 0.1*x_2 + 0.5*x_3 + x_4*0.6 +
-        + 2*np.random.randn(n_samples))
-   df = pd.DataFrame([x_1, x_2, x_3, x_4, y],
-                     ['x_1', 'x_2', 'x_3', 'x_4', 'y']).T
-   # Set x_4 as a categorical variable
-   df['x_4'] = df['x_4'].astype('category')
-   sc = specy.SpecificationCurve(df, 'y', 'x_1', ['x_2', 'x_3', 'x_4'],
-                                 cat_expand=['x_4'])
-   sc.fit()
-   sc.plot()
-
-
-Features
---------
-
-These examples use the first set of **example data**:
-
-.. code-block:: python
-
-    from specification_curve import specification_curve as specy
-    from specification_curve import example as scdata
-    df = scdata.load_example_data1()
-
-* Expand fixed effects into mutually exclusive groups using ``cat_expand``
-
-.. code-block:: python
-
-    y_endog = 'y1'
-    x_exog = 'x1'
-    controls = ['c1', 'c2', 'group1', 'group2']
-    sc = specy.SpecificationCurve(df, y_endog, x_exog, controls,
-                                  cat_expand=['group1', 'group2'])
-    sc.fit()
-    sc.plot()
-
-* Mutually exclude two variables using ``exclu_grp``
-
-.. code-block:: python
-
-    y_endog = 'y1'
-    x_exog = 'x1'
-    controls = ['c1', 'c2', 'group1', 'group2']
-    sc = specy.SpecificationCurve(df, y_endog, x_exog, controls,
-                                      exclu_grps=[['c1', 'c2']])
-    sc.fit()
-    sc.plot()
-
-* Use multiple independent or dependent variables
-
-.. code-block:: python
-
-    x_exog = ['x1', 'x2']
-    y_endog = 'y1'
-    controls = ['c1', 'c2', 'group1', 'group2']
-    sc = specy.SpecificationCurve(df, y_endog, x_exog, controls)
-    sc.fit()
-    sc.plot()
-
-* Save plots to file (format is inferred from file extension)
-
-.. code-block:: python
-
-    sc = specy.SpecificationCurve(df, y_endog, x_exog, controls,
-                                      cat_expand=['group1'])
-    sc.fit()
-    sc.plot(save_path='test_fig.pdf')
-
-* Specification results stored in output DataFrame `df_r`
-
-.. code-block:: python
-
-    sc = specy.SpecificationCurve(df, y_endog, x_exog, controls)
-    sc.fit()
-    print(sc.df_r)
-
-* Other `statsmodels` estimators (OLS is the default) can be used
-
-.. code-block:: python
-
-    import numpy as np
-    import pandas as pd
-    import statsmodels.api as sm
-    n_samples = 1000
-    x_2 = np.random.randint(2, size=n_samples)
-    x_1 = np.random.random(size=n_samples)
-    x_3 = np.random.randint(3, size=n_samples)
-    x_4 = np.random.random(size=n_samples)
-    x_5 = x_1 + 0.05*np.random.randn(n_samples)
-    x_beta = -1 + 3.5*x_1 + 0.2*x_2 + 0.3*x_3
-    prob = 1/(1 + np.exp(-x_beta))
-    y = np.random.binomial(n=1, p=prob, size=n_samples)
-    y2 = np.random.binomial(n=1, p=prob*0.98, size=n_samples)
-    df = pd.DataFrame([x_1, x_2, x_3, x_4, x_5, y, y2],
-                      ['x_1', 'x_2', 'x_3', 'x_4', 'x_5', 'y', 'y2']).T
-    y_endog = ['y', 'y2']
-    x_exog = ['x_1', 'x_5']
-    controls = ['x_3', 'x_2', 'x_4']
-    sc = specy.SpecificationCurve(df, y_endog, x_exog, controls,
-                                  cat_expand='x_3')
-    sc.fit(estimator=sm.Logit)  # sm.Probit also works
-    sc.plot()
-
-* The style of specification plot flexes for very large numbers of specifications
-
-.. code-block:: python
-
-    n_samples = 400
-    # Number of dimensions of continuous
-    # random variables
-    n_dim = 8
-    c_rnd_vars = np.random.random(size=(n_dim, n_samples))
-    c_rnd_vars_names = [f'c_{i}' for i in range(np.shape(c_rnd_vars)[0])]
-    y_1 = (0.3*c_rnd_vars[0, :] +
-           0.5*c_rnd_vars[1, :])
-    y_2 = y_1 + 0.05*np.random.randn(n_samples)
-    df = pd.DataFrame([y_1, y_2], ['y1', 'y2']).T
-    for i, col_name in enumerate(c_rnd_vars_names):
-        df[col_name] = c_rnd_vars[i, :]
-    controls = c_rnd_vars_names[1:]
-    sc = specy.SpecificationCurve(df, ['y1', 'y2'], c_rnd_vars_names[0],
-                                  controls)
-    sc.fit()
-    sc.plot()
-
-* Always include regressors using the `always_include` keyword argument
-
-.. code-block:: python
-
-    df = scdata.load_example_data1()
-    x_exog = 'x1'
-    y_endog = 'y1'
-    controls = ['c2', 'group1', 'group2']
-    sc = specy.SpecificationCurve(df, y_endog, x_exog, controls,
-                                    always_include='c1')
-    sc.fit()
-    sc.plot()
-
-Similar Packages
-----------------
-
-In RStats, there is specr_ (which inspired many design choices in this package) and spec_chart_. Some of the example data in this package is the same as in specr_.
-
-.. _specr: https://github.com/masurp/specr
-.. _spec_chart: https://github.com/ArielOrtizBobea/spec_chart
-
-Credits
--------
-
-This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template.
-
-.. _Cookiecutter: https://github.com/audreyr/cookiecutter
-.. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..9ac2650
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,9 @@
+comment: false
+coverage:
+  status:
+    project:
+      default:
+        target: "100"
+    patch:
+      default:
+        target: "100"
diff --git a/docs/Makefile b/docs/Makefile
deleted file mode 100644
index add43d7..0000000
--- a/docs/Makefile
+++ /dev/null
@@ -1,20 +0,0 @@
-# Minimal makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line.
-SPHINXOPTS    =
-SPHINXBUILD   = python -msphinx
-SPHINXPROJ    = specification_curve
-SOURCEDIR     = .
-BUILDDIR      = _build
-
-# Put it first so that "make" without argument is like "make help".
-help:
-	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-
-.PHONY: help Makefile
-
-# Catch-all target: route all unknown targets to Sphinx using the new
-# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
-%: Makefile
-	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/_config.yml b/docs/_config.yml
new file mode 100644
index 0000000..102de3d
--- /dev/null
+++ b/docs/_config.yml
@@ -0,0 +1,46 @@
+# Book settings
+# Learn more at https://jupyterbook.org/customize/config.html
+
+title: Specification Curve
+author: Arthur Turrell
+# logo: logo.png
+
+# Force re-execution of notebooks on each build.
+# See https://jupyterbook.org/content/execute.html
+execute:
+  execute_notebooks: force
+
+# Define the name of the latex output file for PDF builds
+latex:
+  latex_documents:
+    targetname: book.tex
+
+# Add a bibtex file so that we can create citations
+bibtex_bibfiles:
+  - references.bib
+
+# Information about where the book exists on the web
+repository:
+  url: https://github.com/aeturrell/specification_curve  # Online location of your book
+  path_to_book: docs  # Optional path to your book, relative to the repository root
+  branch: master  # Which branch of the repository should be used when creating links (optional)
+
+launch_buttons:
+  colab_url                 : "https://colab.research.google.com" # The URL of Google Colab
+
+
+# Add GitHub buttons to your book
+# See https://jupyterbook.org/customize/config.html#add-a-link-to-your-repository
+html:
+  use_issues_button: true
+  use_repository_button: true
+  extra_navbar: ""
+  home_page_in_navbar: false  # Whether to include your home page in the left Navigation Bar
+  baseurl: "https://aeturrell.github.io/specification_curve"
+  # favicon: "favicon.ico"
+
+sphinx:
+  extra_extensions:
+  - 'sphinx.ext.autodoc'
+  - 'sphinx.ext.napoleon' # for google-style docstrings
+  - 'sphinx.ext.viewcode' # links to the code
\ No newline at end of file
diff --git a/docs/_toc.yml b/docs/_toc.yml
new file mode 100644
index 0000000..9698d97
--- /dev/null
+++ b/docs/_toc.yml
@@ -0,0 +1,10 @@
+# Table of contents
+# Learn more at https://jupyterbook.org/customize/toc.html
+
+format: jb-book
+root: index
+chapters:
+- file: features
+- file: contributing
+- file: history
+- file: api.rst
\ No newline at end of file
diff --git a/docs/api.rst b/docs/api.rst
new file mode 100644
index 0000000..fcb37ff
--- /dev/null
+++ b/docs/api.rst
@@ -0,0 +1,9 @@
+API Reference
+=============
+
+.. autoclass:: specification_curve::SpecificationCurve
+    :members:
+
+.. autofunction:: specification_curve.load_example_data1
+
+.. autofunction:: specification_curve.load_example_data2
\ No newline at end of file
diff --git a/docs/authors.rst b/docs/authors.rst
deleted file mode 100644
index e122f91..0000000
--- a/docs/authors.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-.. include:: ../AUTHORS.rst
diff --git a/docs/conf.py b/docs/conf.py
deleted file mode 100755
index 030a141..0000000
--- a/docs/conf.py
+++ /dev/null
@@ -1,162 +0,0 @@
-#!/usr/bin/env python
-#
-# specification_curve documentation build configuration file, created by
-# sphinx-quickstart on Fri Jun  9 13:47:02 2017.
-#
-# This file is execfile()d with the current directory set to its
-# containing dir.
-#
-# Note that not all possible configuration values are present in this
-# autogenerated file.
-#
-# All configuration values have a default; values that are commented out
-# serve to show the default.
-
-# If extensions (or modules to document with autodoc) are in another
-# directory, add these directories to sys.path here. If the directory is
-# relative to the documentation root, use os.path.abspath to make it
-# absolute, like shown here.
-#
-import os
-import sys
-sys.path.insert(0, os.path.abspath('..'))
-
-import specification_curve
-
-# -- General configuration ---------------------------------------------
-
-# If your documentation needs a minimal Sphinx version, state it here.
-#
-# needs_sphinx = '1.0'
-
-# Add any Sphinx extension module names here, as strings. They can be
-# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
-
-# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
-
-# The suffix(es) of source filenames.
-# You can specify multiple suffix as a list of string:
-#
-# source_suffix = ['.rst', '.md']
-source_suffix = '.rst'
-
-# The master toctree document.
-master_doc = 'index'
-
-# General information about the project.
-project = 'Specification Curve'
-copyright = "2020, Arthur Turrell"
-author = "Arthur Turrell"
-
-# The version info for the project you're documenting, acts as replacement
-# for |version| and |release|, also used in various other places throughout
-# the built documents.
-#
-# The short X.Y version.
-version = specification_curve.__version__
-# The full version, including alpha/beta/rc tags.
-release = specification_curve.__version__
-
-# The language for content autogenerated by Sphinx. Refer to documentation
-# for a list of supported languages.
-#
-# This is also used if you do content translation via gettext catalogs.
-# Usually you set "language" from the command line for these cases.
-language = None
-
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-# This patterns also effect to html_static_path and html_extra_path
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
-
-# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
-
-# If true, `todo` and `todoList` produce output, else they produce nothing.
-todo_include_todos = False
-
-
-# -- Options for HTML output -------------------------------------------
-
-# The theme to use for HTML and HTML Help pages.  See the documentation for
-# a list of builtin themes.
-#
-html_theme = 'alabaster'
-
-# Theme options are theme-specific and customize the look and feel of a
-# theme further.  For a list of options available for each theme, see the
-# documentation.
-#
-# html_theme_options = {}
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
-
-
-# -- Options for HTMLHelp output ---------------------------------------
-
-# Output file base name for HTML help builder.
-htmlhelp_basename = 'specification_curvedoc'
-
-
-# -- Options for LaTeX output ------------------------------------------
-
-latex_elements = {
-    # The paper size ('letterpaper' or 'a4paper').
-    #
-    # 'papersize': 'letterpaper',
-
-    # The font size ('10pt', '11pt' or '12pt').
-    #
-    # 'pointsize': '10pt',
-
-    # Additional stuff for the LaTeX preamble.
-    #
-    # 'preamble': '',
-
-    # Latex figure (float) alignment
-    #
-    # 'figure_align': 'htbp',
-}
-
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title, author, documentclass
-# [howto, manual, or own class]).
-latex_documents = [
-    (master_doc, 'specification_curve.tex',
-     'Specification Curve Documentation',
-     'Arthur Turrell', 'manual'),
-]
-
-
-# -- Options for manual page output ------------------------------------
-
-# One entry per manual page. List of tuples
-# (source start file, name, description, authors, manual section).
-man_pages = [
-    (master_doc, 'specification_curve',
-     'Specification Curve Documentation',
-     [author], 1)
-]
-
-
-# -- Options for Texinfo output ----------------------------------------
-
-# Grouping the document tree into Texinfo files. List of tuples
-# (source start file, target name, title, author,
-#  dir menu entry, description, category)
-texinfo_documents = [
-    (master_doc, 'specification_curve',
-     'Specification Curve Documentation',
-     author,
-     'specification_curve',
-     'One line description of project.',
-     'Miscellaneous'),
-]
-
-
-
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 0000000..d41fd5b
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1,135 @@
+# Contributing
+
+Thank you for your interest in improving this project. This project is
+open-source under the [MIT license](https://opensource.org/licenses/MIT)
+and welcomes contributions in the form of bug reports, feature requests,
+and pull requests.
+
+Here is a list of important resources for contributors:
+
+- [Source Code](https://github.com/aeturrell/specification_curve)
+- [Documentation](https://aeturrell.github.io/specification_curve/)
+- [Issue Tracker](https://github.com/aeturrell/specification_curve/issues)
+
+## How to report a bug
+
+Report bugs on the [Issue Tracker](https://github.com/aeturrell/specification_curve/issues).
+
+When filing an issue, make sure to answer these questions:
+
+-   Which operating system and Python version are you using?
+-   Which version of this project are you using?
+-   What did you do?
+-   What did you expect to see?
+-   What did you see instead?
+
+The best way to get your bug fixed is to provide a test case, and/or
+steps to reproduce the issue.
+
+## How to request a feature
+
+Request features on the [Issue
+Tracker](https://github.com/aeturrell/specification_curve/issues).
+
+## How to set up your development environment
+
+You need Python 3.8+ and the following tools:
+
+-   [Poetry](https://python-poetry.org/)
+-   [Nox](https://nox.thea.codes/)
+-   [nox-poetry](https://nox-poetry.readthedocs.io/)
+-   [Make](https://www.gnu.org/software/make/)
+
+
+Install the package with development requirements:
+
+```bash
+$ poetry install
+```
+
+You can now run an interactive Python session, or the command-line
+interface:
+
+```bash
+$ poetry run python
+$ poetry run specification_curve
+```
+
+## How to test the project
+
+Run the full test suite:
+
+```bash
+$ nox
+```
+
+List the available Nox sessions:
+
+```bash
+$ nox --list-sessions
+```
+
+You can also run a specific Nox session. For example, invoke the unit
+test suite like this:
+
+```bash
+$ nox --session=tests
+```
+
+Unit tests are located in the `tests` directory, and are written using
+the [pytest](https://pytest.readthedocs.io/) testing framework.
+
+## How to submit changes
+
+Open a [pull request](https://github.com/aeturrell/specification_curve/pulls) to
+submit changes to this project.
+
+Your pull request needs to meet the following guidelines for acceptance:
+
+-   The Nox test suite must pass without errors and warnings.
+-   Include unit tests. This project maintains 100% code coverage.
+-   If your changes add functionality, update the documentation
+    accordingly.
+
+Feel free to submit early, though---we can always iterate on this.
+
+To run linting and code formatting checks before committing your change,
+you can install pre-commit as a Git hook by running the following
+command:
+
+```bash
+$ nox --session=pre-commit -- install
+```
+
+It is recommended to open an issue before starting work on anything.
+This will allow a chance to talk it over with the owners and validate
+your approach.
+
+## How to build the documentation
+
+- Run `make clean`
+- Run `make`
+
+To upload the documentation, it’s
+
+```bash
+poetry run ghp-import -n -p -f docs/_build/html
+```
+
+## How to create a package release
+
+- Open a new branch with the version name
+
+- Change the version in pyproject.toml
+
+- Commit the change with a new version label as the commit message (checking the tests pass)
+
+- Head to github and merge into main
+
+- Draft a new release based on that most recent merge commit, using the new version as the tag
+
+- Run `poetry build` to produce the distributable files
+
+- Confirm the release draft on gitub
+
+- The automatic release github action will push to PyPI.
\ No newline at end of file
diff --git a/docs/contributing.rst b/docs/contributing.rst
deleted file mode 100644
index e582053..0000000
--- a/docs/contributing.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-.. include:: ../CONTRIBUTING.rst
diff --git a/docs/features.ipynb b/docs/features.ipynb
new file mode 100644
index 0000000..ad1f861
--- /dev/null
+++ b/docs/features.ipynb
@@ -0,0 +1,368 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Features\n",
+    "\n",
+    "Let's see some of **Specification Curve**'s features in action."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "tags": [
+     "remove-input"
+    ]
+   },
+   "outputs": [],
+   "source": [
+    "import matplotlib_inline.backend_inline\n",
+    "matplotlib_inline.backend_inline.set_matplotlib_formats(\"svg\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Basic Use"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Here's an example of using **Specification Curve**. Note that, in the below, we can pass strings or lists of string into the arguments of the class `SpecificationCurve`. The programme then automatically performs all of possible regressions of endogeneous variables on exogeneous variables and controls. The estimate that is picked out is the coefficient on the given combination of endogeneous and exogenous variables (with conditioning on the given controls).\n",
+    "\n",
+    "If a control variable is categorical, rather than continuous, it will be treated as a fixed effect."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import specification_curve as specy\n",
+    "\n",
+    "df = specy.load_example_data1()\n",
+    "y_endog = 'y1'  # endogeneous variable\n",
+    "x_exog = 'x1'  # exogeneous variable\n",
+    "controls = ['c1', 'c2', 'group1', 'group2']\n",
+    "sc = specy.SpecificationCurve(\n",
+    "    df,\n",
+    "    y_endog,\n",
+    "    x_exog,\n",
+    "    controls,\n",
+    "    )\n",
+    "sc.fit()\n",
+    "sc.plot()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Grey squares (black lines when there are many specifications) show whether a variable is included in a specification or not. Blue or red markers and error bars show whether the coefficient is positive and significant (at the 0.05 level) or red and significant, respectively.\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Retrieving estimates\n",
+    "\n",
+    "You can retrieve the estimates from the data frame:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "sc = specy.SpecificationCurve(df, y_endog, x_exog, controls)\n",
+    "sc.fit()\n",
+    "sc.df_r.head()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Saving results to file\n",
+    "\n",
+    "Save the plot to file (the format is inferred from file extension):\n",
+    "\n",
+    "```python\n",
+    "sc = specy.SpecificationCurve(df, y_endog, x_exog, controls,\n",
+    "                                  cat_expand=['group1'])\n",
+    "sc.fit()\n",
+    "sc.plot(save_path='test_fig.pdf')\n",
+    "```"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Expanding a categorical variable\n",
+    "\n",
+    "Should you need to, you can expand a categorical variable into its different elements and run those separately. In the example below, the `\"group2\"` categorical variable is expanded like this."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "y_endog = 'y1'  # endogeneous variable\n",
+    "x_exog = 'x1'  # exogeneous variable\n",
+    "controls = ['c1', 'c2', 'group1', 'group2']\n",
+    "sc = specy.SpecificationCurve(\n",
+    "    df,\n",
+    "    y_endog,\n",
+    "    x_exog,\n",
+    "    controls,\n",
+    "    cat_expand=['group2']  # have each fixed effect run separately\n",
+    "    )\n",
+    "sc.fit()\n",
+    "sc.plot()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Using multiple exogeneous variables\n",
+    "\n",
+    "Sometimes, you'd like to check different independent variables (and the coefficients they come with following a regression). This is achieved by passing a list to the exogeneous argument of `SpecificationCurve`. These variations on the independent variables are labelled by `x` in the plot."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "df = specy.load_example_data1()\n",
+    "x_exog = ['x1', 'x2']\n",
+    "y_endog = 'y1'\n",
+    "controls = ['c1', 'c2', 'group1', 'group2']\n",
+    "sc = specy.SpecificationCurve(df, y_endog, x_exog, controls)\n",
+    "sc.fit()\n",
+    "sc.plot()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Excluding some combinations of controls\n",
+    "\n",
+    "Some controls may be redundant, and you might want to exclude them both being used together. The `exclu_grps` keyword argument achieves this.\n",
+    "\n",
+    "In the below example, `\"c1\"` and `\"c2\"` are never run in the same specification."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "df = specy.load_example_data1()\n",
+    "\n",
+    "y_endog = 'y1'\n",
+    "x_exog = 'x1'\n",
+    "controls = ['c1', 'c2', 'group1', 'group2']\n",
+    "sc = specy.SpecificationCurve(df, y_endog, x_exog, controls,\n",
+    "                                  exclu_grps=[['c1', 'c2']])\n",
+    "sc.fit()\n",
+    "sc.plot()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Always include some controls in all specifications\n",
+    "\n",
+    "Likewise, there will be times when you always wish to include a particular control in specifications, and to show this on the plot. The `always_include=` keyword argument helps you to achieve this.\n",
+    "\n",
+    "In the example below, we ask that `\"c1\"` is included in every specification."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "df = specy.load_example_data1()\n",
+    "x_exog = 'x1'\n",
+    "y_endog = 'y1'\n",
+    "controls = ['c2', 'group1', 'group2']\n",
+    "sc = specy.SpecificationCurve(df, y_endog, x_exog, controls,\n",
+    "                                always_include='c1')\n",
+    "sc.fit()\n",
+    "sc.plot()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Flexing the style for very large numbers of specifications\n",
+    "\n",
+    "The default plot type isn't suitable for very large numbers of specifications, but it does automatically switch to a style suited to a large number of specifications.\n",
+    "\n",
+    "Here's an example"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "import pandas as pd\n",
+    "\n",
+    "# Set seed for random numbers\n",
+    "seed_for_prng = 78557\n",
+    "# prng=probabilistic random number generator\n",
+    "prng = np.random.default_rng(seed_for_prng)  "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Generate some fake data\n",
+    "\n",
+    "n_samples = 400\n",
+    "# Number of dimensions of continuous\n",
+    "n_dim = 8\n",
+    "c_rnd_vars = prng.random(size=(n_dim, n_samples))\n",
+    "c_rnd_vars_names = [f'c_{i}' for i in range(np.shape(c_rnd_vars)[0])]\n",
+    "y_1 = (0.4*c_rnd_vars[0, :] -  # This is the true value of the coefficient\n",
+    "       0.2*c_rnd_vars[1, :] +\n",
+    "       0.3*prng.standard_normal(n_samples))\n",
+    "# Next line causes y_2 ests to be much more noisy\n",
+    "y_2 = y_1 - 0.3*np.abs(prng.standard_normal(n_samples))\n",
+    "df = pd.DataFrame([y_1, y_2], ['y1', 'y2']).T\n",
+    "for i, col_name in enumerate(c_rnd_vars_names):\n",
+    "    df[col_name] = c_rnd_vars[i, :]\n",
+    "\n",
+    "controls = c_rnd_vars_names[1:]\n",
+    "\n",
+    "# Run it with Specification Curve\n",
+    "sc = specy.SpecificationCurve(df, ['y1', 'y2'], c_rnd_vars_names[0],\n",
+    "                              controls)\n",
+    "sc.fit()\n",
+    "sc.plot()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Flagging a preferred specification\n",
+    "\n",
+    "Often, in practice, you will have a *preferred* specification that you will use as your estimate. You can specify this and have it be flagged.\n",
+    "\n",
+    "You can achieve this by passing a list of variables that you'd like to be used in your preferred specification via the `preferred_spec` keyword argument.\n",
+    "\n",
+    "In the example below, the preferred specification comes out as being close to the known answer that we constructed."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "\n",
+    "sc = specy.SpecificationCurve(df, ['y1', 'y2'], c_rnd_vars_names[0],\n",
+    "                              controls)\n",
+    "sc.fit()\n",
+    "sc.plot(preferred_spec=[\"y1\", c_rnd_vars_names[0]] + controls)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Using models other than Ordinary Least Squares\n",
+    "\n",
+    "The default model is OLS, but you can pass through other models too."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import statsmodels.api as sm\n",
+    "\n",
+    "# generate some fake data\n",
+    "n_samples = 1000\n",
+    "x_2 = prng.integers(2, size=n_samples)\n",
+    "x_1 = prng.random(size=n_samples)\n",
+    "x_3 = prng.integers(3, size=n_samples)\n",
+    "x_4 = prng.random(size=n_samples)\n",
+    "x_5 = x_1 + 0.05*np.random.randn(n_samples)\n",
+    "x_beta = -1 - 3.5*x_1 + 0.2*x_2 + 0.3*x_3  # NB: coefficient is -3.5\n",
+    "prob = 1/(1 + np.exp(-x_beta))\n",
+    "y = prng.binomial(n=1, p=prob, size=n_samples)\n",
+    "y2 = prng.binomial(n=1, p=prob*0.98, size=n_samples)\n",
+    "df = pd.DataFrame([x_1, x_2, x_3, x_4, x_5, y, y2],\n",
+    "                  ['x_1', 'x_2', 'x_3', 'x_4', 'x_5', 'y', 'y2']).T\n",
+    "\n",
+    "\n",
+    "# Specify the regressions to run \n",
+    "y_endog = ['y', 'y2']\n",
+    "x_exog = ['x_1', 'x_5']\n",
+    "controls = ['x_3', 'x_2', 'x_4']\n",
+    "sc = specy.SpecificationCurve(df, y_endog, x_exog, controls)\n",
+    "# Fit using the logit estimator\n",
+    "sc.fit(estimator=sm.Logit)  # sm.Probit also works\n",
+    "sc.plot()"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3.8.8 ('.venv': poetry)",
+   "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.8.8"
+  },
+  "vscode": {
+   "interpreter": {
+    "hash": "9a56c79ed317bff88c5e0d313be0f07aaff1a9263474366e1d2ff38920c2e652"
+   }
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/docs/history.md b/docs/history.md
new file mode 100644
index 0000000..e5b0154
--- /dev/null
+++ b/docs/history.md
@@ -0,0 +1,44 @@
+# History
+
+## 0.2.6 (2021-05-05)
+
+-   Updates of some dev packages
+-   Release on Zenodo to create DOI
+
+## 0.2.5 (2020-11-22)
+
+-   Added option to always include some regressors
+-   Added option to label preferred specification in plots
+
+## 0.2.4 (2020-09-15)
+
+-   Further fix to pick up example data
+
+## 0.2.3 (2020-09-14)
+
+-   Fixed examples
+-   Bug fix for including csv data in pypi distribution
+
+## 0.2.2 (2020-09-02)
+
+-   More badges in readme
+-   Dropped support for python 3.5
+
+## 0.2.1 (2020-09-02)
+
+-   Switched to object oriented design
+-   Now supports range of statsmodels estimators!
+-   Example showing how to save plots to file in docs
+-   Example showing where estimation results are stored in docs
+-   Docs example of very large number of specifications
+
+## 0.1.1 (2020-08-01)
+
+-   Multiple independent, dependent, and control variables implemented
+    as lists. Mutually exclusive control variables implemented.
+    Expansions of categorical variables into mutually exclusive fixed
+    effects implemented.
+
+## 0.1.0 (2020-07-27)
+
+-   First release on PyPI.
diff --git a/docs/history.rst b/docs/history.rst
deleted file mode 100644
index 2506499..0000000
--- a/docs/history.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-.. include:: ../HISTORY.rst
diff --git a/docs/images/example.png b/docs/images/example.png
deleted file mode 100644
index a436ae3..0000000
Binary files a/docs/images/example.png and /dev/null differ
diff --git a/docs/index.ipynb b/docs/index.ipynb
new file mode 100644
index 0000000..aa3bbed
--- /dev/null
+++ b/docs/index.ipynb
@@ -0,0 +1,224 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Specification Curve\n",
+    "\n",
+    "Specification Curve is a Python package that performs specification curve analysis; it helps with the problem of the \"garden of forking paths\" (many defensible choices) when doing analysis by running many regressions and summarising the effects in an easy to digest chart."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "[![PyPI](https://img.shields.io/pypi/v/specification_curve.svg)](https://pypi.org/project/specification_curve/)\n",
+    "[![Status](https://img.shields.io/pypi/status/specification_curve.svg)](https://pypi.org/project/specification_curve/)\n",
+    "[![Python Version](https://img.shields.io/pypi/pyversions/specification_curve)](https://pypi.org/project/specification_curve)\n",
+    "[![License](https://img.shields.io/pypi/l/specification_curve)](https://opensource.org/licenses/MIT)\n",
+    "[![Tests](https://github.com/aeturrell/specification_curve/workflows/Tests/badge.svg)](https://github.com/aeturrell/specification_curve/actions?workflow=Tests)\n",
+    "[![Codecov](https://codecov.io/gh/aeturrell/specification_curve/branch/main/graph/badge.svg)](https://codecov.io/gh/aeturrell/specification_curve)\n",
+    "[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)\n",
+    "[![Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n",
+    "[![Google Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/gist/aeturrell/438fb066e4471312667268669cef2c11/specification_curve-examples.ipynb)\n",
+    "[![DOI](https://zenodo.org/badge/282989537.svg)](https://zenodo.org/badge/latestdoi/282989537)\n",
+    "[![Downloads](https://static.pepy.tech/badge/specification-curve)](https://pepy.tech/project/Specification_curve)\n",
+    "\n",
+    "[![Linux](https://svgshare.com/i/Zhy.svg)](https://svgshare.com/i/Zhy.svg)\n",
+    "[![macOS](https://svgshare.com/i/ZjP.svg)](https://svgshare.com/i/ZjP.svg)\n",
+    "[![Windows](https://svgshare.com/i/ZhY.svg)](https://svgshare.com/i/ZhY.svg)\n",
+    "\n",
+    "[![Soure](https://img.shields.io/badge/source%20code-github-lightgrey?style=for-the-badge)](https://github.com/aeturrell/specification_curve)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Introduction\n",
+    "\n",
+    "When specifying a causal model, modellers have a number of options. These can be informed by field intelligence, priors, and even misguided attempts to find a significant result. Even with the best of intentions, research teams can reach entirely different conclusions using the same, or similar, data because of different choices made in preparing data or in modelling it.\n",
+    "\n",
+    "Typically this happens when there isn’t a clear way to do \"feature engineering\" on the data or the data are imperfect in some way. For example, you have a high frequency time series which needs to be aggregated to a lower frequency: you could take the maximum, the minimum, or the average over each high frequency time period. A different choice may be appropriate in different settings.\n",
+    "\n",
+    "There's formal evidence that researchers really do make different decisions; one study {cite:p}`silberzahn2018many` gave the same research question---whether soccer referees are more likely to give red cards to players with dark skin tones than to players with light skin tones---to 29 different teams. From the abstract of that paper:\n",
+    "\n",
+    "> Analytic approaches varied widely across the teams, and the estimated effect sizes ranged from 0.89 to 2.93 (Mdn = 1.31) in odds-ratio units. Twenty teams (69%) found a statistically significant positive effect, and 9 teams (31%) did not observe a significant relationship. Overall, the 29 different analyses used 21 unique combinations of covariates. Neither analysts’ prior beliefs about the effect of interest nor their level of expertise readily explained the variation in the outcomes of the analyses. Peer ratings of the quality of the analyses also did not account for the variability.\n",
+    "\n",
+    "So not only can different decisions made, but there seems to be no clearly identifiable reason for them!\n",
+    "\n",
+    "There have since been a number of papers that find similar conclusions {cite:p}`huntington2021influence`.\n",
+    "\n",
+    "Specification curves have been invented {cite:p}`simonsohn2020specification` as a way to better grasp the garden of forking paths that analysts face, and help them show how sensitive their results are to alternative specifications.\n",
+    "\n",
+    "More than one piece of software {cite:p}`masur2019specr` has appeared to help researchers and analysts use this technique: **specification curve** is one such package!"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Quickstart\n",
+    "\n",
+    "You can try out specification curve right now in [Google Colab](https://colab.research.google.com/gist/aeturrell/438fb066e4471312667268669cef2c11/specification_curve-examples.ipynb).\n",
+    "\n",
+    "Here's an example of using **Specification Curve**."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# import specification curve\n",
+    "import specification_curve as specy\n",
+    "\n",
+    "\n",
+    "# Generate some fake data\n",
+    "# ------------------------\n",
+    "import numpy as np\n",
+    "import pandas as pd\n",
+    "# Set seed for random numbers\n",
+    "seed_for_prng = 78557\n",
+    "# prng=probabilistic random number generator\n",
+    "prng = np.random.default_rng(seed_for_prng)  \n",
+    "n_samples = 400\n",
+    "# Number of dimensions\n",
+    "n_dim = 4\n",
+    "c_rnd_vars = prng.random(size=(n_dim, n_samples))\n",
+    "y_1 = (0.4*c_rnd_vars[0, :] -  # THIS IS THE TRUE VALUE OF THE COEFFICIENT\n",
+    "       0.2*c_rnd_vars[1, :] +\n",
+    "       0.3*prng.standard_normal(n_samples))\n",
+    "# Next line causes y_2 ests to be much more noisy\n",
+    "y_2 = y_1 - 0.5*np.abs(prng.standard_normal(n_samples))\n",
+    "# Put data into dataframe\n",
+    "df = pd.DataFrame([y_1, y_2], ['y1', 'y2']).T\n",
+    "df[\"x_1\"] = c_rnd_vars[0, :]\n",
+    "df[\"c_1\"] = c_rnd_vars[1, :]\n",
+    "df[\"c_2\"] = c_rnd_vars[2, :]\n",
+    "df[\"c_3\"] = c_rnd_vars[3, :]\n",
+    "\n",
+    "# Specification Curve Analysis\n",
+    "#-----------------------------\n",
+    "sc = specy.SpecificationCurve(\n",
+    "    df,\n",
+    "    y_endog=['y1', 'y2'],\n",
+    "    x_exog=\"x_1\",\n",
+    "    controls=[\"c_1\", \"c_2\", \"c_3\"])\n",
+    "sc.fit()\n",
+    "sc.plot()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Grey squares (black lines when there are many specifications) show whether a variable is included in a specification or not. Blue or red markers and error bars show whether the coefficient is positive and significant (at the 0.05 level) or red and significant, respectively."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Installation\n",
+    "\n",
+    "You can install **Specification Curve** via pip:\n",
+    "\n",
+    "```bash\n",
+    "$ pip install specification-curve\n",
+    "```\n",
+    "\n",
+    "To install the development version from git, use:\n",
+    "\n",
+    "```bash\n",
+    "$ pip install git+https://github.com/aeturrell/specification_curve.git\n",
+    "```"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Requirements\n",
+    "\n",
+    "You can find a full list of requirements in the [pyproject.toml](https://github.com/aeturrell/skimpy/blob/main/pyproject.toml) file. The\n",
+    "main requirements are:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "tags": [
+     "remove-input"
+    ]
+   },
+   "outputs": [],
+   "source": [
+    "import toml\n",
+    "from pathlib import Path\n",
+    "\n",
+    "config = toml.load(Path(\"../pyproject.toml\"))\n",
+    "dict_main_deps = config[\"tool\"][\"poetry\"][\"dependencies\"]\n",
+    "for key, value in dict_main_deps.items():\n",
+    "    print(f\"{key} {value}\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## License\n",
+    "\n",
+    "Distributed under the terms of the [MIT license](https://opensource.org/licenses/MIT).\n",
+    "\n",
+    "## Credits\n",
+    "\n",
+    "The package is built with [poetry](https://python-poetry.org/), while the documentation is built with [Jupyter Book](https://jupyterbook.org). Tests are run with [nox](https://nox.thea.codes/en/stable/).\n",
+    "\n",
+    "## Similar Packages\n",
+    "\n",
+    "In RStats, there is [specr](https://github.com/masurp/specr) (which inspired many design choices in this package) and [spec_chart](https://github.com/ArielOrtizBobea/spec_chart). Some of the example data in this package is the same as in specr, but please note that results have not been cross-checked across packages."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Bibliography\n",
+    "\n",
+    "```{bibliography} references.bib\n",
+    ":style: alpha\n",
+    "```"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3.8.8 ('.venv': poetry)",
+   "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.8.8"
+  },
+  "vscode": {
+   "interpreter": {
+    "hash": "9a56c79ed317bff88c5e0d313be0f07aaff1a9263474366e1d2ff38920c2e652"
+   }
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/docs/index.rst b/docs/index.rst
deleted file mode 100644
index afc6ad7..0000000
--- a/docs/index.rst
+++ /dev/null
@@ -1,20 +0,0 @@
-Welcome to Specification Curve's documentation!
-======================================
-
-.. toctree::
-   :maxdepth: 2
-   :caption: Contents:
-
-   readme
-   installation
-   usage
-   modules
-   contributing
-   authors
-   history
-
-Indices and tables
-==================
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
diff --git a/docs/installation.rst b/docs/installation.rst
deleted file mode 100644
index dbf8892..0000000
--- a/docs/installation.rst
+++ /dev/null
@@ -1,51 +0,0 @@
-.. highlight:: shell
-
-============
-Installation
-============
-
-
-Stable release
---------------
-
-To install Specification Curve, run this command in your terminal:
-
-.. code-block:: console
-
-    $ pip install specification_curve
-
-This is the preferred method to install Specification Curve, as it will always install the most recent stable release.
-
-If you don't have `pip`_ installed, this `Python installation guide`_ can guide
-you through the process.
-
-.. _pip: https://pip.pypa.io
-.. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/
-
-
-From sources
-------------
-
-The sources for Specification Curve can be downloaded from the `Github repo`_.
-
-You can either clone the public repository:
-
-.. code-block:: console
-
-    $ git clone git://github.com/aeturrell/specification_curve
-
-Or download the `tarball`_:
-
-.. code-block:: console
-
-    $ curl -OJL https://github.com/aeturrell/specification_curve/tarball/master
-
-Once you have a copy of the source, you can install it with:
-
-.. code-block:: console
-
-    $ python setup.py install
-
-
-.. _Github repo: https://github.com/aeturrell/specification_curve
-.. _tarball: https://github.com/aeturrell/specification_curve/tarball/master
diff --git a/docs/make.bat b/docs/make.bat
deleted file mode 100644
index 4f46bcd..0000000
--- a/docs/make.bat
+++ /dev/null
@@ -1,36 +0,0 @@
-@ECHO OFF
-
-pushd %~dp0
-
-REM Command file for Sphinx documentation
-
-if "%SPHINXBUILD%" == "" (
-	set SPHINXBUILD=python -msphinx
-)
-set SOURCEDIR=.
-set BUILDDIR=_build
-set SPHINXPROJ=specification_curve
-
-if "%1" == "" goto help
-
-%SPHINXBUILD% >NUL 2>NUL
-if errorlevel 9009 (
-	echo.
-	echo.The Sphinx module was not found. Make sure you have Sphinx installed,
-	echo.then set the SPHINXBUILD environment variable to point to the full
-	echo.path of the 'sphinx-build' executable. Alternatively you may add the
-	echo.Sphinx directory to PATH.
-	echo.
-	echo.If you don't have Sphinx installed, grab it from
-	echo.http://sphinx-doc.org/
-	exit /b 1
-)
-
-%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
-goto end
-
-:help
-%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
-
-:end
-popd
diff --git a/docs/readme.rst b/docs/readme.rst
deleted file mode 100644
index 72a3355..0000000
--- a/docs/readme.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-.. include:: ../README.rst
diff --git a/docs/references.bib b/docs/references.bib
new file mode 100644
index 0000000..5a863fd
--- /dev/null
+++ b/docs/references.bib
@@ -0,0 +1,39 @@
+@article{silberzahn2018many,
+  title={Many analysts, one data set: Making transparent how variations in analytic choices affect results},
+  author={Silberzahn, Raphael and Uhlmann, Eric L and Martin, Daniel P and Anselmi, Pasquale and Aust, Frederik and Awtrey, Eli and Bahn{\'\i}k, {\v{S}}t{\v{e}}p{\'a}n and Bai, Feng and Bannard, Colin and Bonnier, Evelina and others},
+  journal={Advances in Methods and Practices in Psychological Science},
+  volume={1},
+  number={3},
+  pages={337--356},
+  year={2018},
+  publisher={Sage Publications Sage CA: Los Angeles, CA}
+}
+
+@article{huntington2021influence,
+  title={The influence of hidden researcher decisions in applied microeconomics},
+  author={Huntington-Klein, Nick and Arenas, Andreu and Beam, Emily and Bertoni, Marco and Bloem, Jeffrey R and Burli, Pralhad and Chen, Naibin and Grieco, Paul and Ekpe, Godwin and Pugatch, Todd and others},
+  journal={Economic Inquiry},
+  volume={59},
+  number={3},
+  pages={944--960},
+  year={2021},
+  publisher={Wiley Online Library}
+}
+
+@article{simonsohn2020specification,
+  title={Specification curve analysis},
+  author={Simonsohn, Uri and Simmons, Joseph P and Nelson, Leif D},
+  journal={Nature Human Behaviour},
+  volume={4},
+  number={11},
+  pages={1208--1214},
+  year={2020},
+  publisher={Nature Publishing Group}
+}
+
+@Misc{masur2019specr,
+     title = {specr: Statistical functions for conducting specification curve analyses (Version 0.2.1)},
+     author = {Philipp K. Masur and Michael Scharkow},
+     year = {2019},
+     url = {https://github.com/masurp/specr},
+   }
\ No newline at end of file
diff --git a/docs/usage.rst b/docs/usage.rst
deleted file mode 100644
index 16d72aa..0000000
--- a/docs/usage.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-=====
-Usage
-=====
-
-* TODO
\ No newline at end of file
diff --git a/noxfile.py b/noxfile.py
new file mode 100644
index 0000000..3ca3705
--- /dev/null
+++ b/noxfile.py
@@ -0,0 +1,166 @@
+"""Nox sessions."""
+import shutil
+import sys
+from pathlib import Path
+from textwrap import dedent
+
+import nox
+
+try:
+    from nox_poetry import Session
+    from nox_poetry import session
+except ImportError:
+    message = f"""\
+    Nox failed to import the 'nox-poetry' package.
+
+    Please install it using the following command:
+
+    {sys.executable} -m pip install nox-poetry"""
+    raise SystemExit(dedent(message))
+
+
+package = "specification_curve"
+python_versions = ["3.9", "3.8", "3.10"]
+nox.needs_version = ">= 2021.6.6"
+nox.options.sessions = (
+    "pre-commit",
+    "safety",
+    "mypy",
+    "tests",
+    "typeguard",
+    "xdoctest",
+)
+
+
+def activate_virtualenv_in_precommit_hooks(session: Session) -> None:
+    """Activate virtualenv in hooks installed by pre-commit.
+
+    This function patches git hooks installed by pre-commit to activate the
+    session's virtual environment. This allows pre-commit to locate hooks in
+    that environment when invoked from git.
+
+    Args:
+        session: The Session object.
+    """
+    if session.bin is None:
+        return
+
+    virtualenv = session.env.get("VIRTUAL_ENV")
+    if virtualenv is None:
+        return
+
+    hookdir = Path(".git") / "hooks"
+    if not hookdir.is_dir():
+        return
+
+    for hook in hookdir.iterdir():
+        if hook.name.endswith(".sample") or not hook.is_file():
+            continue
+
+        text = hook.read_text()
+        bindir = repr(session.bin)[1:-1]  # strip quotes
+        if not (
+            Path("A") == Path("a") and bindir.lower() in text.lower() or bindir in text
+        ):
+            continue
+
+        lines = text.splitlines()
+        if not (lines[0].startswith("#!") and "python" in lines[0].lower()):
+            continue
+
+        header = dedent(
+            f"""\
+            import os
+            os.environ["VIRTUAL_ENV"] = {virtualenv!r}
+            os.environ["PATH"] = os.pathsep.join((
+                {session.bin!r},
+                os.environ.get("PATH", ""),
+            ))
+            """
+        )
+
+        lines.insert(1, header)
+        hook.write_text("\n".join(lines))
+
+
+@session(name="pre-commit", python="3.9")
+def precommit(session: Session) -> None:
+    """Lint using pre-commit."""
+    args = session.posargs or ["run", "--all-files", "--show-diff-on-failure"]
+    session.install(
+        "black",
+        "darglint",
+        "flake8",
+        "flake8-bandit",
+        "flake8-bugbear",
+        "flake8-docstrings",
+        "flake8-rst-docstrings",
+        "pep8-naming",
+        "pre-commit",
+        "pre-commit-hooks",
+        "reorder-python-imports",
+    )
+    session.run("pre-commit", *args)
+    if args and args[0] == "install":
+        activate_virtualenv_in_precommit_hooks(session)
+
+
+@session(python="3.9")
+def safety(session: Session) -> None:
+    """Scan dependencies for insecure packages."""
+    requirements = session.poetry.export_requirements()
+    session.install("safety")
+    session.run("safety", "check", "--full-report", f"--file={requirements}")
+
+
+@session(python=python_versions)
+def mypy(session: Session) -> None:
+    """Type-check using mypy."""
+    args = session.posargs or ["src", "tests", "docs/conf.py"]
+    session.install(".")
+    session.install("mypy", "pytest")
+    session.run("mypy", *args)
+    if not session.posargs:
+        session.run("mypy", f"--python-executable={sys.executable}", "noxfile.py")
+
+
+@session(python=python_versions)
+def tests(session: Session) -> None:
+    """Run the test suite."""
+    session.install(".")
+    session.install("coverage[toml]", "pytest", "pygments")
+    try:
+        session.run("coverage", "run", "--parallel", "-m", "pytest", *session.posargs)
+    finally:
+        if session.interactive:
+            session.notify("coverage", posargs=[])
+
+
+@session
+def coverage(session: Session) -> None:
+    """Produce the coverage report."""
+    args = session.posargs or ["report"]
+
+    session.install("coverage[toml]")
+
+    if not session.posargs and any(Path().glob(".coverage.*")):
+        session.run("coverage", "combine")
+
+    session.run("coverage", *args)
+
+
+@session(python=python_versions)
+def typeguard(session: Session) -> None:
+    """Runtime type checking using Typeguard."""
+    session.install(".")
+    session.install("pytest", "typeguard", "pygments")
+    session.run("pytest", f"--typeguard-packages={package}", *session.posargs)
+
+
+@session(python=python_versions)
+def xdoctest(session: Session) -> None:
+    """Run examples with xdoctest."""
+    args = session.posargs or ["all"]
+    session.install(".")
+    session.install("xdoctest[colors]")
+    session.run("python", "-m", "xdoctest", package, *args)
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..ebd272f
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,2887 @@
+[[package]]
+name = "alabaster"
+version = "0.7.12"
+description = "A configurable sidebar-enabled Sphinx theme"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "anyio"
+version = "3.6.2"
+description = "High level compatibility layer for multiple asynchronous event loop implementations"
+category = "dev"
+optional = false
+python-versions = ">=3.6.2"
+
+[package.dependencies]
+idna = ">=2.8"
+sniffio = ">=1.1"
+
+[package.extras]
+doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
+test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"]
+trio = ["trio (>=0.16,<0.22)"]
+
+[[package]]
+name = "appnope"
+version = "0.1.3"
+description = "Disable App Nap on macOS >= 10.9"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "argcomplete"
+version = "2.0.0"
+description = "Bash tab completion for argparse"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+test = ["wheel", "pexpect", "flake8", "coverage"]
+
+[[package]]
+name = "argon2-cffi"
+version = "21.3.0"
+description = "The secure Argon2 password hashing algorithm."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+argon2-cffi-bindings = "*"
+
+[package.extras]
+dev = ["pre-commit", "cogapp", "tomli", "coverage[toml] (>=5.0.2)", "hypothesis", "pytest", "sphinx", "sphinx-notfound-page", "furo"]
+docs = ["sphinx", "sphinx-notfound-page", "furo"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest"]
+
+[[package]]
+name = "argon2-cffi-bindings"
+version = "21.2.0"
+description = "Low-level CFFI bindings for Argon2"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+cffi = ">=1.0.1"
+
+[package.extras]
+dev = ["pytest", "cogapp", "pre-commit", "wheel"]
+tests = ["pytest"]
+
+[[package]]
+name = "asttokens"
+version = "2.0.8"
+description = "Annotate AST trees with source code positions"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+six = "*"
+
+[package.extras]
+test = ["astroid (<=2.5.3)", "pytest"]
+
+[[package]]
+name = "attrs"
+version = "21.4.0"
+description = "Classes Without Boilerplate"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.extras]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
+docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
+
+[[package]]
+name = "babel"
+version = "2.10.3"
+description = "Internationalization utilities"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pytz = ">=2015.7"
+
+[[package]]
+name = "backcall"
+version = "0.2.0"
+description = "Specifications for callback functions passed in to an API"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.11.1"
+description = "Screen-scraping library"
+category = "dev"
+optional = false
+python-versions = ">=3.6.0"
+
+[package.dependencies]
+soupsieve = ">1.2"
+
+[package.extras]
+html5lib = ["html5lib"]
+lxml = ["lxml"]
+
+[[package]]
+name = "black"
+version = "22.10.0"
+description = "The uncompromising code formatter."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
+typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.7.4)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "bleach"
+version = "5.0.1"
+description = "An easy safelist-based HTML-sanitizing tool."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+six = ">=1.9.0"
+webencodings = "*"
+
+[package.extras]
+css = ["tinycss2 (>=1.1.0,<1.2)"]
+dev = ["build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "Sphinx (==4.3.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)", "black (==22.3.0)", "mypy (==0.961)"]
+
+[[package]]
+name = "certifi"
+version = "2022.9.24"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "cffi"
+version = "1.15.1"
+description = "Foreign Function Interface for Python calling C code."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pycparser = "*"
+
+[[package]]
+name = "cfgv"
+version = "3.3.1"
+description = "Validate configuration and produce human readable error messages."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[[package]]
+name = "charset-normalizer"
+version = "2.1.1"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "dev"
+optional = false
+python-versions = ">=3.6.0"
+
+[package.extras]
+unicode_backport = ["unicodedata2"]
+
+[[package]]
+name = "classify-imports"
+version = "4.2.0"
+description = "Utilities for refactoring imports in python-like syntax."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "click"
+version = "8.1.3"
+description = "Composable command line interface toolkit"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+
+[[package]]
+name = "colorlog"
+version = "6.7.0"
+description = "Add colours to the output of Python's logging module."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+
+[package.extras]
+development = ["black", "flake8", "mypy", "pytest", "types-colorama"]
+
+[[package]]
+name = "contourpy"
+version = "1.0.5"
+description = "Python library for calculating contours of 2D quadrilateral grids"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+numpy = ">=1.16"
+
+[package.extras]
+test-no-codebase = ["pillow", "matplotlib", "pytest"]
+test-minimal = ["pytest"]
+test = ["isort", "flake8", "pillow", "matplotlib", "pytest"]
+docs = ["sphinx-rtd-theme", "sphinx", "docutils (<0.18)"]
+bokeh = ["selenium", "bokeh"]
+
+[[package]]
+name = "coverage"
+version = "6.5.0"
+description = "Code coverage measurement for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "cycler"
+version = "0.11.0"
+description = "Composable style cycles"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "debugpy"
+version = "1.6.3"
+description = "An implementation of the Debug Adapter Protocol for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "decorator"
+version = "5.1.1"
+description = "Decorators for Humans"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "defusedxml"
+version = "0.7.1"
+description = "XML bomb protection for Python stdlib modules"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "distlib"
+version = "0.3.6"
+description = "Distribution utilities"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "docutils"
+version = "0.17.1"
+description = "Docutils -- Python Documentation Utilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "dparse"
+version = "0.6.2"
+description = "A parser for Python dependency files"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+packaging = "*"
+toml = "*"
+
+[package.extras]
+pipenv = ["pipenv"]
+conda = ["pyyaml"]
+
+[[package]]
+name = "entrypoints"
+version = "0.4"
+description = "Discover and load entry points from installed packages."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "exceptiongroup"
+version = "1.0.0"
+description = "Backport of PEP 654 (exception groups)"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "executing"
+version = "1.1.1"
+description = "Get the currently executing AST node of a frame, and other information"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+tests = ["rich", "littleutils", "pytest", "asttokens"]
+
+[[package]]
+name = "fastjsonschema"
+version = "2.16.2"
+description = "Fastest Python implementation of JSON schema"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+devel = ["colorama", "jsonschema", "json-spec", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"]
+
+[[package]]
+name = "filelock"
+version = "3.8.0"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"]
+testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"]
+
+[[package]]
+name = "flake8"
+version = "5.0.4"
+description = "the modular source code checker: pep8 pyflakes and co"
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[package.dependencies]
+mccabe = ">=0.7.0,<0.8.0"
+pycodestyle = ">=2.9.0,<2.10.0"
+pyflakes = ">=2.5.0,<2.6.0"
+
+[[package]]
+name = "fonttools"
+version = "4.38.0"
+description = "Tools to manipulate font files"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+all = ["fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "zopfli (>=0.1.4)", "lz4 (>=1.7.4.2)", "matplotlib", "sympy", "skia-pathops (>=0.5.0)", "uharfbuzz (>=0.23.0)", "brotlicffi (>=0.8.0)", "scipy", "brotli (>=1.0.1)", "munkres", "unicodedata2 (>=14.0.0)", "xattr"]
+graphite = ["lz4 (>=1.7.4.2)"]
+interpolatable = ["scipy", "munkres"]
+lxml = ["lxml (>=4.0,<5)"]
+pathops = ["skia-pathops (>=0.5.0)"]
+plot = ["matplotlib"]
+repacker = ["uharfbuzz (>=0.23.0)"]
+symfont = ["sympy"]
+type1 = ["xattr"]
+ufo = ["fs (>=2.2.0,<3)"]
+unicode = ["unicodedata2 (>=14.0.0)"]
+woff = ["zopfli (>=0.1.4)", "brotlicffi (>=0.8.0)", "brotli (>=1.0.1)"]
+
+[[package]]
+name = "furo"
+version = "2022.9.29"
+description = "A clean customisable Sphinx documentation theme."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+beautifulsoup4 = "*"
+pygments = ">=2.7"
+sphinx = ">=4.0,<6.0"
+sphinx-basic-ng = "*"
+
+[[package]]
+name = "ghp-import"
+version = "2.1.0"
+description = "Copy your docs directly to the gh-pages branch."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+python-dateutil = ">=2.8.1"
+
+[package.extras]
+dev = ["wheel", "flake8", "markdown", "twine"]
+
+[[package]]
+name = "gitdb"
+version = "4.0.9"
+description = "Git Object Database"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+smmap = ">=3.0.1,<6"
+
+[[package]]
+name = "gitpython"
+version = "3.1.29"
+description = "GitPython is a python library used to interact with Git repositories"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+gitdb = ">=4.0.1,<5"
+
+[[package]]
+name = "greenlet"
+version = "1.1.3.post0"
+description = "Lightweight in-process concurrent programming"
+category = "dev"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
+
+[package.extras]
+docs = ["sphinx"]
+
+[[package]]
+name = "identify"
+version = "2.5.8"
+description = "File identification library for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+license = ["ukkonen"]
+
+[[package]]
+name = "idna"
+version = "3.4"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "imagesize"
+version = "1.4.1"
+description = "Getting image size from png/jpeg/jpeg2000/gif file"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "importlib-metadata"
+version = "5.0.0"
+description = "Read metadata from Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+zipp = ">=0.5"
+
+[package.extras]
+docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"]
+perf = ["ipython"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
+
+[[package]]
+name = "importlib-resources"
+version = "5.10.0"
+description = "Read resources from Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
+
+[package.extras]
+docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
+
+[[package]]
+name = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "ipykernel"
+version = "6.16.2"
+description = "IPython Kernel for Jupyter"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+appnope = {version = "*", markers = "platform_system == \"Darwin\""}
+debugpy = ">=1.0"
+ipython = ">=7.23.1"
+jupyter-client = ">=6.1.12"
+matplotlib-inline = ">=0.1"
+nest-asyncio = "*"
+packaging = "*"
+psutil = "*"
+pyzmq = ">=17"
+tornado = ">=6.1"
+traitlets = ">=5.1.0"
+
+[package.extras]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt"]
+test = ["flaky", "ipyparallel", "pre-commit", "pytest-cov", "pytest-timeout", "pytest (>=7.0)"]
+
+[[package]]
+name = "ipython"
+version = "8.5.0"
+description = "IPython: Productive Interactive Computing"
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+
+[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.1,<3.1.0"
+pygments = ">=2.4.0"
+stack-data = "*"
+traitlets = ">=5"
+
+[package.extras]
+all = ["black", "Sphinx (>=1.3)", "ipykernel", "nbconvert", "nbformat", "ipywidgets", "notebook", "ipyparallel", "qtconsole", "pytest (<7.1)", "pytest-asyncio", "testpath", "curio", "matplotlib (!=3.2.0)", "numpy (>=1.19)", "pandas", "trio"]
+black = ["black"]
+doc = ["Sphinx (>=1.3)"]
+kernel = ["ipykernel"]
+nbconvert = ["nbconvert"]
+nbformat = ["nbformat"]
+notebook = ["ipywidgets", "notebook"]
+parallel = ["ipyparallel"]
+qtconsole = ["qtconsole"]
+test = ["pytest (<7.1)", "pytest-asyncio", "testpath"]
+test_extra = ["pytest (<7.1)", "pytest-asyncio", "testpath", "curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.19)", "pandas", "trio"]
+
+[[package]]
+name = "ipython-genutils"
+version = "0.2.0"
+description = "Vestigial utilities from IPython"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "ipywidgets"
+version = "7.7.2"
+description = "IPython HTML widgets for Jupyter"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+ipykernel = ">=4.5.1"
+ipython = {version = ">=4.0.0", markers = "python_version >= \"3.3\""}
+ipython-genutils = ">=0.2.0,<0.3.0"
+jupyterlab-widgets = {version = ">=1.0.0,<3", markers = "python_version >= \"3.6\""}
+traitlets = ">=4.3.1"
+widgetsnbextension = ">=3.6.0,<3.7.0"
+
+[package.extras]
+test = ["pytest (>=3.6.0)", "pytest-cov", "mock"]
+
+[[package]]
+name = "jedi"
+version = "0.18.1"
+description = "An autocompletion tool for Python that can be used for text editors."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+parso = ">=0.8.0,<0.9.0"
+
+[package.extras]
+qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
+testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"]
+
+[[package]]
+name = "jinja2"
+version = "3.1.2"
+description = "A very fast and expressive template engine."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
+[[package]]
+name = "jsonschema"
+version = "3.2.0"
+description = "An implementation of JSON Schema validation for Python"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+attrs = ">=17.4.0"
+pyrsistent = ">=0.14.0"
+six = ">=1.11.0"
+
+[package.extras]
+format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"]
+format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"]
+
+[[package]]
+name = "jupyter-book"
+version = "0.13.1"
+description = "Build a book with Jupyter Notebooks and Sphinx."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+click = ">=7.1,<9"
+docutils = ">=0.15,<0.18"
+Jinja2 = "*"
+jsonschema = "<4"
+linkify-it-py = ">=1.0.1,<1.1.0"
+myst-nb = ">=0.13.1,<0.14.0"
+pyyaml = "*"
+sphinx = ">=4,<5"
+sphinx_book_theme = ">=0.3.2,<0.4.0"
+sphinx-comments = "*"
+sphinx-copybutton = "*"
+sphinx-design = ">=0.1.0,<0.2.0"
+sphinx-external-toc = ">=0.2.3,<0.3.0"
+sphinx-jupyterbook-latex = ">=0.4.6,<0.5.0"
+sphinx-multitoc-numbering = ">=0.1.3,<0.2.0"
+sphinx-thebe = ">=0.1.1,<0.2.0"
+sphinx_togglebutton = "*"
+sphinxcontrib-bibtex = ">=2.2.0,<=2.5.0"
+
+[package.extras]
+code_style = ["pre-commit (>=2.12,<3.0)"]
+pdfhtml = ["pyppeteer"]
+sphinx = ["altair", "bokeh", "folium", "ipywidgets", "jupytext", "matplotlib", "nbclient", "numpy", "pandas", "plotly", "sphinx-click", "sphinx-examples", "sphinx-inline-tabs", "sphinx-proof", "sphinxext-rediraffe (>=0.2.3,<0.3.0)", "sympy"]
+testing = ["altair", "beautifulsoup4", "beautifulsoup4", "cookiecutter", "coverage", "jupytext", "matplotlib", "pyppeteer", "pytest (>=6.2.4)", "pytest-cov", "pytest-regressions", "pytest-timeout", "pytest-xdist", "sphinx-click", "sphinx-tabs", "texsoup"]
+
+[[package]]
+name = "jupyter-cache"
+version = "0.4.3"
+description = "A defined interface for working with a cache of jupyter notebooks."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+attrs = "*"
+nbclient = ">=0.2,<0.6"
+nbdime = "*"
+nbformat = "*"
+sqlalchemy = ">=1.3.12,<1.5"
+
+[package.extras]
+testing = ["nbformat (>=5.1)", "pandas", "sympy", "numpy", "matplotlib", "pytest-regressions", "pytest-cov", "pytest (>=3.6,<4)", "coverage", "ipykernel"]
+rtd = ["pydata-sphinx-theme", "sphinx-copybutton", "myst-nb (>=0.7,<1.0)"]
+code_style = ["pre-commit (==1.17.0)", "black", "flake8 (>=3.7.0,<3.8.0)"]
+cli = ["pyyaml", "tabulate", "click-log", "click-completion", "click"]
+
+[[package]]
+name = "jupyter-client"
+version = "7.4.4"
+description = "Jupyter protocol implementation and client libraries"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+entrypoints = "*"
+jupyter-core = ">=4.9.2"
+nest-asyncio = ">=1.5.4"
+python-dateutil = ">=2.8.2"
+pyzmq = ">=23.0"
+tornado = ">=6.2"
+traitlets = "*"
+
+[package.extras]
+doc = ["ipykernel", "myst-parser", "sphinx-rtd-theme", "sphinx (>=1.3.6)", "sphinxcontrib-github-alt"]
+test = ["codecov", "coverage", "ipykernel (>=6.12)", "ipython", "mypy", "pre-commit", "pytest", "pytest-asyncio (>=0.18)", "pytest-cov", "pytest-timeout"]
+
+[[package]]
+name = "jupyter-core"
+version = "4.11.2"
+description = "Jupyter core package. A base package on which Jupyter projects rely."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+pywin32 = {version = ">=1.0", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""}
+traitlets = "*"
+
+[package.extras]
+test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"]
+
+[[package]]
+name = "jupyter-server"
+version = "1.21.0"
+description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+anyio = ">=3.1.0,<4"
+argon2-cffi = "*"
+jinja2 = "*"
+jupyter-client = ">=6.1.12"
+jupyter-core = ">=4.7.0"
+nbconvert = ">=6.4.4"
+nbformat = ">=5.2.0"
+packaging = "*"
+prometheus-client = "*"
+pywinpty = {version = "*", markers = "os_name == \"nt\""}
+pyzmq = ">=17"
+Send2Trash = "*"
+terminado = ">=0.8.3"
+tornado = ">=6.1.0"
+traitlets = ">=5.1"
+websocket-client = "*"
+
+[package.extras]
+test = ["coverage", "ipykernel", "pre-commit", "pytest-console-scripts", "pytest-cov", "pytest-mock", "pytest-timeout", "pytest-tornasync", "pytest (>=7.0)", "requests"]
+
+[[package]]
+name = "jupyter-server-mathjax"
+version = "0.2.6"
+description = "MathJax resources as a Jupyter Server Extension."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+jupyter-server = ">=1.1"
+
+[package.extras]
+test = ["pytest", "jupyter-server"]
+
+[[package]]
+name = "jupyter-sphinx"
+version = "0.3.2"
+description = "Jupyter Sphinx Extensions"
+category = "dev"
+optional = false
+python-versions = ">= 3.6"
+
+[package.dependencies]
+IPython = "*"
+ipywidgets = ">=7.0.0"
+nbconvert = ">=5.5"
+nbformat = "*"
+Sphinx = ">=2"
+
+[[package]]
+name = "jupyterlab-pygments"
+version = "0.2.2"
+description = "Pygments theme using JupyterLab CSS variables"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "jupyterlab-widgets"
+version = "1.1.1"
+description = "A JupyterLab extension."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "kiwisolver"
+version = "1.4.4"
+description = "A fast implementation of the Cassowary constraint solver"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "latexcodec"
+version = "2.0.1"
+description = "A lexer and codec to work with LaTeX code in Python."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+six = ">=1.4.1"
+
+[[package]]
+name = "linkify-it-py"
+version = "1.0.3"
+description = "Links recognition library with FULL unicode support."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+uc-micro-py = "*"
+
+[package.extras]
+test = ["pytest-cov", "pytest", "coverage"]
+doc = ["myst-parser", "sphinx-book-theme", "sphinx"]
+dev = ["black", "flake8", "isort", "pre-commit"]
+benchmark = ["pytest-benchmark", "pytest"]
+
+[[package]]
+name = "lxml"
+version = "4.9.1"
+description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
+
+[package.extras]
+cssselect = ["cssselect (>=0.7)"]
+html5 = ["html5lib"]
+htmlsoup = ["beautifulsoup4"]
+source = ["Cython (>=0.29.7)"]
+
+[[package]]
+name = "markdown-it-py"
+version = "1.1.0"
+description = "Python port of markdown-it. Markdown parsing, done right!"
+category = "dev"
+optional = false
+python-versions = "~=3.6"
+
+[package.dependencies]
+attrs = ">=19,<22"
+
+[package.extras]
+code_style = ["pre-commit (==2.6)"]
+compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.2.2,<3.3.0)", "mistletoe-ebp (>=0.10.0,<0.11.0)", "mistune (>=0.8.4,<0.9.0)", "panflute (>=1.12,<2.0)"]
+linkify = ["linkify-it-py (>=1.0,<2.0)"]
+plugins = ["mdit-py-plugins"]
+rtd = ["myst-nb (==0.13.0a1)", "pyyaml", "sphinx (>=2,<4)", "sphinx-copybutton", "sphinx-panels (>=0.4.0,<0.5.0)", "sphinx-book-theme"]
+testing = ["coverage", "psutil", "pytest (>=3.6,<4)", "pytest-benchmark (>=3.2,<4.0)", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "markupsafe"
+version = "2.1.1"
+description = "Safely add untrusted strings to HTML/XML markup."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "matplotlib"
+version = "3.6.1"
+description = "Python plotting package"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+
+[package.dependencies]
+contourpy = ">=1.0.1"
+cycler = ">=0.10"
+fonttools = ">=4.22.0"
+kiwisolver = ">=1.0.1"
+numpy = ">=1.19"
+packaging = ">=20.0"
+pillow = ">=6.2.0"
+pyparsing = ">=2.2.1"
+python-dateutil = ">=2.7"
+setuptools_scm = ">=7"
+
+[[package]]
+name = "matplotlib-inline"
+version = "0.1.6"
+description = "Inline Matplotlib backend for Jupyter"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+traitlets = "*"
+
+[[package]]
+name = "mccabe"
+version = "0.7.0"
+description = "McCabe checker, plugin for flake8"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "mdit-py-plugins"
+version = "0.2.8"
+description = "Collection of plugins for markdown-it-py"
+category = "dev"
+optional = false
+python-versions = "~=3.6"
+
+[package.dependencies]
+markdown-it-py = ">=1.0,<2.0"
+
+[package.extras]
+code_style = ["pre-commit (==2.6)"]
+rtd = ["myst-parser (==0.14.0a3)", "sphinx-book-theme (>=0.1.0,<0.2.0)"]
+testing = ["coverage", "pytest (>=3.6,<4)", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "mistune"
+version = "0.8.4"
+description = "The fastest markdown parser in pure Python"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "mypy"
+version = "0.982"
+description = "Optional static typing for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+mypy-extensions = ">=0.4.3"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typing-extensions = ">=3.10"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+python2 = ["typed-ast (>=1.4.0,<2)"]
+reports = ["lxml"]
+
+[[package]]
+name = "mypy-extensions"
+version = "0.4.3"
+description = "Experimental type system extensions for programs checked with the mypy typechecker."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "myst-nb"
+version = "0.13.2"
+description = "A Jupyter Notebook Sphinx reader built on top of the MyST markdown parser."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+docutils = ">=0.15,<0.18"
+importlib-metadata = "*"
+ipython = "*"
+ipywidgets = ">=7.0.0,<8"
+jupyter-cache = ">=0.4.1,<0.5.0"
+jupyter-sphinx = ">=0.3.2,<0.4.0"
+myst-parser = ">=0.15.2,<0.16.0"
+nbconvert = ">=5.6,<7"
+nbformat = ">=5.0,<6.0"
+pyyaml = "*"
+sphinx = ">=3.1,<5"
+sphinx-togglebutton = ">=0.3.0,<0.4.0"
+
+[package.extras]
+code_style = ["pre-commit (>=2.12,<3.0)"]
+rtd = ["alabaster", "altair", "bokeh", "coconut (>=1.4.3,<1.5.0)", "ipykernel (>=5.5,<6.0)", "ipywidgets", "jupytext (>=1.11.2,<1.12.0)", "matplotlib", "numpy", "pandas", "plotly", "sphinx-book-theme (>=0.1.0,<0.2.0)", "sphinx-copybutton", "sphinx-panels (>=0.4.1,<0.5.0)", "sphinxcontrib-bibtex", "sympy"]
+testing = ["coverage (<5.0)", "ipykernel (>=5.5,<6.0)", "ipython (<8)", "jupytext (>=1.11.2,<1.12.0)", "matplotlib (>=3.3.0,<3.4.0)", "numpy", "pandas (<1.4)", "pytest (>=5.4,<6.0)", "pytest-cov (>=2.8,<3.0)", "pytest-regressions", "sympy"]
+
+[[package]]
+name = "myst-parser"
+version = "0.15.2"
+description = "An extended commonmark compliant parser, with bridges to docutils & sphinx."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+docutils = ">=0.15,<0.18"
+jinja2 = "*"
+markdown-it-py = ">=1.0.0,<2.0.0"
+mdit-py-plugins = ">=0.2.8,<0.3.0"
+pyyaml = "*"
+sphinx = ">=3.1,<5"
+
+[package.extras]
+code_style = ["pre-commit (>=2.12,<3.0)"]
+linkify = ["linkify-it-py (>=1.0,<2.0)"]
+rtd = ["ipython", "sphinx-book-theme (>=0.1.0,<0.2.0)", "sphinx-panels (>=0.5.2,<0.6.0)", "sphinxcontrib-bibtex (>=2.1,<3.0)", "sphinxext-rediraffe (>=0.2,<1.0)", "sphinxcontrib.mermaid (>=0.6.3,<0.7.0)", "sphinxext-opengraph (>=0.4.2,<0.5.0)"]
+testing = ["beautifulsoup4", "coverage", "docutils (>=0.17.0,<0.18.0)", "pytest (>=3.6,<4)", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "nbclassic"
+version = "0.4.5"
+description = "A web-based notebook environment for interactive computing"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+argon2-cffi = "*"
+ipykernel = "*"
+ipython-genutils = "*"
+jinja2 = "*"
+jupyter-client = ">=6.1.1"
+jupyter-core = ">=4.6.1"
+jupyter-server = ">=1.8"
+nbconvert = ">=5"
+nbformat = "*"
+nest-asyncio = ">=1.5"
+notebook-shim = ">=0.1.0"
+prometheus-client = "*"
+pyzmq = ">=17"
+Send2Trash = ">=1.8.0"
+terminado = ">=0.8.3"
+tornado = ">=6.1"
+traitlets = ">=4.2.1"
+
+[package.extras]
+docs = ["sphinx", "nbsphinx", "sphinxcontrib-github-alt", "sphinx-rtd-theme", "myst-parser"]
+json-logging = ["json-logging"]
+test = ["pytest", "coverage", "requests", "testpath", "nbval", "selenium (==4.1.5)", "pytest-cov", "pytest-tornasync", "requests-unixsocket"]
+
+[[package]]
+name = "nbclient"
+version = "0.5.13"
+description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor."
+category = "dev"
+optional = false
+python-versions = ">=3.7.0"
+
+[package.dependencies]
+jupyter-client = ">=6.1.5"
+nbformat = ">=5.0"
+nest-asyncio = "*"
+traitlets = ">=5.0.0"
+
+[package.extras]
+sphinx = ["Sphinx (>=1.7)", "sphinx-book-theme", "mock", "moto", "myst-parser"]
+test = ["ipython (<8.0.0)", "ipykernel", "ipywidgets (<8.0.0)", "pytest (>=4.1)", "pytest-asyncio", "pytest-cov (>=2.6.1)", "check-manifest", "flake8", "mypy", "xmltodict", "black", "pip (>=18.1)", "wheel (>=0.31.0)", "setuptools (>=38.6.0)", "twine (>=1.11.0)"]
+
+[[package]]
+name = "nbconvert"
+version = "6.5.4"
+description = "Converting Jupyter Notebooks"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+beautifulsoup4 = "*"
+bleach = "*"
+defusedxml = "*"
+entrypoints = ">=0.2.2"
+jinja2 = ">=3.0"
+jupyter-core = ">=4.7"
+jupyterlab-pygments = "*"
+lxml = "*"
+MarkupSafe = ">=2.0"
+mistune = ">=0.8.1,<2"
+nbclient = ">=0.5.0"
+nbformat = ">=5.1"
+packaging = "*"
+pandocfilters = ">=1.4.1"
+pygments = ">=2.4.1"
+tinycss2 = "*"
+traitlets = ">=5.0"
+
+[package.extras]
+all = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pre-commit", "pyppeteer (>=1,<1.1)", "tornado (>=6.1)", "sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"]
+docs = ["sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"]
+serve = ["tornado (>=6.1)"]
+test = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pre-commit", "pyppeteer (>=1,<1.1)"]
+webpdf = ["pyppeteer (>=1,<1.1)"]
+
+[[package]]
+name = "nbdime"
+version = "3.1.1"
+description = "Diff and merge of Jupyter Notebooks"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+colorama = "*"
+GitPython = "<2.1.4 || >2.1.4,<2.1.5 || >2.1.5,<2.1.6 || >2.1.6"
+jinja2 = ">=2.9"
+jupyter-server = "*"
+jupyter-server-mathjax = ">=0.2.2"
+nbformat = "*"
+pygments = "*"
+requests = "*"
+tornado = "*"
+
+[package.extras]
+docs = ["sphinx", "recommonmark", "sphinx-rtd-theme"]
+test = ["pytest (>=3.6)", "pytest-cov", "pytest-timeout", "pytest-tornado", "jupyter-server", "jsonschema", "mock", "notebook", "requests", "tabulate"]
+
+[[package]]
+name = "nbformat"
+version = "5.7.0"
+description = "The Jupyter Notebook format"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+fastjsonschema = "*"
+jsonschema = ">=2.6"
+jupyter-core = "*"
+traitlets = ">=5.1"
+
+[package.extras]
+test = ["check-manifest", "pep440", "pre-commit", "pytest", "testpath"]
+
+[[package]]
+name = "nbstripout"
+version = "0.6.1"
+description = "Strips outputs from Jupyter and IPython notebooks"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+nbformat = "*"
+
+[[package]]
+name = "nest-asyncio"
+version = "1.5.6"
+description = "Patch asyncio to allow nested event loops"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "nodeenv"
+version = "1.7.0"
+description = "Node.js virtual environment builder"
+category = "dev"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+
+[[package]]
+name = "notebook"
+version = "6.5.1"
+description = "A web-based notebook environment for interactive computing"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+argon2-cffi = "*"
+ipykernel = "*"
+ipython-genutils = "*"
+jinja2 = "*"
+jupyter-client = ">=5.3.4"
+jupyter-core = ">=4.6.1"
+nbclassic = "0.4.5"
+nbconvert = ">=5"
+nbformat = "*"
+nest-asyncio = ">=1.5"
+prometheus-client = "*"
+pyzmq = ">=17"
+Send2Trash = ">=1.8.0"
+terminado = ">=0.8.3"
+tornado = ">=6.1"
+traitlets = ">=4.2.1"
+
+[package.extras]
+docs = ["sphinx", "nbsphinx", "sphinxcontrib-github-alt", "sphinx-rtd-theme", "myst-parser"]
+json-logging = ["json-logging"]
+test = ["pytest", "coverage", "requests", "testpath", "nbval", "selenium (==4.1.5)", "pytest-cov", "requests-unixsocket"]
+
+[[package]]
+name = "notebook-shim"
+version = "0.2.0"
+description = "A shim layer for notebook traits and config"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+jupyter-server = ">=1.8,<3"
+
+[package.extras]
+test = ["pytest-tornasync", "pytest-console-scripts", "pytest"]
+
+[[package]]
+name = "nox"
+version = "2022.8.7"
+description = "Flexible test automation."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+argcomplete = ">=1.9.4,<3.0"
+colorlog = ">=2.6.1,<7.0.0"
+packaging = ">=20.9"
+py = ">=1.4,<2.0.0"
+virtualenv = ">=14"
+
+[package.extras]
+tox_to_nox = ["jinja2", "tox"]
+
+[[package]]
+name = "numpy"
+version = "1.23.4"
+description = "NumPy is the fundamental package for array computing with Python."
+category = "main"
+optional = false
+python-versions = ">=3.8"
+
+[[package]]
+name = "packaging"
+version = "21.3"
+description = "Core utilities for Python packages"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
+
+[[package]]
+name = "pandas"
+version = "1.5.1"
+description = "Powerful data structures for data analysis, time series, and statistics"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+
+[package.dependencies]
+numpy = [
+    {version = ">=1.21.0", markers = "python_version >= \"3.10\""},
+    {version = ">=1.20.3", markers = "python_version < \"3.10\""},
+]
+python-dateutil = ">=2.8.1"
+pytz = ">=2020.1"
+
+[package.extras]
+test = ["pytest-xdist (>=1.31)", "pytest (>=6.0)", "hypothesis (>=5.5.3)"]
+
+[[package]]
+name = "pandocfilters"
+version = "1.5.0"
+description = "Utilities for writing pandoc filters in python"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "parso"
+version = "0.8.3"
+description = "A Python Parser"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
+testing = ["docopt", "pytest (<6.0.0)"]
+
+[[package]]
+name = "pathspec"
+version = "0.10.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "patsy"
+version = "0.5.3"
+description = "A Python package for describing statistical models and for building design matrices."
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+numpy = ">=1.4"
+six = "*"
+
+[package.extras]
+test = ["scipy", "pytest-cov", "pytest"]
+
+[[package]]
+name = "pexpect"
+version = "4.8.0"
+description = "Pexpect allows easy control of interactive console applications."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+ptyprocess = ">=0.5"
+
+[[package]]
+name = "pickleshare"
+version = "0.7.5"
+description = "Tiny 'shelve'-like database with concurrency support"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pillow"
+version = "9.2.0"
+description = "Python Imaging Library (Fork)"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"]
+tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
+
+[[package]]
+name = "platformdirs"
+version = "2.5.2"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
+test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
+
+[[package]]
+name = "pluggy"
+version = "1.0.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pre-commit"
+version = "2.20.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+toml = "*"
+virtualenv = ">=20.0.8"
+
+[[package]]
+name = "pre-commit-hooks"
+version = "4.3.0"
+description = "Some out-of-the-box hooks for pre-commit."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+"ruamel.yaml" = ">=0.15"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+
+[[package]]
+name = "prometheus-client"
+version = "0.15.0"
+description = "Python client for the Prometheus monitoring system."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+twisted = ["twisted"]
+
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.31"
+description = "Library for building powerful interactive command lines in Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6.2"
+
+[package.dependencies]
+wcwidth = "*"
+
+[[package]]
+name = "psutil"
+version = "5.9.3"
+description = "Cross-platform lib for process and system monitoring in Python."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.extras]
+test = ["ipaddress", "mock", "enum34", "pywin32", "wmi"]
+
+[[package]]
+name = "ptyprocess"
+version = "0.7.0"
+description = "Run a subprocess in a pseudo terminal"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pure-eval"
+version = "0.2.2"
+description = "Safely evaluate AST nodes without side effects"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+tests = ["pytest"]
+
+[[package]]
+name = "py"
+version = "1.11.0"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "pybtex"
+version = "0.24.0"
+description = "A BibTeX-compatible bibliography processor in Python"
+category = "dev"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*"
+
+[package.dependencies]
+latexcodec = ">=1.0.4"
+PyYAML = ">=3.01"
+six = "*"
+
+[package.extras]
+test = ["pytest"]
+
+[[package]]
+name = "pybtex-docutils"
+version = "1.0.2"
+description = "A docutils backend for pybtex."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+docutils = ">=0.8"
+pybtex = ">=0.16"
+
+[[package]]
+name = "pycodestyle"
+version = "2.9.1"
+description = "Python style guide checker"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "pycparser"
+version = "2.21"
+description = "C parser in Python"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pydata-sphinx-theme"
+version = "0.8.1"
+description = "Bootstrap-based Sphinx theme from the PyData community"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+beautifulsoup4 = "*"
+docutils = "!=0.17.0"
+packaging = "*"
+sphinx = ">=3.5.4,<5"
+
+[package.extras]
+dev = ["pydata-sphinx-theme", "nox", "pre-commit", "pyyaml"]
+coverage = ["pydata-sphinx-theme", "codecov", "pytest-cov"]
+test = ["pydata-sphinx-theme", "pytest"]
+doc = ["xarray", "numpy", "plotly", "jupyter-sphinx", "sphinx-sitemap", "sphinxext-rediraffe", "pytest-regressions", "pytest", "pandas", "myst-parser", "numpydoc"]
+
+[[package]]
+name = "pyflakes"
+version = "2.5.0"
+description = "passive checker of Python programs"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "pygments"
+version = "2.13.0"
+description = "Pygments is a syntax highlighting package written in Python."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+plugins = ["importlib-metadata"]
+
+[[package]]
+name = "pyparsing"
+version = "3.0.9"
+description = "pyparsing module - Classes and methods to define and execute parsing grammars"
+category = "main"
+optional = false
+python-versions = ">=3.6.8"
+
+[package.extras]
+diagrams = ["railroad-diagrams", "jinja2"]
+
+[[package]]
+name = "pyrsistent"
+version = "0.18.1"
+description = "Persistent/Functional/Immutable data structures"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "pytest"
+version = "7.2.0"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+
+[[package]]
+name = "python-dateutil"
+version = "2.8.2"
+description = "Extensions to the standard Python datetime module"
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "pytz"
+version = "2022.5"
+description = "World timezone definitions, modern and historical"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pywin32"
+version = "304"
+description = "Python for Window Extensions"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pywinpty"
+version = "2.0.9"
+description = "Pseudo terminal support for Windows from Python."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "pyyaml"
+version = "6.0"
+description = "YAML parser and emitter for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "pyzmq"
+version = "24.0.1"
+description = "Python bindings for 0MQ"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+cffi = {version = "*", markers = "implementation_name == \"pypy\""}
+py = {version = "*", markers = "implementation_name == \"pypy\""}
+
+[[package]]
+name = "reorder-python-imports"
+version = "3.9.0"
+description = "Tool for reordering python imports"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+classify-imports = ">=4.1"
+
+[[package]]
+name = "requests"
+version = "2.28.1"
+description = "Python HTTP for Humans."
+category = "dev"
+optional = false
+python-versions = ">=3.7, <4"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<3"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "ruamel.yaml"
+version = "0.17.21"
+description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
+category = "dev"
+optional = false
+python-versions = ">=3"
+
+[package.dependencies]
+"ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""}
+
+[package.extras]
+docs = ["ryd"]
+jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]
+
+[[package]]
+name = "ruamel.yaml.clib"
+version = "0.2.7"
+description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "safety"
+version = "2.3.1"
+description = "Checks installed dependencies for known vulnerabilities and licenses."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+Click = ">=8.0.2"
+dparse = ">=0.6.2"
+packaging = ">=21.0"
+requests = "*"
+"ruamel.yaml" = ">=0.17.21"
+
+[package.extras]
+github = ["pygithub (>=1.43.3)", "jinja2 (>=3.1.0)"]
+gitlab = ["python-gitlab (>=1.3.0)"]
+
+[[package]]
+name = "scipy"
+version = "1.9.3"
+description = "Fundamental algorithms for scientific computing in Python"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+
+[package.dependencies]
+numpy = ">=1.18.5,<1.26.0"
+
+[package.extras]
+test = ["pytest", "pytest-cov", "pytest-xdist", "asv", "mpmath", "gmpy2", "threadpoolctl", "scikit-umfpack"]
+doc = ["sphinx (!=4.1.0)", "pydata-sphinx-theme (==0.9.0)", "sphinx-panels (>=0.5.2)", "matplotlib (>2)", "numpydoc", "sphinx-tabs"]
+dev = ["mypy", "typing-extensions", "pycodestyle", "flake8"]
+
+[[package]]
+name = "send2trash"
+version = "1.8.0"
+description = "Send file to trash natively under Mac OS X, Windows and Linux."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+nativelib = ["pyobjc-framework-cocoa", "pywin32"]
+objc = ["pyobjc-framework-cocoa"]
+win32 = ["pywin32"]
+
+[[package]]
+name = "setuptools-scm"
+version = "7.0.5"
+description = "the blessed package to manage your versions by scm tags"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+packaging = ">=20.0"
+tomli = ">=1.0.0"
+typing-extensions = "*"
+
+[package.extras]
+test = ["pytest (>=6.2)", "virtualenv (>20)"]
+toml = ["setuptools (>=42)"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "smmap"
+version = "5.0.0"
+description = "A pure Python implementation of a sliding window memory map manager"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "sniffio"
+version = "1.3.0"
+description = "Sniff out which async library your code is running under"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "snowballstemmer"
+version = "2.2.0"
+description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "soupsieve"
+version = "2.3.2.post1"
+description = "A modern CSS selector implementation for Beautiful Soup."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "sphinx"
+version = "4.5.0"
+description = "Python documentation generator"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+alabaster = ">=0.7,<0.8"
+babel = ">=1.3"
+colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""}
+docutils = ">=0.14,<0.18"
+imagesize = "*"
+importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
+Jinja2 = ">=2.3"
+packaging = "*"
+Pygments = ">=2.0"
+requests = ">=2.5.0"
+snowballstemmer = ">=1.1"
+sphinxcontrib-applehelp = "*"
+sphinxcontrib-devhelp = "*"
+sphinxcontrib-htmlhelp = ">=2.0.0"
+sphinxcontrib-jsmath = "*"
+sphinxcontrib-qthelp = "*"
+sphinxcontrib-serializinghtml = ">=1.1.5"
+
+[package.extras]
+docs = ["sphinxcontrib-websupport"]
+lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.931)", "docutils-stubs", "types-typed-ast", "types-requests"]
+test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"]
+
+[[package]]
+name = "sphinx-basic-ng"
+version = "1.0.0b1"
+description = "A modern skeleton for Sphinx themes."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+sphinx = ">=4.0"
+
+[package.extras]
+docs = ["furo", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs", "ipython"]
+
+[[package]]
+name = "sphinx-book-theme"
+version = "0.3.3"
+description = "A clean book theme for scientific explanations and documentation with Sphinx"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+pydata-sphinx-theme = ">=0.8.0,<0.9.0"
+pyyaml = "*"
+sphinx = ">=3,<5"
+
+[package.extras]
+test = ["sphinx-thebe", "pytest-regressions (>=2.0.1,<2.1.0)", "pytest-cov", "pytest (>=6.0.1,<6.1.0)", "myst-nb (>=0.13.2,<0.14.0)", "coverage", "beautifulsoup4 (>=4.6.1,<5)"]
+doc = ["sphinxext-opengraph", "sphinxcontrib-youtube", "sphinxcontrib-bibtex (>=2.2,<3.0)", "sphinx-thebe (>=0.1.1)", "sphinx-togglebutton (>=0.2.1)", "sphinx-tabs", "sphinx-copybutton", "sphinx-examples", "sphinx-design", "sphinx (>=4.0,<5.0)", "plotly", "pandas", "nbclient", "myst-nb (>=0.13.2,<0.14.0)", "numpydoc", "matplotlib", "numpy", "folium", "ipywidgets", "ablog (>=0.10.13,<0.11.0)"]
+code_style = ["pre-commit (>=2.7.0,<2.8.0)"]
+
+[[package]]
+name = "sphinx-comments"
+version = "0.0.3"
+description = "Add comments and annotation to your documentation."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+sphinx = ">=1.8"
+
+[package.extras]
+testing = ["sphinx-book-theme", "sphinx (>=2)", "pytest-regressions", "pytest", "myst-parser", "beautifulsoup4"]
+sphinx = ["myst-parser", "sphinx-book-theme", "sphinx (>=2)"]
+code_style = ["pre-commit (==1.17.0)", "black", "flake8 (>=3.7.0,<3.8.0)"]
+
+[[package]]
+name = "sphinx-copybutton"
+version = "0.5.0"
+description = "Add a copy button to each of your code cells."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+sphinx = ">=1.8"
+
+[package.extras]
+rtd = ["sphinx-book-theme", "myst-nb", "ipython", "sphinx"]
+code_style = ["pre-commit (==2.12.1)"]
+
+[[package]]
+name = "sphinx-design"
+version = "0.1.0"
+description = "A sphinx extension for designing beautiful, view size responsive web components."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+sphinx = ">=3,<5"
+
+[package.extras]
+theme_sbt = ["sphinx-book-theme (>=0.3.0,<0.4.0)"]
+theme_rtd = ["sphinx-rtd-theme (>=1.0,<2.0)"]
+theme_pydata = ["pydata-sphinx-theme (>=0.8.1,<0.9.0)"]
+theme_furo = ["furo (==2022.04.07)"]
+testing = ["pytest-regressions", "pytest-cov", "pytest (>=6.2,<7.0)", "myst-parser (>=0.17.0,<0.18.0)"]
+rtd = ["myst-parser (>=0.17.0,<0.18.0)"]
+code_style = ["pre-commit (>=2.12,<3.0)"]
+
+[[package]]
+name = "sphinx-external-toc"
+version = "0.2.4"
+description = "A sphinx extension that allows the site-map to be defined in a single YAML file."
+category = "dev"
+optional = false
+python-versions = "~=3.6"
+
+[package.dependencies]
+attrs = ">=20.3,<22"
+click = ">=7.1,<9"
+pyyaml = "*"
+sphinx = ">=3,<5"
+
+[package.extras]
+testing = ["pytest-regressions", "pytest-cov", "pytest (>=3.6,<4)", "coverage"]
+rtd = ["sphinx-book-theme (>=0.0.36)", "myst-parser (>=0.15.0,<0.16.0)"]
+code_style = ["pre-commit (>=2.12,<3.0)"]
+
+[[package]]
+name = "sphinx-jupyterbook-latex"
+version = "0.4.7"
+description = "Latex specific features for jupyter book"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+importlib-resources = {version = "*", markers = "python_version < \"3.9\""}
+sphinx = ">=3,<5"
+
+[package.extras]
+code_style = ["pre-commit (>=2.12,<3.0)"]
+myst = ["myst-nb (>=0.13,<0.14)"]
+rtd = ["myst-parser", "sphinx-book-theme", "sphinx-design", "sphinx-jupyterbook-latex"]
+testing = ["coverage (<5.0)", "myst-nb (>=0.13,<0.14)", "pytest (>=3.6,<4)", "pytest-cov (>=2.8,<3.0)", "pytest-regressions", "sphinx-external-toc (>=0.1.0,<0.3.0)", "sphinxcontrib-bibtex (>=2.2.1,<2.3.0)", "texsoup"]
+
+[[package]]
+name = "sphinx-multitoc-numbering"
+version = "0.1.3"
+description = "Supporting continuous HTML section numbering"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+sphinx = ">=3"
+
+[package.extras]
+testing = ["jupyter-book", "pytest-regressions", "coverage (<5.0)", "pytest-cov (>=2.8,<3.0)", "pytest (>=5.4,<6.0)"]
+rtd = ["myst-parser", "sphinx-book-theme", "sphinx (>=3.0)"]
+code_style = ["pre-commit (==1.17.0)", "black", "flake8 (>=3.7.0,<3.8.0)"]
+
+[[package]]
+name = "sphinx-thebe"
+version = "0.1.2"
+description = "Integrate interactive code blocks into your documentation with Thebe and Binder."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+sphinx = ">=3.5,<5"
+
+[package.extras]
+testing = ["beautifulsoup4", "pytest-regressions", "pytest", "matplotlib"]
+sphinx = ["sphinx-panels", "sphinx-copybutton", "sphinx-book-theme", "myst-nb", "matplotlib"]
+
+[[package]]
+name = "sphinx-togglebutton"
+version = "0.3.2"
+description = "Toggle page content and collapse admonitions in Sphinx."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+docutils = "*"
+sphinx = "*"
+
+[package.extras]
+sphinx = ["sphinx-examples", "sphinx-design", "sphinx-book-theme", "myst-nb", "numpy", "matplotlib"]
+
+[[package]]
+name = "sphinxcontrib-applehelp"
+version = "1.0.2"
+description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.extras]
+lint = ["flake8", "mypy", "docutils-stubs"]
+test = ["pytest"]
+
+[[package]]
+name = "sphinxcontrib-bibtex"
+version = "2.5.0"
+description = "Sphinx extension for BibTeX style citations."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+docutils = ">=0.8"
+importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""}
+pybtex = ">=0.24"
+pybtex-docutils = ">=1.0.0"
+Sphinx = ">=2.1"
+
+[[package]]
+name = "sphinxcontrib-devhelp"
+version = "1.0.2"
+description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.extras]
+lint = ["flake8", "mypy", "docutils-stubs"]
+test = ["pytest"]
+
+[[package]]
+name = "sphinxcontrib-htmlhelp"
+version = "2.0.0"
+description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+lint = ["flake8", "mypy", "docutils-stubs"]
+test = ["pytest", "html5lib"]
+
+[[package]]
+name = "sphinxcontrib-jsmath"
+version = "1.0.1"
+description = "A sphinx extension which renders display math in HTML via JavaScript"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.extras]
+test = ["pytest", "flake8", "mypy"]
+
+[[package]]
+name = "sphinxcontrib-qthelp"
+version = "1.0.3"
+description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.extras]
+lint = ["flake8", "mypy", "docutils-stubs"]
+test = ["pytest"]
+
+[[package]]
+name = "sphinxcontrib-serializinghtml"
+version = "1.1.5"
+description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.extras]
+lint = ["flake8", "mypy", "docutils-stubs"]
+test = ["pytest"]
+
+[[package]]
+name = "sqlalchemy"
+version = "1.4.42"
+description = "Database Abstraction Library"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
+
+[package.dependencies]
+greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"}
+
+[package.extras]
+aiomysql = ["greenlet (!=0.4.17)", "aiomysql"]
+aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"]
+asyncio = ["greenlet (!=0.4.17)"]
+asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3,!=0.2.4)"]
+mariadb_connector = ["mariadb (>=1.0.1,!=1.1.2)"]
+mssql = ["pyodbc"]
+mssql_pymssql = ["pymssql"]
+mssql_pyodbc = ["pyodbc"]
+mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"]
+mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"]
+mysql_connector = ["mysql-connector-python"]
+oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"]
+postgresql = ["psycopg2 (>=2.7)"]
+postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"]
+postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
+postgresql_psycopg2binary = ["psycopg2-binary"]
+postgresql_psycopg2cffi = ["psycopg2cffi"]
+pymysql = ["pymysql (<1)", "pymysql"]
+sqlcipher = ["sqlcipher3-binary"]
+
+[[package]]
+name = "stack-data"
+version = "0.5.1"
+description = "Extract data from python stack frames and tracebacks for informative displays"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+asttokens = "*"
+executing = "*"
+pure-eval = "*"
+
+[package.extras]
+tests = ["cython", "littleutils", "pygments", "typeguard", "pytest"]
+
+[[package]]
+name = "statsmodels"
+version = "0.13.2"
+description = "Statistical computations and models for Python"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+numpy = ">=1.17"
+packaging = ">=21.3"
+pandas = ">=0.25"
+patsy = ">=0.5.2"
+scipy = ">=1.3"
+
+[package.extras]
+build = ["cython (>=0.29.26)"]
+develop = ["cython (>=0.29.26)"]
+docs = ["sphinx", "nbconvert", "jupyter-client", "ipykernel", "matplotlib", "nbformat", "numpydoc", "pandas-datareader"]
+
+[[package]]
+name = "terminado"
+version = "0.17.0"
+description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+ptyprocess = {version = "*", markers = "os_name != \"nt\""}
+pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""}
+tornado = ">=6.1.0"
+
+[package.extras]
+docs = ["pydata-sphinx-theme", "sphinx"]
+test = ["pre-commit", "pytest-timeout", "pytest (>=7.0)"]
+
+[[package]]
+name = "tinycss2"
+version = "1.2.1"
+description = "A tiny CSS parser"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+webencodings = ">=0.4"
+
+[package.extras]
+test = ["flake8", "isort", "pytest"]
+doc = ["sphinx-rtd-theme", "sphinx"]
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "tornado"
+version = "6.2"
+description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
+category = "dev"
+optional = false
+python-versions = ">= 3.7"
+
+[[package]]
+name = "traitlets"
+version = "5.5.0"
+description = ""
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
+test = ["pre-commit", "pytest"]
+
+[[package]]
+name = "typeguard"
+version = "2.13.3"
+description = "Run-time type checker for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.5.3"
+
+[package.extras]
+doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
+test = ["pytest", "typing-extensions", "mypy"]
+
+[[package]]
+name = "typing-extensions"
+version = "4.4.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "uc-micro-py"
+version = "1.0.1"
+description = "Micro subset of unicode data files for linkify-it-py projects."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+test = ["pytest-cov", "pytest", "coverage"]
+
+[[package]]
+name = "urllib3"
+version = "1.26.12"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
+
+[package.extras]
+brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
+secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
+[[package]]
+name = "virtualenv"
+version = "20.16.6"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+distlib = ">=0.3.6,<1"
+filelock = ">=3.4.1,<4"
+platformdirs = ">=2.4,<3"
+
+[package.extras]
+docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"]
+testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"]
+
+[[package]]
+name = "wcwidth"
+version = "0.2.5"
+description = "Measures the displayed width of unicode strings in a terminal"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "webencodings"
+version = "0.5.1"
+description = "Character encoding aliases for legacy web content"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "websocket-client"
+version = "1.4.1"
+description = "WebSocket client for Python with low level API options"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"]
+optional = ["python-socks", "wsaccel"]
+test = ["websockets"]
+
+[[package]]
+name = "widgetsnbextension"
+version = "3.6.1"
+description = "IPython HTML widgets for Jupyter"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+notebook = ">=4.4.1"
+
+[[package]]
+name = "xdoctest"
+version = "1.1.0"
+description = "A rewrite of the builtin doctest module"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+six = "*"
+
+[package.extras]
+tests = ["pytest-cov", "pytest", "typing", "pytest", "pytest-cov", "pytest", "pytest-cov", "pytest", "pytest", "pytest-cov", "pytest", "scikit-build", "pybind11", "ninja", "codecov", "cmake"]
+tests-strict = ["pytest-cov (==3.0.0)", "pytest (==6.2.5)", "typing (==3.7.4)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest-cov (==2.9.0)", "pytest (==4.6.0)", "pytest-cov (==2.8.1)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest-cov (==2.8.1)", "scikit-build (==0.11.1)", "pybind11 (==2.7.1)", "ninja (==1.10.2)", "codecov (==2.0.15)", "cmake (==3.21.2)"]
+runtime-strict = ["six (==1.11.0)"]
+optional = ["ipykernel", "ipython", "jupyter-client", "nbconvert", "jinja2", "jupyter-core", "jedi", "attrs", "pygments", "ipython-genutils", "debugpy", "debugpy", "debugpy", "ipykernel", "debugpy", "ipython", "jupyter-client", "pygments", "tomli", "debugpy", "colorama", "pyflakes"]
+optional-strict = ["ipykernel (==6.0.0)", "IPython (==7.23.1)", "jupyter-client (==7.0.0)", "nbconvert (==6.0.0)", "jinja2 (==3.0.0)", "jupyter-core (==4.7.0)", "jedi (==0.16)", "attrs (==19.2.0)", "Pygments (==2.4.1)", "ipython-genutils (==0.2.0)", "debugpy (==1.6.0)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "ipykernel (==5.2.0)", "debugpy (==1.0.0)", "IPython (==7.10.0)", "jupyter-client (==6.1.5)", "Pygments (==2.0.0)", "tomli (==0.2.0)", "debugpy (==1.3.0)", "colorama (==0.4.1)", "pyflakes (==2.2.0)"]
+jupyter = ["ipykernel", "ipython", "jupyter-client", "nbconvert", "jinja2", "jupyter-core", "jedi", "attrs", "ipython-genutils", "debugpy", "debugpy", "debugpy", "ipykernel", "debugpy", "ipython", "jupyter-client", "debugpy"]
+colors = ["pygments", "pygments", "colorama"]
+all = ["ipykernel", "ipython", "jupyter-client", "nbconvert", "pytest-cov", "jinja2", "jupyter-core", "jedi", "attrs", "pygments", "pytest", "ipython-genutils", "debugpy", "typing", "debugpy", "debugpy", "pytest", "ipykernel", "debugpy", "ipython", "jupyter-client", "pytest-cov", "pytest", "pytest-cov", "pytest", "pygments", "pytest", "debugpy", "pytest-cov", "pytest", "colorama", "six", "scikit-build", "pybind11", "ninja", "codecov", "cmake"]
+all-strict = ["ipykernel (==6.0.0)", "IPython (==7.23.1)", "jupyter-client (==7.0.0)", "nbconvert (==6.0.0)", "pytest-cov (==3.0.0)", "jinja2 (==3.0.0)", "jupyter-core (==4.7.0)", "jedi (==0.16)", "attrs (==19.2.0)", "Pygments (==2.4.1)", "pytest (==6.2.5)", "ipython-genutils (==0.2.0)", "debugpy (==1.6.0)", "typing (==3.7.4)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "pytest (==4.6.0)", "ipykernel (==5.2.0)", "debugpy (==1.0.0)", "IPython (==7.10.0)", "jupyter-client (==6.1.5)", "pytest (==4.6.0)", "pytest-cov (==2.9.0)", "pytest (==4.6.0)", "pytest-cov (==2.8.1)", "Pygments (==2.0.0)", "pytest (==4.6.0)", "debugpy (==1.3.0)", "pytest (==4.6.0)", "pytest-cov (==2.8.1)", "colorama (==0.4.1)", "six (==1.11.0)", "scikit-build (==0.11.1)", "pybind11 (==2.7.1)", "ninja (==1.10.2)", "codecov (==2.0.15)", "cmake (==3.21.2)"]
+
+[[package]]
+name = "zipp"
+version = "3.10.0"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "jaraco.functools", "more-itertools", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
+
+[metadata]
+lock-version = "1.1"
+python-versions = ">=3.8,<4.0.0"
+content-hash = "c1c2339f7d2380b9feac354b60fd6a90df1fc9961a50c32b2d6438fd85b906e3"
+
+[metadata.files]
+alabaster = [
+    {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"},
+    {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
+]
+anyio = []
+appnope = [
+    {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"},
+    {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"},
+]
+argcomplete = []
+argon2-cffi = [
+    {file = "argon2-cffi-21.3.0.tar.gz", hash = "sha256:d384164d944190a7dd7ef22c6aa3ff197da12962bd04b17f64d4e93d934dba5b"},
+    {file = "argon2_cffi-21.3.0-py3-none-any.whl", hash = "sha256:8c976986f2c5c0e5000919e6de187906cfd81fb1c72bf9d88c01177e77da7f80"},
+]
+argon2-cffi-bindings = [
+    {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"},
+    {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"},
+    {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"},
+    {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"},
+    {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"},
+    {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"},
+    {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"},
+    {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"},
+    {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"},
+    {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"},
+    {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"},
+    {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"},
+    {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"},
+]
+asttokens = []
+attrs = [
+    {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
+    {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
+]
+babel = [
+    {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"},
+    {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"},
+]
+backcall = [
+    {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
+    {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
+]
+beautifulsoup4 = [
+    {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"},
+    {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"},
+]
+black = []
+bleach = []
+certifi = []
+cffi = []
+cfgv = [
+    {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
+    {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
+]
+charset-normalizer = []
+classify-imports = []
+click = [
+    {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
+    {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
+]
+colorama = []
+colorlog = []
+contourpy = []
+coverage = []
+cycler = [
+    {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"},
+    {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"},
+]
+debugpy = []
+decorator = [
+    {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
+    {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
+]
+defusedxml = [
+    {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
+    {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
+]
+distlib = []
+docutils = []
+dparse = []
+entrypoints = [
+    {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"},
+    {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"},
+]
+exceptiongroup = []
+executing = []
+fastjsonschema = []
+filelock = []
+flake8 = []
+fonttools = []
+furo = []
+ghp-import = []
+gitdb = [
+    {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"},
+    {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"},
+]
+gitpython = []
+greenlet = []
+identify = []
+idna = []
+imagesize = []
+importlib-metadata = []
+importlib-resources = []
+iniconfig = [
+    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
+ipykernel = []
+ipython = []
+ipython-genutils = [
+    {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"},
+    {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"},
+]
+ipywidgets = []
+jedi = [
+    {file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"},
+    {file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"},
+]
+jinja2 = [
+    {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
+    {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
+]
+jsonschema = [
+    {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"},
+    {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"},
+]
+jupyter-book = []
+jupyter-cache = []
+jupyter-client = []
+jupyter-core = []
+jupyter-server = []
+jupyter-server-mathjax = []
+jupyter-sphinx = []
+jupyterlab-pygments = [
+    {file = "jupyterlab_pygments-0.2.2-py2.py3-none-any.whl", hash = "sha256:2405800db07c9f770863bcf8049a529c3dd4d3e28536638bd7c1c01d2748309f"},
+    {file = "jupyterlab_pygments-0.2.2.tar.gz", hash = "sha256:7405d7fde60819d905a9fa8ce89e4cd830e318cdad22a0030f7a901da705585d"},
+]
+jupyterlab-widgets = []
+kiwisolver = []
+latexcodec = []
+linkify-it-py = []
+lxml = []
+markdown-it-py = [
+    {file = "markdown-it-py-1.1.0.tar.gz", hash = "sha256:36be6bb3ad987bfdb839f5ba78ddf094552ca38ccbd784ae4f74a4e1419fc6e3"},
+    {file = "markdown_it_py-1.1.0-py3-none-any.whl", hash = "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389"},
+]
+markupsafe = [
+    {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
+    {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
+    {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
+    {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
+    {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
+    {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
+    {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
+    {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
+    {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
+    {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
+    {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
+    {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
+    {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
+    {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
+    {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
+    {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
+    {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
+    {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
+    {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
+    {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
+    {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
+    {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
+    {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
+    {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
+    {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
+    {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
+    {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
+    {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
+    {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
+    {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
+    {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
+    {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
+    {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
+    {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
+    {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
+    {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
+    {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
+    {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
+    {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
+    {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
+]
+matplotlib = []
+matplotlib-inline = []
+mccabe = []
+mdit-py-plugins = [
+    {file = "mdit-py-plugins-0.2.8.tar.gz", hash = "sha256:5991cef645502e80a5388ec4fc20885d2313d4871e8b8e320ca2de14ac0c015f"},
+    {file = "mdit_py_plugins-0.2.8-py3-none-any.whl", hash = "sha256:1833bf738e038e35d89cb3a07eb0d227ed647ce7dd357579b65343740c6d249c"},
+]
+mistune = [
+    {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"},
+    {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"},
+]
+mypy = []
+mypy-extensions = [
+    {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
+    {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
+]
+myst-nb = []
+myst-parser = []
+nbclassic = []
+nbclient = [
+    {file = "nbclient-0.5.13-py3-none-any.whl", hash = "sha256:47ac905af59379913c1f8f541098d2550153cf8dc58553cbe18c702b181518b0"},
+    {file = "nbclient-0.5.13.tar.gz", hash = "sha256:40c52c9b5e3c31faecaee69f202b3f53e38d7c1c563de0fadde9d7eda0fdafe8"},
+]
+nbconvert = []
+nbdime = []
+nbformat = []
+nbstripout = []
+nest-asyncio = []
+nodeenv = []
+notebook = []
+notebook-shim = []
+nox = []
+numpy = []
+packaging = [
+    {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
+    {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
+]
+pandas = []
+pandocfilters = [
+    {file = "pandocfilters-1.5.0-py2.py3-none-any.whl", hash = "sha256:33aae3f25fd1a026079f5d27bdd52496f0e0803b3469282162bafdcbdf6ef14f"},
+    {file = "pandocfilters-1.5.0.tar.gz", hash = "sha256:0b679503337d233b4339a817bfc8c50064e2eff681314376a47cb582305a7a38"},
+]
+parso = [
+    {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
+    {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"},
+]
+pathspec = []
+patsy = []
+pexpect = [
+    {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
+    {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
+]
+pickleshare = [
+    {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
+    {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
+]
+pillow = []
+platformdirs = [
+    {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
+    {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
+]
+pluggy = [
+    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
+    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+]
+pre-commit = []
+pre-commit-hooks = [
+    {file = "pre_commit_hooks-4.3.0-py2.py3-none-any.whl", hash = "sha256:9ccaf7c98794659d345080ee1ea0256a55ae059675045eebdbbc17c0be8c7e4b"},
+    {file = "pre_commit_hooks-4.3.0.tar.gz", hash = "sha256:fda598a4c834d030727e6a615722718b47510f4bed72df4c949f95ba9f5aaf88"},
+]
+prometheus-client = []
+prompt-toolkit = []
+psutil = []
+ptyprocess = [
+    {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
+    {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
+]
+pure-eval = [
+    {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
+    {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
+]
+py = [
+    {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
+    {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
+]
+pybtex = []
+pybtex-docutils = []
+pycodestyle = []
+pycparser = [
+    {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
+    {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
+]
+pydata-sphinx-theme = []
+pyflakes = []
+pygments = []
+pyparsing = [
+    {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
+    {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
+]
+pyrsistent = [
+    {file = "pyrsistent-0.18.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1"},
+    {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26"},
+    {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e"},
+    {file = "pyrsistent-0.18.1-cp310-cp310-win32.whl", hash = "sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6"},
+    {file = "pyrsistent-0.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec"},
+    {file = "pyrsistent-0.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b"},
+    {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc"},
+    {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22"},
+    {file = "pyrsistent-0.18.1-cp37-cp37m-win32.whl", hash = "sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8"},
+    {file = "pyrsistent-0.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286"},
+    {file = "pyrsistent-0.18.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6"},
+    {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec"},
+    {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c"},
+    {file = "pyrsistent-0.18.1-cp38-cp38-win32.whl", hash = "sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca"},
+    {file = "pyrsistent-0.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a"},
+    {file = "pyrsistent-0.18.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5"},
+    {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045"},
+    {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c"},
+    {file = "pyrsistent-0.18.1-cp39-cp39-win32.whl", hash = "sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc"},
+    {file = "pyrsistent-0.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07"},
+    {file = "pyrsistent-0.18.1.tar.gz", hash = "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96"},
+]
+pytest = []
+python-dateutil = [
+    {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
+    {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
+]
+pytz = []
+pywin32 = [
+    {file = "pywin32-304-cp310-cp310-win32.whl", hash = "sha256:3c7bacf5e24298c86314f03fa20e16558a4e4138fc34615d7de4070c23e65af3"},
+    {file = "pywin32-304-cp310-cp310-win_amd64.whl", hash = "sha256:4f32145913a2447736dad62495199a8e280a77a0ca662daa2332acf849f0be48"},
+    {file = "pywin32-304-cp310-cp310-win_arm64.whl", hash = "sha256:d3ee45adff48e0551d1aa60d2ec066fec006083b791f5c3527c40cd8aefac71f"},
+    {file = "pywin32-304-cp311-cp311-win32.whl", hash = "sha256:30c53d6ce44c12a316a06c153ea74152d3b1342610f1b99d40ba2795e5af0269"},
+    {file = "pywin32-304-cp311-cp311-win_amd64.whl", hash = "sha256:7ffa0c0fa4ae4077e8b8aa73800540ef8c24530057768c3ac57c609f99a14fd4"},
+    {file = "pywin32-304-cp311-cp311-win_arm64.whl", hash = "sha256:cbbe34dad39bdbaa2889a424d28752f1b4971939b14b1bb48cbf0182a3bcfc43"},
+    {file = "pywin32-304-cp36-cp36m-win32.whl", hash = "sha256:be253e7b14bc601718f014d2832e4c18a5b023cbe72db826da63df76b77507a1"},
+    {file = "pywin32-304-cp36-cp36m-win_amd64.whl", hash = "sha256:de9827c23321dcf43d2f288f09f3b6d772fee11e809015bdae9e69fe13213988"},
+    {file = "pywin32-304-cp37-cp37m-win32.whl", hash = "sha256:f64c0377cf01b61bd5e76c25e1480ca8ab3b73f0c4add50538d332afdf8f69c5"},
+    {file = "pywin32-304-cp37-cp37m-win_amd64.whl", hash = "sha256:bb2ea2aa81e96eee6a6b79d87e1d1648d3f8b87f9a64499e0b92b30d141e76df"},
+    {file = "pywin32-304-cp38-cp38-win32.whl", hash = "sha256:94037b5259701988954931333aafd39cf897e990852115656b014ce72e052e96"},
+    {file = "pywin32-304-cp38-cp38-win_amd64.whl", hash = "sha256:ead865a2e179b30fb717831f73cf4373401fc62fbc3455a0889a7ddac848f83e"},
+    {file = "pywin32-304-cp39-cp39-win32.whl", hash = "sha256:25746d841201fd9f96b648a248f731c1dec851c9a08b8e33da8b56148e4c65cc"},
+    {file = "pywin32-304-cp39-cp39-win_amd64.whl", hash = "sha256:d24a3382f013b21aa24a5cfbfad5a2cd9926610c0affde3e8ab5b3d7dbcf4ac9"},
+]
+pywinpty = []
+pyyaml = [
+    {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
+    {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
+    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
+    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
+    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
+    {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
+    {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
+    {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
+    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
+    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
+    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
+    {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
+    {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
+    {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
+    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
+    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
+    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
+    {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
+    {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
+    {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
+    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
+    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
+    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
+    {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
+    {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
+    {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
+    {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
+    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
+    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
+    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
+    {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
+    {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
+    {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
+]
+pyzmq = []
+reorder-python-imports = []
+requests = []
+"ruamel.yaml" = [
+    {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"},
+    {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"},
+]
+"ruamel.yaml.clib" = []
+safety = []
+scipy = []
+send2trash = [
+    {file = "Send2Trash-1.8.0-py3-none-any.whl", hash = "sha256:f20eaadfdb517eaca5ce077640cb261c7d2698385a6a0f072a4a5447fd49fa08"},
+    {file = "Send2Trash-1.8.0.tar.gz", hash = "sha256:d2c24762fd3759860a0aff155e45871447ea58d2be6bdd39b5c8f966a0c99c2d"},
+]
+setuptools-scm = []
+six = [
+    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+smmap = [
+    {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
+    {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
+]
+sniffio = []
+snowballstemmer = [
+    {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
+    {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
+]
+soupsieve = [
+    {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"},
+    {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"},
+]
+sphinx = []
+sphinx-basic-ng = []
+sphinx-book-theme = []
+sphinx-comments = []
+sphinx-copybutton = []
+sphinx-design = []
+sphinx-external-toc = []
+sphinx-jupyterbook-latex = []
+sphinx-multitoc-numbering = []
+sphinx-thebe = []
+sphinx-togglebutton = []
+sphinxcontrib-applehelp = [
+    {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
+    {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},
+]
+sphinxcontrib-bibtex = []
+sphinxcontrib-devhelp = [
+    {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"},
+    {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"},
+]
+sphinxcontrib-htmlhelp = [
+    {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"},
+    {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"},
+]
+sphinxcontrib-jsmath = [
+    {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
+    {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
+]
+sphinxcontrib-qthelp = [
+    {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"},
+    {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"},
+]
+sphinxcontrib-serializinghtml = [
+    {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"},
+    {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"},
+]
+sqlalchemy = []
+stack-data = []
+statsmodels = []
+terminado = []
+tinycss2 = []
+toml = [
+    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+tomli = [
+    {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+    {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+tornado = []
+traitlets = []
+typeguard = [
+    {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"},
+    {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"},
+]
+typing-extensions = []
+uc-micro-py = []
+urllib3 = []
+virtualenv = []
+wcwidth = [
+    {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
+    {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
+]
+webencodings = [
+    {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
+    {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
+]
+websocket-client = []
+widgetsnbextension = []
+xdoctest = []
+zipp = []
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..2593549
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,71 @@
+[tool.poetry]
+name = "specification_curve"
+version = "0.2.6"
+description = "Specification_Curve"
+authors = ["aeturrell <mail@mail.com>"]
+license = "MIT"
+readme = "README.md"
+homepage = "https://aeturrell.github.io/specification_curve/"
+repository = "https://github.com/aeturrell/specification_curve"
+documentation = "https://aeturrell.github.io/specification_curve/"
+classifiers = [
+    "Development Status :: 3 - Alpha",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+]
+
+[tool.poetry.urls]
+Changelog = "https://github.com/aeturrell/specification_curve/releases"
+
+[tool.poetry.dependencies]
+python = ">=3.8,<4.0.0"
+pandas = "^1.5.1"
+statsmodels = "^0.13.2"
+matplotlib = "^3.6.1"
+
+[tool.poetry.dev-dependencies]
+black = "^22.10.0"
+pre-commit = "^2.20.0"
+Sphinx = "^4.0.0"
+typeguard = "^2.13.3"
+safety = "^2.3.1"
+coverage = "^6.5.0"
+pytest = "^7.2.0"
+flake8 = "^5.0.4"
+pre-commit-hooks = "^4.3.0"
+nox = "^2022.8.7"
+ipykernel = "^6.16.2"
+nbstripout = "^0.6.1"
+jupyter-book = "^0.13.1"
+furo = "^2022.9.29"
+ghp-import = "^2.1.0"
+xdoctest = "^1.1.0"
+mypy = "^0.982"
+reorder-python-imports = "^3.9.0"
+sphinxcontrib-bibtex = "^2.5.0"
+
+[tool.poetry.scripts]
+specification_curve = "specification_curve.__main__:main"
+
+[tool.coverage.paths]
+source = ["src", "*/site-packages"]
+
+[tool.coverage.run]
+branch = true
+source = ["specification_curve"]
+
+[tool.coverage.report]
+show_missing = true
+fail_under = 98
+
+[tool.mypy]
+strict = true
+pretty = true
+show_column_numbers = true
+show_error_codes = true
+show_error_context = true
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/requirements_dev.txt b/requirements_dev.txt
deleted file mode 100644
index e21489e..0000000
--- a/requirements_dev.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-pip==21.2.4
-bump2version==1.0.1
-wheel==0.37.0
-watchdog==2.1.5
-flake8==3.9.2
-tox==3.24.3
-coverage==5.5
-Sphinx==4.2.0
-twine==3.4.2
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 1be0b76..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,21 +0,0 @@
-[bumpversion]
-current_version = 0.2.6
-commit = True
-tag = True
-
-[bumpversion:file:setup.py]
-search = version='{current_version}'
-replace = version='{new_version}'
-
-[bumpversion:file:specification_curve/__init__.py]
-search = __version__ = '{current_version}'
-replace = __version__ = '{new_version}'
-
-[bdist_wheel]
-universal = 1
-
-[flake8]
-exclude = docs
-
-[aliases]
-
diff --git a/setup.py b/setup.py
deleted file mode 100644
index d3a1e6d..0000000
--- a/setup.py
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/env python
-
-"""The setup script."""
-
-from setuptools import setup, find_packages
-
-with open('README.rst') as readme_file:
-    readme = readme_file.read()
-
-with open('HISTORY.rst') as history_file:
-    history = history_file.read()
-
-requirements = ['statsmodels', 'matplotlib', 'numpy', 'pandas',
-                'scikit-learn']
-
-setup_requirements = [ ]
-
-test_requirements = ['numpy', 'pandas', 'statsmodels']
-
-setup(
-    author="Arthur Turrell",
-    author_email='a.turrell09@imperial.ac.uk',
-    python_requires='>=3.6',
-    classifiers=[
-        'Development Status :: 3 - Alpha',
-        'Intended Audience :: Developers',
-        'License :: OSI Approved :: MIT License',
-        'Natural Language :: English',
-        'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.6',
-        'Programming Language :: Python :: 3.7',
-        'Programming Language :: Python :: 3.8',
-    ],
-    description="Specification Curve is a Python package that performs specification curve analysis.",
-    install_requires=requirements,
-    license="MIT license",
-    long_description=readme + '\n\n' + history,
-    include_package_data=True,
-    keywords='specification_curve',
-    name='specification_curve',
-    packages=find_packages(include=['specification_curve', 'specification_curve.*']),
-    setup_requires=setup_requirements,
-    test_suite='tests',
-    tests_require=test_requirements,
-    url='https://github.com/aeturrell/specification_curve',
-    version='0.2.6',
-    zip_safe=False,
-)
diff --git a/specification_curve/__init__.py b/specification_curve/__init__.py
deleted file mode 100644
index 52c4ca5..0000000
--- a/specification_curve/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""Top-level package for Specification Curve."""
-
-__author__ = """Arthur Turrell"""
-__email__ = 'a.turrell09@imperial.ac.uk'
-__version__ = '0.2.6'
diff --git a/specification_curve/example.py b/specification_curve/example.py
deleted file mode 100644
index 343b705..0000000
--- a/specification_curve/example.py
+++ /dev/null
@@ -1,107 +0,0 @@
-"""
-Example datasets
-----------------
-To try out specification curve analyses.
-"""
-
-
-import numpy as np
-import pandas as pd
-import os
-import pkg_resources
-
-
-EXAMPLE_FILE = (pkg_resources
-                .resource_filename('specification_curve',
-                                   os.path.join('data',
-                                                'example_data.csv')))
-
-
-def load_example_data1():
-    # Example data
-    df = pd.read_csv(EXAMPLE_FILE,
-                     index_col=0)
-    num_cols = [x for x in df.columns if x not in ['group1', 'group2']]
-    for col in num_cols:
-        df[col] = df[col].astype(np.double)
-    cat_cols = [x for x in df.columns if x not in num_cols]
-    for col in cat_cols:
-        df[col] = df[col].astype('category')
-    return df
-
-
-def load_example_data2():
-    n_samples = 300
-    np.random.seed(1332)
-    x_1 = np.random.random(size=n_samples)
-    x_2 = np.random.random(size=n_samples)
-    x_3 = np.random.random(size=n_samples)
-    x_4 = np.random.randint(2, size=n_samples)
-    y = (0.8*x_1 + 0.1*x_2 + 0.5*x_3 + x_4*0.6 +
-         + 2*np.random.randn(n_samples))
-    df = pd.DataFrame([x_1, x_2, x_3, x_4, y],
-                      ['x_1', 'x_2', 'x_3', 'x_4', 'y']).T
-    # Set x_4 as a categorical variable
-    df['x_4'] = df['x_4'].astype('category')
-    return df
-
-
-# Example chart:
-
-# df = load_example_data1()
-# y_endog = ['y2']
-# x_exog = ['x1', 'x2']
-# controls = ['c1', 'c2', 'group1', 'group2']
-# df_r = sc.spec_curve(df, y_endog, x_exog, controls,
-#                      cat_expand=['group2'],
-#                      save_path='example.png')
-
-# # Example dataset 1
-# df = load_example_data1()
-# x_exog = 'x2'
-# y_endog = 'y2'
-# ctrls = ['c1', 'c2', 'group1', 'group2']
-
-# df_r = sc.spec_curve(df, y_endog, x_exog, ctrls)
-# df_r = sc.spec_curve(df, y_endog, x_exog, ctrls,
-#                      cat_expand=['group1'])
-# df_r = sc.spec_curve(df, y_endog, x_exog, ctrls,
-#                      exclu_grps=[['c1', 'c2']])
-# df_r = sc.spec_curve(df, y_endog, x_exog, ctrls,
-#                      cat_expand=['group1', 'group2'])
-# df_r = sc.spec_curve(df, y_endog, x_exog, ctrls,
-#                      cat_expand=['group1', 'group2'],
-#                      exclu_grps=[['c1', 'c2']])
-# # Desired behaviour here is no overlap between grp1 and grp2 TODO
-# df_r = sc.spec_curve(df, y_endog, x_exog, ctrls,
-#                      cat_expand=['group1', 'group2'],
-#                      exclu_grps=[['group1', 'group2']])
-# # Tests list interpretation
-# df_r = sc.spec_curve(df, y_endog, x_exog, ctrls,
-#                      cat_expand='group1',
-#                      exclu_grps=['c1', 'c2'])
-
-# df = load_example_data1()
-# x_exog = ['x1', 'x2']
-# y_endog = ['y1', 'y2']
-# ctrls = ['c1', 'c2', 'group1', 'group2']
-# controls = ctrls
-# df_r = sc.spec_curve(df, y_endog, x_exog, ctrls)
-# df_r = sc.spec_curve(df, y_endog, x_exog, ctrls,
-#                      cat_expand=['group1', 'group2'])
-# # same issue affects this one:
-# x_exog = 'x2'
-# y_endog = 'y2'
-# df_r = sc.spec_curve(df, y_endog, x_exog, ctrls,
-#                      cat_expand=['group1'])
-
-# df_r = sc.spec_curve(df, y_endog, x_exog, ctrls,
-#                      cat_expand=['group1'],
-#                      save_path='here.pdf')
-
-# # Example dataset 2
-# df = load_example_data2()
-# y_endog = 'y'
-# x_exog = 'x_1'
-# controls = ['x_2', 'x_3', 'x_4', 'x_5']
-# exclu_grps = [['x_4', 'x_5']]
diff --git a/specification_curve/specification_curve.py b/specification_curve/specification_curve.py
deleted file mode 100644
index dbd0b97..0000000
--- a/specification_curve/specification_curve.py
+++ /dev/null
@@ -1,388 +0,0 @@
-"""
-Specification Curve
--------------------
-A package that produces specification curve analysis.
-"""
-
-import pandas as pd
-import statsmodels.api as sm
-import numpy as np
-import matplotlib.pyplot as plt
-from collections import Counter
-from itertools import combinations
-import copy
-import itertools
-import matplotlib.patches as patches
-import matplotlib.ticker as ticker
-from math import log10, floor
-
-
-def _round_to_1(x):
-    """
-    Rounds numbers to 1 s.f.
-    """
-    return round(x, -int(floor(log10(abs(x)))) + 1)
-
-
-def _double_list_check(XX):
-    """
-    If a list, return as list of lists
-    """
-    if(not(any(isinstance(el, list) for el in XX))):
-        XX = [XX]
-    return XX
-
-
-def _single_list_check_str(X):
-    """
-    If type is string, return string in list
-    """
-    if(type(X) == str):
-        X = [X]
-    return X
-
-
-def _remove_overlapping_vars(list_to_check, includes_list):
-    """ Checks, and removes, any variable in list_to_check that is also in
-    includes_list, returning list_to_check without overlapping variable
-    names.
-    """
-    return [x for x in list_to_check if x not in includes_list]
-
-
-def _pretty_plots():
-    """
-    Uses specification curve package's pretty plot style.
-    Overrides existing style.
-    """
-    jsonPlotSettings = {'ytick.labelsize': 16,
-                        'xtick.labelsize': 16,
-                        'font.size': 22,
-                        'figure.figsize': (10, 5),
-                        'axes.titlesize': 22,
-                        'axes.labelsize': 18,
-                        'lines.linewidth': 2,
-                        'lines.markersize': 3,
-                        'legend.fontsize': 11,
-                        'mathtext.fontset': 'stix',
-                        'font.family': 'STIXGeneral'}
-    plt.style.use(jsonPlotSettings)
-
-
-def _excl_combs(lst, r, excludes):
-    """lst = [1, 2, 3, 4, 5, 6]
-       excludes = [{1, 3}]
-       gen = _excl_combs(lst, 2, excludes)
-    """
-    if(excludes != [[]]):
-        return [comb for comb in combinations(lst, r)
-                if not any(e.issubset(comb) for e in excludes)]
-    else:
-        return list(combinations(lst, r))
-
-
-def _flatn_list(nested_list):
-    """ Flattens nested list """
-    return list(itertools.chain.from_iterable(nested_list))
-
-
-class SpecificationCurve():
-    """
-    Specification curve object
-
-    Uses a model to perform all variants of a specification.
-    Stores the results of those regressions in a tidy format pandas dataframe.
-    Plots the regressions in chart that can optionally be saved.
-    Will iterate over multiple inputs for exog. and endog. variables.
-    Note that categorical variables that are expanded cannot be mutually
-    excluded from other categorical variables that are expanded.
-
-    """
-    def __init__(self, df, y_endog, x_exog, controls, exclu_grps=[[]],
-                 cat_expand=[], always_include=[]):
-        """
-        :list[string] y_endog: dependent variable(s)
-        :list[string] x_exog: independent variable(s)
-        :list[string] controls: variables to control for
-        :list[list[string]] exclu_grps: each list should have controls
-        that are mutually exclusive in.
-        :list[string] cat_expand: categorical variables to test one at a time
-        :list[string] always_include: regressors to always include
-        """
-        self.df = df.copy()
-        self.y_endog = y_endog
-        self.x_exog = x_exog
-        self.controls = controls
-        self.exclu_grps = exclu_grps
-        self.cat_expand = cat_expand
-        self.always_include = always_include
-
-    def fit(self, estimator=sm.OLS):
-        """
-        Fits a specification curve.
-
-        :statsmodels.regression.linear_model estimator: statsmodels estimator
-        object. Default is OLS.
-        """
-        self.estimator = estimator
-        self.controls = _single_list_check_str(self.controls)
-        self.cat_expand = _single_list_check_str(self.cat_expand)
-        self.exclu_grps = _double_list_check(self.exclu_grps)
-        self.always_include = _single_list_check_str(self.always_include)
-        self.y_endog = _single_list_check_str(self.y_endog)
-        self.x_exog = _single_list_check_str(self.x_exog)
-        # If any of always include in any other list, remove it from other list
-        self.controls = _remove_overlapping_vars(self.controls,
-                                                 self.always_include)
-        self.x_exog = _remove_overlapping_vars(self.x_exog,
-                                               self.always_include)
-        self.ctrl_combs = self._compute_combinations()
-        self.df_r = self._spec_curve_regression()
-        print('Fit complete')
-
-    def _reg_func(self, y_endog, x_exog, reg_vars):
-        # NB: get dummies
-        # transforms by default any col that is object or cat
-        xf = pd.get_dummies(self.df, prefix_sep='=')
-        new_cols = [x for x in xf.columns if x not in self.df.columns]
-        gone_cols = [x for x in self.df.columns if x not in xf.columns]
-        reg_vars_here = copy.copy(reg_vars)
-        reg_vars_here.extend(new_cols)
-        [reg_vars_here.remove(x) for x in gone_cols if x in reg_vars_here]
-        return self.estimator(xf[y_endog], xf[[x_exog]+reg_vars_here]).fit()
-
-    def _compute_combinations(self):
-        """
-        Finds all possible combinations of variables.
-        Changes df to have dummies for cat expand columns.
-        """
-        init_cols = (self.y_endog + self.x_exog +
-                     self.controls + self.always_include)
-        self.df = self.df[init_cols]
-        new_cols = []
-        # Warning: hard-coded prefix
-        if(self.cat_expand != []):
-            self.df = pd.get_dummies(self.df, columns=self.cat_expand,
-                                     prefix_sep=' = ')
-            new_cols = [x for x in self.df.columns if x not in init_cols]
-            # Now change the controls
-            [self.controls.remove(x) for x in self.cat_expand]
-            self.controls.extend(new_cols)
-            # Create mapping from cat expand to new cols
-            oldnew = dict(zip([x for x in self.cat_expand],
-                              [[x for x in new_cols if y in x]
-                               for y in self.cat_expand]))
-            # Now exclude the groups that combine any new cols
-            for x in self.cat_expand:
-                if(self.exclu_grps == [[]]):
-                    self.exclu_grps = [oldnew[x]]
-                else:
-                    self.exclu_grps.append(oldnew[x])
-        # Find any subsets of excluded combs and add all variants
-        for x in self.exclu_grps:
-            if(len(x) > 2):
-                sub_combs = [list(combinations(x, y))
-                             for y in range(2, len(x))]
-                sub_combs = [item for sublist in sub_combs for item in sublist]
-                # Turn all the tuples into lists
-                sub_combs = [list(x) for x in sub_combs]
-                self.exclu_grps = self.exclu_grps + sub_combs
-        # Turn mutually exclusive groups into sets
-        if(self.exclu_grps != [[]]):
-            self.exclu_grps = [set(x) for x in self.exclu_grps]
-        # Get all combinations excluding mutually excl groups
-        ctrl_combs = [_excl_combs(self.controls, k, self.exclu_grps)
-                      for k in range(len(self.controls)+1)]
-        # Flatten this into a single list of tuples
-        ctrl_combs = _flatn_list(ctrl_combs)
-        # Turn all the tuples into lists
-        ctrl_combs = [list(x) for x in ctrl_combs]
-        # Add in always included regressors
-        ctrl_combs = [x + self.always_include for x in ctrl_combs]
-        return ctrl_combs
-
-    def _spec_curve_regression(self):
-        """Performs all regressions for a specification curve.
-
-        Estimates model with estimator as given in fitting function.
-        Assumes that all controls and fixed effects should be varied.
-
-        :returns: pandas dataframe of results
-        """
-        # Regressions - order of loop matters here
-        reg_results = [[[self._reg_func(y, x, ctrl_vars)
-                         for ctrl_vars in self.ctrl_combs]
-                        for x in self.x_exog]
-                       for y in self.y_endog]
-        reg_results = _flatn_list(_flatn_list(reg_results))
-        # Order matters here: x ends up as first var,
-        # y as second
-        combs = [[[[x] + [y] + ctrl_vars for ctrl_vars in self.ctrl_combs]
-                  for x in self.x_exog]
-                 for y in self.y_endog]
-        combs = _flatn_list(_flatn_list(combs))
-        df_r = pd.DataFrame(combs)
-        df_r = df_r.rename(columns={0: 'x_exog', 1: 'y_endog'})
-        df_r['Results'] = reg_results
-        df_r['Coefficient'] = df_r.apply(
-            lambda row: row['Results'].params[row['x_exog']], axis=1)
-        df_r['Specification'] = combs
-        df_r['bse'] = df_r.apply(
-            lambda row: row['Results'].bse[row['x_exog']], axis=1)
-        df_r['pvalues'] = [x.pvalues for x in reg_results]
-        df_r['pvalues'] = df_r['pvalues'].apply(lambda x: dict(x))
-        # Re-order by coefficient
-        df_r = df_r.sort_values('Coefficient')
-        cols_to_keep = ['Results', 'Coefficient',
-                        'Specification', 'bse', 'pvalues',
-                        'x_exog', 'y_endog']
-        df_r = (df_r.drop(
-            [x for x in df_r.columns if x not in cols_to_keep], axis=1))
-        df_r = df_r.reset_index().drop('index', axis=1)
-        df_r.index.names = ['Specification No.']
-        df_r['Specification'] = df_r['Specification'].apply(
-            lambda x: sorted(x))
-        df_r['SpecificationCounts'] = df_r['Specification'].apply(
-            lambda x: Counter(x))
-        return df_r
-
-    def plot(self, save_path=None, pretty_plots=True,
-             preferred_spec=[]):
-        """
-        Makes plots of fitted specification curve.
-
-        :string save_path: exported fig filename
-        :bool pretty_plots: whether to use this package's figure formatting
-        :list[str] preferred_spec: preferred specification
-        """
-        if(pretty_plots):
-            _pretty_plots()
-        # Set up blocks for showing what effects are included
-        df_spec = self.df_r['SpecificationCounts'].apply(pd.Series).fillna(0.)
-        df_spec = df_spec.replace(0., False).replace(1., True)
-        df_spec = df_spec.T
-        df_spec = df_spec.sort_index()
-        # Label the preferred specification as True
-        self.df_r['preferred'] = False
-        if preferred_spec:
-            self.df_r['preferred'] = self.df_r['Specification'].apply(
-                lambda x: Counter(x) == Counter(preferred_spec))
-        # This is quite hacky
-        new_ctrl_names = list(
-            set(_flatn_list(self.df_r['Specification'].values)))
-        new_ctrl_names = [x for x in new_ctrl_names if
-                          x not in self.x_exog + self.y_endog]
-        new_ctrl_names.sort()
-        name = self.x_exog + self.y_endog + new_ctrl_names
-        group = (['x_exog' for x in range(len(self.x_exog))] +
-                 ['y_endog' for y in range(len(self.y_endog))] +
-                 [var.split(' = ')[0] for var in new_ctrl_names])
-        block_df = pd.DataFrame(group, index=name, columns=['group'])
-        group_map = dict(zip(block_df['group'], block_df['group']))
-        group_map.update(dict(zip([x for x in group if x in new_ctrl_names],
-                                  ['control'
-                                   for x in group if x in new_ctrl_names])))
-        block_df['group'] = block_df['group'].apply(lambda x: group_map[x])
-        counts = (block_df.reset_index().groupby('group').count().rename(
-            columns={'index': 'counts'}))
-        block_df = pd.merge(block_df.reset_index(),
-                            counts.reset_index(),
-                            on=['group']).set_index('index')
-        block_df = block_df.loc[block_df['counts'] > 1, :]
-        index_dict = dict(zip(block_df['group'].unique(),
-                              range(len(block_df['group'].unique()))))
-        block_df['group_index'] = block_df['group'].apply(
-            lambda x: index_dict[x])
-        heights = ([2] +
-                   [0.3*np.log(x+1)
-                    for x in block_df['group_index'].value_counts(sort=False)])
-        # Make the plot
-        plt.close('all')
-        fig = plt.figure(constrained_layout=False, figsize=(12, 8))
-        spec = fig.add_gridspec(ncols=1, nrows=len(heights),
-                                height_ratios=heights, wspace=0.05)
-        axarr = []
-        for row in range(len(heights)):
-            axarr.append(fig.add_subplot(spec[row, 0]))
-        # Do the coefficient first
-        axarr[0].axhline(y=np.median(self.df_r['Coefficient']),
-                         color='k',
-                         lw=0.3,
-                         alpha=1,
-                         label='Median coefficient',
-                         dashes=[12, 5])
-        # Colour the significant ones differently
-        self.df_r['color_coeff'] = 'black'
-        self.df_r['coeff_pvals'] = (self
-                                    .df_r
-                                    .apply(lambda row:
-                                           row['pvalues'][row['x_exog']],
-                                           axis=1))
-        self.df_r.loc[self.df_r['coeff_pvals'] < 0.05, 'color_coeff'] = 'blue'
-        for color in self.df_r['color_coeff'].unique():
-            slice_df_r = self.df_r.loc[self.df_r['color_coeff'] == color]
-            markers, caps, bars = axarr[0].errorbar(slice_df_r.index,
-                                                    slice_df_r['Coefficient'],
-                                                    yerr=slice_df_r['bse'],
-                                                    ls='none', color=color,
-                                                    alpha=0.8, zorder=1,
-                                                    elinewidth=2,
-                                                    marker='o')
-            [bar.set_alpha(0.4) for bar in bars]
-            [cap.set_alpha(0.4) for cap in caps]
-        # If there is a preferred spec, label it.
-        if preferred_spec:
-            loc_y = (self.df_r.loc[self.df_r['preferred'],
-                                   'Coefficient'])
-            loc_x = loc_y.index[0]
-            loc_y = loc_y.values[0]
-            cn_styl = "angle3,angleA=0,angleB=-90"
-            axarr[0].annotate('Preferred specification',
-                              xy=(loc_x, loc_y), xycoords='data',
-                              xytext=(3, 100), textcoords='offset points',
-                              fontsize='x-small',
-                              arrowprops=dict(arrowstyle="fancy",
-                                              fc="0.4", ec="none",
-                                              connectionstyle=cn_styl))
-        axarr[0].legend(frameon=True, loc='lower right',
-                        ncol=1, handlelength=2)
-        axarr[0].set_ylabel('Coefficient')
-        axarr[0].set_title('Specification curve analysis')
-        max_height = (self.df_r['Coefficient'] + self.df_r['bse']).max()
-        min_height = (self.df_r['Coefficient'] - self.df_r['bse']).min()
-        ylims = (min_height/1.2, 1.2*max_height)
-        axarr[0].set_ylim(_round_to_1(ylims[0]),
-                          _round_to_1(ylims[1]))
-        # Now do the blocks - each group get its own array
-        wid = 0.6
-        hei = wid/3
-        color_dict = {True: '#9B9B9B', False: '#F2F2F2'}
-        if(len(self.df_r) > 160):
-            wid = 0.01
-            color_dict = {True: 'k', False: '#FFFFFF'}
-        for ax_num, ax in enumerate(axarr[1:]):
-            block_index = block_df.loc[block_df['group_index']
-                                       == ax_num, :].index
-            df_sp_sl = df_spec.loc[block_index, :].copy()
-            for i, row_name in enumerate(df_sp_sl.index):
-                for j, col_name in enumerate(df_sp_sl.columns):
-                    color = color_dict[df_sp_sl.iloc[i, j]]
-                    sq = patches.Rectangle(
-                        (j-wid/2, i-hei/2), wid, hei, fill=True, color=color)
-                    ax.add_patch(sq)
-            ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True))
-            ax.yaxis.set_major_locator(ticker.MaxNLocator(integer=True))
-            ax.set_yticks(range(len(list(df_sp_sl.index.values))))
-            ax.set_yticklabels(list(df_sp_sl.index.values))
-            ax.set_xticklabels([])
-            ax.set_ylim(-hei, len(df_sp_sl)-hei*4)
-            ax.set_xlim(-wid, len(df_sp_sl.columns))
-            for place in ['right', 'top', 'bottom']:
-                ax.spines[place].set_visible(False)
-        for ax in axarr:
-            ax.set_xticks([], minor=True)
-            ax.set_xticks([])
-            ax.set_xlim(-wid, len(df_spec.columns))
-        if(save_path is not None):
-            plt.savefig(save_path, dpi=300)
-        plt.show()
diff --git a/src/specification_curve/__init__.py b/src/specification_curve/__init__.py
new file mode 100644
index 0000000..9bbe1d8
--- /dev/null
+++ b/src/specification_curve/__init__.py
@@ -0,0 +1,538 @@
+"""
+Specification Curve
+-------------------
+A package that produces specification curve analysis.
+"""
+import copy
+import itertools
+import os
+from collections import Counter
+from collections import defaultdict
+from itertools import combinations
+from math import floor
+from math import log10
+
+import matplotlib.patches as patches
+import matplotlib.pyplot as plt
+import matplotlib.ticker as ticker
+import numpy as np
+import pandas as pd
+import pkg_resources
+import statsmodels.api as sm
+
+
+EXAMPLE_FILE = pkg_resources.resource_filename(
+    "specification_curve", os.path.join("data", "example_data.csv")
+)
+
+
+def _round_to_1(x):
+    """
+    Rounds numbers to 1 s.f.
+    """
+    return round(x, -int(floor(log10(abs(x)))) + 1)
+
+
+def _double_list_check(XX):
+    """
+    If a list, return as list of lists
+    """
+    if not (any(isinstance(el, list) for el in XX)):
+        XX = [XX]
+    return XX
+
+
+def _single_list_check_str(X):
+    """
+    If type is string, return string in list
+    """
+    if type(X) == str:
+        X = [X]
+    return X
+
+
+def _remove_overlapping_vars(list_to_check, includes_list):
+    """Checks, and removes, any variable in list_to_check that is also in
+    includes_list, returning list_to_check without overlapping variable
+    names.
+    """
+    return [x for x in list_to_check if x not in includes_list]
+
+
+def _pretty_plots():
+    """
+    Uses specification curve package's pretty plot style.
+    Overrides existing style.
+    """
+    json_plot_settings = {
+        "ytick.labelsize": 16,
+        "xtick.labelsize": 16,
+        "font.size": 22,
+        "figure.figsize": (10, 5),
+        "axes.titlesize": 22,
+        "axes.labelsize": 18,
+        "lines.linewidth": 2,
+        "lines.markersize": 3,
+        "legend.fontsize": 11,
+        "mathtext.fontset": "stix",
+        # "axes.grid": True,
+        # "grid.color": "gray",
+        # "grid.alpha": 0.5,
+        # "grid.linewidth": 0.3,
+        "font.family": "STIXGeneral",
+    }
+    plt.style.use(json_plot_settings)
+
+
+def _excl_combs(lst, r, excludes):
+    """lst = [1, 2, 3, 4, 5, 6]
+    excludes = [{1, 3}]
+    gen = _excl_combs(lst, 2, excludes)
+    """
+    if excludes != [[]]:
+        return [
+            comb
+            for comb in combinations(lst, r)
+            if not any(e.issubset(comb) for e in excludes)
+        ]
+    else:
+        return list(combinations(lst, r))
+
+
+def _flatn_list(nested_list):
+    """Flattens nested list"""
+    return list(itertools.chain.from_iterable(nested_list))
+
+
+class SpecificationCurve:
+    """Specification curve object.
+
+    Uses a model to perform all variants of a specification.
+    Stores the results of those regressions in a tidy format pandas dataframe.
+    Plots the regressions in chart that can optionally be saved.
+    Will iterate over multiple inputs for exog. and endog. variables.
+    Note that categorical variables that are expanded cannot be mutually
+    excluded from other categorical variables that are expanded.
+
+    """
+
+    def __init__(
+        self,
+        df: pd.DataFrame,
+        y_endog,
+        x_exog,
+        controls,
+        exclu_grps=[[]],
+        cat_expand=[],
+        always_include=[],
+    ):
+        """Specification curve object constructor.
+
+        Args:
+            df (pd.DataFrame): Data to perform regression on
+            y_endog (_type_): _description_
+            x_exog (_type_): _description_
+            controls (_type_): _description_
+            exclu_grps (list, optional): _description_. Defaults to [[]].
+            cat_expand (list, optional): _description_. Defaults to [].
+            always_include (list, optional): _description_. Defaults to [].
+        """
+        self.df = df.copy()
+        self.y_endog = y_endog
+        self.x_exog = x_exog
+        self.controls = controls
+        self.exclu_grps = exclu_grps
+        self.cat_expand = cat_expand
+        self.always_include = always_include
+
+    def fit(self, estimator=sm.OLS) -> None:
+        """Fits a specification curve by performing regressions.
+
+        Args:
+            estimator (statsmodels.regression.linear_model or statsmodels.discrete.discrete_model, optional): statsmodels estimator. Defaults to sm.OLS.
+        """
+        self.estimator = estimator
+        self.controls = _single_list_check_str(self.controls)
+        self.cat_expand = _single_list_check_str(self.cat_expand)
+        self.exclu_grps = _double_list_check(self.exclu_grps)
+        self.always_include = _single_list_check_str(self.always_include)
+        self.y_endog = _single_list_check_str(self.y_endog)
+        self.x_exog = _single_list_check_str(self.x_exog)
+        # If any of always include in any other list, remove it from other list
+        self.controls = _remove_overlapping_vars(self.controls, self.always_include)
+        self.x_exog = _remove_overlapping_vars(self.x_exog, self.always_include)
+        self.ctrl_combs = self._compute_combinations()
+        self.df_r = self._spec_curve_regression()
+        print("Fit complete")
+
+    def _reg_func(self, y_endog, x_exog, reg_vars):
+        # NB: get dummies
+        # transforms by default any col that is object or cat
+        xf = pd.get_dummies(self.df, prefix_sep="=")
+        new_cols = [x for x in xf.columns if x not in self.df.columns]
+        gone_cols = [x for x in self.df.columns if x not in xf.columns]
+        reg_vars_here = copy.copy(reg_vars)
+        reg_vars_here.extend(new_cols)
+        [reg_vars_here.remove(x) for x in gone_cols if x in reg_vars_here]
+        return self.estimator(xf[y_endog], xf[[x_exog] + reg_vars_here]).fit()
+
+    def _compute_combinations(self):
+        """
+        Finds all possible combinations of variables.
+        Changes df to have dummies for cat expand columns.
+        """
+        init_cols = self.y_endog + self.x_exog + self.controls + self.always_include
+        self.df = self.df[init_cols]
+        new_cols = []
+        # Warning: hard-coded prefix
+        if self.cat_expand != []:
+            self.df = pd.get_dummies(self.df, columns=self.cat_expand, prefix_sep=" = ")
+            new_cols = [x for x in self.df.columns if x not in init_cols]
+            # Now change the controls
+            [self.controls.remove(x) for x in self.cat_expand]
+            self.controls.extend(new_cols)
+            # Create mapping from cat expand to new cols
+            oldnew = dict(
+                zip(
+                    [x for x in self.cat_expand],
+                    [[x for x in new_cols if y in x] for y in self.cat_expand],
+                )
+            )
+            # Now exclude the groups that combine any new cols
+            for x in self.cat_expand:
+                if self.exclu_grps == [[]]:
+                    self.exclu_grps = [oldnew[x]]
+                else:
+                    self.exclu_grps.append(oldnew[x])
+        # Find any subsets of excluded combs and add all variants
+        for x in self.exclu_grps:
+            if len(x) > 2:
+                sub_combs = [list(combinations(x, y)) for y in range(2, len(x))]
+                sub_combs = [item for sublist in sub_combs for item in sublist]
+                # Turn all the tuples into lists
+                sub_combs = [list(x) for x in sub_combs]
+                self.exclu_grps = self.exclu_grps + sub_combs
+        # Turn mutually exclusive groups into sets
+        if self.exclu_grps != [[]]:
+            self.exclu_grps = [set(x) for x in self.exclu_grps]
+        # Get all combinations excluding mutually excl groups
+        ctrl_combs = [
+            _excl_combs(self.controls, k, self.exclu_grps)
+            for k in range(len(self.controls) + 1)
+        ]
+        # Flatten this into a single list of tuples
+        ctrl_combs = _flatn_list(ctrl_combs)
+        # Turn all the tuples into lists
+        ctrl_combs = [list(x) for x in ctrl_combs]
+        # Add in always included regressors
+        ctrl_combs = [x + self.always_include for x in ctrl_combs]
+        return ctrl_combs
+
+    def _spec_curve_regression(self):
+        """Performs all regressions for a specification curve.
+
+        Estimates model with estimator as given in fitting function.
+        Assumes that all controls and fixed effects should be varied.
+
+        :returns: pandas dataframe of results
+        """
+        # Regressions - order of loop matters here
+        reg_results = [
+            [
+                [self._reg_func(y, x, ctrl_vars) for ctrl_vars in self.ctrl_combs]
+                for x in self.x_exog
+            ]
+            for y in self.y_endog
+        ]
+        reg_results = _flatn_list(_flatn_list(reg_results))
+        # Order matters here: x ends up as first var,
+        # y as second
+        combs = [
+            [
+                [[x] + [y] + ctrl_vars for ctrl_vars in self.ctrl_combs]
+                for x in self.x_exog
+            ]
+            for y in self.y_endog
+        ]
+        combs = _flatn_list(_flatn_list(combs))
+        df_r = pd.DataFrame(combs)
+        df_r = df_r.rename(columns={0: "x_exog", 1: "y_endog"})
+        df_r["Results"] = reg_results
+        df_r["Coefficient"] = df_r.apply(
+            lambda row: row["Results"].params[row["x_exog"]], axis=1
+        )
+        df_r["Specification"] = combs
+        df_r["bse"] = df_r.apply(lambda row: row["Results"].bse[row["x_exog"]], axis=1)
+        # Pull out the confidence interval around the exogeneous variable
+        df_r["conf_int"] = df_r.apply(
+            lambda row: np.array(row["Results"].conf_int().loc[row["x_exog"]]), axis=1
+        )
+        df_r["pvalues"] = [x.pvalues for x in reg_results]
+        df_r["pvalues"] = df_r["pvalues"].apply(lambda x: dict(x))
+        # Re-order by coefficient: makes plots look more continuous
+        df_r = df_r.sort_values("Coefficient")
+        cols_to_keep = [
+            "Results",
+            "Coefficient",
+            "Specification",
+            "bse",
+            "pvalues",
+            "conf_int",
+            "x_exog",
+            "y_endog",
+        ]
+        df_r = df_r.drop([x for x in df_r.columns if x not in cols_to_keep], axis=1)
+        df_r = df_r.reset_index().drop("index", axis=1)
+        df_r.index.names = ["Specification No."]
+        df_r["Specification"] = df_r["Specification"].apply(lambda x: sorted(x))
+        df_r["SpecificationCounts"] = df_r["Specification"].apply(lambda x: Counter(x))
+        return df_r
+
+    def plot(self, save_path=None, pretty_plots=True, preferred_spec=[]):
+        """Makes plots of fitted specification curve.
+
+        Args:
+            save_path (_type_, optional): Eexported fig filename. Defaults to None.
+            pretty_plots (bool, optional): whether to use this package's figure formatting. Defaults to True.
+            preferred_spec (list, optional): preferred specification. Defaults to [].
+        """
+        if pretty_plots:
+            _pretty_plots()
+        # Set up blocks for showing what effects are included
+        df_spec = self.df_r["SpecificationCounts"].apply(pd.Series).fillna(0.0)
+        df_spec = df_spec.replace(0.0, False).replace(1.0, True)
+        df_spec = df_spec.T
+        df_spec = df_spec.sort_index()
+        # Label the preferred specification as True
+        self.df_r["preferred"] = False
+        if preferred_spec:
+            self.df_r["preferred"] = self.df_r["Specification"].apply(
+                lambda x: Counter(x) == Counter(preferred_spec)
+            )
+        # This is quite hacky
+        new_ctrl_names = list(set(_flatn_list(self.df_r["Specification"].values)))
+        new_ctrl_names = [
+            x for x in new_ctrl_names if x not in self.x_exog + self.y_endog
+        ]
+        new_ctrl_names.sort()
+        name = self.x_exog + self.y_endog + new_ctrl_names
+        group = (
+            ["x_exog" for x in range(len(self.x_exog))]
+            + ["y_endog" for y in range(len(self.y_endog))]
+            + [var.split(" = ")[0] for var in new_ctrl_names]
+        )
+        block_df = pd.DataFrame(group, index=name, columns=["group"])
+        group_map = dict(zip(block_df["group"], block_df["group"]))
+        group_map.update(
+            dict(
+                zip(
+                    [x for x in group if x in new_ctrl_names],
+                    ["control" for x in group if x in new_ctrl_names],
+                )
+            )
+        )
+        block_df["group"] = block_df["group"].apply(lambda x: group_map[x])
+        counts = (
+            block_df.reset_index()
+            .groupby("group")
+            .count()
+            .rename(columns={"index": "counts"})
+        )
+        block_df = pd.merge(
+            block_df.reset_index(), counts.reset_index(), on=["group"]
+        ).set_index("index")
+        block_df = block_df.loc[block_df["counts"] > 1, :]
+        index_dict = dict(
+            zip(block_df["group"].unique(), range(len(block_df["group"].unique())))
+        )
+        block_df["group_index"] = block_df["group"].apply(lambda x: index_dict[x])
+        heights = [2] + [
+            0.3 * np.log(x + 1)
+            for x in block_df["group_index"].value_counts(sort=False)
+        ]
+        # Make the plot
+        plt.close("all")
+        fig = plt.figure(constrained_layout=False, figsize=(12, 8))
+        spec = fig.add_gridspec(
+            ncols=1, nrows=len(heights), height_ratios=heights, wspace=0.05
+        )
+        axarr = []
+        for row in range(len(heights)):
+            axarr.append(fig.add_subplot(spec[row, 0]))
+        # Add a line showing where the median coefficient is
+        axarr[0].axhline(
+            y=np.median(self.df_r["Coefficient"]),
+            color="k",
+            lw=0.3,
+            alpha=1,
+            label="Median coefficient",
+            dashes=[12, 5],
+        )
+        # Annotate the median coefficient line with text
+        axarr[0].text(
+            x=0.1,
+            y=np.median(self.df_r["Coefficient"]) * 1.02,
+            s="Median coefficient",
+            fontsize=12,
+            color="gray",
+            zorder=5,
+        )
+        # Colour the significant estimate values differently
+        self.df_r["color_coeff"] = "black"
+        self.df_r["coeff_pvals"] = self.df_r.apply(
+            lambda row: row["pvalues"][row["x_exog"]], axis=1
+        )
+        self.df_r.loc[self.df_r["coeff_pvals"] < 0.05, "color_coeff"] = "blue"
+        red_condition = (self.df_r["Coefficient"] < 0) & (
+            self.df_r["coeff_pvals"] < 0.05
+        )
+        self.df_r.loc[red_condition, "color_coeff"] = "red"
+        for color in self.df_r["color_coeff"].unique():
+            slice_df_r = self.df_r.loc[self.df_r["color_coeff"] == color]
+            a = (
+                slice_df_r["Coefficient"]
+                - np.stack(slice_df_r["conf_int"].to_numpy())[:, 0]
+            )
+            b = (
+                np.stack(slice_df_r["conf_int"].to_numpy())[:, 1]
+                - slice_df_r["Coefficient"]
+            )
+            y_err_correct_shape = np.stack((a, b))
+            markers, caps, bars = axarr[0].errorbar(
+                slice_df_r.index,
+                slice_df_r["Coefficient"],
+                yerr=y_err_correct_shape,
+                ls="none",
+                color=color,
+                alpha=0.8,
+                zorder=1,
+                elinewidth=2,
+                marker="o",
+            )
+            [bar.set_alpha(0.4) for bar in bars]
+            [cap.set_alpha(0.4) for cap in caps]
+        # If there is a preferred spec, label it.
+        if preferred_spec:
+            loc_y = self.df_r.loc[self.df_r["preferred"], "Coefficient"]
+            loc_x = loc_y.index[0]
+            loc_y = loc_y.values[0]
+            cn_styl = "angle3,angleA=0,angleB=-90"
+            axarr[0].annotate(
+                f"Preferred specification: {loc_y:+.2f}",
+                xy=(loc_x, loc_y),
+                xycoords="data",
+                xytext=(-30, 60),
+                textcoords="offset points",
+                fontsize=10,
+                arrowprops=dict(
+                    arrowstyle="fancy", fc="0.4", ec="none", connectionstyle=cn_styl
+                ),
+            )
+        axarr[0].set_ylabel("Coefficient")
+        axarr[0].set_title("Specification curve analysis")
+        max_height = self.df_r["conf_int"].apply(lambda x: x.max()).max()
+        min_height = self.df_r["conf_int"].apply(lambda x: x.min()).min()
+        ylims = (min_height / 1.2, 1.2 * max_height)
+        axarr[0].set_ylim(_round_to_1(ylims[0]), _round_to_1(ylims[1]))
+        # Now do the blocks - each group get its own array
+        wid = 0.5
+        hei = wid / 2.5
+        color_dict = {True: "#9B9B9B", False: "#F2F2F2"}
+        if len(self.df_r) > 160:
+            wid = 0.01
+            color_dict = {True: "k", False: "#FFFFFF"}
+        # Define group names to put on RHS of plot
+
+        def return_string():
+            """Convenience function to setup default dict.
+
+            Returns:
+                str: "subset"
+            """
+            return "subset"
+
+        block_name_dict = defaultdict(return_string)
+        block_name_dict.update({"x_exog": "x", "y_endog": "y", "control": "controls"})
+        for ax_num, ax in enumerate(axarr[1:]):
+            block_index = block_df.loc[block_df["group_index"] == ax_num, :].index
+            df_sp_sl = df_spec.loc[block_index, :].copy()
+            for i, row_name in enumerate(df_sp_sl.index):
+                for j, col_name in enumerate(df_sp_sl.columns):
+                    color = color_dict[df_sp_sl.iloc[i, j]]
+                    sq = patches.Rectangle(
+                        (j - wid / 2, i - hei / 2), wid, hei, fill=True, color=color
+                    )
+                    ax.add_patch(sq)
+            ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True))
+            ax.yaxis.set_major_locator(ticker.MaxNLocator(integer=True))
+            ax.set_yticks(range(len(list(df_sp_sl.index.values))))
+            # Add text on the RHS that describes what each block is
+            ax.text(
+                x=len(df_sp_sl.columns) + 1,
+                y=np.mean(ax.get_yticks()),
+                s=block_name_dict[
+                    block_df.loc[block_df["group_index"] == ax_num, "group"].iloc[0]
+                ],
+                rotation=-90,
+                fontsize=11,
+                horizontalalignment="center",
+                verticalalignment="center",
+            )
+            ax.set_yticklabels(list(df_sp_sl.index.values), fontsize=12)
+            ax.set_xticklabels([])
+            ax.set_ylim(-hei, len(df_sp_sl) - hei * 4)
+            ax.set_xlim(-wid, len(df_sp_sl.columns))
+            for place in ["right", "top", "bottom"]:
+                ax.spines[place].set_visible(False)
+        for ax in axarr:
+            ax.set_xticks([], minor=True)
+            ax.set_xticks([])
+            ax.set_xlim(-wid, len(df_spec.columns))
+        if save_path is not None:
+            plt.savefig(save_path, dpi=300)
+        plt.show()
+
+
+def load_example_data1() -> pd.DataFrame:
+    """Retrieves example data from a file included with the package.
+
+    Returns:
+        pd.DataFrame: Example data suitable for regression.
+    """
+    # Example data
+    df = pd.read_csv(EXAMPLE_FILE, index_col=0)
+    num_cols = [x for x in df.columns if x not in ["group1", "group2"]]
+    for col in num_cols:
+        df[col] = df[col].astype(np.double)
+    cat_cols = [x for x in df.columns if x not in num_cols]
+    for col in cat_cols:
+        df[col] = df[col].astype("category")
+    return df
+
+
+def load_example_data2() -> pd.DataFrame:
+    """Generates fake data.
+
+    Returns:
+        pd.DataFrame: Example data suitable for regression.
+    """
+    # Set seed for random numbers
+    seed_for_prng = 78557
+    # prng=probabilistic random number generator
+    prng = np.random.default_rng(seed_for_prng)
+    n_samples = 500
+    x_1 = prng.random(size=n_samples)
+    x_2 = prng.random(size=n_samples)
+    x_3 = prng.random(size=n_samples)
+    x_4 = prng.integers(2, size=n_samples)
+    y = (
+        0.8 * x_1
+        + 0.1 * x_2
+        + 0.5 * x_3
+        + x_4 * 0.6
+        + +2 * prng.standard_normal(n_samples)
+    )
+    df = pd.DataFrame([x_1, x_2, x_3, x_4, y], ["x_1", "x_2", "x_3", "x_4", "y"]).T
+    # Set x_4 as a categorical variable
+    df["x_4"] = df["x_4"].astype("category")
+    return df
diff --git a/src/specification_curve/__main__.py b/src/specification_curve/__main__.py
new file mode 100644
index 0000000..68e1400
--- /dev/null
+++ b/src/specification_curve/__main__.py
@@ -0,0 +1,1 @@
+"""specification_curve."""
diff --git a/specification_curve/data/example_data.csv b/src/specification_curve/data/example_data.csv
similarity index 100%
rename from specification_curve/data/example_data.csv
rename to src/specification_curve/data/example_data.csv
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index 08336bf..0000000
--- a/tox.ini
+++ /dev/null
@@ -1,19 +0,0 @@
-[tox]
-envlist = py36, py37, py38, flake8
-
-[travis]
-python =
-    3.8: py38
-    3.7: py37
-    3.6: py36
-
-[testenv:flake8]
-basepython = python
-deps = flake8
-commands = flake8 specification_curve tests
-
-[testenv]
-setenv =
-    PYTHONPATH = {toxinidir}
-
-commands = python setup.py test


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..dddc46e
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,125 @@
+name: Tests
+
+on:
+  - push
+  - pull_request
+
+jobs:
+  tests:
+    name: ${{ matrix.session }} ${{ matrix.python-version }} / ${{ matrix.os }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - { python-version: 3.9, os: ubuntu-latest, session: "pre-commit" }
+          - { python-version: 3.9, os: ubuntu-latest, session: "safety" }
+          - { python-version: 3.9, os: ubuntu-latest, session: "mypy" }
+          - { python-version: 3.8, os: ubuntu-latest, session: "mypy" }
+          - { python-version: "3.10", os: ubuntu-latest, session: "mypy" }
+          - { python-version: 3.9, os: ubuntu-latest, session: "tests" }
+          - { python-version: 3.8, os: ubuntu-latest, session: "tests" }
+          - { python-version: "3.10", os: ubuntu-latest, session: "tests" }
+          - { python-version: 3.9, os: windows-latest, session: "tests" }
+          - { python-version: 3.9, os: macos-latest, session: "tests" }
+          - { python-version: 3.9, os: ubuntu-latest, session: "typeguard" }
+          - { python-version: 3.9, os: ubuntu-latest, session: "xdoctest" }
+
+    env:
+      NOXSESSION: ${{ matrix.session }}
+
+    steps:
+      - name: Check out the repository
+        uses: actions/checkout@v3
+
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v4.3.0
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - name: Upgrade pip
+        run: |
+          pip install --constraint=.github/workflows/constraints.txt pip
+          pip --version
+      - name: Install Poetry
+        run: |
+          pip install --constraint=.github/workflows/constraints.txt poetry
+          poetry --version
+      - name: Install Nox
+        run: |
+          pip install --constraint=.github/workflows/constraints.txt nox nox-poetry
+          nox --version
+      - name: Compute pre-commit cache key
+        if: matrix.session == 'pre-commit'
+        id: pre-commit-cache
+        shell: python
+        run: |
+          import hashlib
+          import sys
+          python = "py{}.{}".format(*sys.version_info[:2])
+          payload = sys.version.encode() + sys.executable.encode()
+          digest = hashlib.sha256(payload).hexdigest()
+          result = "${{ runner.os }}-{}-{}-pre-commit".format(python, digest[:8])
+          print("::set-output name=result::{}".format(result))
+      - name: Restore pre-commit cache
+        uses: actions/cache@v3.0.11
+        if: matrix.session == 'pre-commit'
+        with:
+          path: ~/.cache/pre-commit
+          key: ${{ steps.pre-commit-cache.outputs.result }}-${{ hashFiles('.pre-commit-config.yaml') }}
+          restore-keys: |
+            ${{ steps.pre-commit-cache.outputs.result }}-
+      - name: Run Nox
+        run: |
+          poetry run nox --force-color --python=${{ matrix.python-version }}
+      - name: Upload coverage data
+        if: always() && matrix.session == 'tests'
+        uses: "actions/upload-artifact@v3"
+        with:
+          name: coverage-data
+          path: ".coverage.*"
+
+      - name: Upload documentation
+        if: matrix.session == 'docs-build'
+        uses: actions/upload-artifact@v3
+        with:
+          name: docs
+          path: docs/_build
+
+  coverage:
+    runs-on: ubuntu-latest
+    needs: tests
+    steps:
+      - name: Check out the repository
+        uses: actions/checkout@v3
+
+      - name: Set up Python 3.9
+        uses: actions/setup-python@v4.3.0
+        with:
+          python-version: 3.9
+
+      - name: Upgrade pip
+        run: |
+          pip install --constraint=.github/workflows/constraints.txt pip
+          pip --version
+      - name: Install Poetry
+        run: |
+          pip install --constraint=.github/workflows/constraints.txt poetry
+          poetry --version
+      - name: Install Nox
+        run: |
+          pip install --constraint=.github/workflows/constraints.txt nox nox-poetry
+          nox --version
+      - name: Download coverage data
+        uses: actions/download-artifact@v3
+        with:
+          name: coverage-data
+
+      - name: Combine coverage data and display human readable report
+        run: |
+          poetry run nox --force-color --session=coverage
+      - name: Create coverage report
+        run: |
+          nox --force-color --session=coverage -- xml
+      - name: Upload coverage report
+        uses: codecov/codecov-action@v3.1.1
diff --git a/tests/test_specification_curve.py b/tests/test_specification_curve.py
index d388b80..3268549 100644
--- a/tests/test_specification_curve.py
+++ b/tests/test_specification_curve.py
@@ -1,176 +1,185 @@
 #!/usr/bin/env python
-
 """Tests for `specification_curve` package."""
-
-import unittest
 import os
+from unittest.mock import patch
+
 import numpy as np
 import pandas as pd
 import statsmodels.api as sm
 from scipy.stats import norm
 
-from specification_curve import specification_curve as specy
-from specification_curve import example as scdata
-
-
-class TestSpecification_curve(unittest.TestCase):
-    """Tests for `specification_curve` package."""
-
-    def setUp(self):
-        """Set up test fixtures, if any."""
-
-    def tearDown(self):
-        """Tear down test fixtures, if any."""
-
-    def test_000_basic(self):
-        """Test vanilla run."""
-        df = scdata.load_example_data2()
-        y_endog = 'y'
-        x_exog = 'x_1'
-        controls = ['x_2', 'x_3', 'x_4']
-        sc = specy.SpecificationCurve(df, y_endog, x_exog, controls)
-        sc.fit()
-        sc.plot()
-        sc.df_r.head()
-
-    def test_001_fe_grp(self):
-        """Test expand multiple FE groups. Docs feature 1."""
-        df = scdata.load_example_data1()
-        y_endog = 'y1'
-        x_exog = 'x1'
-        controls = ['c1', 'c2', 'group1', 'group2']
-        sc = specy.SpecificationCurve(df, y_endog, x_exog, controls,
-                                      cat_expand=['group1', 'group2'])
-        sc.fit()
-        sc.plot()
-        sc.df_r.head()
-
-    def test_002_docs_feat_two(self):
-        """Test docs feature 2."""
-        df = scdata.load_example_data1()
-        y_endog = 'y1'
-        x_exog = 'x1'
-        controls = ['c1', 'c2', 'group1', 'group2']
-        sc = specy.SpecificationCurve(df, y_endog, x_exog, controls,
-                                      exclu_grps=[['c1', 'c2']])
-        sc.fit()
-        sc.plot()
-        sc.df_r.head()
-
-    def test_004_docs_feat_three(self):
-        """Test docs feature 3: multiple dependent or independent variables"""
-        df = scdata.load_example_data1()
-        x_exog = ['x1', 'x2']
-        y_endog = 'y1'
-        controls = ['c1', 'c2', 'group1', 'group2']
-        sc = specy.SpecificationCurve(df, y_endog, x_exog, controls)
-        sc.fit()
-        sc.plot()
-        sc.df_r.head()
-
-    def test_005_save_fig(self):
-        """Test save fig.."""
-        df = scdata.load_example_data1()
-        x_exog = ['x1', 'x2']
-        y_endog = 'y1'
-        controls = ['c1', 'c2', 'group1', 'group2']
-        sc = specy.SpecificationCurve(df, y_endog, x_exog, controls,
-                                      cat_expand=['group1'])
-        sc.fit()
-        sc.plot(save_path='test_fig.pdf')
-        os.remove('test_fig.pdf')
-
-    def test_006_logit_estimator(self):
-        """ Test running with different statsmodels estimators -
-            here logitistic
-        """
-        n_samples = 1000
-        x_2 = np.random.randint(2, size=n_samples)
-        x_1 = np.random.random(size=n_samples)
-        x_3 = np.random.randint(3, size=n_samples)
-        x_4 = np.random.random(size=n_samples)
-        x_5 = x_1 + 0.05*np.random.randn(n_samples)
-        x_beta = -1 + 3.5*x_1 + 0.2*x_2 + 0.3*x_3
-        prob = 1/(1 + np.exp(-x_beta))
-        y = np.random.binomial(n=1, p=prob, size=n_samples)
-        y2 = np.random.binomial(n=1, p=prob*0.98, size=n_samples)
-        df = pd.DataFrame([x_1, x_2, x_3, x_4, x_5, y, y2],
-                          ['x_1', 'x_2', 'x_3', 'x_4', 'x_5', 'y', 'y2']).T
-        y_endog = ['y', 'y2']
-        x_exog = ['x_1', 'x_5']
-        controls = ['x_3', 'x_2', 'x_4']
-        sc = specy.SpecificationCurve(df, y_endog, x_exog, controls,
-                                      cat_expand='x_3')
-        sc.fit(estimator=sm.Logit)
-        sc.plot()
-        sc.df_r.head()
-
-    def test_007_probit_estimator(self):
-        """
-        Running with different statsmodels estimators - here probit
-        """
-        n_samples = 1000
-        x_2 = np.random.randint(2, size=n_samples)
-        x_1 = np.random.random(size=n_samples)
-        x_3 = np.random.randint(3, size=n_samples)
-        x_4 = np.random.random(size=n_samples)
-        x_5 = x_1 + 0.05*np.random.randn(n_samples)
-        x_beta = -1 + 3.5*x_1 + 0.2*x_2 + 0.3*x_3
-        prob = norm.cdf(x_beta)
-        y = np.random.binomial(n=1, p=prob, size=n_samples)
-        y2 = np.random.binomial(n=1, p=prob*0.98, size=n_samples)
-        df = pd.DataFrame([x_1, x_2, x_3, x_4, x_5, y, y2],
-                          ['x_1', 'x_2', 'x_3', 'x_4', 'x_5', 'y', 'y2']).T
-        y_endog = ['y', 'y2']
-        x_exog = ['x_1', 'x_5']
-        controls = ['x_3', 'x_2', 'x_4']
-        sc = specy.SpecificationCurve(df, y_endog, x_exog, controls,
-                                      cat_expand='x_3')
-        sc.fit(estimator=sm.Probit)
-        sc.plot()
-        sc.df_r.head()
-
-    def test_008_large_no_specifications(self):
-        """
-        Test a very large set of specifications
-        """
-        n_samples = 400
-        # Number of dimensions of continuous
-        # random variables
-        n_dim = 8
-        c_rnd_vars = np.random.random(size=(n_dim, n_samples))
-        c_rnd_vars_names = [f'c_{i}' for i in range(np.shape(c_rnd_vars)[0])]
-        y_1 = (0.3*c_rnd_vars[0, :] +
-               0.5*c_rnd_vars[1, :])
-        y_2 = y_1 + 0.05*np.random.randn(n_samples)
-        df = pd.DataFrame([y_1, y_2], ['y1', 'y2']).T
-        for i, col_name in enumerate(c_rnd_vars_names):
-            df[col_name] = c_rnd_vars[i, :]
-        controls = c_rnd_vars_names[1:]
-        sc = specy.SpecificationCurve(df, ['y1', 'y2'], c_rnd_vars_names[0],
-                                      controls)
-        sc.fit()
-        sc.plot()
-
-    def test_009_always_include(self):
-        """Test of always include."""
-        df = scdata.load_example_data1()
-        x_exog = 'x1'
-        y_endog = 'y1'
-        controls = ['c2', 'group1', 'group2']
-        sc = specy.SpecificationCurve(df, y_endog, x_exog, controls,
-                                      always_include='c1')
-        sc.fit()
-        sc.plot()
-        sc.df_r.head()
-
-    def test_010_preferred_specification(self):
-        """Test of labelling preferred specification."""
-        df = scdata.load_example_data1()
-        x_exog = 'x1'
-        y_endog = 'y1'
-        controls = ['c2', 'group1', 'group2']
-        sc = specy.SpecificationCurve(df, y_endog, x_exog, controls)
-        sc.fit()
-        sc.plot(preferred_spec=['group1', 'x1', 'y1'])
-        sc.df_r.head()
+import specification_curve as specy
+
+
+@patch("matplotlib.pyplot.show")
+def test_000_basic_plot(mock_show):
+    df = specy.load_example_data2()
+    y_endog = "y"
+    x_exog = "x_1"
+    controls = ["x_2", "x_3", "x_4"]
+    sc = specy.SpecificationCurve(df, y_endog, x_exog, controls)
+    sc.fit()
+    sc.plot()
+    mock_show.assert_called_once()
+
+
+@patch("matplotlib.pyplot.show")
+def test_001_fe_grp(mock_show):
+    """Test expand multiple FE groups. Docs feature 1."""
+    df = specy.load_example_data1()
+    y_endog = "y1"
+    x_exog = "x1"
+    controls = ["c1", "c2", "group1", "group2"]
+    sc = specy.SpecificationCurve(
+        df, y_endog, x_exog, controls, cat_expand=["group1", "group2"]
+    )
+    sc.fit()
+    sc.plot()
+    mock_show.assert_called_once()
+
+
+@patch("matplotlib.pyplot.show")
+def test_002_docs_feat_two(mock_show):
+    """Test docs feature 2."""
+    df = specy.load_example_data1()
+    y_endog = "y1"
+    x_exog = "x1"
+    controls = ["c1", "c2", "group1", "group2"]
+    sc = specy.SpecificationCurve(
+        df, y_endog, x_exog, controls, exclu_grps=[["c1", "c2"]]
+    )
+    sc.fit()
+    sc.plot()
+    mock_show.assert_called_once()
+
+
+@patch("matplotlib.pyplot.show")
+def test_004_docs_feat_three(mock_show):
+    """Test docs feature 3: multiple dependent or independent variables"""
+    df = specy.load_example_data1()
+    x_exog = ["x1", "x2"]
+    y_endog = "y1"
+    controls = ["c1", "c2", "group1", "group2"]
+    sc = specy.SpecificationCurve(df, y_endog, x_exog, controls)
+    sc.fit()
+    sc.plot()
+    mock_show.assert_called_once()
+
+
+@patch("matplotlib.pyplot.show")
+def test_005_save_fig(mock_show):
+    """Test save fig.."""
+    df = specy.load_example_data1()
+    x_exog = ["x1", "x2"]
+    y_endog = "y1"
+    controls = ["c1", "c2", "group1", "group2"]
+    sc = specy.SpecificationCurve(df, y_endog, x_exog, controls, cat_expand=["group1"])
+    sc.fit()
+    sc.plot(save_path="test_fig.pdf")
+    os.remove("test_fig.pdf")
+    mock_show.assert_called_once()
+
+
+@patch("matplotlib.pyplot.show")
+def test_006_logit_estimator(mock_show):
+    """Test running with different statsmodels estimators -
+    here logitistic
+    """
+    n_samples = 1000
+    x_2 = np.random.randint(2, size=n_samples)
+    x_1 = np.random.random(size=n_samples)
+    x_3 = np.random.randint(3, size=n_samples)
+    x_4 = np.random.random(size=n_samples)
+    x_5 = x_1 + 0.05 * np.random.randn(n_samples)
+    x_beta = -1 + 3.5 * x_1 + 0.2 * x_2 + 0.3 * x_3
+    prob = 1 / (1 + np.exp(-x_beta))
+    y = np.random.binomial(n=1, p=prob, size=n_samples)
+    y2 = np.random.binomial(n=1, p=prob * 0.98, size=n_samples)
+    df = pd.DataFrame(
+        [x_1, x_2, x_3, x_4, x_5, y, y2],
+        ["x_1", "x_2", "x_3", "x_4", "x_5", "y", "y2"],
+    ).T
+    y_endog = ["y", "y2"]
+    x_exog = ["x_1", "x_5"]
+    controls = ["x_3", "x_2", "x_4"]
+    sc = specy.SpecificationCurve(df, y_endog, x_exog, controls, cat_expand="x_3")
+    sc.fit(estimator=sm.Logit)
+    sc.plot()
+    mock_show.assert_called_once()
+
+
+@patch("matplotlib.pyplot.show")
+def test_007_probit_estimator(mock_show):
+    """
+    Running with different statsmodels estimators - here probit
+    """
+    n_samples = 1000
+    x_2 = np.random.randint(2, size=n_samples)
+    x_1 = np.random.random(size=n_samples)
+    x_3 = np.random.randint(3, size=n_samples)
+    x_4 = np.random.random(size=n_samples)
+    x_5 = x_1 + 0.05 * np.random.randn(n_samples)
+    x_beta = -1 + 3.5 * x_1 + 0.2 * x_2 + 0.3 * x_3
+    prob = norm.cdf(x_beta)
+    y = np.random.binomial(n=1, p=prob, size=n_samples)
+    y2 = np.random.binomial(n=1, p=prob * 0.98, size=n_samples)
+    df = pd.DataFrame(
+        [x_1, x_2, x_3, x_4, x_5, y, y2],
+        ["x_1", "x_2", "x_3", "x_4", "x_5", "y", "y2"],
+    ).T
+    y_endog = ["y", "y2"]
+    x_exog = ["x_1", "x_5"]
+    controls = ["x_3", "x_2", "x_4"]
+    sc = specy.SpecificationCurve(df, y_endog, x_exog, controls, cat_expand="x_3")
+    sc.fit(estimator=sm.Probit)
+    sc.plot()
+    mock_show.assert_called_once()
+
+
+@patch("matplotlib.pyplot.show")
+def test_008_large_no_specifications(mock_show):
+    """
+    Test a very large set of specifications
+    """
+    n_samples = 400
+    # Number of dimensions of continuous
+    # random variables
+    n_dim = 8
+    c_rnd_vars = np.random.random(size=(n_dim, n_samples))
+    c_rnd_vars_names = [f"c_{i}" for i in range(np.shape(c_rnd_vars)[0])]
+    y_1 = 0.3 * c_rnd_vars[0, :] + 0.5 * c_rnd_vars[1, :]
+    y_2 = y_1 + 0.05 * np.random.randn(n_samples)
+    df = pd.DataFrame([y_1, y_2], ["y1", "y2"]).T
+    for i, col_name in enumerate(c_rnd_vars_names):
+        df[col_name] = c_rnd_vars[i, :]
+    controls = c_rnd_vars_names[1:]
+    sc = specy.SpecificationCurve(df, ["y1", "y2"], c_rnd_vars_names[0], controls)
+    sc.fit()
+    sc.plot()
+    mock_show.assert_called_once()
+
+
+@patch("matplotlib.pyplot.show")
+def test_009_always_include(mock_show):
+    """Test of always include."""
+    df = specy.load_example_data1()
+    x_exog = "x1"
+    y_endog = "y1"
+    controls = ["c2", "group1", "group2"]
+    sc = specy.SpecificationCurve(df, y_endog, x_exog, controls, always_include="c1")
+    sc.fit()
+    sc.plot()
+    mock_show.assert_called_once()
+
+
+@patch("matplotlib.pyplot.show")
+def test_010_preferred_specification(mock_show):
+    """Test of labelling preferred specification."""
+    df = specy.load_example_data1()
+    x_exog = "x1"
+    y_endog = "y1"
+    controls = ["c2", "group1", "group2"]
+    sc = specy.SpecificationCurve(df, y_endog, x_exog, controls)
+    sc.fit()
+    sc.plot(preferred_spec=["group1", "x1", "y1"])
+    mock_show.assert_called_once()