From 446ae05f1b4fcde41f12476e272b750d6db3aad8 Mon Sep 17 00:00:00 2001 From: Alex Lee Date: Mon, 2 Sep 2024 15:54:31 +0200 Subject: [PATCH 1/4] Implement the codespell pre-commit hook --- .codespell-whitelist.txt | 2 ++ .pre-commit-config.yaml | 18 ++++++++++++++++++ README.md | 2 +- causalpy/data/simulate_data.py | 4 ++-- causalpy/experiments/instrumental_variable.py | 2 +- .../inverse_propensity_weighting.py | 2 +- causalpy/experiments/prepostfit.py | 4 ++-- causalpy/plot_utils.py | 2 +- causalpy/pymc_models.py | 2 +- causalpy/tests/test_pymc_models.py | 2 +- docs/source/index.md | 2 +- docs/source/knowledgebase/glossary.rst | 10 +++++----- pyproject.toml | 3 +++ 13 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 .codespell-whitelist.txt diff --git a/.codespell-whitelist.txt b/.codespell-whitelist.txt new file mode 100644 index 00000000..cfa6b223 --- /dev/null +++ b/.codespell-whitelist.txt @@ -0,0 +1,2 @@ +nd +cace diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 72287e1b..a2a4d001 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,3 +41,21 @@ repos: # needed to make excludes in pyproject.toml work # see here https://github.com/econchick/interrogate/issues/60#issuecomment-735436566 pass_filenames: false + - repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + args: [ + "-S", + "*.csv", + "-S", + "*.ipynb", + "-S", + "pyproject.toml", + "--ignore-words=.codespell-whitelist.txt", + # Write changes in place + "-w", + ] + additional_dependencies: + # Support pyproject.toml configuration + - tomli diff --git a/README.md b/README.md index 9e319b8b..af815c05 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ This is appropriate when you have multiple units, one of which is treated. You b > The data (treated and untreated units), pre-treatment model fit, and counterfactual (i.e. the synthetic control) are plotted (top). The causal impact is shown as a blue shaded region. The Bayesian analysis shows shaded Bayesian credible regions of the model fit and counterfactual. Also shown is the causal impact (middle) and cumulative causal impact (bottom). ### Geographical lift (Geolift) -We can also use synthetic control methods to analyse data from geographical lift studies. For example, we can try to evaluate the causal impact of an intervention (e.g. a marketing campaign) run in one geographical area by using control geographical areas which are similar to the intervention area but which did not recieve the specific marketing intervention. +We can also use synthetic control methods to analyse data from geographical lift studies. For example, we can try to evaluate the causal impact of an intervention (e.g. a marketing campaign) run in one geographical area by using control geographical areas which are similar to the intervention area but which did not receive the specific marketing intervention. ### ANCOVA diff --git a/causalpy/data/simulate_data.py b/causalpy/data/simulate_data.py index 5b77d172..0dfff360 100644 --- a/causalpy/data/simulate_data.py +++ b/causalpy/data/simulate_data.py @@ -291,7 +291,7 @@ def generate_ancova_data( N=200, pre_treatment_means=np.array([10, 12]), treatment_effect=2, sigma=1 ): """ - Generate ANCOVA eample data + Generate ANCOVA example data Example -------- @@ -440,7 +440,7 @@ def generate_seasonality(n=12, amplitude=1, length_scale=0.5): def periodic_kernel(x1, x2, period=1, length_scale=1, amplitude=1): - """Generate a periodic kernal for gaussian process""" + """Generate a periodic kernel for gaussian process""" return amplitude**2 * np.exp( -2 * np.sin(np.pi * np.abs(x1 - x2) / period) ** 2 / length_scale**2 ) diff --git a/causalpy/experiments/instrumental_variable.py b/causalpy/experiments/instrumental_variable.py index e318b9d1..a272ad3e 100644 --- a/causalpy/experiments/instrumental_variable.py +++ b/causalpy/experiments/instrumental_variable.py @@ -44,7 +44,7 @@ class InstrumentalVariable(BaseExperiment): :param model: A PyMC model :param priors: An optional dictionary of priors for the mus and sigmas of both regressions. If priors are not - specified we will substitue MLE estimates for the beta + specified we will substitute MLE estimates for the beta coefficients. Greater control can be achieved by specifying the priors directly e.g. priors = { "mus": [0, 0], diff --git a/causalpy/experiments/inverse_propensity_weighting.py b/causalpy/experiments/inverse_propensity_weighting.py index a84f360d..3e87046c 100644 --- a/causalpy/experiments/inverse_propensity_weighting.py +++ b/causalpy/experiments/inverse_propensity_weighting.py @@ -195,7 +195,7 @@ def make_doubly_robust_adjustment(self, ps): m1 = sk_lin_reg().fit(X[t == 1].astype(float), self.y[t == 1]) m0_pred = m0.predict(X) m1_pred = m1.predict(X) - ## Compromise between outcome and treatement assignment model + ## Compromise between outcome and treatment assignment model weighted_outcome0 = (1 - t) * (self.y - m0_pred) / (1 - X["ps"]) + m0_pred weighted_outcome1 = t * (self.y - m1_pred) / X["ps"] + m1_pred return weighted_outcome0, weighted_outcome1, None, None diff --git a/causalpy/experiments/prepostfit.py b/causalpy/experiments/prepostfit.py index abdae6e0..127a0799 100644 --- a/causalpy/experiments/prepostfit.py +++ b/causalpy/experiments/prepostfit.py @@ -311,7 +311,7 @@ class InterruptedTimeSeries(PrePostFit): :param data: A pandas dataframe :param treatment_time: - The time when treatment occured, should be in reference to the data index + The time when treatment occurred, should be in reference to the data index :param formula: A statistical model formula :param model: @@ -352,7 +352,7 @@ class SyntheticControl(PrePostFit): :param data: A pandas dataframe :param treatment_time: - The time when treatment occured, should be in reference to the data index + The time when treatment occurred, should be in reference to the data index :param formula: A statistical model formula :param model: diff --git a/causalpy/plot_utils.py b/causalpy/plot_utils.py index 9cf7962f..df1c0376 100644 --- a/causalpy/plot_utils.py +++ b/causalpy/plot_utils.py @@ -73,7 +73,7 @@ def plot_xY( ax=ax, **plot_hdi_kwargs, ) - # Return handle to patch. We get a list of the childen of the axis. Filter for just + # Return handle to patch. We get a list of the children of the axis. Filter for just # the PolyCollection objects. Take the last one. h_patch = list( filter(lambda x: isinstance(x, PolyCollection), ax_hdi.get_children()) diff --git a/causalpy/pymc_models.py b/causalpy/pymc_models.py index 4702a4c4..39d26025 100644 --- a/causalpy/pymc_models.py +++ b/causalpy/pymc_models.py @@ -27,7 +27,7 @@ class PyMCModel(pm.Model): - """A wraper class for PyMC models. This provides a scikit-learn like interface with + """A wrapper class for PyMC models. This provides a scikit-learn like interface with methods like `fit`, `predict`, and `score`. It also provides other methods which are useful for causal inference. diff --git a/causalpy/tests/test_pymc_models.py b/causalpy/tests/test_pymc_models.py index 132d4759..79ad53e8 100644 --- a/causalpy/tests/test_pymc_models.py +++ b/causalpy/tests/test_pymc_models.py @@ -142,7 +142,7 @@ def test_idata_property(): @pytest.mark.parametrize("seed", seeds) def test_result_reproducibility(seed): """Test that we can reproduce the results from the model. We could in theory test - this with all the model and experiment types, but what is being targetted is + this with all the model and experiment types, but what is being targeted is the PyMCModel.fit method, so we should be safe testing with just one model. Here we use the DifferenceInDifferences experiment class.""" # Load the data diff --git a/docs/source/index.md b/docs/source/index.md index 4d527813..14e7960f 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -98,7 +98,7 @@ This is appropriate when you have multiple units, one of which is treated. You b ![Synthetic Control](./_static/synthetic_control_pymc.svg) ### Geographical Lift / Geolift -We can also use synthetic control methods to analyse data from geographical lift studies. For example, we can try to evaluate the causal impact of an intervention (e.g. a marketing campaign) run in one geographical area by using control geographical areas which are similar to the intervention area but which did not recieve the specific marketing intervention. +We can also use synthetic control methods to analyse data from geographical lift studies. For example, we can try to evaluate the causal impact of an intervention (e.g. a marketing campaign) run in one geographical area by using control geographical areas which are similar to the intervention area but which did not receive the specific marketing intervention. ### ANCOVA diff --git a/docs/source/knowledgebase/glossary.rst b/docs/source/knowledgebase/glossary.rst index 8ced4597..9ae8e4f5 100644 --- a/docs/source/knowledgebase/glossary.rst +++ b/docs/source/knowledgebase/glossary.rst @@ -9,11 +9,11 @@ Glossary Average treatment effect ATE - The average treatement effect across all units. + The average treatment effect across all units. Average treatment effect on the treated ATT - The average effect of the treatment on the units that recieved it. Also called Treatment on the treated. + The average effect of the treatment on the units that received it. Also called Treatment on the treated. Change score analysis A statistical procedure where the outcome variable is the difference between the posttest and protest scores. @@ -48,7 +48,7 @@ Glossary Local Average Treatment effect LATE - Also known asthe complier average causal effect (CACE), is the effect of a treatment for subjects who comply with the experimental treatment assigned to their sample group. It is the quantity we're estimating in IV designs. + Also known asthe compiler average causal effect (CACE), is the effect of a treatment for subjects who comply with the experimental treatment assigned to their sample group. It is the quantity we're estimating in IV designs. Non-equivalent group designs NEGD @@ -76,7 +76,7 @@ Glossary Where units are assigned to conditions at random. Randomized experiment - An emprical comparison used to estimate the effects of treatments where units are assigned to treatment conditions randomly. + An empirical comparison used to estimate the effects of treatments where units are assigned to treatment conditions randomly. Regression discontinuity design RDD @@ -96,7 +96,7 @@ Glossary Treatment on the treated effect TOT - The average effect of the treatment on the units that recieved it. Also called the average treatment effect on the treated (ATT). + The average effect of the treatment on the units that received it. Also called the average treatment effect on the treated (ATT). Treatment effect The difference in outcomes between what happened after a treatment is implemented and what would have happened (see Counterfactual) if the treatment had not been implemented, assuming everything else had been the same. diff --git a/pyproject.toml b/pyproject.toml index 17208d10..cc199185 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,3 +122,6 @@ badge-format = "svg" extend-select = [ "I", # isort ] + +[tools.codespell] +ignore-words = ".codespell/ignore_words.txt" From db9e69b33d49576c411bf7edf094cd61d5671db9 Mon Sep 17 00:00:00 2001 From: Alex Lee Date: Thu, 5 Sep 2024 12:15:37 +0200 Subject: [PATCH 2/4] Create a notebook conversion script that the pre-commit job now calls for codespell. --- .codespell-whitelist.txt | 2 - .codespell/codespell-whitelist.txt | 3 + .codespell/notebook_to_markdown.py | 71 +++++++++++++++++++ .pre-commit-config.yaml | 30 ++++++-- docs/source/_static/interrogate_badge.svg | 6 +- docs/source/knowledgebase/quasi_dags.ipynb | 2 +- docs/source/notebooks/ancova_pymc.ipynb | 2 +- docs/source/notebooks/did_pymc.ipynb | 2 +- docs/source/notebooks/did_pymc_banks.ipynb | 4 +- docs/source/notebooks/geolift1.ipynb | 2 +- docs/source/notebooks/inv_prop_pymc.ipynb | 6 +- docs/source/notebooks/its_covid.ipynb | 4 +- docs/source/notebooks/its_pymc.ipynb | 4 +- .../notebooks/iv_weak_instruments.ipynb | 12 ++-- .../source/notebooks/multi_cell_geolift.ipynb | 8 +-- docs/source/notebooks/rd_pymc.ipynb | 2 +- docs/source/notebooks/rd_pymc_drinking.ipynb | 2 +- docs/source/notebooks/rkink_pymc.ipynb | 2 +- docs/source/notebooks/sc_pymc.ipynb | 4 +- docs/source/notebooks/sc_pymc_brexit.ipynb | 4 +- pyproject.toml | 2 + 21 files changed, 133 insertions(+), 41 deletions(-) delete mode 100644 .codespell-whitelist.txt create mode 100644 .codespell/codespell-whitelist.txt create mode 100644 .codespell/notebook_to_markdown.py diff --git a/.codespell-whitelist.txt b/.codespell-whitelist.txt deleted file mode 100644 index cfa6b223..00000000 --- a/.codespell-whitelist.txt +++ /dev/null @@ -1,2 +0,0 @@ -nd -cace diff --git a/.codespell/codespell-whitelist.txt b/.codespell/codespell-whitelist.txt new file mode 100644 index 00000000..02e5ba47 --- /dev/null +++ b/.codespell/codespell-whitelist.txt @@ -0,0 +1,3 @@ +nD +CACE +compliers diff --git a/.codespell/notebook_to_markdown.py b/.codespell/notebook_to_markdown.py new file mode 100644 index 00000000..11681bad --- /dev/null +++ b/.codespell/notebook_to_markdown.py @@ -0,0 +1,71 @@ +# Copyright 2024 The PyMC Labs Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This is a simple script that converts the jupyter notebooks into markdown +for easier (and cleaner) parsing for the codespell check. Whitelisted words +are maintained within this directory in the `codespeel-whitelist.txt`. For +more information on this pre-commit hook please visit the github homepage +for the project: https://github.com/codespell-project/codespell. +""" + +import argparse +import os +from glob import glob + +import nbformat +from nbconvert import MarkdownExporter + + +def notebook_to_markdown(pattern: str, output_dir: str) -> None: + """ + Utility to convert jupyter notebook to markdown files. + + :param pattern: + str that is a glob appropriate pattern to search + :param output_dir: + str directory to save the markdown files to + """ + for f_name in glob(pattern, recursive=True): + with open(f_name, "r", encoding="utf-8") as f: + nb = nbformat.read(f, as_version=4) + + markdown_exporter = MarkdownExporter() + (body, _) = markdown_exporter.from_notebook_node(nb) + + os.makedirs(output_dir, exist_ok=True) + + output_file = os.path.join( + output_dir, os.path.splitext(os.path.basename(f_name))[0] + ".md" + ) + + with open(output_file, "w", encoding="utf-8") as f: + f.write(body) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "-p", + "--pattern", + help="the glob appropriate pattern to search for jupyter notebooks", + default="docs/**/*.ipynb", + ) + parser.add_argument( + "-t", + "--tempdir", + help="temporary directory to save the converted notebooks", + default="tmp_markdown", + ) + args = parser.parse_args() + notebook_to_markdown(args.pattern, args.tempdir) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a2a4d001..3db98699 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: exclude: &exclude_pattern 'iv_weak_instruments.ipynb' args: ["--maxkb=1500"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.2 + rev: v0.6.3 hooks: # Run the linter - id: ruff @@ -41,6 +41,16 @@ repos: # needed to make excludes in pyproject.toml work # see here https://github.com/econchick/interrogate/issues/60#issuecomment-735436566 pass_filenames: false + - repo: local + hooks: + - id: convert-notebooks + name: Convert Notebooks to Markdown + entry: python ./.codespell/notebook_to_markdown.py + language: python + pass_filenames: false + always_run: false + additional_dependencies: ["nbconvert", "nbformat"] + args: ["--tempdir", "tmp_markdown"] - repo: https://github.com/codespell-project/codespell rev: v2.3.0 hooks: @@ -49,13 +59,21 @@ repos: "-S", "*.csv", "-S", - "*.ipynb", - "-S", "pyproject.toml", - "--ignore-words=.codespell-whitelist.txt", - # Write changes in place - "-w", + "-S", + "*.svg", + "-S", + "*.ipynb", + "--ignore-words=./.codespell/codespell-whitelist.txt", ] additional_dependencies: # Support pyproject.toml configuration - tomli + - repo: local + hooks: + - id: remove-temp-directory-notebooks + name: Remove temporary directory for codespell + entry: bash -c 'rm -rf tmp_markdown && exit 0' + language: system + always_run: true + pass_filenames: false diff --git a/docs/source/_static/interrogate_badge.svg b/docs/source/_static/interrogate_badge.svg index 73800a2f..2d6395ba 100644 --- a/docs/source/_static/interrogate_badge.svg +++ b/docs/source/_static/interrogate_badge.svg @@ -1,5 +1,5 @@ - interrogate: 90.0% + interrogate: 90.1% @@ -12,8 +12,8 @@ interrogate interrogate - 90.0% - 90.0% + 90.1% + 90.1% diff --git a/docs/source/knowledgebase/quasi_dags.ipynb b/docs/source/knowledgebase/quasi_dags.ipynb index 5eeec27e..2c780c40 100644 --- a/docs/source/knowledgebase/quasi_dags.ipynb +++ b/docs/source/knowledgebase/quasi_dags.ipynb @@ -104,7 +104,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This leads us to Randomized Controlled Trials (RCTs) which are considered the gold standard for estimating causal effects. One reason for this is that we (as experimenters) intervene in the system by assigning units to treatment by {term}`random assignment`. Because of this intervention, any causal influence of the confounders upon the treatment $\\mathbf{X} \\rightarrow Z$ is broken - treamtent is now soley determined by the randomisation process, $R \\rightarrow T$. The following causal DAG illustrates the structure of an RCT." + "This leads us to Randomized Controlled Trials (RCTs) which are considered the gold standard for estimating causal effects. One reason for this is that we (as experimenters) intervene in the system by assigning units to treatment by {term}`random assignment`. Because of this intervention, any causal influence of the confounders upon the treatment $\\mathbf{X} \\rightarrow Z$ is broken - treamtent is now solely determined by the randomisation process, $R \\rightarrow T$. The following causal DAG illustrates the structure of an RCT." ] }, { diff --git a/docs/source/notebooks/ancova_pymc.ipynb b/docs/source/notebooks/ancova_pymc.ipynb index 50faf453..a2daa024 100644 --- a/docs/source/notebooks/ancova_pymc.ipynb +++ b/docs/source/notebooks/ancova_pymc.ipynb @@ -222,7 +222,7 @@ "## Run the analysis\n", "\n", ":::{note}\n", - "The `random_seed` keyword argument for the PyMC sampler is not neccessary. We use it here so that the results are reproducible.\n", + "The `random_seed` keyword argument for the PyMC sampler is not necessary. We use it here so that the results are reproducible.\n", ":::" ] }, diff --git a/docs/source/notebooks/did_pymc.ipynb b/docs/source/notebooks/did_pymc.ipynb index 72364638..e44c6117 100644 --- a/docs/source/notebooks/did_pymc.ipynb +++ b/docs/source/notebooks/did_pymc.ipynb @@ -148,7 +148,7 @@ "## Run the analysis\n", "\n", ":::{note}\n", - "The `random_seed` keyword argument for the PyMC sampler is not neccessary. We use it here so that the results are reproducible.\n", + "The `random_seed` keyword argument for the PyMC sampler is not necessary. We use it here so that the results are reproducible.\n", ":::" ] }, diff --git a/docs/source/notebooks/did_pymc_banks.ipynb b/docs/source/notebooks/did_pymc_banks.ipynb index 6c8fab8e..82e802b9 100644 --- a/docs/source/notebooks/did_pymc_banks.ipynb +++ b/docs/source/notebooks/did_pymc_banks.ipynb @@ -329,7 +329,7 @@ "* $\\mu_i$ is the expected value of the outcome (number of banks in business) for the $i^{th}$ observation.\n", "* $\\beta_0$ is an intercept term to capture the basiline number of banks in business of the control group, in the pre-intervention period.\n", "* `district` is a dummy variable, so $\\beta_{d}$ will represent a main effect of district, that is any offset of the treatment group relative to the control group.\n", - "* `post_treatment` is also a dummy variable which captures any shift in the outcome after the treatment time, regardless of the recieving treatment or not.\n", + "* `post_treatment` is also a dummy variable which captures any shift in the outcome after the treatment time, regardless of the receiving treatment or not.\n", "* the interaction of the two dummary variables `district:post_treatment` will only take on values of 1 for the treatment group after the intervention. Therefore $\\beta_{\\Delta}$ will represent our estimated causal effect." ] }, @@ -515,7 +515,7 @@ "source": [ "## Analysis 2 - DiD with multiple pre/post observations\n", "\n", - "Now we'll do a difference in differences analysis of the full dataset. This approach has similarities to {term}`CITS` (Comparative Interrupted Time-Series) with a single control over time. Although slightly abitrary, we distinguish between the two techniques on whether there is enough time series data for CITS to capture the time series patterns." + "Now we'll do a difference in differences analysis of the full dataset. This approach has similarities to {term}`CITS` (Comparative Interrupted Time-Series) with a single control over time. Although slightly arbitrary, we distinguish between the two techniques on whether there is enough time series data for CITS to capture the time series patterns." ] }, { diff --git a/docs/source/notebooks/geolift1.ipynb b/docs/source/notebooks/geolift1.ipynb index a41c6d1f..caf187f1 100644 --- a/docs/source/notebooks/geolift1.ipynb +++ b/docs/source/notebooks/geolift1.ipynb @@ -269,7 +269,7 @@ "We can use `CausalPy`'s API to run this procedure, but using Bayesian inference methods as follows:\n", "\n", ":::{note}\n", - "The `random_seed` keyword argument for the PyMC sampler is not neccessary. We use it here so that the results are reproducible.\n", + "The `random_seed` keyword argument for the PyMC sampler is not necessary. We use it here so that the results are reproducible.\n", ":::" ] }, diff --git a/docs/source/notebooks/inv_prop_pymc.ipynb b/docs/source/notebooks/inv_prop_pymc.ipynb index 76f06887..844c1b6a 100644 --- a/docs/source/notebooks/inv_prop_pymc.ipynb +++ b/docs/source/notebooks/inv_prop_pymc.ipynb @@ -22,9 +22,9 @@ "\n", "In this notebook we will briefly demonstrate how to use propensity score weighting schemes to recover treatment effects in the analysis of observational data. We will first showcase the method with a simulated data example drawn from Lucy D’Agostino McGowan's [excellent blog](https://livefreeordichotomize.com/posts/2019-01-17-understanding-propensity-score-weighting/) on inverse propensity score weighting. Then we shall apply the same techniques to NHEFS data set discussed in Miguel Hernan and Robins' _Causal Inference: What if_ [book](https://www.hsph.harvard.edu/miguel-hernan/causal-inference-book/). This data set measures the effect of quitting smoking between the period of 1971 and 1982. At each of these two points in time the participant's weight was recorded, and we seek to estimate the effect of quitting in the intervening years on the weight recorded in 1982.\n", "\n", - "We will use inverse propensity score weighting techniques to estimate the average treatment effect. There are a range of weighting techniques available: we have implemented `raw`, `robust`, `doubly robust` and `overlap` weighting schemes all of which aim to estimate the average treatment effect. The idea of a propensity score (very broadly) is to derive a one-number summary of individual's probability of adopting a particular treatment. This score is typically calculated by fitting a predictive logit model on all an individual's observed attributes predicting whether or not the those attributes drive the indivdual towards the treatment status. In the case of the NHEFS data we want a model to measure the propensity for each individual to quit smoking. \n", + "We will use inverse propensity score weighting techniques to estimate the average treatment effect. There are a range of weighting techniques available: we have implemented `raw`, `robust`, `doubly robust` and `overlap` weighting schemes all of which aim to estimate the average treatment effect. The idea of a propensity score (very broadly) is to derive a one-number summary of individual's probability of adopting a particular treatment. This score is typically calculated by fitting a predictive logit model on all an individual's observed attributes predicting whether or not the those attributes drive the individual towards the treatment status. In the case of the NHEFS data we want a model to measure the propensity for each individual to quit smoking. \n", "\n", - "The reason we want this propensity score is because with observed data we often have a kind of imbalance in our covariate profiles across treatment groups. Meaning our data might be unrepresentative in some crucial aspect. This prevents us cleanly reading off treatment effects by looking at simple group differences. These \"imbalances\" can be driven by selection effects into the treatment status so that if we want to estimate the average treatment effect in the population as a whole we need to be wary that our sample might not give us generalisable insight into the treatment differences. Using propensity scores as a measure of the prevalance to adopt the treatment status in the population, we can cleverly weight the observed data to privilege observations of \"rare\" occurence in each group. For example, if smoking is the treatment status and regular running is generally not common among the group of smokers, then on the occasion we see a smoker marathon runner we should heavily weight their outcome measure to overcome their low prevalence in the treated group but real presence in the unmeasured population. Inverse propensity weighting tries to define weighting schemes are inversely proportional to an individual's propensity score so as to better recover an estimate which mitigates (somewhat) the risk of selection effect bias. For more details and illustration of these themes see the PyMC examples [write up](https://www.pymc.io/projects/examples/en/latest/causal_inference/bayesian_nonparametric_causal.html) on Non-Parametric Bayesian methods. {cite:p}`forde2024nonparam`\n" + "The reason we want this propensity score is because with observed data we often have a kind of imbalance in our covariate profiles across treatment groups. Meaning our data might be unrepresentative in some crucial aspect. This prevents us cleanly reading off treatment effects by looking at simple group differences. These \"imbalances\" can be driven by selection effects into the treatment status so that if we want to estimate the average treatment effect in the population as a whole we need to be wary that our sample might not give us generalisable insight into the treatment differences. Using propensity scores as a measure of the prevalance to adopt the treatment status in the population, we can cleverly weight the observed data to privilege observations of \"rare\" occurrence in each group. For example, if smoking is the treatment status and regular running is generally not common among the group of smokers, then on the occasion we see a smoker marathon runner we should heavily weight their outcome measure to overcome their low prevalence in the treated group but real presence in the unmeasured population. Inverse propensity weighting tries to define weighting schemes are inversely proportional to an individual's propensity score so as to better recover an estimate which mitigates (somewhat) the risk of selection effect bias. For more details and illustration of these themes see the PyMC examples [write up](https://www.pymc.io/projects/examples/en/latest/causal_inference/bayesian_nonparametric_causal.html) on Non-Parametric Bayesian methods. {cite:p}`forde2024nonparam`\n" ] }, { @@ -832,7 +832,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We see here how the particular weighting scheme was able to recover the true treatment effect by defining a contrast in a different pseudo population. This is a useful reminder in that, while propensity score weighting methods are aids to inference in observational data, not all weighting schemes are created equal and we need to be careful in our assessment of when each is applied appropriately. Fundamentally the weighting scheme of choice should be tied to the question of what are you trying to estimate. Aronow and Miller's _Foundations of Agnostic Statistics_ {cite:p}`aronowFoundations` has a good explantion of the differences between the `raw`, `robust` and `doubly robust` weighting schemes. In some sense these offer an escalating series of refined estimators each trying to improve the variance in the ATE estimate. The `doubly robust` approach also tries to offer some guarantees against model misspecification. The `overlap` estimator represents an attempt to calculate the ATE among the population with the overlapping propensity scores. This can be used to guard against poor inference in cases where propensity score distributions have large non-overlapping regions." + "We see here how the particular weighting scheme was able to recover the true treatment effect by defining a contrast in a different pseudo population. This is a useful reminder in that, while propensity score weighting methods are aids to inference in observational data, not all weighting schemes are created equal and we need to be careful in our assessment of when each is applied appropriately. Fundamentally the weighting scheme of choice should be tied to the question of what are you trying to estimate. Aronow and Miller's _Foundations of Agnostic Statistics_ {cite:p}`aronowFoundations` has a good explanation of the differences between the `raw`, `robust` and `doubly robust` weighting schemes. In some sense these offer an escalating series of refined estimators each trying to improve the variance in the ATE estimate. The `doubly robust` approach also tries to offer some guarantees against model misspecification. The `overlap` estimator represents an attempt to calculate the ATE among the population with the overlapping propensity scores. This can be used to guard against poor inference in cases where propensity score distributions have large non-overlapping regions." ] }, { diff --git a/docs/source/notebooks/its_covid.ipynb b/docs/source/notebooks/its_covid.ipynb index d12ec10b..dbba8e81 100644 --- a/docs/source/notebooks/its_covid.ipynb +++ b/docs/source/notebooks/its_covid.ipynb @@ -167,7 +167,7 @@ "\n", "* `date` + `year`: self explanatory\n", "* `month`: month, numerically encoded. Needs to be treated as a categorical variable\n", - "* `temp`: average UK temperature (Celcius)\n", + "* `temp`: average UK temperature (Celsius)\n", "* `t`: time\n", "* `pre`: boolean flag indicating pre or post intervention" ] @@ -182,7 +182,7 @@ "In this example we are going to standardize the data. So we have to be careful in how we interpret the inferred regression coefficients, and the posterior predictions will be in this standardized space.\n", "\n", ":::{note}\n", - "The `random_seed` keyword argument for the PyMC sampler is not neccessary. We use it here so that the results are reproducible.\n", + "The `random_seed` keyword argument for the PyMC sampler is not necessary. We use it here so that the results are reproducible.\n", ":::" ] }, diff --git a/docs/source/notebooks/its_pymc.ipynb b/docs/source/notebooks/its_pymc.ipynb index 4ba95219..bc9d8dc3 100644 --- a/docs/source/notebooks/its_pymc.ipynb +++ b/docs/source/notebooks/its_pymc.ipynb @@ -163,7 +163,7 @@ "Run the analysis\n", "\n", ":::{note}\n", - "The `random_seed` keyword argument for the PyMC sampler is not neccessary. We use it here so that the results are reproducible.\n", + "The `random_seed` keyword argument for the PyMC sampler is not necessary. We use it here so that the results are reproducible.\n", ":::" ] }, @@ -304,7 +304,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As well as the model coefficients, we might be interested in the avarage causal impact and average cumulative causal impact.\n", + "As well as the model coefficients, we might be interested in the average causal impact and average cumulative causal impact.\n", "\n", ":::{note}\n", "Better output for the summary statistics are in progress!\n", diff --git a/docs/source/notebooks/iv_weak_instruments.ipynb b/docs/source/notebooks/iv_weak_instruments.ipynb index 7ebbdd06..b03f46cf 100644 --- a/docs/source/notebooks/iv_weak_instruments.ipynb +++ b/docs/source/notebooks/iv_weak_instruments.ipynb @@ -155,7 +155,7 @@ "source": [ "#### Digression: Sampling Multivariate Normals\n", "\n", - "How can we measure this correlation between instrument and treatment? How much correlation should we expect? In the `CausalPy` implementation of instrumental variable regression we model this correlation explicity using an LKJ Cholesky prior on a Multivariate Normal distribution. It's worth a small digression here to show how sampling from this distribution under different priors can shape the correlations of the joint-distribution. We'll show below how this offers us a mechanism to impose constraints on our beliefs about the relationships between our instruments. " + "How can we measure this correlation between instrument and treatment? How much correlation should we expect? In the `CausalPy` implementation of instrumental variable regression we model this correlation explicitly using an LKJ Cholesky prior on a Multivariate Normal distribution. It's worth a small digression here to show how sampling from this distribution under different priors can shape the correlations of the joint-distribution. We'll show below how this offers us a mechanism to impose constraints on our beliefs about the relationships between our instruments. " ] }, { @@ -241,7 +241,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In the above series of plots we have sampled from two different parameterisations of an LKJ prior probability distribution. This distribution is a prior on the expected values and covariance structure of a multivariate normal probability distribution. We have shown different parameterisations of the prior lead to more or less correlated realisations of the bivariate normal we're sampling. Here we can see that increasing the `eta` parameter on the LKJ prior shrinks the range of admissable correlations parameters. By default the `CausalPy` implementation of the `InstrumentalVariable` class samples from a bivariate normal distribution of the treatment and the outcome with a parameter setting of `eta=2`. This is worth knowing if your model makes such potential correlations very unlikely. We will show below how you can apply priors to these parameters in the instrumental variable context. " + "In the above series of plots we have sampled from two different parameterisations of an LKJ prior probability distribution. This distribution is a prior on the expected values and covariance structure of a multivariate normal probability distribution. We have shown different parameterisations of the prior lead to more or less correlated realisations of the bivariate normal we're sampling. Here we can see that increasing the `eta` parameter on the LKJ prior shrinks the range of admissible correlations parameters. By default the `CausalPy` implementation of the `InstrumentalVariable` class samples from a bivariate normal distribution of the treatment and the outcome with a parameter setting of `eta=2`. This is worth knowing if your model makes such potential correlations very unlikely. We will show below how you can apply priors to these parameters in the instrumental variable context. " ] }, { @@ -349,7 +349,7 @@ "df = cp.load_data(\"schoolReturns\")\n", "\n", "\n", - "def poly(x, p): # replicate R's poly decompostion function\n", + "def poly(x, p): # replicate R's poly decomposition function\n", " X = np.transpose(np.vstack([x**k for k in range(p + 1)]))\n", " return np.linalg.qr(X)[0][:, 1:]\n", "\n", @@ -715,7 +715,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "There is at least some indication here that the proximity to college has an impact on educational attainment, and that education seems to drive upward the achieved wages. Typical approaches to instrumental variable regressions try to quantify the strength of the instrument formally. But even visually here the evidence that proximity to college would induce increased levels of education seems compelling. For instance, we see a clear upward sloping trend relationship between educational attainment and wage-acquisition. But additionally, in the first plot we can see a sparse presence of individuals who achieved the maximum educational outcomes among those with poor proximity to 4 year colleges i.e. fewer red dots than blue in the ellipses. A similar story plays out in the second plot but focused on the maximal educational attainment assuming the proxmity of a 2 year college. These observations should tilt our view of the conditional expectations for wage growth based on proximty to college. " + "There is at least some indication here that the proximity to college has an impact on educational attainment, and that education seems to drive upward the achieved wages. Typical approaches to instrumental variable regressions try to quantify the strength of the instrument formally. But even visually here the evidence that proximity to college would induce increased levels of education seems compelling. For instance, we see a clear upward sloping trend relationship between educational attainment and wage-acquisition. But additionally, in the first plot we can see a sparse presence of individuals who achieved the maximum educational outcomes among those with poor proximity to 4 year colleges i.e. fewer red dots than blue in the ellipses. A similar story plays out in the second plot but focused on the maximal educational attainment assuming the proxmity of a 2 year college. These observations should tilt our view of the conditional expectations for wage growth based on proximity to college. " ] }, { @@ -724,7 +724,7 @@ "source": [ "### Justificatory Models\n", "\n", - "We start with the simple regression context. This serves two purposes: (i) we can explore how the effect of `education` is measured in a simple regression and we can (ii) benchmark the efficacy of our instrument `nearcollege_indcator` in the context of trying to predict `education`. These regressions are effectively diagnostic tests of our instrument. In what follows we'll look seperately at\n", + "We start with the simple regression context. This serves two purposes: (i) we can explore how the effect of `education` is measured in a simple regression and we can (ii) benchmark the efficacy of our instrument `nearcollege_indcator` in the context of trying to predict `education`. These regressions are effectively diagnostic tests of our instrument. In what follows we'll look separately at\n", "\n", "- (i) the first stage regression of the LATE estimate, \n", "- (ii) the reduced form regression and \n", @@ -3155,7 +3155,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The uncertainty in the correlation implied in the last model kind of undermines this model specification. If our argument about the instrument is to be compelling we would expect __relevance__ to hold. A model specification which degrades the relevance by means of reduced correlation is perhaps too extreme. We have in effect degraded the relevance of our instrument and still recover a strong positive effect for `beta_z[education]`. The point here is not to argue about the parameter settings, just to show that multiple models need to be considered and some sensetivity testing is always warranted when justifying an IV design. " + "The uncertainty in the correlation implied in the last model kind of undermines this model specification. If our argument about the instrument is to be compelling we would expect __relevance__ to hold. A model specification which degrades the relevance by means of reduced correlation is perhaps too extreme. We have in effect degraded the relevance of our instrument and still recover a strong positive effect for `beta_z[education]`. The point here is not to argue about the parameter settings, just to show that multiple models need to be considered and some sensitivity testing is always warranted when justifying an IV design. " ] }, { diff --git a/docs/source/notebooks/multi_cell_geolift.ipynb b/docs/source/notebooks/multi_cell_geolift.ipynb index 4c1fbc86..ebba88e9 100644 --- a/docs/source/notebooks/multi_cell_geolift.ipynb +++ b/docs/source/notebooks/multi_cell_geolift.ipynb @@ -10,7 +10,7 @@ "\n", "This may be a particularly common use case in marketing, where a company may want to understand the impact of a marketing campaign in multiple regions. But these methods are not restricted to marketing of course - the methods shown here are general. Another concrete use case may be in public health, where a public health intervention may be rolled out in multiple regions.\n", "\n", - "This notebook focusses on the situation where the treatment has already taken place, and now we want to understand the causal effects of the treatments that were executed. Much work likely preceeded this analysis, such as asking yourself questions like \"which geos should I run the treatment in?\", \"what should the treatment be?\" But these pre-treatment questions are not the focus of this notebook.\n", + "This notebook focusses on the situation where the treatment has already taken place, and now we want to understand the causal effects of the treatments that were executed. Much work likely preceded this analysis, such as asking yourself questions like \"which geos should I run the treatment in?\", \"what should the treatment be?\" But these pre-treatment questions are not the focus of this notebook.\n", "\n", "We can imagine two scenarios (there may be more), and show how we can tailor our analysis to each:\n", "\n", @@ -310,7 +310,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Always vizualise the data before starting the analysis. Our rather uncreative naming scheme uses `u` to represent untreated geos, and `t` to represent treated geos. The number after the `u` or `t` represents the geo number." + "Always visualise the data before starting the analysis. Our rather uncreative naming scheme uses `u` to represent untreated geos, and `t` to represent treated geos. The number after the `u` or `t` represents the geo number." ] }, { @@ -559,7 +559,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Let's vizualise this aggregated geo and compare it to the individual treated geo's." + "Let's visualise this aggregated geo and compare it to the individual treated geo's." ] }, { @@ -1120,7 +1120,7 @@ "source": [ "Now let's plot the weightings of the untreated geos for each treated geo. Note that `sigma` is the model's estimate of the standard deviation of the observation noise.\n", "\n", - "If we wanted to produce seperate plots for each target geo, we could do so like this:\n", + "If we wanted to produce separate plots for each target geo, we could do so like this:\n", "\n", "```python\n", "fig, axs = plt.subplots(len(treated), 1, figsize=(8, 4 * len(treated)), sharex=True)\n", diff --git a/docs/source/notebooks/rd_pymc.ipynb b/docs/source/notebooks/rd_pymc.ipynb index 348c89a7..be23d3c4 100644 --- a/docs/source/notebooks/rd_pymc.ipynb +++ b/docs/source/notebooks/rd_pymc.ipynb @@ -51,7 +51,7 @@ "metadata": {}, "source": [ ":::{note}\n", - "The `random_seed` keyword argument for the PyMC sampler is not neccessary. We use it here so that the results are reproducible.\n", + "The `random_seed` keyword argument for the PyMC sampler is not necessary. We use it here so that the results are reproducible.\n", ":::" ] }, diff --git a/docs/source/notebooks/rd_pymc_drinking.ipynb b/docs/source/notebooks/rd_pymc_drinking.ipynb index 03af17b0..4ffca0db 100644 --- a/docs/source/notebooks/rd_pymc_drinking.ipynb +++ b/docs/source/notebooks/rd_pymc_drinking.ipynb @@ -89,7 +89,7 @@ "metadata": {}, "source": [ ":::{note}\n", - "The `random_seed` keyword argument for the PyMC sampler is not neccessary. We use it here so that the results are reproducible.\n", + "The `random_seed` keyword argument for the PyMC sampler is not necessary. We use it here so that the results are reproducible.\n", ":::" ] }, diff --git a/docs/source/notebooks/rkink_pymc.ipynb b/docs/source/notebooks/rkink_pymc.ipynb index 0c147d07..2c1d004c 100644 --- a/docs/source/notebooks/rkink_pymc.ipynb +++ b/docs/source/notebooks/rkink_pymc.ipynb @@ -599,7 +599,7 @@ "source": [ "## Example 3 - basis spline model\n", "\n", - "As a final example to demonstrate that we need not be constrained to polynomial functions, we can use a basis spline model. This takes advantage of the capability of `patsy` to generate design matricies with basis splines. Note that we will use the same simulated dataset as the previous example." + "As a final example to demonstrate that we need not be constrained to polynomial functions, we can use a basis spline model. This takes advantage of the capability of `patsy` to generate design matrices with basis splines. Note that we will use the same simulated dataset as the previous example." ] }, { diff --git a/docs/source/notebooks/sc_pymc.ipynb b/docs/source/notebooks/sc_pymc.ipynb index 93621c2b..f9260913 100644 --- a/docs/source/notebooks/sc_pymc.ipynb +++ b/docs/source/notebooks/sc_pymc.ipynb @@ -57,7 +57,7 @@ "## Run the analysis\n", "\n", ":::{note}\n", - "The `random_seed` keyword argument for the PyMC sampler is not neccessary. We use it here so that the results are reproducible.\n", + "The `random_seed` keyword argument for the PyMC sampler is not necessary. We use it here so that the results are reproducible.\n", ":::" ] }, @@ -195,7 +195,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As well as the model coefficients, we might be interested in the avarage causal impact and average cumulative causal impact." + "As well as the model coefficients, we might be interested in the average causal impact and average cumulative causal impact." ] }, { diff --git a/docs/source/notebooks/sc_pymc_brexit.ipynb b/docs/source/notebooks/sc_pymc_brexit.ipynb index 359b274f..1a8f39cd 100644 --- a/docs/source/notebooks/sc_pymc_brexit.ipynb +++ b/docs/source/notebooks/sc_pymc_brexit.ipynb @@ -7,7 +7,7 @@ "source": [ "# The effects of Brexit\n", "\n", - "The aim of this notebook is to estimate the causal impact of Brexit upon the UK's GDP. This will be done using the {term}`synthetic control` approch. As such, it is similar to the policy brief \"What can we know about the cost of Brexit so far?\" {cite:p}`brexit2022policybrief` from the Center for European Reform. That approach did not use Bayesian estimation methods however.\n", + "The aim of this notebook is to estimate the causal impact of Brexit upon the UK's GDP. This will be done using the {term}`synthetic control` approach. As such, it is similar to the policy brief \"What can we know about the cost of Brexit so far?\" {cite:p}`brexit2022policybrief` from the Center for European Reform. That approach did not use Bayesian estimation methods however.\n", "\n", "I did not use the GDP data from the above report however as it had been scaled in some way that was hard for me to understand how it related to the absolute GDP figures. Instead, GDP data was obtained courtesy of Prof. Dooruj Rambaccussing. Raw data is in units of billions of USD." ] @@ -225,7 +225,7 @@ "metadata": {}, "source": [ ":::{note}\n", - "The `random_seed` keyword argument for the PyMC sampler is not neccessary. We use it here so that the results are reproducible.\n", + "The `random_seed` keyword argument for the PyMC sampler is not necessary. We use it here so that the results are reproducible.\n", ":::" ] }, diff --git a/pyproject.toml b/pyproject.toml index cc199185..8dcf84eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,8 @@ docs = [ "sphinx-notfound-page", "ipywidgets", "sphinx-design", + "nbformat", + "nbconvert", ] lint = ["interrogate", "pre-commit", "ruff"] test = ["pytest", "pytest-cov"] From 4d26e6dae6d5a3325d009dcb0bb6122c6a6c9201 Mon Sep 17 00:00:00 2001 From: Alex Lee Date: Fri, 6 Sep 2024 10:03:03 +0200 Subject: [PATCH 3/4] fixing issue with asthe and complier --- .codespell/codespell-whitelist.txt | 1 + docs/source/knowledgebase/glossary.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.codespell/codespell-whitelist.txt b/.codespell/codespell-whitelist.txt index 02e5ba47..630d0198 100644 --- a/.codespell/codespell-whitelist.txt +++ b/.codespell/codespell-whitelist.txt @@ -1,3 +1,4 @@ nD CACE compliers +complier diff --git a/docs/source/knowledgebase/glossary.rst b/docs/source/knowledgebase/glossary.rst index 9ae8e4f5..e5046fbd 100644 --- a/docs/source/knowledgebase/glossary.rst +++ b/docs/source/knowledgebase/glossary.rst @@ -48,7 +48,7 @@ Glossary Local Average Treatment effect LATE - Also known asthe compiler average causal effect (CACE), is the effect of a treatment for subjects who comply with the experimental treatment assigned to their sample group. It is the quantity we're estimating in IV designs. + Also known as the complier average causal effect (CACE), is the effect of a treatment for subjects who comply with the experimental treatment assigned to their sample group. It is the quantity we're estimating in IV designs. Non-equivalent group designs NEGD From df16e4b7563d2ebd3eeb851ea330b244173b4202 Mon Sep 17 00:00:00 2001 From: Alex Lee Date: Mon, 9 Sep 2024 17:50:26 +0200 Subject: [PATCH 4/4] Adding test coverage to the codespell utility --- .github/workflows/ci.yml | 2 + .pre-commit-config.yaml | 4 +- .../.codespell}/codespell-whitelist.txt | 0 .../.codespell}/notebook_to_markdown.py | 0 .../.codespell/test_data/test_notebook.ipynb | 31 +++++++++++++ .../.codespell/test_notebook_to_markdown.py | 43 +++++++++++++++++++ docs/source/_static/interrogate_badge.svg | 6 +-- 7 files changed, 81 insertions(+), 5 deletions(-) rename {.codespell => docs/source/.codespell}/codespell-whitelist.txt (100%) rename {.codespell => docs/source/.codespell}/notebook_to_markdown.py (100%) create mode 100644 docs/source/.codespell/test_data/test_notebook.ipynb create mode 100644 docs/source/.codespell/test_notebook_to_markdown.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7788e870..3b2dfd14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,8 @@ jobs: run: pip install -e .[test] - name: Run doctests run: pytest --doctest-modules --ignore=causalpy/tests/ causalpy/ + - name: Run extra tests + run: pytest docs/source/.codespell/test_notebook_to_markdown.py - name: Run tests run: pytest --cov-report=xml --no-cov-on-fail - name: Upload coverage to Codecov diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3db98699..274e03fe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: hooks: - id: convert-notebooks name: Convert Notebooks to Markdown - entry: python ./.codespell/notebook_to_markdown.py + entry: python ./docs/source/.codespell/notebook_to_markdown.py language: python pass_filenames: false always_run: false @@ -64,7 +64,7 @@ repos: "*.svg", "-S", "*.ipynb", - "--ignore-words=./.codespell/codespell-whitelist.txt", + "--ignore-words=./docs/source/.codespell/codespell-whitelist.txt", ] additional_dependencies: # Support pyproject.toml configuration diff --git a/.codespell/codespell-whitelist.txt b/docs/source/.codespell/codespell-whitelist.txt similarity index 100% rename from .codespell/codespell-whitelist.txt rename to docs/source/.codespell/codespell-whitelist.txt diff --git a/.codespell/notebook_to_markdown.py b/docs/source/.codespell/notebook_to_markdown.py similarity index 100% rename from .codespell/notebook_to_markdown.py rename to docs/source/.codespell/notebook_to_markdown.py diff --git a/docs/source/.codespell/test_data/test_notebook.ipynb b/docs/source/.codespell/test_data/test_notebook.ipynb new file mode 100644 index 00000000..6ad8f527 --- /dev/null +++ b/docs/source/.codespell/test_data/test_notebook.ipynb @@ -0,0 +1,31 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"{os.__file}__\")\n", + "\n", + "# Speling mistake." + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/source/.codespell/test_notebook_to_markdown.py b/docs/source/.codespell/test_notebook_to_markdown.py new file mode 100644 index 00000000..38fc9977 --- /dev/null +++ b/docs/source/.codespell/test_notebook_to_markdown.py @@ -0,0 +1,43 @@ +# Copyright 2024 The PyMC Labs Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Notebook to markdown tests.""" + +import os +from tempfile import TemporaryDirectory + +import pytest +from notebook_to_markdown import notebook_to_markdown + + +@pytest.fixture +def data_dir() -> str: + """Get current directory.""" + return os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data") + + +def test_notebook_to_markdown_empty_pattern(data_dir: str) -> None: + """Test basic functionality of notebook_to_markdown with empty pattern.""" + with TemporaryDirectory() as tmp_dir: + pattern = "*.missing" + notebook_to_markdown(f"{data_dir}/{pattern}", tmp_dir) + assert len(os.listdir(tmp_dir)) == 0 + + +def test_notebook_to_markdown(data_dir: str) -> None: + """Test basic functionality of notebook_to_markdown with a correct pattern.""" + with TemporaryDirectory() as tmp_dir: + pattern = "*.ipynb" + notebook_to_markdown(f"{data_dir}/{pattern}", tmp_dir) + assert len(os.listdir(tmp_dir)) == 1 + assert "test_notebook.md" in os.listdir(tmp_dir) diff --git a/docs/source/_static/interrogate_badge.svg b/docs/source/_static/interrogate_badge.svg index 2d6395ba..73800a2f 100644 --- a/docs/source/_static/interrogate_badge.svg +++ b/docs/source/_static/interrogate_badge.svg @@ -1,5 +1,5 @@ - interrogate: 90.1% + interrogate: 90.0% @@ -12,8 +12,8 @@ interrogate interrogate - 90.1% - 90.1% + 90.0% + 90.0%