diff --git a/.gitignore b/.gitignore index 3115f62b..ec1194b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ -*.egg-info -*.DS_Store __pycache__ +_build +.ipynb_checkpoints *_checkpoints +*.DS_Store +*.egg-info +build/ dist/ +docs/_build/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..d7e5d9dc --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,69 @@ +# CONTRIBUTING + +This repository is under active development by a small number of contributors at the moment. Once the code and API has settled a bit we will open up and welcome contributions. But not yet. + +## Setup for local development + +1. Create a new environment using Python >=3.8, for example 3.10 + +``` +conda create --name CausalPy python=3.10 +``` + +2. Activate environment: + +``` +conda activate CausalPy +``` + +3. Install the package in editable mode + +``` +pip install -e . +``` + +4. Install development dependencies + +``` +pip install -r requirements-dev.txt +pip install -r requirements-docs.txt +``` + +5. You may also need to run this to get pre-commit checks working + +``` +pre-commit install +``` + +6. Note: You may have to run the following command to make Jupyter Lab aware of the `CausalPy` environment. + +``` +python -m ipykernel install --user --name CausalPy +``` + +## Building the documentation locally + +Ensure the right packages (in `requirements-docs.txt`) are available in the environment. See the steps above. + +A local build of the docs is achieved by: + +```bash +cd docs +make html +``` + +Sometimes not all changes are recognised. In that case run: + +```bash +make clean && make html +``` + +Docs are built in `docs/_build`, but these docs are _not_ committed to the GitHub repository due to `.gitignore`. + +## New releases [work in progress] + +1. Bump the release version in `causalpy/version.py`. This is automatically read by `setup.py` and `docs/config.py`. +2. Update on pypi.org. In the root directory: + - `python setup.py sdist` + - update to pypi.org with `twine upload dist/*` +3. _??? Do I have to do anything to update the docs on [`readthedocs`](https://readthedocs.org)???_ diff --git a/README.md b/README.md index 904252c8..fe9d98ed 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,16 @@ Another distinctive feature of this package is the ability to use different mode ## Installation -[coming soon] +To get the latest release: +```bash +pip install CausalPy +``` + +Alternatively, if you want the very latest version of the package you can install from GitHub: + +```bash +pip install git+https://github.com/pymc-labs/CausalPy.git +``` ## Roadmap @@ -113,57 +122,12 @@ Here are some general resources about causal inference: * Huntington-Klein, N. (2021). [The effect: An introduction to research design and causality](https://theeffectbook.net). Chapman and Hall/CRC. * Reichardt, C. S. (2019). Quasi-experimentation: A guide to design and analysis. Guilford Publications. -## Contributions - -This repository is under active development by a small number of contributors at the moment. Once the code and API has settled a bit we will open up and welcome contributions. But not yet. - ## License [Apache License 2.0](LICENSE) --- -## Local development - -1. Create a new environment using Python >=3.8, for example 3.10 - -``` -conda create --name CausalPy python=3.10 -``` - -2. Activate environment: - -``` -conda activate CausalPy -``` - -3. Install the package in editable mode - -``` -pip install -e . -``` - -4. Install development dependencies - -``` -pip install -r requirements-dev.txt -pip install -r requirements-docs.txt -``` - -5. You may also need to run this to get pre-commit checks working - -``` -pre-commit install -``` - -6. Note: You may have to run the following command to make Jupyter Lab aware of the `CausalPy` environment. - -``` -python -m ipykernel install --user --name CausalPy -``` - ---- - ## Support diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d4bb2cbb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +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/api_plot_utils.rst b/docs/api_plot_utils.rst new file mode 100644 index 00000000..471b61d7 --- /dev/null +++ b/docs/api_plot_utils.rst @@ -0,0 +1,9 @@ +:mod:`causalpy.plot_utils` +========================== + +.. toctree:: + :maxdepth: 1 + +.. automodule:: causalpy.plot_utils + :members: + :undoc-members: diff --git a/docs/api_pymc_experiments.rst b/docs/api_pymc_experiments.rst new file mode 100644 index 00000000..64215aad --- /dev/null +++ b/docs/api_pymc_experiments.rst @@ -0,0 +1,9 @@ +:mod:`causalpy.pymc_experiments` +================================ + +.. toctree:: + :maxdepth: 1 + +.. automodule:: causalpy.pymc_experiments + :members: + :undoc-members: diff --git a/docs/api_pymc_models.rst b/docs/api_pymc_models.rst new file mode 100644 index 00000000..6fb182d8 --- /dev/null +++ b/docs/api_pymc_models.rst @@ -0,0 +1,9 @@ +:mod:`causalpy.pymc_models` +=========================== + +.. toctree:: + :maxdepth: 1 + +.. automodule:: causalpy.pymc_models + :members: + :undoc-members: diff --git a/docs/api_skl_experiments.rst b/docs/api_skl_experiments.rst new file mode 100644 index 00000000..9e3977a0 --- /dev/null +++ b/docs/api_skl_experiments.rst @@ -0,0 +1,9 @@ +:mod:`causalpy.skl_experiments` +=============================== + +.. toctree:: + :maxdepth: 1 + +.. automodule:: causalpy.skl_experiments + :members: + :undoc-members: diff --git a/docs/api_skl_models.rst b/docs/api_skl_models.rst new file mode 100644 index 00000000..fbbc71c2 --- /dev/null +++ b/docs/api_skl_models.rst @@ -0,0 +1,9 @@ +:mod:`causalpy.skl_models` +========================== + +.. toctree:: + :maxdepth: 1 + +.. automodule:: causalpy.skl_models + :members: + :undoc-members: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..e0f7f8a7 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,76 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + + +# -- Path setup -------------------------------------------------------------- +# 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("../mypackage")) + + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "CausalPy" +copyright = "2022, Benjamin T. Vincent" +author = "Benjamin T. Vincent" + +from causalpy.version import __version__ + +release = __version__ +print(f"{release=}") + + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "sphinx.ext.mathjax", + "sphinx.ext.autodoc", + "sphinx_autodoc_typehints", + "nbsphinx", + "myst_parser", +] + +source_suffix = [".rst", ".md"] +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# MyST options for working with markdown files. Info about extensions here https://myst-parser.readthedocs.io/en/latest/syntax/optional.html?highlight=math#admonition-directives +myst_enable_extensions = ["dollarmath", "amsmath", "colon_fence", "linkify"] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] +# TODO: version seems not to be displayed despite setting this to True +html_theme_options = { + "display_version": True, +} + +# -- Options for autodoc ---------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration + +# Automatically extract typehints when specified and place them in +# descriptions of the relevant function/method. +autodoc_typehints = "description" + +# Don't show class signature with the class' name. +autodoc_class_signature = "separated" + +# Add "Edit on Github" link. Replaces "view page source" ---------------------- +html_context = { + "display_github": True, # Integrate GitHub + "github_user": "pymc-labs", # Username + "github_repo": "CausalPy", # Repo name + "github_version": "master", # Version + "conf_py_path": "/docs/", # Path in the checkout to the docs root +} diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..c6f6e186 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,41 @@ +.. CausalPy documentation master file, created by + sphinx-quickstart on Mon Nov 14 18:28:13 2022. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +CausalPy - Causal inference in quasi-experimental settings +========================================================== + +A Python package focussing on causal inference in quasi-experimental settings. The package allows for sophisticated Bayesian model fitting methods to be used in addition to traditional OLS. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +Documentation outline +===================== + +.. toctree:: + :caption: Examples + :titlesonly: + + notebooks/skl_demos.ipynb + notebooks/pymc_demos.ipynb + +.. toctree:: + :caption: API Reference + :titlesonly: + + api_skl_experiments + api_skl_models + api_pymc_experiments + api_pymc_models + api_plot_utils + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..32bb2452 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/notebooks/pymc_demos.ipynb b/docs/notebooks/pymc_demos.ipynb similarity index 99% rename from notebooks/pymc_demos.ipynb rename to docs/notebooks/pymc_demos.ipynb index 7708d08a..ea8e1ea2 100644 --- a/notebooks/pymc_demos.ipynb +++ b/docs/notebooks/pymc_demos.ipynb @@ -62,7 +62,7 @@ "outputs": [], "source": [ "sc_data_path = (\n", - " pathlib.Path.cwd().parents[0] / \"causalpy\" / \"data\" / \"synthetic_control.csv\"\n", + " pathlib.Path.cwd().parents[1] / \"causalpy\" / \"data\" / \"synthetic_control.csv\"\n", ")\n", "df = pd.read_csv(sc_data_path)\n", "treatment_time = 70" @@ -360,7 +360,7 @@ "\n", "if export_images:\n", " plt.savefig(\n", - " pathlib.Path.cwd().parents[0] / \"img\" / \"synthetic_control_pymc.svg\",\n", + " pathlib.Path.cwd().parents[1] / \"img\" / \"synthetic_control_pymc.svg\",\n", " bbox_inches=\"tight\",\n", " format=\"svg\",\n", " )" @@ -379,7 +379,7 @@ "metadata": {}, "outputs": [], "source": [ - "its_data_path = pathlib.Path.cwd().parents[0] / \"causalpy\" / \"data\" / \"its.csv\"\n", + "its_data_path = pathlib.Path.cwd().parents[1] / \"causalpy\" / \"data\" / \"its.csv\"\n", "df = pd.read_csv(its_data_path, parse_dates=[\"date\"])\n", "df.set_index(\"date\", inplace=True)\n", "treatment_time = pd.to_datetime(\"2017-01-01\")" @@ -676,7 +676,7 @@ "\n", "if export_images:\n", " plt.savefig(\n", - " pathlib.Path.cwd().parents[0] / \"img\" / \"interrupted_time_series_pymc.svg\",\n", + " pathlib.Path.cwd().parents[1] / \"img\" / \"interrupted_time_series_pymc.svg\",\n", " bbox_inches=\"tight\",\n", " format=\"svg\",\n", " )" @@ -695,7 +695,7 @@ "metadata": {}, "outputs": [], "source": [ - "did_data_path = pathlib.Path.cwd().parents[0] / \"causalpy\" / \"data\" / \"did.csv\"\n", + "did_data_path = pathlib.Path.cwd().parents[1] / \"causalpy\" / \"data\" / \"did.csv\"\n", "\n", "df = pd.read_csv(did_data_path)" ] @@ -989,7 +989,7 @@ "\n", "if export_images:\n", " plt.savefig(\n", - " pathlib.Path.cwd().parents[0] / \"img\" / \"difference_in_differences_pymc.svg\",\n", + " pathlib.Path.cwd().parents[1] / \"img\" / \"difference_in_differences_pymc.svg\",\n", " bbox_inches=\"tight\",\n", " format=\"svg\",\n", " )" @@ -1009,7 +1009,7 @@ "outputs": [], "source": [ "rd_data_path = (\n", - " pathlib.Path.cwd().parents[0] / \"causalpy\" / \"data\" / \"regression_discontinuity.csv\"\n", + " pathlib.Path.cwd().parents[1] / \"causalpy\" / \"data\" / \"regression_discontinuity.csv\"\n", ")\n", "df = pd.read_csv(rd_data_path)" ] @@ -1304,7 +1304,7 @@ "\n", "if export_images:\n", " plt.savefig(\n", - " pathlib.Path.cwd().parents[0] / \"img\" / \"regression_discontinuity_pymc.svg\",\n", + " pathlib.Path.cwd().parents[1] / \"img\" / \"regression_discontinuity_pymc.svg\",\n", " bbox_inches=\"tight\",\n", " format=\"svg\",\n", " )" @@ -1323,7 +1323,7 @@ "metadata": {}, "outputs": [], "source": [ - "rd_data_path = pathlib.Path.cwd().parents[0] / \"causalpy\" / \"data\" / \"drinking.csv\"\n", + "rd_data_path = pathlib.Path.cwd().parents[1] / \"causalpy\" / \"data\" / \"drinking.csv\"\n", "df = (\n", " pd.read_csv(rd_data_path)[[\"agecell\", \"all\", \"mva\", \"suicide\"]]\n", " .rename(columns={\"agecell\": \"age\"})\n", diff --git a/notebooks/skl_demos.ipynb b/docs/notebooks/skl_demos.ipynb similarity index 99% rename from notebooks/skl_demos.ipynb rename to docs/notebooks/skl_demos.ipynb index 2ca75eda..204b8c0c 100644 --- a/notebooks/skl_demos.ipynb +++ b/docs/notebooks/skl_demos.ipynb @@ -9,7 +9,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -21,7 +21,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -31,9 +31,18 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 13, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" + ] + } + ], "source": [ "%load_ext autoreload\n", "%autoreload 2" @@ -41,7 +50,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -57,12 +66,12 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "sc_data_path = (\n", - " pathlib.Path.cwd().parents[0] / \"causalpy\" / \"data\" / \"synthetic_control.csv\"\n", + " pathlib.Path.cwd().parents[1] / \"causalpy\" / \"data\" / \"synthetic_control.csv\"\n", ")\n", "df = pd.read_csv(sc_data_path)\n", "treatment_time = 70" @@ -77,7 +86,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -108,7 +117,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -135,7 +144,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 18, "metadata": {}, "outputs": [ { @@ -165,7 +174,7 @@ "\n", "if export_images:\n", " plt.savefig(\n", - " pathlib.Path.cwd().parents[0] / \"img\" / \"synthetic_control_skl.svg\",\n", + " pathlib.Path.cwd().parents[1] / \"img\" / \"synthetic_control_skl.svg\",\n", " bbox_inches=\"tight\",\n", " format=\"svg\",\n", " )" @@ -173,7 +182,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 19, "metadata": {}, "outputs": [ { @@ -200,11 +209,11 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 20, "metadata": {}, "outputs": [], "source": [ - "its_data_path = pathlib.Path.cwd().parents[0] / \"causalpy\" / \"data\" / \"its.csv\"\n", + "its_data_path = pathlib.Path.cwd().parents[1] / \"causalpy\" / \"data\" / \"its.csv\"\n", "df = pd.read_csv(its_data_path, parse_dates=[\"date\"])\n", "df.set_index(\"date\", inplace=True)\n", "treatment_time = pd.to_datetime(\"2017-01-01\")" @@ -212,7 +221,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 21, "metadata": {}, "outputs": [ { @@ -241,7 +250,7 @@ "\n", "if export_images:\n", " plt.savefig(\n", - " pathlib.Path.cwd().parents[0] / \"img\" / \"interrupted_time_series_skl.svg\",\n", + " pathlib.Path.cwd().parents[1] / \"img\" / \"interrupted_time_series_skl.svg\",\n", " bbox_inches=\"tight\",\n", " format=\"svg\",\n", " )" @@ -256,17 +265,17 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 22, "metadata": {}, "outputs": [], "source": [ - "did_data_path = pathlib.Path.cwd().parents[0] / \"causalpy\" / \"data\" / \"did.csv\"\n", + "did_data_path = pathlib.Path.cwd().parents[1] / \"causalpy\" / \"data\" / \"did.csv\"\n", "data = pd.read_csv(did_data_path)" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 23, "metadata": {}, "outputs": [ { @@ -303,7 +312,7 @@ "\n", "if export_images:\n", " plt.savefig(\n", - " pathlib.Path.cwd().parents[0] / \"img\" / \"difference_in_differences_skl.svg\",\n", + " pathlib.Path.cwd().parents[1] / \"img\" / \"difference_in_differences_skl.svg\",\n", " bbox_inches=\"tight\",\n", " format=\"svg\",\n", " )" @@ -318,19 +327,19 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 24, "metadata": {}, "outputs": [], "source": [ "rd_data_path = (\n", - " pathlib.Path.cwd().parents[0] / \"causalpy\" / \"data\" / \"regression_discontinuity.csv\"\n", + " pathlib.Path.cwd().parents[1] / \"causalpy\" / \"data\" / \"regression_discontinuity.csv\"\n", ")\n", "data = pd.read_csv(rd_data_path)" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 25, "metadata": {}, "outputs": [ { @@ -359,7 +368,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 26, "metadata": {}, "outputs": [ { @@ -385,7 +394,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 27, "metadata": {}, "outputs": [ { @@ -416,7 +425,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 28, "metadata": {}, "outputs": [ { @@ -445,7 +454,7 @@ "\n", "if export_images:\n", " plt.savefig(\n", - " pathlib.Path.cwd().parents[0] / \"img\" / \"regression_discontinuity_skl.svg\",\n", + " pathlib.Path.cwd().parents[1] / \"img\" / \"regression_discontinuity_skl.svg\",\n", " bbox_inches=\"tight\",\n", " format=\"svg\",\n", " )" @@ -460,11 +469,11 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 29, "metadata": {}, "outputs": [], "source": [ - "rd_data_path = pathlib.Path.cwd().parents[0] / \"causalpy\" / \"data\" / \"drinking.csv\"\n", + "rd_data_path = pathlib.Path.cwd().parents[1] / \"causalpy\" / \"data\" / \"drinking.csv\"\n", "df = (\n", " pd.read_csv(rd_data_path)[[\"agecell\", \"all\", \"mva\", \"suicide\"]]\n", " .rename(columns={\"agecell\": \"age\"})\n", @@ -475,7 +484,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 30, "metadata": {}, "outputs": [ { @@ -506,7 +515,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 31, "metadata": {}, "outputs": [ { diff --git a/requirements-docs.txt b/requirements-docs.txt index 2699b914..952e74c8 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,3 +1,11 @@ ipykernel +linkify-it-py +myst_parser +nbsphinx pathlib +recommonmark +sphinx +sphinx-autodoc-typehints +sphinx-design +sphinx-rtd-theme statsmodels diff --git a/setup.py b/setup.py index 5d511142..f658c536 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ import os +import sys from setuptools import find_packages, setup @@ -13,19 +14,16 @@ def get_long_description(): return f.read() -def get_version(): - with open(VERSION_FILE, encoding="utf-8") as f: - exec(f.read()) - return vars()["__version__"] - +# get version +sys.path.insert(0, os.path.abspath("../mypackage")) +from causalpy.version import __version__ with open(REQUIREMENTS_FILE) as f: install_reqs = f.read().splitlines() - setup( name="CausalPy", - version=get_version(), + version=__version__, description="Causal inference for quasi-experiments in Python", long_description=get_long_description(), long_description_content_type="text/markdown",