├── .coveragerc ├── .github └── workflows │ ├── codecov.yml │ └── python-package.yml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE.txt ├── MANIFEST.in ├── docs ├── Makefile ├── README.rst ├── _build │ ├── doctrees │ │ ├── PIP_DOC.doctree │ │ ├── README.doctree │ │ ├── api.doctree │ │ ├── environment.pickle │ │ ├── fit.doctree │ │ ├── index.doctree │ │ └── model_selection.doctree │ └── html │ │ ├── .buildinfo │ │ ├── PIP_DOC.html │ │ ├── README.html │ │ ├── _images │ │ ├── example.png │ │ └── example2.png │ │ ├── _sources │ │ ├── PIP_DOC.rst.txt │ │ ├── README.rst.txt │ │ ├── api.rst.txt │ │ ├── fit.rst.txt │ │ ├── index.rst.txt │ │ └── model_selection.rst.txt │ │ ├── _static │ │ ├── _sphinx_javascript_frameworks_compat.js │ │ ├── alabaster.css │ │ ├── basic.css │ │ ├── css │ │ │ ├── badge_only.css │ │ │ ├── fonts │ │ │ │ ├── Roboto-Slab-Bold.woff │ │ │ │ ├── Roboto-Slab-Bold.woff2 │ │ │ │ ├── Roboto-Slab-Regular.woff │ │ │ │ ├── Roboto-Slab-Regular.woff2 │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ ├── fontawesome-webfont.svg │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ ├── fontawesome-webfont.woff │ │ │ │ ├── fontawesome-webfont.woff2 │ │ │ │ ├── lato-bold-italic.woff │ │ │ │ ├── lato-bold-italic.woff2 │ │ │ │ ├── lato-bold.woff │ │ │ │ ├── lato-bold.woff2 │ │ │ │ ├── lato-normal-italic.woff │ │ │ │ ├── lato-normal-italic.woff2 │ │ │ │ ├── lato-normal.woff │ │ │ │ └── lato-normal.woff2 │ │ │ └── theme.css │ │ ├── custom.css │ │ ├── doctools.js │ │ ├── documentation_options.js │ │ ├── file.png │ │ ├── jquery-3.5.1.js │ │ ├── jquery-3.6.0.js │ │ ├── jquery.js │ │ ├── js │ │ │ ├── badge_only.js │ │ │ ├── html5shiv-printshiv.min.js │ │ │ ├── html5shiv.min.js │ │ │ └── theme.js │ │ ├── language_data.js │ │ ├── minus.png │ │ ├── plus.png │ │ ├── pygments.css │ │ ├── searchtools.js │ │ ├── sphinx_highlight.js │ │ ├── underscore-1.13.1.js │ │ └── underscore.js │ │ ├── api.html │ │ ├── fit.html │ │ ├── genindex.html │ │ ├── index.html │ │ ├── model_selection.html │ │ ├── objects.inv │ │ ├── py-modindex.html │ │ ├── search.html │ │ └── searchindex.js ├── api.rst ├── conf.py ├── index.rst └── make.bat ├── docs_requirements.txt ├── example.png ├── meta.yaml ├── paper ├── example.png ├── example2.png ├── paper.bib ├── paper.md └── paper.py ├── piecewise_regression ├── .png ├── __init__.py ├── data_validation.py ├── davies.py ├── main.py ├── model_selection.py └── r_squared_calc.py ├── requirements.txt ├── setup.py ├── setup_notes.txt ├── tests-manual ├── check_breakpoint_realistic_confidence_interval.py ├── check_davies_has_realistic_p_values.py ├── check_estimates_have_realistic_confidence_intervals.py └── manual_testing.py └── tests ├── __init__.py ├── __pycache__ ├── __init__.cpython-38.pyc └── test_1_breakpoint.cpython-38.pyc ├── data └── data.txt ├── test_data_validation.py ├── test_fit.py ├── test_fit_validation.py ├── test_model_selection.py ├── test_muggeo.py ├── test_next_breakpoint.py └── test_r_squared.py /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc 2 | [report] 3 | show_missing = True 4 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: codecov 2 | on: [push] 3 | jobs: 4 | run: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 10 | env: 11 | OS: ubuntu-latest 12 | PYTHON: ${{ matrix.python-version }} 13 | CODECOV_TOKEN: "5628677a-b891-462c-9828-35a8a796b1ea" 14 | steps: 15 | - uses: actions/checkout@master 16 | - name: Setup Python 17 | uses: actions/setup-python@master 18 | with: 19 | python-version: 3.7 20 | - name: Generate coverage report 21 | run: | 22 | python -m pip install --upgrade pip 23 | python -m pip install flake8 pytest pytest-cov 24 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 25 | pytest --cov=./ --cov-report=xml 26 | - name: Upload coverage to Codecov 27 | uses: codecov/codecov-action@v2 28 | with: 29 | env_vars: OS,PYTHON, CODECOV_TOKEN 30 | fail_ci_if_error: true 31 | name: codecov-umbrella 32 | path_to_write_report: tests/coverage/codecov_report.txt 33 | verbose: true 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Tests 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 pytest 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | - name: Lint with flake8 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 37 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 38 | - name: Test with pytest 39 | run: | 40 | pytest 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Source for the following rules: https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # Distribution / packaging 8 | .Python 9 | build/ 10 | develop-eggs/ 11 | dist/ 12 | downloads/ 13 | eggs/ 14 | .eggs/ 15 | lib/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | wheels/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | MANIFEST 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .nox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | .pytest_cache/ 48 | 49 | 50 | # Environments 51 | .env 52 | .venv 53 | env/ 54 | venv/ 55 | ENV/ 56 | env.bak/ 57 | venv.bak/ 58 | 59 | # Jupyter 60 | docs/.ipynb_checkpoints/* 61 | 62 | 63 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | 6 | # Required 7 | 8 | version: 2 9 | 10 | 11 | # Set the OS, Python version and other tools you might need 12 | 13 | build: 14 | 15 | os: ubuntu-22.04 16 | 17 | tools: 18 | 19 | python: "3.11" 20 | 21 | # You can also specify other tool versions: 22 | 23 | # nodejs: "20" 24 | 25 | # rust: "1.70" 26 | 27 | # golang: "1.20" 28 | 29 | 30 | python: 31 | install: 32 | - requirements: "docs_requirements.txt" 33 | 34 | 35 | # Build documentation in the "docs/" directory with Sphinx 36 | 37 | sphinx: 38 | 39 | configuration: docs/conf.py 40 | 41 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 42 | 43 | # builder: "dirhtml" 44 | 45 | # Fail on all warnings to avoid broken references 46 | 47 | # fail_on_warning: true 48 | 49 | 50 | # Optionally build your docs in additional formats such as PDF and ePub 51 | 52 | # formats: 53 | 54 | # - pdf 55 | 56 | # - epub 57 | 58 | 59 | # Optional but recommended, declare the Python requirements required 60 | 61 | # to build your documentation 62 | 63 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 64 | 65 | # python: 66 | 67 | # install: 68 | 69 | # - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Charlie Pilgrim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include docs/README.rst -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/README.rst: -------------------------------------------------------------------------------- 1 | ========================================================== 2 | piecewise-regression (aka segmented regression) in python 3 | ========================================================== 4 | :piecewise-regression: fitting straight line models with breakpoints 5 | :Author: Charlie Pilgrim 6 | :Version: 1.5.0 7 | :Github: https://github.com/chasmani/piecewise-regression 8 | :Documentation: https://piecewise-regression.readthedocs.io/en/master/index.html 9 | :Paper: https://joss.theoj.org/papers/10.21105/joss.03859 10 | 11 | .. image:: https://github.com/chasmani/piecewise-regression/actions/workflows/python-package.yml/badge.svg 12 | :target: https://github.com/chasmani/piecewise-regression/actions/workflows/python-package.yml 13 | :alt: Build Status 14 | .. image:: https://codecov.io/gh/chasmani/piecewise-regression/branch/master/graph/badge.svg 15 | :target: https://codecov.io/gh/chasmani/piecewise-regression 16 | :alt: Test Coverage Status 17 | .. image:: https://readthedocs.org/projects/piecewise-regression/badge/?version=latest 18 | :target: https://piecewise-regression.readthedocs.io/en/latest/?badge=latest 19 | :alt: Documentation Status 20 | .. image:: https://badge.fury.io/py/piecewise-regression.svg 21 | :target: https://badge.fury.io/py/piecewise-regression 22 | :alt: PyPi location 23 | .. image:: https://joss.theoj.org/papers/b64e5e7d746efc5d91462a51b3fc5bf8/status.svg 24 | :target: https://joss.theoj.org/papers/b64e5e7d746efc5d91462a51b3fc5bf8 25 | :alt: Review Status 26 | .. image:: https://img.shields.io/badge/license-MIT-blue.svg 27 | :target: https://github.com/chasmani/piecewise-regresssion/blob/master/LICENSE 28 | :alt: License information 29 | 30 | | 31 | 32 | Easy-to-use piecewise regression (aka segmented regression) in Python. For fitting straight lines to data where there are one or more changes in gradient (known as breakpoints). Based on Muggeo's paper "Estimating regression models with unknown break-points" (2003). 33 | 34 | When using the package, please cite the `accompanying paper `_. 35 | 36 | Example: 37 | 38 | .. image:: https://raw.githubusercontent.com/chasmani/piecewise-regression/master/paper/example.png 39 | :alt: basic-example-plot-github 40 | 41 | Code examples below, and more in this `Google Colab Jupyter Notebook `_. 42 | 43 | Installation 44 | ======================== 45 | 46 | You can install piecewise-regression using python's `pip package index `_ :: 47 | 48 | pip install piecewise-regression 49 | 50 | The package is tested on Python 3.7, 3.8, 3.9 and 3.10. 51 | 52 | Getting started 53 | ======================== 54 | 55 | The package requires some x and y data to fit. You need to specify either a) some initial breakpoint guesses as `start_values` or b) how many breakpoints you want to fit as `n_breakpoints` (or both). Here is an elementary example, assuming we already have some data `x` and `y`: :: 56 | 57 | import piecewise_regression 58 | pw_fit = piecewise_regression.Fit(x, y, n_breakpoints=2) 59 | pw_fit.summary() 60 | 61 | Example 62 | ======================== 63 | *For demonstration purposes, substitute with your own data to fit.* 64 | 65 | 1. Start-off generating some data with a breakpoint: :: 66 | 67 | import piecewise_regression 68 | import numpy as np 69 | 70 | alpha_1 = -4 71 | alpha_2 = -2 72 | constant = 100 73 | breakpoint_1 = 7 74 | n_points = 200 75 | np.random.seed(0) 76 | xx = np.linspace(0, 20, n_points) 77 | yy = constant + alpha_1*xx + (alpha_2-alpha_1) * np.maximum(xx - breakpoint_1, 0) + np.random.normal(size=n_points) 78 | 79 | 80 | 2. Fit the model: :: 81 | 82 | # Given some data, fit the model 83 | pw_fit = piecewise_regression.Fit(xx, yy, start_values=[5], n_breakpoints=1) 84 | 85 | # Print a summary of the fit 86 | pw_fit.summary() 87 | 88 | Example output: :: 89 | 90 | Breakpoint Regression Results 91 | ==================================================================================================== 92 | No. Observations 200 93 | No. Model Parameters 4 94 | Degrees of Freedom 196 95 | Res. Sum of Squares 193.264 96 | Total Sum of Squares 46201.8 97 | R Squared 0.995817 98 | Adjusted R Squared 0.995731 99 | Converged: True 100 | ==================================================================================================== 101 | ==================================================================================================== 102 | Estimate Std Err t P>|t| [0.025 0.975] 103 | ---------------------------------------------------------------------------------------------------- 104 | const 100.726 0.244 413.63 3.1e-290 100.25 101.21 105 | alpha1 -4.21998 0.0653 -64.605 4.37e-134 -4.3488 -4.0912 106 | beta1 2.18914 0.0689 31.788 - 2.0533 2.325 107 | breakpoint1 6.48706 0.137 - - 6.2168 6.7573 108 | ---------------------------------------------------------------------------------------------------- 109 | These alphas(gradients of segments) are estimated from betas(change in gradient) 110 | ---------------------------------------------------------------------------------------------------- 111 | alpha2 -2.03084 0.0218 -93.068 3.66e-164 -2.0739 -1.9878 112 | ==================================================================================================== 113 | Davies test for existence of at least 1 breakpoint: p=5.13032e-295 (e.g. p<0.05 means reject null hypothesis of no breakpoints at 5% significance) 114 | 115 | This includes estimates for all the model variables, along with confidence intervals. The Davies test is a hypothesis test for the existence of at least one breakpoint, against the null hypothesis of no breakpoints. Following Muggeo ("segmented: An R Package to Fit Regression Models with Broken-Line Relationships" 2008), this uses the Davies test with the Wald statistic on the breakpoint change in gradient. 116 | 117 | 3. Optional: Plotting the data and model results: :: 118 | 119 | import matplotlib.pyplot as plt 120 | 121 | # Plot the data, fit, breakpoints and confidence intervals 122 | pw_fit.plot_data(color="grey", s=20) 123 | # Pass in standard matplotlib keywords to control any of the plots 124 | pw_fit.plot_fit(color="red", linewidth=4) 125 | pw_fit.plot_breakpoints() 126 | pw_fit.plot_breakpoint_confidence_intervals() 127 | plt.xlabel("x") 128 | plt.ylabel("y") 129 | plt.show() 130 | plt.close() 131 | 132 | .. image:: https://raw.githubusercontent.com/chasmani/piecewise-regression/master/paper/example2.png 133 | :alt: fit-example-plot-github 134 | 135 | 136 | You can extract data as well: :: 137 | 138 | # Get the key results of the fit 139 | pw_results = pw_fit.get_results() 140 | pw_estimates = pw_results["estimates"] 141 | 142 | 143 | How It Works 144 | ====================== 145 | 146 | The package implements Muggeo's iterative algorithm (Muggeo "Estimating regression models with unknown break-points" (2003)) to find breakpoints quickly. This method simultaneously fits breakpoint positions and the linear models for the different fit segments, and it gives confidence intervals for all the model estimates. See the accompanying paper for more details. 147 | 148 | Muggeo's method doesn't always converge on the best solution - sometimes, it finds a locally optimal solution or doesn't converge at all. For this reason, the Fit method also implements a process called bootstrap restarting which involves taking a bootstrapped resample of the data to try to find a better solution. The number of times this process runs can be controlled with n_boot. To run the Fit without bootstrap restarting, set ``n_boot=0``. 149 | 150 | If you do not have (or do not want to use) initial guesses for the number of breakpoints, you can set it to ``n_breakpoints=3``, and the algorithm will randomly generate start_values. With a 50% chance, the bootstrap restarting algorithm will either use the best currently converged breakpoints or randomly generate new ``start_values``, escaping the local optima in two ways in order to find better global optima. 151 | 152 | As is often the case with fitting non-linear models, even with these measures, the algorithm is not guaranteed to converge to a global optimum. However, increasing ``n_boot`` raises the probability of global convergence at the cost of computation time. 153 | 154 | 155 | Model Selection 156 | ========================== 157 | 158 | In addition to the main Fit tool, the package also offers a ModelSelection option based on the Bayesian Information Criterion (BIC). This additional tool is opinionated in it's choices (e.g. using the BIC) and not as thorough as the main Fit function. In particular, the models are generated with random start_values, which can influence the model fit and give different values for the BIC. The tool can help explore other possible models but we recommend that caution and domain knowledge are used when interpreting the results. :: 159 | 160 | ms = piecewise_regression.ModelSelection(x, y, max_breakpoints=6) 161 | 162 | Example output: :: 163 | 164 | Breakpoint Model Comparision Results 165 | ==================================================================================================== 166 | n_breakpoints BIC converged RSS 167 | ---------------------------------------------------------------------------------------------------- 168 | 0 421.09 True 1557.4 169 | 1 14.342 True 193.26 170 | 2 22.825 True 191.23 171 | 3 24.169 True 182.59 172 | 4 29.374 True 177.73 173 | 5 False 174 | 6 False 175 | 176 | Minimum BIC (Bayesian Information Criterion) suggests the best model 177 | 178 | The data of the model fits can be accessed in :: 179 | 180 | ms.models 181 | 182 | For a robust comparision, you could run the ModelSelection tools many times and take the lowest BIC for each model. 183 | 184 | 185 | Testing 186 | ============ 187 | 188 | The package includes comprehensive tests. 189 | 190 | To run all tests, from the main directory run (requires the pytest library): :: 191 | 192 | pytest 193 | 194 | To get code coverage, run (requires pytest and pytest-cov libraries): :: 195 | 196 | pytest --cov=./ 197 | 198 | There are also a series of simulation tests that check the estimates have realistic confidence intervals, and the Davies test gives realistic p-values. These can be found in the folder "tests-manual". 199 | 200 | Requirements 201 | ============= 202 | 203 | See requirements.txt for specific version numbers. Required packages, and their uses are: 204 | 205 | - matplotlib for plotting. 206 | - numpy for simple data handling and data transformations. 207 | - scipy for statistical tests including using t-distributions and Gaussians. 208 | - statsmodels for performing ordinary least squares. 209 | 210 | Community Guidelines and Contributing 211 | =================================================== 212 | 213 | We welcome community participation! 214 | 215 | Sourced from `Open Source Guide: How to contribute. `_ 216 | 217 | **Open an issue in the following situations:** 218 | 219 | - Report an error you can’t solve yourself 220 | - Discuss a high-level topic or idea (for example, community, vision or policies) 221 | - Propose a new feature or other project ideas 222 | 223 | **Tips for communicating on issues:** 224 | 225 | - If you see an open issue that you want to tackle, comment on the issue to let people know you’re on it. That way, people are less likely to duplicate your work. 226 | - If an issue was opened a while ago, it’s possible that it’s being addressed somewhere else, or has already been resolved, so comment to ask for confirmation before starting work. 227 | - If you opened an issue, but figured out the answer later on your own, comment on the issue to let people know, then close the issue. Even documenting that outcome is a contribution to the project. 228 | 229 | **Open a pull request in the following situations:** 230 | 231 | - Submit trivial fixes (for example, a typo, a broken link or an obvious error) 232 | - Start work on a contribution that was already asked for, or that you’ve already discussed, in an issue 233 | 234 | **Tips for submitting PRs:** 235 | 236 | - Reference any relevant issues or supporting documentation in your PR (for example, “Closes #37.”) 237 | - Include screenshots of the before and after if your changes include differences in HTML/CSS. Drag and drop the images into the body of your pull request. 238 | - Test your changes by running them against any existing tests if they exist and create new ones when needed. Whether tests exist or not, make sure your changes don’t break the existing project. 239 | - Contribute in the style of the project to the best of your abilities. 240 | 241 | Installing From Source 242 | =========================== 243 | 244 | To install from source: :: 245 | 246 | git clone https://github.com/chasmani/piecewise-regression 247 | cd piecewise_regression 248 | python3 setup.py install --user 249 | 250 | 251 | Documentation 252 | ============== 253 | `Full docs, including an API reference. `_ 254 | 255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /docs/_build/doctrees/PIP_DOC.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/doctrees/PIP_DOC.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/README.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/doctrees/README.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/api.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/doctrees/api.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/environment.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/doctrees/environment.pickle -------------------------------------------------------------------------------- /docs/_build/doctrees/fit.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/doctrees/fit.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/index.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/doctrees/index.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/model_selection.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/doctrees/model_selection.doctree -------------------------------------------------------------------------------- /docs/_build/html/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: 5b909a9c30817d1108e3905be21db110 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /docs/_build/html/PIP_DOC.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <no title> — piecewise-regression 1 documentation 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 46 | 47 |
51 | 52 |
53 |
54 |
55 | 62 |
63 |
64 |
65 |
66 | 67 |

piecewise-regression provides tools for fitting continuous straight line models to data with breakpoint(s) where the gradient changes.

68 |

For docs and more information, visit the Github repo at https://github.com/chasmani/piecewise-regression.

69 | 70 | 71 |
72 |
73 |
74 | 75 |
76 | 77 |
78 |

© Copyright 2021, Charlie Pilgrim.

79 |
80 | 81 | Built with Sphinx using a 82 | theme 83 | provided by Read the Docs. 84 | 85 | 86 |
87 |
88 |
89 |
90 |
91 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /docs/_build/html/_images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_images/example.png -------------------------------------------------------------------------------- /docs/_build/html/_images/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_images/example2.png -------------------------------------------------------------------------------- /docs/_build/html/_sources/PIP_DOC.rst.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | piecewise-regression provides tools for fitting continuous straight line models to data with breakpoint(s) where the gradient changes. 4 | 5 | For docs and more information, visit the Github repo at https://github.com/chasmani/piecewise-regression. 6 | 7 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/README.rst.txt: -------------------------------------------------------------------------------- 1 | ========================================================== 2 | piecewise-regression (aka segmented regression) in python 3 | ========================================================== 4 | :piecewise-regression: fitting straight line models with breakpoints 5 | :Author: Charlie Pilgrim 6 | :Version: 1.5.0 7 | :Github: https://github.com/chasmani/piecewise-regression 8 | :Documentation: https://piecewise-regression.readthedocs.io/en/master/index.html 9 | :Paper: https://joss.theoj.org/papers/10.21105/joss.03859 10 | 11 | .. image:: https://github.com/chasmani/piecewise-regression/actions/workflows/python-package.yml/badge.svg 12 | :target: https://github.com/chasmani/piecewise-regression/actions/workflows/python-package.yml 13 | :alt: Build Status 14 | .. image:: https://codecov.io/gh/chasmani/piecewise-regression/branch/master/graph/badge.svg 15 | :target: https://codecov.io/gh/chasmani/piecewise-regression 16 | :alt: Test Coverage Status 17 | .. image:: https://readthedocs.org/projects/piecewise-regression/badge/?version=latest 18 | :target: https://piecewise-regression.readthedocs.io/en/latest/?badge=latest 19 | :alt: Documentation Status 20 | .. image:: https://badge.fury.io/py/piecewise-regression.svg 21 | :target: https://badge.fury.io/py/piecewise-regression 22 | :alt: PyPi location 23 | .. image:: https://joss.theoj.org/papers/b64e5e7d746efc5d91462a51b3fc5bf8/status.svg 24 | :target: https://joss.theoj.org/papers/b64e5e7d746efc5d91462a51b3fc5bf8 25 | :alt: Review Status 26 | .. image:: https://img.shields.io/badge/license-MIT-blue.svg 27 | :target: https://github.com/chasmani/piecewise-regresssion/blob/master/LICENSE 28 | :alt: License information 29 | 30 | | 31 | 32 | Easy-to-use piecewise regression (aka segmented regression) in Python. For fitting straight lines to data where there are one or more changes in gradient (known as breakpoints). Based on Muggeo's paper "Estimating regression models with unknown break-points" (2003). 33 | 34 | When using the package, please cite the `accompanying paper `_. 35 | 36 | Example: 37 | 38 | .. image:: https://raw.githubusercontent.com/chasmani/piecewise-regression/master/paper/example.png 39 | :alt: basic-example-plot-github 40 | 41 | Code examples below, and more in this `Google Colab Jupyter Notebook `_. 42 | 43 | Installation 44 | ======================== 45 | 46 | You can install piecewise-regression using python's `pip package index `_ :: 47 | 48 | pip install piecewise-regression 49 | 50 | The package is tested on Python 3.7, 3.8, 3.9 and 3.10. 51 | 52 | Getting started 53 | ======================== 54 | 55 | The package requires some x and y data to fit. You need to specify either a) some initial breakpoint guesses as `start_values` or b) how many breakpoints you want to fit as `n_breakpoints` (or both). Here is an elementary example, assuming we already have some data `x` and `y`: :: 56 | 57 | import piecewise_regression 58 | pw_fit = piecewise_regression.Fit(x, y, n_breakpoints=2) 59 | pw_fit.summary() 60 | 61 | Example 62 | ======================== 63 | *For demonstration purposes, substitute with your own data to fit.* 64 | 65 | 1. Start-off generating some data with a breakpoint: :: 66 | 67 | import piecewise_regression 68 | import numpy as np 69 | 70 | alpha_1 = -4 71 | alpha_2 = -2 72 | constant = 100 73 | breakpoint_1 = 7 74 | n_points = 200 75 | np.random.seed(0) 76 | xx = np.linspace(0, 20, n_points) 77 | yy = constant + alpha_1*xx + (alpha_2-alpha_1) * np.maximum(xx - breakpoint_1, 0) + np.random.normal(size=n_points) 78 | 79 | 80 | 2. Fit the model: :: 81 | 82 | # Given some data, fit the model 83 | pw_fit = piecewise_regression.Fit(xx, yy, start_values=[5], n_breakpoints=1) 84 | 85 | # Print a summary of the fit 86 | pw_fit.summary() 87 | 88 | Example output: :: 89 | 90 | Breakpoint Regression Results 91 | ==================================================================================================== 92 | No. Observations 200 93 | No. Model Parameters 4 94 | Degrees of Freedom 196 95 | Res. Sum of Squares 193.264 96 | Total Sum of Squares 46201.8 97 | R Squared 0.995817 98 | Adjusted R Squared 0.995731 99 | Converged: True 100 | ==================================================================================================== 101 | ==================================================================================================== 102 | Estimate Std Err t P>|t| [0.025 0.975] 103 | ---------------------------------------------------------------------------------------------------- 104 | const 100.726 0.244 413.63 3.1e-290 100.25 101.21 105 | alpha1 -4.21998 0.0653 -64.605 4.37e-134 -4.3488 -4.0912 106 | beta1 2.18914 0.0689 31.788 - 2.0533 2.325 107 | breakpoint1 6.48706 0.137 - - 6.2168 6.7573 108 | ---------------------------------------------------------------------------------------------------- 109 | These alphas(gradients of segments) are estimated from betas(change in gradient) 110 | ---------------------------------------------------------------------------------------------------- 111 | alpha2 -2.03084 0.0218 -93.068 3.66e-164 -2.0739 -1.9878 112 | ==================================================================================================== 113 | Davies test for existence of at least 1 breakpoint: p=5.13032e-295 (e.g. p<0.05 means reject null hypothesis of no breakpoints at 5% significance) 114 | 115 | This includes estimates for all the model variables, along with confidence intervals. The Davies test is a hypothesis test for the existence of at least one breakpoint, against the null hypothesis of no breakpoints. Following Muggeo ("segmented: An R Package to Fit Regression Models with Broken-Line Relationships" 2008), this uses the Davies test with the Wald statistic on the breakpoint change in gradient. 116 | 117 | 3. Optional: Plotting the data and model results: :: 118 | 119 | import matplotlib.pyplot as plt 120 | 121 | # Plot the data, fit, breakpoints and confidence intervals 122 | pw_fit.plot_data(color="grey", s=20) 123 | # Pass in standard matplotlib keywords to control any of the plots 124 | pw_fit.plot_fit(color="red", linewidth=4) 125 | pw_fit.plot_breakpoints() 126 | pw_fit.plot_breakpoint_confidence_intervals() 127 | plt.xlabel("x") 128 | plt.ylabel("y") 129 | plt.show() 130 | plt.close() 131 | 132 | .. image:: https://raw.githubusercontent.com/chasmani/piecewise-regression/master/paper/example2.png 133 | :alt: fit-example-plot-github 134 | 135 | 136 | You can extract data as well: :: 137 | 138 | # Get the key results of the fit 139 | pw_results = pw_fit.get_results() 140 | pw_estimates = pw_results["estimates"] 141 | 142 | 143 | How It Works 144 | ====================== 145 | 146 | The package implements Muggeo's iterative algorithm (Muggeo "Estimating regression models with unknown break-points" (2003)) to find breakpoints quickly. This method simultaneously fits breakpoint positions and the linear models for the different fit segments, and it gives confidence intervals for all the model estimates. See the accompanying paper for more details. 147 | 148 | Muggeo's method doesn't always converge on the best solution - sometimes, it finds a locally optimal solution or doesn't converge at all. For this reason, the Fit method also implements a process called bootstrap restarting which involves taking a bootstrapped resample of the data to try to find a better solution. The number of times this process runs can be controlled with n_boot. To run the Fit without bootstrap restarting, set ``n_boot=0``. 149 | 150 | If you do not have (or do not want to use) initial guesses for the number of breakpoints, you can set it to ``n_breakpoints=3``, and the algorithm will randomly generate start_values. With a 50% chance, the bootstrap restarting algorithm will either use the best currently converged breakpoints or randomly generate new ``start_values``, escaping the local optima in two ways in order to find better global optima. 151 | 152 | As is often the case with fitting non-linear models, even with these measures, the algorithm is not guaranteed to converge to a global optimum. However, increasing ``n_boot`` raises the probability of global convergence at the cost of computation time. 153 | 154 | 155 | Model Selection 156 | ========================== 157 | 158 | In addition to the main Fit tool, the package also offers a ModelSelection option based on the Bayesian Information Criterion (BIC). This additional tool is opinionated in it's choices (e.g. using the BIC) and not as thorough as the main Fit function. In particular, the models are generated with random start_values, which can influence the model fit and give different values for the BIC. The tool can help explore other possible models but we recommend that caution and domain knowledge are used when interpreting the results. :: 159 | 160 | ms = piecewise_regression.ModelSelection(x, y, max_breakpoints=6) 161 | 162 | Example output: :: 163 | 164 | Breakpoint Model Comparision Results 165 | ==================================================================================================== 166 | n_breakpoints BIC converged RSS 167 | ---------------------------------------------------------------------------------------------------- 168 | 0 421.09 True 1557.4 169 | 1 14.342 True 193.26 170 | 2 22.825 True 191.23 171 | 3 24.169 True 182.59 172 | 4 29.374 True 177.73 173 | 5 False 174 | 6 False 175 | 176 | Minimum BIC (Bayesian Information Criterion) suggests the best model 177 | 178 | The data of the model fits can be accessed in :: 179 | 180 | ms.models 181 | 182 | For a robust comparision, you could run the ModelSelection tools many times and take the lowest BIC for each model. 183 | 184 | 185 | Testing 186 | ============ 187 | 188 | The package includes comprehensive tests. 189 | 190 | To run all tests, from the main directory run (requires the pytest library): :: 191 | 192 | pytest 193 | 194 | To get code coverage, run (requires pytest and pytest-cov libraries): :: 195 | 196 | pytest --cov=./ 197 | 198 | There are also a series of simulation tests that check the estimates have realistic confidence intervals, and the Davies test gives realistic p-values. These can be found in the folder "tests-manual". 199 | 200 | Requirements 201 | ============= 202 | 203 | See requirements.txt for specific version numbers. Required packages, and their uses are: 204 | 205 | - matplotlib for plotting. 206 | - numpy for simple data handling and data transformations. 207 | - scipy for statistical tests including using t-distributions and Gaussians. 208 | - statsmodels for performing ordinary least squares. 209 | 210 | Community Guidelines and Contributing 211 | =================================================== 212 | 213 | We welcome community participation! 214 | 215 | Sourced from `Open Source Guide: How to contribute. `_ 216 | 217 | **Open an issue in the following situations:** 218 | 219 | - Report an error you can’t solve yourself 220 | - Discuss a high-level topic or idea (for example, community, vision or policies) 221 | - Propose a new feature or other project ideas 222 | 223 | **Tips for communicating on issues:** 224 | 225 | - If you see an open issue that you want to tackle, comment on the issue to let people know you’re on it. That way, people are less likely to duplicate your work. 226 | - If an issue was opened a while ago, it’s possible that it’s being addressed somewhere else, or has already been resolved, so comment to ask for confirmation before starting work. 227 | - If you opened an issue, but figured out the answer later on your own, comment on the issue to let people know, then close the issue. Even documenting that outcome is a contribution to the project. 228 | 229 | **Open a pull request in the following situations:** 230 | 231 | - Submit trivial fixes (for example, a typo, a broken link or an obvious error) 232 | - Start work on a contribution that was already asked for, or that you’ve already discussed, in an issue 233 | 234 | **Tips for submitting PRs:** 235 | 236 | - Reference any relevant issues or supporting documentation in your PR (for example, “Closes #37.”) 237 | - Include screenshots of the before and after if your changes include differences in HTML/CSS. Drag and drop the images into the body of your pull request. 238 | - Test your changes by running them against any existing tests if they exist and create new ones when needed. Whether tests exist or not, make sure your changes don’t break the existing project. 239 | - Contribute in the style of the project to the best of your abilities. 240 | 241 | Installing From Source 242 | =========================== 243 | 244 | To install from source: :: 245 | 246 | git clone https://github.com/chasmani/piecewise-regression 247 | cd piecewise_regression 248 | python3 setup.py install --user 249 | 250 | 251 | Documentation 252 | ============== 253 | `Full docs, including an API reference. `_ 254 | 255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/api.rst.txt: -------------------------------------------------------------------------------- 1 | 2 | API 3 | ================================================ 4 | 5 | Main 6 | ---------- 7 | The main module includes the Fit function, which runs the bootstrap restarting algorithm. 8 | 9 | .. automodule:: piecewise_regression.main 10 | :members: 11 | 12 | Model selection 13 | --------------------- 14 | The model selection module is experimental. It compares models with different `n_breakpoints` using the Bayesian Information Criterion. 15 | 16 | .. automodule:: piecewise_regression.model_selection 17 | :members: 18 | 19 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/fit.rst.txt: -------------------------------------------------------------------------------- 1 | .. automodule:: piecewise_regression.main 2 | :members: -------------------------------------------------------------------------------- /docs/_build/html/_sources/index.rst.txt: -------------------------------------------------------------------------------- 1 | .. piecewise-regression documentation master file, created by 2 | sphinx-quickstart on Tue Sep 14 14:20:43 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | piecewise-regression 7 | ================================================ 8 | 9 | .. include:: README.rst 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :caption: Contents: 14 | 15 | api 16 | 17 | 18 | 19 | Indices and tables 20 | ================== 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/model_selection.rst.txt: -------------------------------------------------------------------------------- 1 | .. automodule:: piecewise_regression.model_selection 2 | :members: -------------------------------------------------------------------------------- /docs/_build/html/_static/_sphinx_javascript_frameworks_compat.js: -------------------------------------------------------------------------------- 1 | /* Compatability shim for jQuery and underscores.js. 2 | * 3 | * Copyright Sphinx contributors 4 | * Released under the two clause BSD licence 5 | */ 6 | 7 | /** 8 | * small helper function to urldecode strings 9 | * 10 | * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL 11 | */ 12 | jQuery.urldecode = function(x) { 13 | if (!x) { 14 | return x 15 | } 16 | return decodeURIComponent(x.replace(/\+/g, ' ')); 17 | }; 18 | 19 | /** 20 | * small helper function to urlencode strings 21 | */ 22 | jQuery.urlencode = encodeURIComponent; 23 | 24 | /** 25 | * This function returns the parsed url parameters of the 26 | * current request. Multiple values per key are supported, 27 | * it will always return arrays of strings for the value parts. 28 | */ 29 | jQuery.getQueryParameters = function(s) { 30 | if (typeof s === 'undefined') 31 | s = document.location.search; 32 | var parts = s.substr(s.indexOf('?') + 1).split('&'); 33 | var result = {}; 34 | for (var i = 0; i < parts.length; i++) { 35 | var tmp = parts[i].split('=', 2); 36 | var key = jQuery.urldecode(tmp[0]); 37 | var value = jQuery.urldecode(tmp[1]); 38 | if (key in result) 39 | result[key].push(value); 40 | else 41 | result[key] = [value]; 42 | } 43 | return result; 44 | }; 45 | 46 | /** 47 | * highlight a given string on a jquery object by wrapping it in 48 | * span elements with the given class name. 49 | */ 50 | jQuery.fn.highlightText = function(text, className) { 51 | function highlight(node, addItems) { 52 | if (node.nodeType === 3) { 53 | var val = node.nodeValue; 54 | var pos = val.toLowerCase().indexOf(text); 55 | if (pos >= 0 && 56 | !jQuery(node.parentNode).hasClass(className) && 57 | !jQuery(node.parentNode).hasClass("nohighlight")) { 58 | var span; 59 | var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); 60 | if (isInSVG) { 61 | span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); 62 | } else { 63 | span = document.createElement("span"); 64 | span.className = className; 65 | } 66 | span.appendChild(document.createTextNode(val.substr(pos, text.length))); 67 | node.parentNode.insertBefore(span, node.parentNode.insertBefore( 68 | document.createTextNode(val.substr(pos + text.length)), 69 | node.nextSibling)); 70 | node.nodeValue = val.substr(0, pos); 71 | if (isInSVG) { 72 | var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); 73 | var bbox = node.parentElement.getBBox(); 74 | rect.x.baseVal.value = bbox.x; 75 | rect.y.baseVal.value = bbox.y; 76 | rect.width.baseVal.value = bbox.width; 77 | rect.height.baseVal.value = bbox.height; 78 | rect.setAttribute('class', className); 79 | addItems.push({ 80 | "parent": node.parentNode, 81 | "target": rect}); 82 | } 83 | } 84 | } 85 | else if (!jQuery(node).is("button, select, textarea")) { 86 | jQuery.each(node.childNodes, function() { 87 | highlight(this, addItems); 88 | }); 89 | } 90 | } 91 | var addItems = []; 92 | var result = this.each(function() { 93 | highlight(this, addItems); 94 | }); 95 | for (var i = 0; i < addItems.length; ++i) { 96 | jQuery(addItems[i].parent).before(addItems[i].target); 97 | } 98 | return result; 99 | }; 100 | 101 | /* 102 | * backward compatibility for jQuery.browser 103 | * This will be supported until firefox bug is fixed. 104 | */ 105 | if (!jQuery.browser) { 106 | jQuery.uaMatch = function(ua) { 107 | ua = ua.toLowerCase(); 108 | 109 | var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || 110 | /(webkit)[ \/]([\w.]+)/.exec(ua) || 111 | /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || 112 | /(msie) ([\w.]+)/.exec(ua) || 113 | ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || 114 | []; 115 | 116 | return { 117 | browser: match[ 1 ] || "", 118 | version: match[ 2 ] || "0" 119 | }; 120 | }; 121 | jQuery.browser = {}; 122 | jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; 123 | } 124 | -------------------------------------------------------------------------------- /docs/_build/html/_static/alabaster.css: -------------------------------------------------------------------------------- 1 | @import url("basic.css"); 2 | 3 | /* -- page layout ----------------------------------------------------------- */ 4 | 5 | body { 6 | font-family: Georgia, serif; 7 | font-size: 17px; 8 | background-color: #fff; 9 | color: #000; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | 15 | div.document { 16 | width: 940px; 17 | margin: 30px auto 0 auto; 18 | } 19 | 20 | div.documentwrapper { 21 | float: left; 22 | width: 100%; 23 | } 24 | 25 | div.bodywrapper { 26 | margin: 0 0 0 220px; 27 | } 28 | 29 | div.sphinxsidebar { 30 | width: 220px; 31 | font-size: 14px; 32 | line-height: 1.5; 33 | } 34 | 35 | hr { 36 | border: 1px solid #B1B4B6; 37 | } 38 | 39 | div.body { 40 | background-color: #fff; 41 | color: #3E4349; 42 | padding: 0 30px 0 30px; 43 | } 44 | 45 | div.body > .section { 46 | text-align: left; 47 | } 48 | 49 | div.footer { 50 | width: 940px; 51 | margin: 20px auto 30px auto; 52 | font-size: 14px; 53 | color: #888; 54 | text-align: right; 55 | } 56 | 57 | div.footer a { 58 | color: #888; 59 | } 60 | 61 | p.caption { 62 | font-family: inherit; 63 | font-size: inherit; 64 | } 65 | 66 | 67 | div.relations { 68 | display: none; 69 | } 70 | 71 | 72 | div.sphinxsidebar a { 73 | color: #444; 74 | text-decoration: none; 75 | border-bottom: 1px dotted #999; 76 | } 77 | 78 | div.sphinxsidebar a:hover { 79 | border-bottom: 1px solid #999; 80 | } 81 | 82 | div.sphinxsidebarwrapper { 83 | padding: 18px 10px; 84 | } 85 | 86 | div.sphinxsidebarwrapper p.logo { 87 | padding: 0; 88 | margin: -10px 0 0 0px; 89 | text-align: center; 90 | } 91 | 92 | div.sphinxsidebarwrapper h1.logo { 93 | margin-top: -10px; 94 | text-align: center; 95 | margin-bottom: 5px; 96 | text-align: left; 97 | } 98 | 99 | div.sphinxsidebarwrapper h1.logo-name { 100 | margin-top: 0px; 101 | } 102 | 103 | div.sphinxsidebarwrapper p.blurb { 104 | margin-top: 0; 105 | font-style: normal; 106 | } 107 | 108 | div.sphinxsidebar h3, 109 | div.sphinxsidebar h4 { 110 | font-family: Georgia, serif; 111 | color: #444; 112 | font-size: 24px; 113 | font-weight: normal; 114 | margin: 0 0 5px 0; 115 | padding: 0; 116 | } 117 | 118 | div.sphinxsidebar h4 { 119 | font-size: 20px; 120 | } 121 | 122 | div.sphinxsidebar h3 a { 123 | color: #444; 124 | } 125 | 126 | div.sphinxsidebar p.logo a, 127 | div.sphinxsidebar h3 a, 128 | div.sphinxsidebar p.logo a:hover, 129 | div.sphinxsidebar h3 a:hover { 130 | border: none; 131 | } 132 | 133 | div.sphinxsidebar p { 134 | color: #555; 135 | margin: 10px 0; 136 | } 137 | 138 | div.sphinxsidebar ul { 139 | margin: 10px 0; 140 | padding: 0; 141 | color: #000; 142 | } 143 | 144 | div.sphinxsidebar ul li.toctree-l1 > a { 145 | font-size: 120%; 146 | } 147 | 148 | div.sphinxsidebar ul li.toctree-l2 > a { 149 | font-size: 110%; 150 | } 151 | 152 | div.sphinxsidebar input { 153 | border: 1px solid #CCC; 154 | font-family: Georgia, serif; 155 | font-size: 1em; 156 | } 157 | 158 | div.sphinxsidebar hr { 159 | border: none; 160 | height: 1px; 161 | color: #AAA; 162 | background: #AAA; 163 | 164 | text-align: left; 165 | margin-left: 0; 166 | width: 50%; 167 | } 168 | 169 | div.sphinxsidebar .badge { 170 | border-bottom: none; 171 | } 172 | 173 | div.sphinxsidebar .badge:hover { 174 | border-bottom: none; 175 | } 176 | 177 | /* To address an issue with donation coming after search */ 178 | div.sphinxsidebar h3.donation { 179 | margin-top: 10px; 180 | } 181 | 182 | /* -- body styles ----------------------------------------------------------- */ 183 | 184 | a { 185 | color: #004B6B; 186 | text-decoration: underline; 187 | } 188 | 189 | a:hover { 190 | color: #6D4100; 191 | text-decoration: underline; 192 | } 193 | 194 | div.body h1, 195 | div.body h2, 196 | div.body h3, 197 | div.body h4, 198 | div.body h5, 199 | div.body h6 { 200 | font-family: Georgia, serif; 201 | font-weight: normal; 202 | margin: 30px 0px 10px 0px; 203 | padding: 0; 204 | } 205 | 206 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } 207 | div.body h2 { font-size: 180%; } 208 | div.body h3 { font-size: 150%; } 209 | div.body h4 { font-size: 130%; } 210 | div.body h5 { font-size: 100%; } 211 | div.body h6 { font-size: 100%; } 212 | 213 | a.headerlink { 214 | color: #DDD; 215 | padding: 0 4px; 216 | text-decoration: none; 217 | } 218 | 219 | a.headerlink:hover { 220 | color: #444; 221 | background: #EAEAEA; 222 | } 223 | 224 | div.body p, div.body dd, div.body li { 225 | line-height: 1.4em; 226 | } 227 | 228 | div.admonition { 229 | margin: 20px 0px; 230 | padding: 10px 30px; 231 | background-color: #EEE; 232 | border: 1px solid #CCC; 233 | } 234 | 235 | div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { 236 | background-color: #FBFBFB; 237 | border-bottom: 1px solid #fafafa; 238 | } 239 | 240 | div.admonition p.admonition-title { 241 | font-family: Georgia, serif; 242 | font-weight: normal; 243 | font-size: 24px; 244 | margin: 0 0 10px 0; 245 | padding: 0; 246 | line-height: 1; 247 | } 248 | 249 | div.admonition p.last { 250 | margin-bottom: 0; 251 | } 252 | 253 | div.highlight { 254 | background-color: #fff; 255 | } 256 | 257 | dt:target, .highlight { 258 | background: #FAF3E8; 259 | } 260 | 261 | div.warning { 262 | background-color: #FCC; 263 | border: 1px solid #FAA; 264 | } 265 | 266 | div.danger { 267 | background-color: #FCC; 268 | border: 1px solid #FAA; 269 | -moz-box-shadow: 2px 2px 4px #D52C2C; 270 | -webkit-box-shadow: 2px 2px 4px #D52C2C; 271 | box-shadow: 2px 2px 4px #D52C2C; 272 | } 273 | 274 | div.error { 275 | background-color: #FCC; 276 | border: 1px solid #FAA; 277 | -moz-box-shadow: 2px 2px 4px #D52C2C; 278 | -webkit-box-shadow: 2px 2px 4px #D52C2C; 279 | box-shadow: 2px 2px 4px #D52C2C; 280 | } 281 | 282 | div.caution { 283 | background-color: #FCC; 284 | border: 1px solid #FAA; 285 | } 286 | 287 | div.attention { 288 | background-color: #FCC; 289 | border: 1px solid #FAA; 290 | } 291 | 292 | div.important { 293 | background-color: #EEE; 294 | border: 1px solid #CCC; 295 | } 296 | 297 | div.note { 298 | background-color: #EEE; 299 | border: 1px solid #CCC; 300 | } 301 | 302 | div.tip { 303 | background-color: #EEE; 304 | border: 1px solid #CCC; 305 | } 306 | 307 | div.hint { 308 | background-color: #EEE; 309 | border: 1px solid #CCC; 310 | } 311 | 312 | div.seealso { 313 | background-color: #EEE; 314 | border: 1px solid #CCC; 315 | } 316 | 317 | div.topic { 318 | background-color: #EEE; 319 | } 320 | 321 | p.admonition-title { 322 | display: inline; 323 | } 324 | 325 | p.admonition-title:after { 326 | content: ":"; 327 | } 328 | 329 | pre, tt, code { 330 | font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 331 | font-size: 0.9em; 332 | } 333 | 334 | .hll { 335 | background-color: #FFC; 336 | margin: 0 -12px; 337 | padding: 0 12px; 338 | display: block; 339 | } 340 | 341 | img.screenshot { 342 | } 343 | 344 | tt.descname, tt.descclassname, code.descname, code.descclassname { 345 | font-size: 0.95em; 346 | } 347 | 348 | tt.descname, code.descname { 349 | padding-right: 0.08em; 350 | } 351 | 352 | img.screenshot { 353 | -moz-box-shadow: 2px 2px 4px #EEE; 354 | -webkit-box-shadow: 2px 2px 4px #EEE; 355 | box-shadow: 2px 2px 4px #EEE; 356 | } 357 | 358 | table.docutils { 359 | border: 1px solid #888; 360 | -moz-box-shadow: 2px 2px 4px #EEE; 361 | -webkit-box-shadow: 2px 2px 4px #EEE; 362 | box-shadow: 2px 2px 4px #EEE; 363 | } 364 | 365 | table.docutils td, table.docutils th { 366 | border: 1px solid #888; 367 | padding: 0.25em 0.7em; 368 | } 369 | 370 | table.field-list, table.footnote { 371 | border: none; 372 | -moz-box-shadow: none; 373 | -webkit-box-shadow: none; 374 | box-shadow: none; 375 | } 376 | 377 | table.footnote { 378 | margin: 15px 0; 379 | width: 100%; 380 | border: 1px solid #EEE; 381 | background: #FDFDFD; 382 | font-size: 0.9em; 383 | } 384 | 385 | table.footnote + table.footnote { 386 | margin-top: -15px; 387 | border-top: none; 388 | } 389 | 390 | table.field-list th { 391 | padding: 0 0.8em 0 0; 392 | } 393 | 394 | table.field-list td { 395 | padding: 0; 396 | } 397 | 398 | table.field-list p { 399 | margin-bottom: 0.8em; 400 | } 401 | 402 | /* Cloned from 403 | * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 404 | */ 405 | .field-name { 406 | -moz-hyphens: manual; 407 | -ms-hyphens: manual; 408 | -webkit-hyphens: manual; 409 | hyphens: manual; 410 | } 411 | 412 | table.footnote td.label { 413 | width: .1px; 414 | padding: 0.3em 0 0.3em 0.5em; 415 | } 416 | 417 | table.footnote td { 418 | padding: 0.3em 0.5em; 419 | } 420 | 421 | dl { 422 | margin: 0; 423 | padding: 0; 424 | } 425 | 426 | dl dd { 427 | margin-left: 30px; 428 | } 429 | 430 | blockquote { 431 | margin: 0 0 0 30px; 432 | padding: 0; 433 | } 434 | 435 | ul, ol { 436 | /* Matches the 30px from the narrow-screen "li > ul" selector below */ 437 | margin: 10px 0 10px 30px; 438 | padding: 0; 439 | } 440 | 441 | pre { 442 | background: #EEE; 443 | padding: 7px 30px; 444 | margin: 15px 0px; 445 | line-height: 1.3em; 446 | } 447 | 448 | div.viewcode-block:target { 449 | background: #ffd; 450 | } 451 | 452 | dl pre, blockquote pre, li pre { 453 | margin-left: 0; 454 | padding-left: 30px; 455 | } 456 | 457 | tt, code { 458 | background-color: #ecf0f3; 459 | color: #222; 460 | /* padding: 1px 2px; */ 461 | } 462 | 463 | tt.xref, code.xref, a tt { 464 | background-color: #FBFBFB; 465 | border-bottom: 1px solid #fff; 466 | } 467 | 468 | a.reference { 469 | text-decoration: none; 470 | border-bottom: 1px dotted #004B6B; 471 | } 472 | 473 | /* Don't put an underline on images */ 474 | a.image-reference, a.image-reference:hover { 475 | border-bottom: none; 476 | } 477 | 478 | a.reference:hover { 479 | border-bottom: 1px solid #6D4100; 480 | } 481 | 482 | a.footnote-reference { 483 | text-decoration: none; 484 | font-size: 0.7em; 485 | vertical-align: top; 486 | border-bottom: 1px dotted #004B6B; 487 | } 488 | 489 | a.footnote-reference:hover { 490 | border-bottom: 1px solid #6D4100; 491 | } 492 | 493 | a:hover tt, a:hover code { 494 | background: #EEE; 495 | } 496 | 497 | 498 | @media screen and (max-width: 870px) { 499 | 500 | div.sphinxsidebar { 501 | display: none; 502 | } 503 | 504 | div.document { 505 | width: 100%; 506 | 507 | } 508 | 509 | div.documentwrapper { 510 | margin-left: 0; 511 | margin-top: 0; 512 | margin-right: 0; 513 | margin-bottom: 0; 514 | } 515 | 516 | div.bodywrapper { 517 | margin-top: 0; 518 | margin-right: 0; 519 | margin-bottom: 0; 520 | margin-left: 0; 521 | } 522 | 523 | ul { 524 | margin-left: 0; 525 | } 526 | 527 | li > ul { 528 | /* Matches the 30px from the "ul, ol" selector above */ 529 | margin-left: 30px; 530 | } 531 | 532 | .document { 533 | width: auto; 534 | } 535 | 536 | .footer { 537 | width: auto; 538 | } 539 | 540 | .bodywrapper { 541 | margin: 0; 542 | } 543 | 544 | .footer { 545 | width: auto; 546 | } 547 | 548 | .github { 549 | display: none; 550 | } 551 | 552 | 553 | 554 | } 555 | 556 | 557 | 558 | @media screen and (max-width: 875px) { 559 | 560 | body { 561 | margin: 0; 562 | padding: 20px 30px; 563 | } 564 | 565 | div.documentwrapper { 566 | float: none; 567 | background: #fff; 568 | } 569 | 570 | div.sphinxsidebar { 571 | display: block; 572 | float: none; 573 | width: 102.5%; 574 | margin: 50px -30px -20px -30px; 575 | padding: 10px 20px; 576 | background: #333; 577 | color: #FFF; 578 | } 579 | 580 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, 581 | div.sphinxsidebar h3 a { 582 | color: #fff; 583 | } 584 | 585 | div.sphinxsidebar a { 586 | color: #AAA; 587 | } 588 | 589 | div.sphinxsidebar p.logo { 590 | display: none; 591 | } 592 | 593 | div.document { 594 | width: 100%; 595 | margin: 0; 596 | } 597 | 598 | div.footer { 599 | display: none; 600 | } 601 | 602 | div.bodywrapper { 603 | margin: 0; 604 | } 605 | 606 | div.body { 607 | min-height: 0; 608 | padding: 0; 609 | } 610 | 611 | .rtd_doc_footer { 612 | display: none; 613 | } 614 | 615 | .document { 616 | width: auto; 617 | } 618 | 619 | .footer { 620 | width: auto; 621 | } 622 | 623 | .footer { 624 | width: auto; 625 | } 626 | 627 | .github { 628 | display: none; 629 | } 630 | } 631 | 632 | 633 | /* misc. */ 634 | 635 | .revsys-inline { 636 | display: none!important; 637 | } 638 | 639 | /* Make nested-list/multi-paragraph items look better in Releases changelog 640 | * pages. Without this, docutils' magical list fuckery causes inconsistent 641 | * formatting between different release sub-lists. 642 | */ 643 | div#changelog > div.section > ul > li > p:only-child { 644 | margin-bottom: 0; 645 | } 646 | 647 | /* Hide fugly table cell borders in ..bibliography:: directive output */ 648 | table.docutils.citation, table.docutils.citation td, table.docutils.citation th { 649 | border: none; 650 | /* Below needed in some edge cases; if not applied, bottom shadows appear */ 651 | -moz-box-shadow: none; 652 | -webkit-box-shadow: none; 653 | box-shadow: none; 654 | } 655 | 656 | 657 | /* relbar */ 658 | 659 | .related { 660 | line-height: 30px; 661 | width: 100%; 662 | font-size: 0.9rem; 663 | } 664 | 665 | .related.top { 666 | border-bottom: 1px solid #EEE; 667 | margin-bottom: 20px; 668 | } 669 | 670 | .related.bottom { 671 | border-top: 1px solid #EEE; 672 | } 673 | 674 | .related ul { 675 | padding: 0; 676 | margin: 0; 677 | list-style: none; 678 | } 679 | 680 | .related li { 681 | display: inline; 682 | } 683 | 684 | nav#rellinks { 685 | float: right; 686 | } 687 | 688 | nav#rellinks li+li:before { 689 | content: "|"; 690 | } 691 | 692 | nav#breadcrumbs li+li:before { 693 | content: "\00BB"; 694 | } 695 | 696 | /* Hide certain items when printing */ 697 | @media print { 698 | div.related { 699 | display: none; 700 | } 701 | } -------------------------------------------------------------------------------- /docs/_build/html/_static/css/badge_only.css: -------------------------------------------------------------------------------- 1 | .clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/css/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/css/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/css/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/css/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-bold-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/css/fonts/lato-bold-italic.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-bold-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/css/fonts/lato-bold-italic.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/css/fonts/lato-bold.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/css/fonts/lato-bold.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-normal-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/css/fonts/lato-normal-italic.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-normal-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/css/fonts/lato-normal-italic.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-normal.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/css/fonts/lato-normal.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-normal.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/css/fonts/lato-normal.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* This file intentionally left blank. */ 2 | -------------------------------------------------------------------------------- /docs/_build/html/_static/doctools.js: -------------------------------------------------------------------------------- 1 | /* 2 | * doctools.js 3 | * ~~~~~~~~~~~ 4 | * 5 | * Base JavaScript utilities for all Sphinx HTML documentation. 6 | * 7 | * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | "use strict"; 12 | 13 | const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ 14 | "TEXTAREA", 15 | "INPUT", 16 | "SELECT", 17 | "BUTTON", 18 | ]); 19 | 20 | const _ready = (callback) => { 21 | if (document.readyState !== "loading") { 22 | callback(); 23 | } else { 24 | document.addEventListener("DOMContentLoaded", callback); 25 | } 26 | }; 27 | 28 | /** 29 | * Small JavaScript module for the documentation. 30 | */ 31 | const Documentation = { 32 | init: () => { 33 | Documentation.initDomainIndexTable(); 34 | Documentation.initOnKeyListeners(); 35 | }, 36 | 37 | /** 38 | * i18n support 39 | */ 40 | TRANSLATIONS: {}, 41 | PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), 42 | LOCALE: "unknown", 43 | 44 | // gettext and ngettext don't access this so that the functions 45 | // can safely bound to a different name (_ = Documentation.gettext) 46 | gettext: (string) => { 47 | const translated = Documentation.TRANSLATIONS[string]; 48 | switch (typeof translated) { 49 | case "undefined": 50 | return string; // no translation 51 | case "string": 52 | return translated; // translation exists 53 | default: 54 | return translated[0]; // (singular, plural) translation tuple exists 55 | } 56 | }, 57 | 58 | ngettext: (singular, plural, n) => { 59 | const translated = Documentation.TRANSLATIONS[singular]; 60 | if (typeof translated !== "undefined") 61 | return translated[Documentation.PLURAL_EXPR(n)]; 62 | return n === 1 ? singular : plural; 63 | }, 64 | 65 | addTranslations: (catalog) => { 66 | Object.assign(Documentation.TRANSLATIONS, catalog.messages); 67 | Documentation.PLURAL_EXPR = new Function( 68 | "n", 69 | `return (${catalog.plural_expr})` 70 | ); 71 | Documentation.LOCALE = catalog.locale; 72 | }, 73 | 74 | /** 75 | * helper function to focus on search bar 76 | */ 77 | focusSearchBar: () => { 78 | document.querySelectorAll("input[name=q]")[0]?.focus(); 79 | }, 80 | 81 | /** 82 | * Initialise the domain index toggle buttons 83 | */ 84 | initDomainIndexTable: () => { 85 | const toggler = (el) => { 86 | const idNumber = el.id.substr(7); 87 | const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); 88 | if (el.src.substr(-9) === "minus.png") { 89 | el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; 90 | toggledRows.forEach((el) => (el.style.display = "none")); 91 | } else { 92 | el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; 93 | toggledRows.forEach((el) => (el.style.display = "")); 94 | } 95 | }; 96 | 97 | const togglerElements = document.querySelectorAll("img.toggler"); 98 | togglerElements.forEach((el) => 99 | el.addEventListener("click", (event) => toggler(event.currentTarget)) 100 | ); 101 | togglerElements.forEach((el) => (el.style.display = "")); 102 | if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); 103 | }, 104 | 105 | initOnKeyListeners: () => { 106 | // only install a listener if it is really needed 107 | if ( 108 | !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && 109 | !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS 110 | ) 111 | return; 112 | 113 | document.addEventListener("keydown", (event) => { 114 | // bail for input elements 115 | if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; 116 | // bail with special keys 117 | if (event.altKey || event.ctrlKey || event.metaKey) return; 118 | 119 | if (!event.shiftKey) { 120 | switch (event.key) { 121 | case "ArrowLeft": 122 | if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; 123 | 124 | const prevLink = document.querySelector('link[rel="prev"]'); 125 | if (prevLink && prevLink.href) { 126 | window.location.href = prevLink.href; 127 | event.preventDefault(); 128 | } 129 | break; 130 | case "ArrowRight": 131 | if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; 132 | 133 | const nextLink = document.querySelector('link[rel="next"]'); 134 | if (nextLink && nextLink.href) { 135 | window.location.href = nextLink.href; 136 | event.preventDefault(); 137 | } 138 | break; 139 | } 140 | } 141 | 142 | // some keyboard layouts may need Shift to get / 143 | switch (event.key) { 144 | case "/": 145 | if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; 146 | Documentation.focusSearchBar(); 147 | event.preventDefault(); 148 | } 149 | }); 150 | }, 151 | }; 152 | 153 | // quick alias for translations 154 | const _ = Documentation.gettext; 155 | 156 | _ready(Documentation.init); 157 | -------------------------------------------------------------------------------- /docs/_build/html/_static/documentation_options.js: -------------------------------------------------------------------------------- 1 | const DOCUMENTATION_OPTIONS = { 2 | VERSION: '1', 3 | LANGUAGE: 'en', 4 | COLLAPSE_INDEX: false, 5 | BUILDER: 'html', 6 | FILE_SUFFIX: '.html', 7 | LINK_SUFFIX: '.html', 8 | HAS_SOURCE: true, 9 | SOURCELINK_SUFFIX: '.txt', 10 | NAVIGATION_WITH_KEYS: false, 11 | SHOW_SEARCH_SUMMARY: true, 12 | ENABLE_SEARCH_SHORTCUTS: true, 13 | }; -------------------------------------------------------------------------------- /docs/_build/html/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/file.png -------------------------------------------------------------------------------- /docs/_build/html/_static/js/badge_only.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=4)}({4:function(e,t,r){}}); -------------------------------------------------------------------------------- /docs/_build/html/_static/js/html5shiv-printshiv.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve HTML5 Shiv 3.7.3-pre | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 3 | */ 4 | !function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=y.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=y.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),y.elements=c+" "+a,j(b)}function f(a){var b=x[a[v]];return b||(b={},w++,a[v]=w,x[w]=b),b}function g(a,c,d){if(c||(c=b),q)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():u.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||t.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),q)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return y.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(y,b.frag)}function j(a){a||(a=b);var d=f(a);return!y.shivCSS||p||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),q||i(a,d),a}function k(a){for(var b,c=a.getElementsByTagName("*"),e=c.length,f=RegExp("^(?:"+d().join("|")+")$","i"),g=[];e--;)b=c[e],f.test(b.nodeName)&&g.push(b.applyElement(l(b)));return g}function l(a){for(var b,c=a.attributes,d=c.length,e=a.ownerDocument.createElement(A+":"+a.nodeName);d--;)b=c[d],b.specified&&e.setAttribute(b.nodeName,b.nodeValue);return e.style.cssText=a.style.cssText,e}function m(a){for(var b,c=a.split("{"),e=c.length,f=RegExp("(^|[\\s,>+~])("+d().join("|")+")(?=[[\\s,>+~#.:]|$)","gi"),g="$1"+A+"\\:$2";e--;)b=c[e]=c[e].split("}"),b[b.length-1]=b[b.length-1].replace(f,g),c[e]=b.join("}");return c.join("{")}function n(a){for(var b=a.length;b--;)a[b].removeNode()}function o(a){function b(){clearTimeout(g._removeSheetTimer),d&&d.removeNode(!0),d=null}var d,e,g=f(a),h=a.namespaces,i=a.parentWindow;return!B||a.printShived?a:("undefined"==typeof h[A]&&h.add(A),i.attachEvent("onbeforeprint",function(){b();for(var f,g,h,i=a.styleSheets,j=[],l=i.length,n=Array(l);l--;)n[l]=i[l];for(;h=n.pop();)if(!h.disabled&&z.test(h.media)){try{f=h.imports,g=f.length}catch(o){g=0}for(l=0;g>l;l++)n.push(f[l]);try{j.push(h.cssText)}catch(o){}}j=m(j.reverse().join("")),e=k(a),d=c(a,j)}),i.attachEvent("onafterprint",function(){n(e),clearTimeout(g._removeSheetTimer),g._removeSheetTimer=setTimeout(b,500)}),a.printShived=!0,a)}var p,q,r="3.7.3",s=a.html5||{},t=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,u=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",w=0,x={};!function(){try{var a=b.createElement("a");a.innerHTML="",p="hidden"in a,q=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){p=!0,q=!0}}();var y={elements:s.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:r,shivCSS:s.shivCSS!==!1,supportsUnknownElements:q,shivMethods:s.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=y,j(b);var z=/^$|\b(?:all|print)\b/,A="html5shiv",B=!q&&function(){var c=b.documentElement;return!("undefined"==typeof b.namespaces||"undefined"==typeof b.parentWindow||"undefined"==typeof c.applyElement||"undefined"==typeof c.removeNode||"undefined"==typeof a.attachEvent)}();y.type+=" print",y.shivPrint=o,o(b),"object"==typeof module&&module.exports&&(module.exports=y)}("undefined"!=typeof window?window:this,document); -------------------------------------------------------------------------------- /docs/_build/html/_static/js/html5shiv.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 3 | */ 4 | !function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3-pre",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document); -------------------------------------------------------------------------------- /docs/_build/html/_static/js/theme.js: -------------------------------------------------------------------------------- 1 | !function(n){var e={};function t(i){if(e[i])return e[i].exports;var o=e[i]={i:i,l:!1,exports:{}};return n[i].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=n,t.c=e,t.d=function(n,e,i){t.o(n,e)||Object.defineProperty(n,e,{enumerable:!0,get:i})},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.t=function(n,e){if(1&e&&(n=t(n)),8&e)return n;if(4&e&&"object"==typeof n&&n&&n.__esModule)return n;var i=Object.create(null);if(t.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:n}),2&e&&"string"!=typeof n)for(var o in n)t.d(i,o,function(e){return n[e]}.bind(null,o));return i},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,"a",e),e},t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.p="",t(t.s=0)}([function(n,e,t){t(1),n.exports=t(3)},function(n,e,t){(function(){var e="undefined"!=typeof window?window.jQuery:t(2);n.exports.ThemeNav={navBar:null,win:null,winScroll:!1,winResize:!1,linkScroll:!1,winPosition:0,winHeight:null,docHeight:null,isRunning:!1,enable:function(n){var t=this;void 0===n&&(n=!0),t.isRunning||(t.isRunning=!0,e((function(e){t.init(e),t.reset(),t.win.on("hashchange",t.reset),n&&t.win.on("scroll",(function(){t.linkScroll||t.winScroll||(t.winScroll=!0,requestAnimationFrame((function(){t.onScroll()})))})),t.win.on("resize",(function(){t.winResize||(t.winResize=!0,requestAnimationFrame((function(){t.onResize()})))})),t.onResize()})))},enableSticky:function(){this.enable(!0)},init:function(n){n(document);var e=this;this.navBar=n("div.wy-side-scroll:first"),this.win=n(window),n(document).on("click","[data-toggle='wy-nav-top']",(function(){n("[data-toggle='wy-nav-shift']").toggleClass("shift"),n("[data-toggle='rst-versions']").toggleClass("shift")})).on("click",".wy-menu-vertical .current ul li a",(function(){var t=n(this);n("[data-toggle='wy-nav-shift']").removeClass("shift"),n("[data-toggle='rst-versions']").toggleClass("shift"),e.toggleCurrent(t),e.hashChange()})).on("click","[data-toggle='rst-current-version']",(function(){n("[data-toggle='rst-versions']").toggleClass("shift-up")})),n("table.docutils:not(.field-list,.footnote,.citation)").wrap("
"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t0 63 | var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 64 | var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 65 | var s_v = "^(" + C + ")?" + v; // vowel in stem 66 | 67 | this.stemWord = function (w) { 68 | var stem; 69 | var suffix; 70 | var firstch; 71 | var origword = w; 72 | 73 | if (w.length < 3) 74 | return w; 75 | 76 | var re; 77 | var re2; 78 | var re3; 79 | var re4; 80 | 81 | firstch = w.substr(0,1); 82 | if (firstch == "y") 83 | w = firstch.toUpperCase() + w.substr(1); 84 | 85 | // Step 1a 86 | re = /^(.+?)(ss|i)es$/; 87 | re2 = /^(.+?)([^s])s$/; 88 | 89 | if (re.test(w)) 90 | w = w.replace(re,"$1$2"); 91 | else if (re2.test(w)) 92 | w = w.replace(re2,"$1$2"); 93 | 94 | // Step 1b 95 | re = /^(.+?)eed$/; 96 | re2 = /^(.+?)(ed|ing)$/; 97 | if (re.test(w)) { 98 | var fp = re.exec(w); 99 | re = new RegExp(mgr0); 100 | if (re.test(fp[1])) { 101 | re = /.$/; 102 | w = w.replace(re,""); 103 | } 104 | } 105 | else if (re2.test(w)) { 106 | var fp = re2.exec(w); 107 | stem = fp[1]; 108 | re2 = new RegExp(s_v); 109 | if (re2.test(stem)) { 110 | w = stem; 111 | re2 = /(at|bl|iz)$/; 112 | re3 = new RegExp("([^aeiouylsz])\\1$"); 113 | re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); 114 | if (re2.test(w)) 115 | w = w + "e"; 116 | else if (re3.test(w)) { 117 | re = /.$/; 118 | w = w.replace(re,""); 119 | } 120 | else if (re4.test(w)) 121 | w = w + "e"; 122 | } 123 | } 124 | 125 | // Step 1c 126 | re = /^(.+?)y$/; 127 | if (re.test(w)) { 128 | var fp = re.exec(w); 129 | stem = fp[1]; 130 | re = new RegExp(s_v); 131 | if (re.test(stem)) 132 | w = stem + "i"; 133 | } 134 | 135 | // Step 2 136 | re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; 137 | if (re.test(w)) { 138 | var fp = re.exec(w); 139 | stem = fp[1]; 140 | suffix = fp[2]; 141 | re = new RegExp(mgr0); 142 | if (re.test(stem)) 143 | w = stem + step2list[suffix]; 144 | } 145 | 146 | // Step 3 147 | re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; 148 | if (re.test(w)) { 149 | var fp = re.exec(w); 150 | stem = fp[1]; 151 | suffix = fp[2]; 152 | re = new RegExp(mgr0); 153 | if (re.test(stem)) 154 | w = stem + step3list[suffix]; 155 | } 156 | 157 | // Step 4 158 | re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; 159 | re2 = /^(.+?)(s|t)(ion)$/; 160 | if (re.test(w)) { 161 | var fp = re.exec(w); 162 | stem = fp[1]; 163 | re = new RegExp(mgr1); 164 | if (re.test(stem)) 165 | w = stem; 166 | } 167 | else if (re2.test(w)) { 168 | var fp = re2.exec(w); 169 | stem = fp[1] + fp[2]; 170 | re2 = new RegExp(mgr1); 171 | if (re2.test(stem)) 172 | w = stem; 173 | } 174 | 175 | // Step 5 176 | re = /^(.+?)e$/; 177 | if (re.test(w)) { 178 | var fp = re.exec(w); 179 | stem = fp[1]; 180 | re = new RegExp(mgr1); 181 | re2 = new RegExp(meq1); 182 | re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); 183 | if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) 184 | w = stem; 185 | } 186 | re = /ll$/; 187 | re2 = new RegExp(mgr1); 188 | if (re.test(w) && re2.test(w)) { 189 | re = /.$/; 190 | w = w.replace(re,""); 191 | } 192 | 193 | // and turn initial Y back to y 194 | if (firstch == "y") 195 | w = firstch.toLowerCase() + w.substr(1); 196 | return w; 197 | } 198 | } 199 | 200 | -------------------------------------------------------------------------------- /docs/_build/html/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/minus.png -------------------------------------------------------------------------------- /docs/_build/html/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/_static/plus.png -------------------------------------------------------------------------------- /docs/_build/html/_static/pygments.css: -------------------------------------------------------------------------------- 1 | pre { line-height: 125%; } 2 | td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } 3 | span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } 4 | td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } 5 | span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } 6 | .highlight .hll { background-color: #ffffcc } 7 | .highlight { background: #f8f8f8; } 8 | .highlight .c { color: #3D7B7B; font-style: italic } /* Comment */ 9 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 10 | .highlight .k { color: #008000; font-weight: bold } /* Keyword */ 11 | .highlight .o { color: #666666 } /* Operator */ 12 | .highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ 13 | .highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ 14 | .highlight .cp { color: #9C6500 } /* Comment.Preproc */ 15 | .highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ 16 | .highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ 17 | .highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ 18 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 19 | .highlight .ge { font-style: italic } /* Generic.Emph */ 20 | .highlight .gr { color: #E40000 } /* Generic.Error */ 21 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 22 | .highlight .gi { color: #008400 } /* Generic.Inserted */ 23 | .highlight .go { color: #717171 } /* Generic.Output */ 24 | .highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 25 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 26 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 27 | .highlight .gt { color: #0044DD } /* Generic.Traceback */ 28 | .highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ 29 | .highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ 30 | .highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ 31 | .highlight .kp { color: #008000 } /* Keyword.Pseudo */ 32 | .highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ 33 | .highlight .kt { color: #B00040 } /* Keyword.Type */ 34 | .highlight .m { color: #666666 } /* Literal.Number */ 35 | .highlight .s { color: #BA2121 } /* Literal.String */ 36 | .highlight .na { color: #687822 } /* Name.Attribute */ 37 | .highlight .nb { color: #008000 } /* Name.Builtin */ 38 | .highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 39 | .highlight .no { color: #880000 } /* Name.Constant */ 40 | .highlight .nd { color: #AA22FF } /* Name.Decorator */ 41 | .highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */ 42 | .highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ 43 | .highlight .nf { color: #0000FF } /* Name.Function */ 44 | .highlight .nl { color: #767600 } /* Name.Label */ 45 | .highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 46 | .highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ 47 | .highlight .nv { color: #19177C } /* Name.Variable */ 48 | .highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 49 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 50 | .highlight .mb { color: #666666 } /* Literal.Number.Bin */ 51 | .highlight .mf { color: #666666 } /* Literal.Number.Float */ 52 | .highlight .mh { color: #666666 } /* Literal.Number.Hex */ 53 | .highlight .mi { color: #666666 } /* Literal.Number.Integer */ 54 | .highlight .mo { color: #666666 } /* Literal.Number.Oct */ 55 | .highlight .sa { color: #BA2121 } /* Literal.String.Affix */ 56 | .highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ 57 | .highlight .sc { color: #BA2121 } /* Literal.String.Char */ 58 | .highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ 59 | .highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ 60 | .highlight .s2 { color: #BA2121 } /* Literal.String.Double */ 61 | .highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ 62 | .highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ 63 | .highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ 64 | .highlight .sx { color: #008000 } /* Literal.String.Other */ 65 | .highlight .sr { color: #A45A77 } /* Literal.String.Regex */ 66 | .highlight .s1 { color: #BA2121 } /* Literal.String.Single */ 67 | .highlight .ss { color: #19177C } /* Literal.String.Symbol */ 68 | .highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ 69 | .highlight .fm { color: #0000FF } /* Name.Function.Magic */ 70 | .highlight .vc { color: #19177C } /* Name.Variable.Class */ 71 | .highlight .vg { color: #19177C } /* Name.Variable.Global */ 72 | .highlight .vi { color: #19177C } /* Name.Variable.Instance */ 73 | .highlight .vm { color: #19177C } /* Name.Variable.Magic */ 74 | .highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /docs/_build/html/_static/sphinx_highlight.js: -------------------------------------------------------------------------------- 1 | /* Highlighting utilities for Sphinx HTML documentation. */ 2 | "use strict"; 3 | 4 | const SPHINX_HIGHLIGHT_ENABLED = true 5 | 6 | /** 7 | * highlight a given string on a node by wrapping it in 8 | * span elements with the given class name. 9 | */ 10 | const _highlight = (node, addItems, text, className) => { 11 | if (node.nodeType === Node.TEXT_NODE) { 12 | const val = node.nodeValue; 13 | const parent = node.parentNode; 14 | const pos = val.toLowerCase().indexOf(text); 15 | if ( 16 | pos >= 0 && 17 | !parent.classList.contains(className) && 18 | !parent.classList.contains("nohighlight") 19 | ) { 20 | let span; 21 | 22 | const closestNode = parent.closest("body, svg, foreignObject"); 23 | const isInSVG = closestNode && closestNode.matches("svg"); 24 | if (isInSVG) { 25 | span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); 26 | } else { 27 | span = document.createElement("span"); 28 | span.classList.add(className); 29 | } 30 | 31 | span.appendChild(document.createTextNode(val.substr(pos, text.length))); 32 | const rest = document.createTextNode(val.substr(pos + text.length)); 33 | parent.insertBefore( 34 | span, 35 | parent.insertBefore( 36 | rest, 37 | node.nextSibling 38 | ) 39 | ); 40 | node.nodeValue = val.substr(0, pos); 41 | /* There may be more occurrences of search term in this node. So call this 42 | * function recursively on the remaining fragment. 43 | */ 44 | _highlight(rest, addItems, text, className); 45 | 46 | if (isInSVG) { 47 | const rect = document.createElementNS( 48 | "http://www.w3.org/2000/svg", 49 | "rect" 50 | ); 51 | const bbox = parent.getBBox(); 52 | rect.x.baseVal.value = bbox.x; 53 | rect.y.baseVal.value = bbox.y; 54 | rect.width.baseVal.value = bbox.width; 55 | rect.height.baseVal.value = bbox.height; 56 | rect.setAttribute("class", className); 57 | addItems.push({ parent: parent, target: rect }); 58 | } 59 | } 60 | } else if (node.matches && !node.matches("button, select, textarea")) { 61 | node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); 62 | } 63 | }; 64 | const _highlightText = (thisNode, text, className) => { 65 | let addItems = []; 66 | _highlight(thisNode, addItems, text, className); 67 | addItems.forEach((obj) => 68 | obj.parent.insertAdjacentElement("beforebegin", obj.target) 69 | ); 70 | }; 71 | 72 | /** 73 | * Small JavaScript module for the documentation. 74 | */ 75 | const SphinxHighlight = { 76 | 77 | /** 78 | * highlight the search words provided in localstorage in the text 79 | */ 80 | highlightSearchWords: () => { 81 | if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight 82 | 83 | // get and clear terms from localstorage 84 | const url = new URL(window.location); 85 | const highlight = 86 | localStorage.getItem("sphinx_highlight_terms") 87 | || url.searchParams.get("highlight") 88 | || ""; 89 | localStorage.removeItem("sphinx_highlight_terms") 90 | url.searchParams.delete("highlight"); 91 | window.history.replaceState({}, "", url); 92 | 93 | // get individual terms from highlight string 94 | const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); 95 | if (terms.length === 0) return; // nothing to do 96 | 97 | // There should never be more than one element matching "div.body" 98 | const divBody = document.querySelectorAll("div.body"); 99 | const body = divBody.length ? divBody[0] : document.querySelector("body"); 100 | window.setTimeout(() => { 101 | terms.forEach((term) => _highlightText(body, term, "highlighted")); 102 | }, 10); 103 | 104 | const searchBox = document.getElementById("searchbox"); 105 | if (searchBox === null) return; 106 | searchBox.appendChild( 107 | document 108 | .createRange() 109 | .createContextualFragment( 110 | '" 114 | ) 115 | ); 116 | }, 117 | 118 | /** 119 | * helper function to hide the search marks again 120 | */ 121 | hideSearchWords: () => { 122 | document 123 | .querySelectorAll("#searchbox .highlight-link") 124 | .forEach((el) => el.remove()); 125 | document 126 | .querySelectorAll("span.highlighted") 127 | .forEach((el) => el.classList.remove("highlighted")); 128 | localStorage.removeItem("sphinx_highlight_terms") 129 | }, 130 | 131 | initEscapeListener: () => { 132 | // only install a listener if it is really needed 133 | if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; 134 | 135 | document.addEventListener("keydown", (event) => { 136 | // bail for input elements 137 | if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; 138 | // bail with special keys 139 | if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; 140 | if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { 141 | SphinxHighlight.hideSearchWords(); 142 | event.preventDefault(); 143 | } 144 | }); 145 | }, 146 | }; 147 | 148 | _ready(() => { 149 | /* Do not call highlightSearchWords() when we are on the search page. 150 | * It will highlight words from the *previous* search query. 151 | */ 152 | if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); 153 | SphinxHighlight.initEscapeListener(); 154 | }); 155 | -------------------------------------------------------------------------------- /docs/_build/html/genindex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Index — piecewise-regression 1 documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 52 | 53 |
57 | 58 |
59 |
60 |
61 |
    62 |
  • 63 | 64 |
  • 65 |
  • 66 |
67 |
68 |
69 |
70 |
71 | 72 | 73 |

Index

74 | 75 |
76 | B 77 | | C 78 | | F 79 | | G 80 | | M 81 | | N 82 | | P 83 | | S 84 | 85 |
86 |

B

87 | 88 | 92 | 98 |
99 | 100 |

C

101 | 102 | 110 | 118 |
119 | 120 |

F

121 | 122 | 126 | 130 |
131 | 132 |

G

133 | 134 | 142 | 152 |
153 | 154 |

M

155 | 156 | 169 | 173 |
174 | 175 |

N

176 | 177 | 181 |
182 | 183 |

P

184 | 185 | 205 | 221 |
222 | 223 |

S

224 | 225 | 229 | 233 |
234 | 235 | 236 | 237 |
238 |
239 |
240 | 241 |
242 | 243 |
244 |

© Copyright 2021, Charlie Pilgrim.

245 |
246 | 247 | Built with Sphinx using a 248 | theme 249 | provided by Read the Docs. 250 | 251 | 252 |
253 |
254 |
255 |
256 |
257 | 262 | 263 | 264 | -------------------------------------------------------------------------------- /docs/_build/html/model_selection.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <no title> — piecewise-regression 1 documentation 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 | 32 |
33 | 34 |
35 |
36 | class piecewise_regression.model_selection.ModelSelection(xx, yy, max_breakpoints=10, n_boot=20, max_iterations=30, tolerance=1e-05, min_distance_between_breakpoints=0.01, min_distance_to_edge=0.02, verbose=True)
37 |

Experimental - uses simple BIC based on simple linear model.

38 |
39 | 40 | 41 | 42 |
43 | 44 |
45 |
46 | 86 |
87 |
88 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /docs/_build/html/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/docs/_build/html/objects.inv -------------------------------------------------------------------------------- /docs/_build/html/py-modindex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Python Module Index — piecewise-regression 1 documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 55 | 56 |
60 | 61 |
62 |
63 |
64 |
    65 |
  • 66 | 67 |
  • 68 |
  • 69 |
70 |
71 |
72 |
73 |
74 | 75 | 76 |

Python Module Index

77 | 78 |
79 | p 80 |
81 | 82 | 83 | 84 | 86 | 87 | 89 | 92 | 93 | 94 | 97 | 98 | 99 | 102 |
 
85 | p
90 | piecewise_regression 91 |
    95 | piecewise_regression.main 96 |
    100 | piecewise_regression.model_selection 101 |
103 | 104 | 105 |
106 |
107 |
108 | 109 |
110 | 111 |
112 |

© Copyright 2021, Charlie Pilgrim.

113 |
114 | 115 | Built with Sphinx using a 116 | theme 117 | provided by Read the Docs. 118 | 119 | 120 |
121 |
122 |
123 |
124 |
125 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /docs/_build/html/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Search — piecewise-regression 1 documentation 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 55 | 56 |
60 | 61 |
62 |
63 |
64 |
    65 |
  • 66 | 67 |
  • 68 |
  • 69 |
70 |
71 |
72 |
73 |
74 | 75 | 82 | 83 | 84 |
85 | 86 |
87 | 88 |
89 |
90 |
91 | 92 |
93 | 94 |
95 |

© Copyright 2021, Charlie Pilgrim.

96 |
97 | 98 | Built with Sphinx using a 99 | theme 100 | provided by Read the Docs. 101 | 102 | 103 |
104 |
105 |
106 |
107 |
108 | 113 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | 2 | API 3 | ================================================ 4 | 5 | Main 6 | ---------- 7 | The main module includes the Fit function, which runs the bootstrap restarting algorithm. 8 | 9 | .. automodule:: piecewise_regression.main 10 | :members: 11 | 12 | Model selection 13 | --------------------- 14 | The model selection module is experimental. It compares models with different `n_breakpoints` using the Bayesian Information Criterion. 15 | 16 | .. automodule:: piecewise_regression.model_selection 17 | :members: 18 | 19 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | import sys 21 | import os 22 | project = 'piecewise-regression' 23 | copyright = '2021, Charlie Pilgrim' 24 | author = 'Charlie Pilgrim' 25 | 26 | # The full version, including alpha/beta/rc tags 27 | release = '1' 28 | 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | 'sphinx.ext.duration', 37 | 'sphinx.ext.doctest', 38 | 'sphinx.ext.autodoc', 39 | 'sphinx.ext.autosummary', 40 | 'sphinx.ext.intersphinx', 41 | ] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # List of patterns, relative to source directory, that match files and 47 | # directories to ignore when looking for source files. 48 | # This pattern also affects html_static_path and html_extra_path. 49 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 50 | 51 | intersphinx_mapping = { 52 | 'python': ('https://docs.python.org/3/', None), 53 | 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), 54 | } 55 | intersphinx_disabled_domains = ['std'] 56 | 57 | 58 | # -- Options for HTML output ------------------------------------------------- 59 | 60 | # The theme to use for HTML and HTML Help pages. See the documentation for 61 | # a list of builtin themes. 62 | # 63 | html_theme = 'sphinx_rtd_theme' 64 | 65 | # Add any paths that contain custom static files (such as style sheets) here, 66 | # relative to this directory. They are copied after the builtin static files, 67 | # so a file named "default.css" will overwrite the builtin "default.css". 68 | html_static_path = [] 69 | 70 | 71 | sys.path.insert(0, os.path.abspath('..')) 72 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. piecewise-regression documentation master file, created by 2 | sphinx-quickstart on Tue Sep 14 14:20:43 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | piecewise-regression 7 | ================================================ 8 | 9 | .. include:: README.rst 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :caption: Contents: 14 | 15 | api 16 | 17 | 18 | 19 | Indices and tables 20 | ================== 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs_requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-rtd-theme==1.2.2 -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/example.png -------------------------------------------------------------------------------- /meta.yaml: -------------------------------------------------------------------------------- 1 | {% set name = "piecewise-regression" %} 2 | {% set version = "1.4.2" %} 3 | 4 | 5 | package: 6 | name: {{ name|lower }} 7 | version: {{ version }} 8 | 9 | source: 10 | url: https://pypi.io/packages/source/{{ name[0] }}/{{ name }}/piecewise-regression-{{ version }}.tar.gz 11 | sha256: 73541954c71253cbe6bdc0b768a27408df649bd6786ee6676cb30fe1290085b6 12 | 13 | build: 14 | number: 0 15 | noarch: python 16 | script: {{ PYTHON }} -m pip install . -vv 17 | 18 | requirements: 19 | host: 20 | - pip 21 | - python >=3.6 22 | run: 23 | - matplotlib-base 24 | - numpy 25 | - python >=3.6 26 | - scipy 27 | - statsmodels 28 | 29 | test: 30 | imports: 31 | - piecewise_regression 32 | - tests 33 | commands: 34 | - pip check 35 | requires: 36 | - pip 37 | 38 | about: 39 | home: https://github.com/chasmani/piecewise-regression 40 | summary: piecewise (segmented) regression in python 41 | license: MIT 42 | license_file: LICENSE.txt 43 | 44 | extra: 45 | recipe-maintainers: 46 | - chasmani -------------------------------------------------------------------------------- /paper/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/paper/example.png -------------------------------------------------------------------------------- /paper/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/paper/example2.png -------------------------------------------------------------------------------- /paper/paper.bib: -------------------------------------------------------------------------------- 1 | @article{muggeo2003estimating, 2 | title={Estimating regression models with unknown break-points}, 3 | author={Muggeo, Vito MR}, 4 | journal={Statistics in medicine}, 5 | volume={22}, 6 | number={19}, 7 | pages={3055--3071}, 8 | year={2003}, 9 | publisher={Wiley Online Library} 10 | } 11 | 12 | @article{muggeo2008segmented, 13 | title={Segmented: an {R} package to fit regression models with broken-line relationships}, 14 | author={Muggeo, Vito MR and others}, 15 | journal={R news}, 16 | volume={8}, 17 | number={1}, 18 | pages={20--25}, 19 | year={2008} 20 | } 21 | 22 | @article{toms2003piecewise, 23 | title={Piecewise regression: a tool for identifying ecological thresholds}, 24 | author={Toms, Judith D and Lesperance, Mary L}, 25 | journal={Ecology}, 26 | volume={84}, 27 | number={8}, 28 | pages={2034--2041}, 29 | year={2003}, 30 | publisher={Wiley Online Library} 31 | } 32 | 33 | @article{ryan2002defining, 34 | title={Defining phases of bedload transport using piecewise regression}, 35 | author={Ryan, Sandra E and Porth, Laurie S and Troendle, CA}, 36 | journal={Earth Surface Processes and Landforms}, 37 | volume={27}, 38 | number={9}, 39 | pages={971--990}, 40 | year={2002}, 41 | publisher={Wiley Online Library} 42 | } 43 | 44 | @article{virtanen2020scipy, 45 | title={SciPy 1.0: fundamental algorithms for scientific computing in {Python}}, 46 | author={Virtanen, Pauli and Gommers, Ralf and Oliphant, Travis E and Haberland, Matt and Reddy, Tyler and Cournapeau, David and Burovski, Evgeni and Peterson, Pearu and Weckesser, Warren and Bright, Jonathan and others}, 47 | journal={Nature methods}, 48 | volume={17}, 49 | number={3}, 50 | pages={261--272}, 51 | year={2020}, 52 | publisher={Nature Publishing Group} 53 | } 54 | 55 | @article{newville2016lmfit, 56 | title={LMFIT: Non-linear least-square minimization and curve-fitting for {Python}}, 57 | author={Newville, Matthew and Stensitzki, Till and Allen, Daniel B and Rawlik, Michal and Ingargiola, Antonino and Nelson, Andrew}, 58 | journal={Astrophysics Source Code Library}, 59 | pages={ascl--1606}, 60 | year={2016} 61 | } 62 | 63 | @article{jekel2019pwlf, 64 | title={PWLF: a {Python} library for fitting 1D continuous piecewise linear functions}, 65 | author={Jekel, Charles F and Venter, Gerhard}, 66 | journal={URL: https://github. com/cjekel/piecewise\_linear\_fit\_py}, 67 | year={2019} 68 | } 69 | 70 | 71 | 72 | @article{wagner2002segmented, 73 | title={Segmented regression analysis of interrupted time series studies in medication use research}, 74 | author={Wagner, Anita K and Soumerai, Stephen B and Zhang, Fang and Ross-Degnan, Dennis}, 75 | journal={Journal of clinical pharmacy and therapeutics}, 76 | volume={27}, 77 | number={4}, 78 | pages={299--309}, 79 | year={2002}, 80 | publisher={Wiley Online Library} 81 | } 82 | 83 | 84 | @inproceedings{seabold2010statsmodels, 85 | title={Statsmodels: Econometric and statistical modeling with {Python}}, 86 | author={Seabold, Skipper and Perktold, Josef}, 87 | booktitle={Proceedings of the 9th Python in Science Conference}, 88 | volume={57}, 89 | pages={61}, 90 | year={2010}, 91 | organization={Austin, TX} 92 | } 93 | 94 | @article{wood2001minimizing, 95 | title={Minimizing model fitting objectives that contain spurious local minima by bootstrap restarting}, 96 | author={Wood, Simon N}, 97 | journal={Biometrics}, 98 | volume={57}, 99 | number={1}, 100 | pages={240--244}, 101 | year={2001}, 102 | publisher={Wiley Online Library} 103 | } 104 | 105 | @article{davies1987hypothesis, 106 | title={Hypothesis testing when a nuisance parameter is present only under the alternative}, 107 | author={Davies, Robert B}, 108 | journal={Biometrika}, 109 | volume={74}, 110 | number={1}, 111 | pages={33--43}, 112 | year={1987}, 113 | publisher={Oxford University Press} 114 | } 115 | 116 | @article{wit2012all, 117 | title={‘All models are wrong...’: an introduction to model uncertainty}, 118 | author={Wit, Ernst and Heuvel, Edwin van den and Romeijn, Jan-Willem}, 119 | journal={Statistica Neerlandica}, 120 | volume={66}, 121 | number={3}, 122 | pages={217--236}, 123 | year={2012}, 124 | publisher={Wiley Online Library} 125 | } 126 | 127 | -------------------------------------------------------------------------------- /paper/paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'piecewise-regression (aka segmented regression) in Python' 3 | tags: 4 | - Python 5 | - regression 6 | - statistics 7 | - segmented regression 8 | - breakpoint analysis 9 | authors: 10 | - name: Charlie Pilgrim 11 | orcid: 0000-0002-3800-677X 12 | affiliation: "1, 2" 13 | affiliations: 14 | - name: Centre for Doctoral Training in Mathematics for Real-World Systems, University of Warwick, Coventry, UK 15 | index: 1 16 | - name: The Alan Turing Institute, London, UK 17 | index: 2 18 | date: 18 November 2021 19 | bibliography: paper.bib 20 | 21 | --- 22 | 23 | # Summary 24 | 25 | Piecewise regression (also known as segmented regression, broken-line regression, or breakpoint analysis) fits a linear regression model to data that includes one or more breakpoints where the gradient changes. The `piecewise-regression` Python package uses the approach described by Muggeo [@muggeo2003estimating], where the breakpoint positions and the straight line models are simultaneously fit using an iterative method. This easy-to-use package includes an automatic comprehensive statistical analysis that gives confidence intervals for all model variables and hypothesis testing for the existence of breakpoints. 26 | 27 | # Statement of Need 28 | 29 | A common problem in many fields is to fit a continuous straight line model to data that includes some change(s) in gradient known as breakpoint(s). Examples include investigating medical interventions [@wagner2002segmented], ecological thresholds [@toms2003piecewise], and geological phase transitions [@ryan2002defining]. Fitting such models involves the global problem of finding estimates for the breakpoint positions and the local problem of fitting line segments given breakpoints. Possible approaches involve using linear regression to fit line segments together with a global optimisation algorithm to find breakpoints—for example, an evolutionary algorithm as in the `pwlf` python package [@jekel2019pwlf]. Or one could take a non-linear least-squares approach using `scipy` [@virtanen2020scipy] or the `lmfit` python package [@newville2016lmfit]. Muggeo [@muggeo2003estimating] derived an alternative method whereby the breakpoint positions and the line segment models are fitted simultaneously using an iterative method, which is computationally efficient and allows for robust statistical analysis. Many R packages implement this method, including the `segmented` R package written by Muggeo himself [@muggeo2008segmented]. However, before the `piecewise-regression` package, there were not comparable resources in Python. 30 | 31 | # Example 32 | 33 | An example plot is shown in \autoref{fig:example}. Data was generated with 3 breakpoints and some noise, and a model was then fit to that data. The plot shows the maximum likelihood estimators for the straight line segments and breakpoint positions. The package automatically carries out a Davies hypothesis test [@davies1987hypothesis] for the existence of at least 1 breakpoint, in this example finding strong evidence for breakpoints with $p<0.001$. 34 | 35 | ![An example model fit (red line) to data (grey markers). The estimated breakpoint positions (blue lines) and confidence intervals (shaded blue regions) are shown. The data was generated using a piecewise linear model with a constant level of Gaussian noise. For example, this could represent observations with a sampling error of some physical process that undergoes phase transitions. \label{fig:example}](example.png) 36 | 37 | # How It Works 38 | 39 | We follow here the derivation by Muggeo [@muggeo2003estimating]. The general form of the model with one breakpoint is 40 | 41 | \begin{equation} 42 | y = \alpha x + c + \beta (x-\psi) H(x-\psi) + \zeta \,, 43 | \end{equation} 44 | 45 | where given some data, $x$, $y$, we are trying to estimate the gradient of the first segment, $\alpha$, the intercept of the first segment, $c$, the change in gradient from the first to second segments, $\beta$, and the breakpoint position, $\psi$. $H$ is the Heaviside step function and $\zeta$ is a noise term. This cannot be solved directly through linear regression as the relationship is non-linear. We can take a linear approximation by a Taylor expansion around some initial guess for the breakpoint, $\psi^{(0)}$, 46 | 47 | \begin{equation} 48 | y \approx \alpha x + c + \beta (x - \psi^{(0)}) H (x - \psi^{(0)}) - \beta (\psi - \psi^{(0)}) H(x - \psi^{(0)}) + \zeta \,. \label{eqn:expansion} 49 | \end{equation} 50 | 51 | 52 | This is now a linear relationship and we can find a new breakpoint estimate, $\psi^{(1)}$, through ordinary linear regression using the `statsmodels` python package [@seabold2010statsmodels]. We iterate in this way until the breakpoint estimate converges, at which point we stop the algorithm. If considering multiple breakpoints, the same approach is followed using a multivariate Taylor expansion around an initial guess for each of the breakpoints. 53 | 54 | Muggeo's iterative algorithm is not guaranteed to converge on a globally optimal solution. Instead, it can converge to a local optimum or diverge. To address this limitation, we also implement bootstrap restarting [@wood2001minimizing], again following Muggeo's approach [@muggeo2008segmented]. The bootstrap restarting algorithm generates a non-parametric bootstrap of the data through resampling, which is then used to find new breakpoint values that may find a better global solution. This is repeated several times to escape local optima. 55 | 56 | # Model Selection 57 | 58 | The standard algorithm finds a good fit with a given number of breakpoints. In some instances we might not know how many breakpoints to expect in the data. We provide a tool to compare models with different numbers of breakpoints based on minimising the Bayesian Information Criterion [@wit2012all], which takes into account the value of the likelihood function while including a penalty for the number of model parameters, to avoid overfitting. When applied to the example in \autoref{fig:example}, a model with 3 breakpoints is the preferred choice. 59 | 60 | # Features 61 | 62 | The package includes the following features: 63 | 64 | - Standard fit using the iterative method described by Muggeo. 65 | - Bootstrap restarting to escape local optima. 66 | - Bootstrap restarting with randomised initial breakpoint guesses. 67 | - Calculation of standard errors and confidence intervals. 68 | - Davies hypothesis test for the existence of a breakpoint. 69 | - Customisable plots of fits. 70 | - Customisable plots of algorithm iterations. 71 | - Printable summary. 72 | - Summary data output. 73 | - Comprehensive tests. 74 | - Model comparision with an unknown number of breakpoints, with the best fit based on the Bayesian information criterion. 75 | 76 | The package can be downloaded through the [Python Package Index](https://pypi.org/project/piecewise-regression/). The full code is publicly available on [github](https://github.com/chasmani/piecewise-regression). Documentation, including an API reference, can be found at [Read The Docs](https://piecewise-regression.readthedocs.io/en/latest/). 77 | 78 | # Acknowledgements 79 | 80 | I acknowledge support from Thomas Hills. The work was funded by the EPSRC grant for the Mathematics for Real-World Systems CDT at Warwick (grant number EP/L015374/1). 81 | 82 | # References 83 | -------------------------------------------------------------------------------- /paper/paper.py: -------------------------------------------------------------------------------- 1 | from piecewise_regression import Fit, ModelSelection 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | 5 | import os 6 | import sys 7 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 8 | 9 | 10 | def plot_basic_example(): 11 | """ 12 | Example for some data 13 | 14 | """ 15 | np.random.seed(1) 16 | 17 | alpha = 4 18 | beta_1 = -8 19 | beta_2 = -2 20 | beta_3 = 5 21 | intercept = 100 22 | breakpoint_1 = 5 23 | breakpoint_2 = 11 24 | breakpoint_3 = 16 25 | 26 | n_points = 200 27 | noise = 5 28 | 29 | xx = np.linspace(0, 20, n_points) 30 | 31 | yy = intercept + alpha*xx 32 | yy += beta_1 * np.maximum(xx - breakpoint_1, 0) 33 | yy += beta_2 * np.maximum(xx - breakpoint_2, 0) 34 | yy += beta_3 * np.maximum(xx - breakpoint_3, 0) 35 | yy += np.random.normal(size=n_points) * noise 36 | 37 | bp_fit = Fit(xx, yy, start_values=[3, 7, 10]) 38 | 39 | bp_fit.summary() 40 | 41 | bp_fit.plot_data(color="grey", s=20) 42 | bp_fit.plot_fit(color="red", linewidth=4) 43 | bp_fit.plot_breakpoints() 44 | bp_fit.plot_breakpoint_confidence_intervals() 45 | 46 | plt.xlabel("x") 47 | plt.ylabel("y") 48 | 49 | plt.savefig("example.png", dpi=300) 50 | 51 | # Given some data, fit the model 52 | ms = ModelSelection(xx, yy, max_breakpoints=6) 53 | print(ms) 54 | 55 | plt.show() 56 | 57 | 58 | def plot_basic_example_2(): 59 | 60 | # Generate some test data with 1 breakpoint 61 | alpha_1 = -4 62 | alpha_2 = -2 63 | intercept = 100 64 | breakpoint_1 = 7 65 | n_points = 200 66 | np.random.seed(0) 67 | 68 | xx = np.linspace(0, 20, n_points) 69 | yy = intercept + alpha_1*xx + \ 70 | (alpha_2-alpha_1) * np.maximum(xx - breakpoint_1, 0) + \ 71 | np.random.normal(size=n_points) 72 | 73 | # Given some data, fit the model 74 | bp_fit = Fit(xx, yy, start_values=[5], n_breakpoints=1) 75 | 76 | # Print a summary of the fit 77 | bp_fit.summary() 78 | 79 | # Plot the data, fit, breakpoints and confidence intervals 80 | bp_fit.plot_data(color="grey", s=20) 81 | # Pass in standard matplotlib keywords to control any of the plots 82 | bp_fit.plot_fit(color="red", linewidth=4) 83 | bp_fit.plot_breakpoints() 84 | bp_fit.plot_breakpoint_confidence_intervals() 85 | plt.xlabel("x") 86 | plt.ylabel("y") 87 | plt.savefig("example2.png") 88 | plt.show() 89 | plt.close() 90 | 91 | 92 | def model_selection_basic_example(): 93 | 94 | # Generate some test data with 1 breakpoint 95 | alpha_1 = -4 96 | alpha_2 = -2 97 | intercept = 100 98 | breakpoint_1 = 7 99 | n_points = 200 100 | np.random.seed(0) 101 | 102 | xx = np.linspace(0, 20, n_points) 103 | yy = intercept + alpha_1*xx + \ 104 | (alpha_2-alpha_1) * np.maximum(xx - breakpoint_1, 0) + \ 105 | np.random.normal(size=n_points) 106 | 107 | # Given some data, fit the model 108 | ms = ModelSelection(xx, yy, max_breakpoints=6) 109 | print(ms) 110 | 111 | 112 | if __name__ == "__main__": 113 | plot_basic_example() 114 | -------------------------------------------------------------------------------- /piecewise_regression/.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/piecewise_regression/.png -------------------------------------------------------------------------------- /piecewise_regression/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import Fit, Muggeo 2 | from .model_selection import ModelSelection 3 | from .davies import davies_test 4 | 5 | __all__ = ['Fit', 'Muggeo', 'ModelSelection', 'davies_test'] 6 | -------------------------------------------------------------------------------- /piecewise_regression/data_validation.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | 4 | 5 | def validate_boolean(var, var_name): 6 | if isinstance(var, bool): 7 | return var 8 | else: 9 | raise ValueError( 10 | "{} must be a Boolean: True or False".format(var_name)) 11 | 12 | 13 | def validate_positive_integer(var, var_name): 14 | if isinstance(var, bool): 15 | raise ValueError("{} must be a positive Integer".format(var_name)) 16 | if isinstance(var, int) and var > 0: 17 | return var 18 | else: 19 | raise ValueError("{} must be a positive Integer".format(var_name)) 20 | 21 | 22 | def validate_non_negative_integer(var, var_name): 23 | 24 | if isinstance(var, bool): 25 | raise ValueError("{} must be a non-negative Integer".format(var_name)) 26 | 27 | if isinstance(var, int) and var >= 0: 28 | return var 29 | else: 30 | raise ValueError("{} must be a non-negative Integer".format(var_name)) 31 | 32 | 33 | def validate_positive_number(var, var_name): 34 | if isinstance(var, bool): 35 | raise ValueError("{} must be a Float".format(var_name)) 36 | if (isinstance(var, float) or isinstance(var, int)) and var > 0: 37 | return var 38 | else: 39 | raise ValueError("{} must be a Float".format(var_name)) 40 | 41 | 42 | def validate_list_of_numbers(var, var_name, min_length): 43 | """ 44 | Allowed types: 45 | List of integers of floats 46 | Numpy array of integers or floats 47 | """ 48 | value_error_text = "{} must be a list of numbers with minimum length {}" 49 | value_error_text = value_error_text.format(var_name, min_length) 50 | 51 | # If its a list, convert it to a numpy array 52 | if isinstance(var, list): 53 | var = np.array(var) 54 | 55 | # If its not a numpy array at this point, raise a value error 56 | if not isinstance(var, np.ndarray): 57 | raise ValueError(value_error_text) 58 | 59 | # Check the array has numebrs in it 60 | if not np.issubdtype(var.dtype, np.number): 61 | raise ValueError(value_error_text) 62 | 63 | if len(var) < min_length: 64 | raise ValueError(value_error_text) 65 | 66 | return var 67 | -------------------------------------------------------------------------------- /piecewise_regression/davies.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import scipy 4 | import math 5 | 6 | 7 | def get_test_statistic_wald(xx, yy, theta): 8 | 9 | 10 | import statsmodels.api as sm 11 | 12 | Z = np.array([xx]) 13 | 14 | UU = [(xx - theta ) * np.heaviside(xx-theta, 1)] 15 | VV = [np.heaviside(xx - theta, 1)] 16 | 17 | Z = np.concatenate((Z, UU, VV)) 18 | Z = Z.T 19 | Z = sm.add_constant(Z, has_constant='add') 20 | 21 | results = sm.OLS(endog=yy, exog=Z).fit() 22 | 23 | beta_hat = results.params[2] 24 | se_beta_hat = results.bse[2] 25 | return beta_hat/se_beta_hat 26 | 27 | 28 | def davies_test(xx, yy, k=10, alternative="two_sided"): 29 | """ 30 | Significance test for the existence of a breakpoint 31 | Null hypothesis is that there is no breakpoint, or that the change in 32 | gradient is zero. 33 | Alternative hypothesis is that there is a breakpoint, with a non-zero 34 | change in gradient. 35 | The change is gradient is a function of the breakpoint position. 36 | The breakpoint posiition is a nuisannce parameter that only exists in the 37 | alternative hypothesis. 38 | Based on Davies (1987), "Hypothesis Testing when a nuisance parameter is 39 | present only under the alternative". 40 | 41 | :param xx: Data series in x-axis (same axis as the breakpoints). 42 | :type xx: list of floats 43 | 44 | :param yy: Data series in y-axis. 45 | :type yy: list of floats 46 | 47 | :param k: A control parameter that determines the number of points to 48 | consider within the xx range. 49 | :type k: int 50 | 51 | :param alternative: Whether to consider a two-sided hypothesis test, 52 | or a one sided test with change of gradient greater or less than zero. 53 | For existence of a breakpoint, use "two-sided". 54 | :type alternative: str. One of "two_sided", "less", "greater" 55 | 56 | """ 57 | # Centre the x values - makes no difference to existence of a breakpoint 58 | # The Davies test has this as an assumption 59 | xx_davies = xx - np.mean(xx) 60 | yy_davies = yy 61 | 62 | # As in Muggeo's R package "segmented", cut from second to second to last 63 | # data point 64 | # Need more data in the xx than in [L,U] for the test to work 65 | # Take off points form edge to be conservative 66 | L = xx_davies[2] 67 | U = xx_davies[-3] 68 | 69 | # More thetas is better 70 | thetas = np.linspace(L, U, k) 71 | # For each value of theta, compute a test statistic 72 | test_stats = [] 73 | for theta in thetas: 74 | test_stat = get_test_statistic_wald(xx_davies, yy_davies, theta) 75 | test_stats.append(test_stat) 76 | if alternative == "two_sided": 77 | # Two sided test, M as defined by Davies 78 | M = np.max(np.abs(test_stats)) 79 | elif alternative == "less": 80 | M = np.abs(np.min(test_stats)) 81 | elif alternative == "greater": 82 | M = np.max(test_stats) 83 | 84 | # Use formulas from Davies 85 | V = 0 86 | for i in range(len(thetas) - 1): 87 | V += np.abs(test_stats[i + 1] - test_stats[i]) 88 | 89 | p = scipy.stats.norm.cdf(-M) + V * np.exp(-.5 * M ** 90 | 2) * 1 / (np.sqrt(8 * math.pi)) 91 | 92 | if alternative == "two_sided": 93 | return p * 2 94 | else: 95 | return p 96 | 97 | -------------------------------------------------------------------------------- /piecewise_regression/model_selection.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import numpy as np 4 | import statsmodels.api as sm 5 | 6 | try: 7 | import piecewise_regression.r_squared_calc as r_squared_calc 8 | from piecewise_regression.main import Fit 9 | except ImportError: 10 | import r_squared_calc 11 | from main import Fit 12 | 13 | 14 | class ModelSelection: 15 | """ 16 | Experimental - uses simple BIC based on simple linear model. 17 | """ 18 | 19 | def __init__( 20 | self, xx, yy, max_breakpoints=10, n_boot=100, 21 | max_iterations=30, tolerance=10**-5, 22 | min_distance_between_breakpoints=0.01, min_distance_to_edge=0.02, 23 | verbose=True): 24 | 25 | # The actual fit model objects 26 | self.models = [] 27 | # The model summary data 28 | self.model_summaries = [] 29 | 30 | self.stop = False 31 | 32 | if verbose: 33 | print("Running fit with n_breakpoint = 0 . . ") 34 | 35 | self.no_breakpoint_fit(xx, yy) 36 | 37 | min_d_between_bps = min_distance_between_breakpoints 38 | for k in range(1, max_breakpoints + 1): 39 | if verbose: 40 | print("Running fit with n_breakpoint = {} . . ".format(k)) 41 | bootstrapped_fit = Fit( 42 | xx, yy, n_breakpoints=k, verbose=False, 43 | n_boot=n_boot, max_iterations=max_iterations, 44 | tolerance=tolerance, 45 | min_distance_between_breakpoints=min_d_between_bps, 46 | min_distance_to_edge=min_distance_to_edge) 47 | fit_summary = bootstrapped_fit.get_results() 48 | fit_summary["n_breakpoints"] = k 49 | self.model_summaries.append(fit_summary) 50 | self.models.append(bootstrapped_fit) 51 | 52 | self.summary() 53 | 54 | def summary(self): 55 | 56 | header = "\n{:^70}\n".format("Breakpoint Model Comparision Results") 57 | 58 | line_length = 100 59 | double_line = "=" * line_length + "\n" 60 | single_line = "-" * line_length + "\n" 61 | 62 | table_header_template = "{:<15} {:>12} {:>12} {:>12} \n" 63 | table_header = table_header_template.format( 64 | "n_breakpoints", "BIC", "converged", "RSS") 65 | table_row_template = "{:<15} {:>12.5} {:>12} {:>12.5} \n" 66 | 67 | table_contents = header 68 | table_contents += double_line 69 | 70 | table_contents += table_header 71 | table_contents += single_line 72 | 73 | for model_summary in self.model_summaries: 74 | 75 | if model_summary["converged"]: 76 | model_row = table_row_template.format( 77 | model_summary["n_breakpoints"], 78 | model_summary["bic"], 79 | str(model_summary["converged"]), 80 | model_summary["rss"]) 81 | else: 82 | model_row = table_row_template.format( 83 | model_summary["n_breakpoints"], "", 84 | str(model_summary["converged"]), "") 85 | 86 | table_contents += model_row 87 | 88 | print(table_contents) 89 | 90 | print("Min BIC (Bayesian Information Criterion) suggests best model") 91 | 92 | def no_breakpoint_fit(self, xx, yy): 93 | 94 | Z = np.array([xx]) 95 | Z = Z.T 96 | Z = sm.add_constant(Z, has_constant='add') 97 | # Basic OLS fit 98 | results = sm.OLS(endog=np.array(yy), exog=Z).fit() 99 | 100 | # get the predicted values 101 | ff = [(results.params[0] + results.params[1] * x) for x in xx] 102 | 103 | # Get Rss 104 | rss, tss, r_2, adjusted_r_2 = r_squared_calc.get_r_squared( 105 | yy, ff, n_params=2) 106 | 107 | # Calcualte BIC 108 | n = len(xx) # No. data points 109 | k = 2 # No. parameters 110 | bic = n * np.log(rss / n) + k * np.log(n) 111 | 112 | fit_data = { 113 | "bic": bic, 114 | "n_breakpoints": 0, 115 | "estimates": {}, 116 | "converged": True, 117 | "rss": rss 118 | } 119 | 120 | fit_data["estimates"]["const"] = results.params[0] 121 | fit_data["estimates"]["alpha1"] = results.params[1] 122 | 123 | self.model_summaries.append(fit_data) 124 | 125 | 126 | if __name__ == "__main__": 127 | 128 | pass 129 | 130 | 131 | -------------------------------------------------------------------------------- /piecewise_regression/r_squared_calc.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | 4 | 5 | def get_r_squared(yy, ff, n_params): 6 | 7 | n_data = len(yy) 8 | yy_mean = np.mean(yy) 9 | 10 | # Calculate residual and total sum of squares 11 | residual_sum_squares = 0 12 | total_sum_squares = 0 13 | for i in range(n_data): 14 | residual_sum_squares += (yy[i] - ff[i])**2 15 | total_sum_squares += (yy[i] - yy_mean)**2 16 | 17 | # R Squares 18 | r_squared = 1 - residual_sum_squares / total_sum_squares 19 | 20 | # Adjusted R squared 21 | adj_r_squared = 1 - (1 - r_squared) * \ 22 | (n_data - 1) / (n_data - n_params - 1) 23 | 24 | return residual_sum_squares, total_sum_squares, r_squared, adj_r_squared 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib==3.3.4 2 | numpy==1.19.5;python_version<"3.10" 3 | scipy==1.6.1;python_version<"3.10" 4 | statsmodels==0.13.0;python_version<"3.10" 5 | numpy==1.23.5;python_version>="3.10" 6 | scipy==1.11.1;python_version>="3.10" 7 | statsmodels==0.14.0;python_version>="3.10" -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | # This call to setup() does all the work 4 | setuptools.setup( 5 | name="piecewise-regression", 6 | version="1.5.0", 7 | description="piecewise (segmented) regression in python", 8 | long_description= "piecewise-regression provides tools for fitting " 9 | "continuous straight line models to data with " 10 | "breakpoint(s) where the gradient changes. " 11 | "" 12 | "For docs and more information, " 13 | "visit the Github repo at " 14 | "https://github.com/chasmani/piecewise-regression.", 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/chasmani/piecewise-regression", 17 | author="Charlie Pilgrim", 18 | author_email="pilgrimcharlie2@gmail.com", 19 | license="MIT", 20 | classifiers=[ 21 | "License :: OSI Approved :: MIT License", 22 | "Programming Language :: Python :: 3", 23 | "Programming Language :: Python :: 3.7", 24 | "Programming Language :: Python :: 3.8", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | ], 28 | packages=setuptools.find_packages(), 29 | install_requires=["numpy", "matplotlib", "scipy", "statsmodels"], 30 | ) 31 | 32 | -------------------------------------------------------------------------------- /setup_notes.txt: -------------------------------------------------------------------------------- 1 | To send a new version to the package manager: 2 | 3 | 1. Make sure everything works 4 | 2. Update the version number in setup.py and README.rst 5 | 3. 'python setup.py sdist bdist_wheel' - this creates a dist package in the dist/ folder 6 | 4. 'twine upload --skip-existing dist/*' 7 | 8 | To make new docs: 9 | 1. navigate to the docs folder in terminal 10 | 2. type make html 11 | 3. Maybe also need to do 'python setup.py sdist bdist_wheel' 12 | 4. you might need to update the theme. Can be set in conf.py. Might also need to install 'sudo pip3 install sphinx-rtd-theme' 13 | 5. Push it all to github 14 | 6. On readthedocs.org - add github repo 15 | 7. It might take a few hours to propogate to read the docs. 16 | 8. You might need to add a requirements.txt, and maybe add it in the Advances Settings on readthedocs.org 17 | 18 | Images: 19 | If including an image in readme.rst. It needs to display in 3 places: 20 | - Github. Can use relative or absolute link to github hosted image 21 | - PyPI. Needs absolute link to github hosted image 22 | - ReadTheDocs. -------------------------------------------------------------------------------- /tests-manual/check_breakpoint_realistic_confidence_interval.py: -------------------------------------------------------------------------------- 1 | 2 | from piecewise_regression.main import Muggeo, Fit 3 | import numpy as np 4 | 5 | import os 6 | import sys 7 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 8 | 9 | 10 | def check_p_values(): 11 | 12 | p_count = 0 13 | 14 | sample_size = 1000 15 | 16 | actual_bp = 2 17 | 18 | for seed in range(sample_size): 19 | print("Working on {} of {} . . . ".format(seed, sample_size)) 20 | np.random.seed(seed) 21 | 22 | xx_bp, yy_bp = generate_data(actual_bp) 23 | 24 | bp_fit = Muggeo(xx_bp, yy_bp, [5], verbose=False) 25 | 26 | bp_ci = bp_fit.best_fit.estimates["breakpoint1"]["confidence_interval"] 27 | if bp_ci[0] <= actual_bp and bp_ci[1] >= actual_bp: 28 | p_count += 1 29 | 30 | print(p_count) 31 | 32 | print("{} of {} estimates were within the confidence interval.".format( 33 | p_count, sample_size)) 34 | print("This should be approximately 95%") 35 | 36 | 37 | def check_p_values_fit(): 38 | 39 | p_count = 0 40 | 41 | sample_size = 100 42 | 43 | actual_bp = 2 44 | 45 | for seed in range(sample_size): 46 | print("Working on {} of {} . . . ".format(seed, sample_size)) 47 | np.random.seed(seed) 48 | 49 | xx_bp, yy_bp = generate_data(actual_bp) 50 | 51 | bp_fit = Fit(xx_bp, yy_bp, [5], verbose=False) 52 | 53 | bp_ci = bp_fit.best_muggeo.best_fit.estimates["breakpoint1"]["confidence_interval"] 54 | if bp_ci[0] <= actual_bp and bp_ci[1] >= actual_bp: 55 | p_count += 1 56 | 57 | print(p_count) 58 | 59 | print("{} of {} estimates were within the confidence interval.".format( 60 | p_count, sample_size)) 61 | print("This should be approximately 95%") 62 | 63 | 64 | def generate_data(breakpoint_1): 65 | 66 | intercept = 5 67 | alpha = 1 68 | beta_1 = 3 69 | n_points = 50 70 | noise = 1 71 | 72 | xx_bp = np.linspace(-9.5, 9.5, n_points) 73 | 74 | yy_bp = intercept + alpha*xx_bp + beta_1 * \ 75 | np.maximum(xx_bp - breakpoint_1, 0) + noise * \ 76 | np.random.normal(size=len(xx_bp)) 77 | 78 | return xx_bp, yy_bp 79 | 80 | 81 | if __name__ == "__main__": 82 | check_p_values_fit() 83 | -------------------------------------------------------------------------------- /tests-manual/check_davies_has_realistic_p_values.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 5 | 6 | 7 | from piecewise_regression.davies import davies_test 8 | import numpy as np 9 | 10 | 11 | 12 | def check_p_values(alternative="two_sided", breakpoint_1=0, beta_1=0, breakpoint_2=0, beta_2=0, noise_scale=1): 13 | 14 | p_count_20 = 0 15 | p_count_10 = 0 16 | p_count_5 = 0 17 | p_count_2 = 0 18 | p_count_1 = 0 19 | 20 | p_values = [] 21 | 22 | sample_size = 1000 23 | 24 | for seed in range(sample_size): 25 | np.random.seed(seed) 26 | 27 | xx_bp, yy_bp = generate_data(breakpoint_1, beta_1, breakpoint_2, beta_2, noise_scale) 28 | 29 | p = davies_test(xx_bp, yy_bp, alternative=alternative) 30 | 31 | if p < 0.2: 32 | p_count_20 += 1 33 | if p < 0.05: 34 | p_count_5 += 1 35 | if p < 0.02: 36 | p_count_2 += 1 37 | if p < 0.01: 38 | p_count_1 += 1 39 | 40 | p_values.append(p) 41 | 42 | # plt.hist(p_values) 43 | # plt.show() 44 | 45 | print("P values from empirical testing, along with expected p-values") 46 | print("p-values \t\t\t0.2 \t0.05\t0.02\t0.01") 47 | print("fraction less than \t{:.3f}\t{:.3f}\t{:.3f}\t{:.3f}".format( 48 | p_count_20/sample_size, p_count_5/sample_size, 49 | p_count_2/sample_size, p_count_1/sample_size)) 50 | 51 | 52 | def generate_data(breakpoint_1=0, beta_1=0, breakpoint_2=0, beta_2=0, noise_scale=1): 53 | 54 | intercept = 5 55 | alpha = 1 56 | n_points = 100 57 | 58 | xx_bp = np.linspace(-9.5, 9.5, n_points) 59 | 60 | yy_bp = intercept + alpha*xx_bp + \ 61 | beta_1 * np.maximum(xx_bp - breakpoint_1, 0) + \ 62 | beta_2 * np.maximum(xx_bp - breakpoint_2, 0) + \ 63 | np.random.normal(size=len(xx_bp), scale=noise_scale) 64 | 65 | return xx_bp, yy_bp 66 | 67 | 68 | def test_under_null_hypothesis_no_breakpoints(): 69 | 70 | print("For all of these checks, data is generated without breakpoints.") 71 | print("We repeat this many times, and record how many times gave a signifincat result.") 72 | print("We report the significance level and fraction passing the test.") 73 | print("The fraction testing should be below the significance level") 74 | # Assume no breakpoint 75 | check_p_values(alternative="two_sided") 76 | check_p_values(alternative="less") 77 | check_p_values(alternative="greater") 78 | 79 | # Low noise 80 | check_p_values(alternative="two_sided", noise_scale=0.1) 81 | check_p_values(alternative="less", noise_scale=0.1) 82 | check_p_values(alternative="greater", noise_scale=0.1) 83 | 84 | # High noise 85 | check_p_values(alternative="two_sided", noise_scale=10) 86 | check_p_values(alternative="less", noise_scale=10) 87 | check_p_values(alternative="greater", noise_scale=10) 88 | 89 | 90 | def test_with_breakpoints(): 91 | print("\n\nFor all of these checks, data is generated with breakpoints.") 92 | print("We repeat this many times, and record how many times gave a signifincat result.") 93 | print("We report the significance level and fraction passing the test.") 94 | print("The fraction testing should be much higher than the significance level") 95 | print("This will depend on the change in gradient being a greater scale than the noise") 96 | 97 | 98 | breakpoints = [-1,0,5] 99 | beta_1s = [0.1,-0.2, -1, 3, 5] 100 | noises = [0.1, 1, 10] 101 | for breakpoint_1 in breakpoints: 102 | for beta_1 in beta_1s: 103 | for noise in noises: 104 | for alternative in ["two_sided"]: 105 | print("\nbeta_1 is {}. noise is {}".format(beta_1, noise)) 106 | check_p_values(alternative=alternative, breakpoint_1=breakpoint_1, beta_1=beta_1, noise_scale=noise) 107 | 108 | 109 | def test_with_two_breakpoints(): 110 | print("\n\nFor all of these checks, data is generated with breakpoints.") 111 | print("We repeat this many times, and record how many times gave a signifincat result.") 112 | print("We report the significance level and fraction passing the test.") 113 | print("The fraction testing should be much higher than the significance level") 114 | print("This will depend on the change in gradient being a greater scale than the noise") 115 | 116 | 117 | breakpoints = [-1,0,5] 118 | beta_1s = [0.1,-0.2, -1, 3,5] 119 | noises = [0.1, 1, 10] 120 | for breakpoint_1 in breakpoints: 121 | for beta_1 in beta_1s: 122 | for noise in noises: 123 | for alternative in ["two_sided"]: 124 | print("\nbeta_1 is {}. noise is {}".format(beta_1, noise)) 125 | breakpoint_2 = breakpoint_1 + 3 126 | beta_2 = - beta_1 127 | check_p_values(alternative=alternative, breakpoint_1=breakpoint_1, beta_1=beta_1, breakpoint_2=breakpoint_2, beta_2=beta_2, noise_scale=noise) 128 | 129 | 130 | def test_one_sided_with_breakpoints(): 131 | 132 | print("\n\nFor all of these checks, data is generated with breakpoints.") 133 | print("Here we look at one-side results.") 134 | print("We should see alternating high and low pass rates") 135 | breakpoints = [-3,-1,0,2,4,5] 136 | beta_1s = [3,5,-1,1,10] 137 | for breakpoint_1 in breakpoints: 138 | for beta_1 in beta_1s: 139 | for alternative in ["greater", "less"]: 140 | print("\nActual beta_1 is {}, looking at {} than 0".format(beta_1, alternative)) 141 | check_p_values(alternative=alternative, breakpoint_1=breakpoint_1, beta_1=beta_1) 142 | 143 | 144 | if __name__ == "__main__": 145 | 146 | #test_under_null_hypothesis_no_breakpoints() 147 | test_with_two_breakpoints() 148 | #test_one_sided_with_breakpoints() 149 | 150 | 151 | -------------------------------------------------------------------------------- /tests-manual/check_estimates_have_realistic_confidence_intervals.py: -------------------------------------------------------------------------------- 1 | 2 | from piecewise_regression.main import Fit 3 | import numpy as np 4 | 5 | import os 6 | import sys 7 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 8 | 9 | 10 | def check_p_values_fit(): 11 | 12 | p_counts = { 13 | "alpha1": 0, 14 | "beta1": 0, 15 | "alpha2": 0, 16 | "const": 0 17 | } 18 | 19 | sample_size = 1000 20 | 21 | actuals = { 22 | "alpha1": 1, 23 | "beta1": 3, 24 | "alpha2": 4, 25 | "const": 5 26 | } 27 | 28 | for seed in range(sample_size): 29 | print("Working on {} of {} . . . ".format(seed, sample_size)) 30 | np.random.seed(seed) 31 | 32 | xx_bp, yy_bp = generate_data( 33 | intercept=actuals["const"], alpha=actuals["alpha1"], 34 | beta_1=actuals["beta1"]) 35 | 36 | bp_fit = Fit(xx_bp, yy_bp, [5], verbose=False) 37 | 38 | for estimate in ["alpha1", "beta1", "alpha2", "const"]: 39 | est_ci = bp_fit.best_muggeo.best_fit.estimates[estimate]["confidence_interval"] 40 | if est_ci[0] <= actuals[estimate] and est_ci[1] >= actuals[estimate]: 41 | p_counts[estimate] += 1 42 | 43 | print("Percentages in confidence interval:") 44 | for estimate in ["alpha1", "beta1", "alpha2", "const"]: 45 | print("{} : {}".format(estimate, p_counts[estimate]/sample_size)) 46 | print("This should be approximately 95%") 47 | 48 | 49 | def generate_data(intercept, alpha, beta_1): 50 | 51 | intercept = 5 52 | alpha = 1 53 | beta_1 = 3 54 | n_points = 50 55 | noise = 1 56 | breakpoint_1 = 2 57 | 58 | xx_bp = np.linspace(-9.5, 9.5, n_points) 59 | 60 | yy_bp = intercept + alpha*xx_bp + beta_1 * \ 61 | np.maximum(xx_bp - breakpoint_1, 0) + noise * \ 62 | np.random.normal(size=len(xx_bp)) 63 | 64 | return xx_bp, yy_bp 65 | 66 | 67 | if __name__ == "__main__": 68 | check_p_values_fit() 69 | -------------------------------------------------------------------------------- /tests-manual/manual_testing.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 5 | 6 | from piecewise_regression import ModelSelection 7 | from piecewise_regression import Fit 8 | import numpy as np 9 | import matplotlib.pyplot as plt 10 | 11 | 12 | 13 | 14 | def on_data_1(): 15 | 16 | alpha = -4 17 | beta_1 = -2 18 | intercept = 100 19 | breakpoint_1 = 7 20 | 21 | n_points = 200 22 | 23 | xx = np.linspace(0, 20, n_points) 24 | yy = intercept + alpha*xx + beta_1 * \ 25 | np.maximum(xx - breakpoint_1, 0) + np.random.normal(size=n_points) 26 | 27 | pw_fit = Fit(xx, yy, start_values=[5]) 28 | 29 | pw_fit.summary() 30 | 31 | print("p-value is ", pw_fit.davies) 32 | 33 | pw_results = pw_fit.get_results() 34 | pw_estimates = pw_results["estimates"] 35 | print(pw_results) 36 | 37 | print(pw_estimates) 38 | 39 | pw_bootstrap_history = pw_fit.bootstrap_history 40 | print(pw_bootstrap_history) 41 | 42 | print(pw_fit.get_params()) 43 | 44 | # print(bp_fit.breakpoint_history) 45 | 46 | # bp_fit.plot_data() 47 | # plt.show() 48 | 49 | 50 | def on_data_1b(): 51 | 52 | alpha = -4 53 | beta_1 = -4 54 | beta_2 = 4 55 | intercept = 100 56 | breakpoint_1 = 7 57 | breakpoint_2 = 12 58 | 59 | n_points = 200 60 | 61 | xx = np.linspace(0, 20, n_points) 62 | 63 | yy = intercept + alpha*xx + beta_1 * np.maximum( 64 | xx - breakpoint_1, 0) + beta_2 * np.maximum( 65 | xx-breakpoint_2, 0) + np.random.normal(size=n_points) 66 | 67 | bp_fit = Fit(xx, yy, start_values=[5, 10]) 68 | 69 | bp_fit.summary() 70 | 71 | bp_fit.plot_best_muggeo_breakpoint_history() 72 | plt.show() 73 | 74 | bp_fit.plot_data() 75 | bp_fit.plot_fit(color="red", linewidth=4) 76 | bp_fit.plot_breakpoints() 77 | bp_fit.plot_breakpoint_confidence_intervals() 78 | plt.show() 79 | 80 | 81 | def on_data_1c(): 82 | 83 | alpha = -4 84 | beta_1 = -2 85 | beta_2 = 4 86 | beta_3 = 1 87 | intercept = 100 88 | breakpoint_1 = 7 89 | breakpoint_2 = 13 90 | breakpoint_3 = 14 91 | 92 | n_points = 200 93 | 94 | xx = np.linspace(0, 20, n_points) 95 | 96 | yy = intercept + alpha*xx 97 | yy += beta_1 * np.maximum(xx - breakpoint_1, 0) 98 | yy += beta_2 * np.maximum(xx - breakpoint_2, 0) 99 | yy += beta_3 * np.maximum(xx - breakpoint_3, 0) 100 | yy += np.random.normal(size=n_points) 101 | 102 | bp_fit = Fit(xx, yy, start_values=[5, 10, 16]) 103 | 104 | bp_fit.summary() 105 | 106 | bp_fit.plot_data() 107 | bp_fit.plot_fit(color="red", linewidth=4) 108 | bp_fit.plot_breakpoints() 109 | bp_fit.plot_breakpoint_confidence_intervals() 110 | 111 | print("The fit data: ", bp_fit.__dict__) 112 | 113 | print(bp_fit.get_params()) 114 | 115 | plt.show() 116 | plt.close() 117 | 118 | bp_fit.plot_best_muggeo_breakpoint_history() 119 | plt.legend() 120 | plt.show() 121 | plt.close() 122 | 123 | bp_fit.plot_bootstrap_restarting_history() 124 | plt.legend() 125 | plt.show() 126 | plt.close() 127 | 128 | 129 | def model_selection_1(): 130 | 131 | alpha = -4 132 | beta_1 = -2 133 | intercept = 100 134 | breakpoint_1 = 17 135 | 136 | n_points = 100 137 | 138 | xx = np.linspace(10, 30, n_points) 139 | yy = intercept + alpha*xx + beta_1 * \ 140 | np.maximum(xx - breakpoint_1, 0) + np.random.normal(size=n_points) 141 | 142 | ModelSelection(xx, yy, max_breakpoints=6) 143 | 144 | 145 | def model_selection_2(): 146 | 147 | alpha = -4 148 | beta_1 = -4 149 | beta_2 = 4 150 | intercept = 100 151 | breakpoint_1 = 7 152 | breakpoint_2 = 12 153 | 154 | n_points = 200 155 | 156 | xx = np.linspace(0, 20, n_points) 157 | 158 | yy = intercept + alpha*xx + beta_1 * np.maximum( 159 | xx - breakpoint_1, 0) + beta_2 * np.maximum( 160 | xx-breakpoint_2, 0) + np.random.normal(size=n_points) 161 | 162 | ModelSelection(xx, yy) 163 | 164 | 165 | def fit_3_check_this_makes_sense(): 166 | 167 | np.random.seed(0) 168 | 169 | alpha = 10 170 | beta_1 = -8 171 | beta_2 = -6 172 | beta_3 = 10 173 | intercept = 100 174 | breakpoint_1 = 7 175 | breakpoint_2 = 10 176 | breakpoint_3 = 14 177 | 178 | n_points = 200 179 | 180 | xx = np.linspace(0, 20, n_points) 181 | 182 | yy = intercept + alpha*xx 183 | yy += beta_1 * np.maximum(xx - breakpoint_1, 0) 184 | yy += beta_2 * np.maximum(xx - breakpoint_2, 0) 185 | yy += beta_3 * np.maximum(xx - breakpoint_3, 0) 186 | yy += np.random.normal(size=n_points) 187 | 188 | pr = Fit(xx, yy, n_breakpoints=2) 189 | pr.plot() 190 | plt.show() 191 | 192 | pr3 = Fit(xx, yy, n_breakpoints=3) 193 | pr3.plot() 194 | plt.show() 195 | 196 | pr4 = Fit(xx, yy, n_breakpoints=4) 197 | pr4.plot() 198 | plt.show() 199 | 200 | ModelSelection(xx, yy, max_breakpoints=6) 201 | 202 | 203 | def fit_with_initally_diverging(): 204 | np.random.seed(2) 205 | 206 | alpha = 10 207 | beta_1 = -8 208 | beta_2 = 3 209 | beta_3 = 10 210 | intercept = 100 211 | breakpoint_1 = 7 212 | breakpoint_2 = 10 213 | breakpoint_3 = 14 214 | 215 | n_points = 200 216 | 217 | xx = np.linspace(0, 20, n_points) 218 | 219 | yy = intercept + alpha*xx 220 | yy += beta_1 * np.maximum(xx - breakpoint_1, 0) 221 | yy += beta_2 * np.maximum(xx - breakpoint_2, 0) 222 | yy += beta_3 * np.maximum(xx - breakpoint_3, 0) 223 | yy += np.random.normal(size=n_points) 224 | 225 | pr = Fit(xx, yy, n_breakpoints=2) 226 | print(pr.summary) 227 | 228 | 229 | def fit_with_initially_diverging_start_values(): 230 | 231 | np.random.seed(0) 232 | 233 | alpha = 10 234 | beta_1 = -8 235 | beta_2 = 3 236 | beta_3 = 10 237 | intercept = 100 238 | breakpoint_1 = 7 239 | breakpoint_2 = 10 240 | breakpoint_3 = 14 241 | 242 | n_points = 200 243 | 244 | xx = np.linspace(0, 20, n_points) 245 | 246 | yy = intercept + alpha*xx 247 | yy += beta_1 * np.maximum(xx - breakpoint_1, 0) 248 | yy += beta_2 * np.maximum(xx - breakpoint_2, 0) 249 | yy += beta_3 * np.maximum(xx - breakpoint_3, 0) 250 | yy += np.random.normal(size=n_points) 251 | 252 | pr = Fit(xx, yy, start_values=[2.15646833, 0.98300926], n_boot=20) 253 | pr.summary() 254 | 255 | 256 | def fit_with_initially_diverging_start_values_b(): 257 | 258 | np.random.seed(0) 259 | 260 | alpha = 10 261 | beta_1 = -8 262 | beta_2 = 3 263 | beta_3 = 10 264 | intercept = 100 265 | breakpoint_1 = 7 266 | breakpoint_2 = 10 267 | breakpoint_3 = 14 268 | 269 | n_points = 200 270 | 271 | xx = np.linspace(0, 20, n_points) 272 | 273 | yy = intercept + alpha*xx 274 | yy += beta_1 * np.maximum(xx - breakpoint_1, 0) 275 | yy += beta_2 * np.maximum(xx - breakpoint_2, 0) 276 | yy += beta_3 * np.maximum(xx - breakpoint_3, 0) 277 | yy += np.random.normal(size=n_points) 278 | 279 | pr = Fit(xx, yy, start_values=[1.2, 0.53], n_boot=25) 280 | pr.summary() 281 | 282 | 283 | def fit_with_straight_line(): 284 | 285 | np.random.seed(1) 286 | 287 | alpha = 10 288 | intercept = 100 289 | 290 | n_points = 200 291 | 292 | xx = np.linspace(0, 20, n_points) 293 | 294 | yy = intercept + alpha*xx 295 | yy += np.random.normal(size=n_points) 296 | 297 | pr = Fit(xx, yy, n_breakpoints=1, n_boot=25) 298 | pr.summary() 299 | 300 | 301 | def model_comparision_straight_line(): 302 | 303 | np.random.seed(0) 304 | 305 | alpha = 10 306 | intercept = 100 307 | 308 | n_points = 200 309 | 310 | xx = np.linspace(0, 20, n_points) 311 | 312 | yy = intercept + alpha*xx 313 | yy += np.random.normal(size=n_points) 314 | 315 | ModelSelection(xx, yy, max_breakpoints=6) 316 | 317 | 318 | if __name__ == "__main__": 319 | 320 | on_data_1c() 321 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/tests/__init__.py -------------------------------------------------------------------------------- /tests/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/tests/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /tests/__pycache__/test_1_breakpoint.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasmani/piecewise-regression/1f58fc331f7a6e4460f8ba6da4a86aae8755d90b/tests/__pycache__/test_1_breakpoint.cpython-38.pyc -------------------------------------------------------------------------------- /tests/test_data_validation.py: -------------------------------------------------------------------------------- 1 | from piecewise_regression.data_validation import ( 2 | validate_positive_number, 3 | validate_boolean, 4 | validate_positive_integer, 5 | validate_list_of_numbers, 6 | validate_non_negative_integer 7 | ) 8 | import unittest 9 | 10 | import os 11 | import sys 12 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 13 | 14 | 15 | class TestDataValidation(unittest.TestCase): 16 | 17 | def test_positive_number(self): 18 | 19 | valid_inputs = [0.111, 3, 6770, 23.1, 7, 0.00001] 20 | 21 | invalid_inputs = [-1, 0, "hi", [1, 1], True, False, None] 22 | 23 | for valid in valid_inputs: 24 | var_return = validate_positive_number(valid, "test") 25 | self.assertEqual(var_return, valid) 26 | 27 | for invalid in invalid_inputs: 28 | test_kwargs = {"var": invalid, "var_name": "test"} 29 | self.assertRaises( 30 | ValueError, 31 | validate_positive_number, 32 | **test_kwargs) 33 | 34 | def test_boolean(self): 35 | 36 | valid_inputs = [True, False] 37 | 38 | invalid_inputs = [-1, 0, "hi", [1, 1], 22, 1.11] 39 | 40 | for valid in valid_inputs: 41 | var_return = validate_boolean(valid, "test") 42 | self.assertEqual(var_return, valid) 43 | 44 | for invalid in invalid_inputs: 45 | test_kwargs = {"var": invalid, "var_name": "test"} 46 | self.assertRaises( 47 | ValueError, 48 | validate_boolean, 49 | **test_kwargs) 50 | 51 | def test_positive_integer(self): 52 | 53 | valid_inputs = [1, 2, 31, 6543] 54 | 55 | invalid_inputs = [-1, 0, "hi", [1, 1], 1.11, True, False, None] 56 | 57 | for valid in valid_inputs: 58 | var_return = validate_positive_integer(valid, "test") 59 | self.assertEqual(var_return, valid) 60 | 61 | for invalid in invalid_inputs: 62 | test_kwargs = {"var": invalid, "var_name": "test"} 63 | self.assertRaises( 64 | ValueError, 65 | validate_positive_integer, 66 | **test_kwargs) 67 | 68 | def test_non_negative_integer(self): 69 | 70 | valid_inputs = [1, 2, 0, 31, 6543] 71 | 72 | invalid_inputs = [-1, "hi", [1, 1], 1.11, True, False, None] 73 | 74 | for valid in valid_inputs: 75 | var_return = validate_non_negative_integer(valid, "test") 76 | self.assertEqual(var_return, valid) 77 | 78 | for invalid in invalid_inputs: 79 | test_kwargs = {"var": invalid, "var_name": "test"} 80 | self.assertRaises( 81 | ValueError, 82 | validate_non_negative_integer, 83 | **test_kwargs) 84 | 85 | def test_list_of_numbers(self): 86 | 87 | valid_inputs = [ 88 | [1, 1, 1], 89 | [0.1, 43, 12], 90 | [2, 1] 91 | ] 92 | 93 | invalid_inputs = [ 94 | -1, "hi", [1, "h"], 1.11, True, False, None, 95 | [], [1]] 96 | 97 | for valid in valid_inputs: 98 | var_return = validate_list_of_numbers(valid, "test", 2) 99 | self.assertListEqual(list(var_return), list(valid)) 100 | 101 | for invalid in invalid_inputs: 102 | test_kwargs = { 103 | "var": invalid, "var_name": "test", 104 | "min_length": 2 105 | } 106 | self.assertRaises( 107 | ValueError, 108 | validate_list_of_numbers, 109 | **test_kwargs) 110 | 111 | 112 | if __name__ == '__main__': 113 | unittest.main() 114 | -------------------------------------------------------------------------------- /tests/test_fit_validation.py: -------------------------------------------------------------------------------- 1 | from piecewise_regression import Fit 2 | import copy 3 | 4 | import numpy as np 5 | import unittest 6 | 7 | import os 8 | import sys 9 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 10 | 11 | 12 | class TestValidation(unittest.TestCase): 13 | 14 | def test_with_invalid_data_types(self): 15 | 16 | xx = np.linspace(0, 10) 17 | yy = np.linspace(0, 10) 18 | 19 | KWARGS = { 20 | "xx": xx, 21 | "yy": yy, 22 | "start_values": [1, 2] 23 | } 24 | 25 | # Lots of invalid data types 26 | for test_variable in ["xx", "yy", "start_values"]: 27 | for invalid_value in [None, "hi", 12.1, 12, 0, []]: 28 | 29 | new_kwargs = copy.deepcopy(KWARGS) 30 | new_kwargs[test_variable] = invalid_value 31 | 32 | self.assertRaises(ValueError, Fit, **new_kwargs) 33 | 34 | for test_variable in [ 35 | "max_iterations", "tolerance", 36 | "min_distance_between_breakpoints", 37 | "min_distance_to_edge"]: 38 | for invalid_value in [None, "hi", [1, 1, 1, 1, 1], [], 0, -3]: 39 | 40 | new_kwargs = copy.deepcopy(KWARGS) 41 | new_kwargs[test_variable] = invalid_value 42 | 43 | self.assertRaises(ValueError, Fit, **new_kwargs) 44 | 45 | def test_without_enough_args(self): 46 | 47 | xx = np.linspace(0, 10) 48 | yy = np.linspace(0, 10) 49 | 50 | self.assertRaises(ValueError, Fit, xx, yy) 51 | self.assertRaises(TypeError, Fit, xx, start_values=[3]) 52 | self.assertRaises(TypeError, Fit, yy, start_values=[3]) 53 | 54 | 55 | if __name__ == '__main__': 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /tests/test_model_selection.py: -------------------------------------------------------------------------------- 1 | from piecewise_regression.main import Fit 2 | from piecewise_regression.model_selection import ModelSelection 3 | 4 | import numpy as np 5 | import unittest 6 | from importlib.machinery import SourceFileLoader 7 | 8 | import os 9 | import sys 10 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 11 | 12 | DATA_SOURCE = "tests/data/data.txt" 13 | 14 | 15 | class TestFit(unittest.TestCase): 16 | 17 | def test_it_works_with_differnet_options(self): 18 | """ 19 | Chekc the the n_breakpoints converged True/False boolean gives the 20 | same answer with model selection and fit 21 | """ 22 | # This is influenced by random seeds, so might not pass 23 | # with different random seeds (although it doensnt mean it isn't 24 | # working) 25 | np.random.seed(2) 26 | 27 | data = SourceFileLoader('data', DATA_SOURCE).load_module() 28 | 29 | xx = np.array(data.MUGGEO_1_XX) 30 | yy = np.array(data.MUGGEO_1_YY) 31 | 32 | ms = ModelSelection(xx, yy, n_boot=20) 33 | 34 | # For each n_breakpoints, chekc the ModelSelection vs fit results 35 | for n_breakpoints in range(1,10): 36 | fit = Fit(xx, yy, n_breakpoints=n_breakpoints, verbose=False, n_boot=20) 37 | 38 | fit_converged = fit.get_results()["converged"] 39 | ms_converged = ms.model_summaries[n_breakpoints]["converged"] 40 | print(n_breakpoints, fit_converged, ms_converged) 41 | 42 | 43 | self.assertEqual(fit_converged, ms_converged) 44 | 45 | if __name__ == '__main__': 46 | unittest.main() 47 | -------------------------------------------------------------------------------- /tests/test_muggeo.py: -------------------------------------------------------------------------------- 1 | from piecewise_regression.main import Muggeo 2 | 3 | import numpy as np 4 | import unittest 5 | from importlib.machinery import SourceFileLoader 6 | 7 | import os 8 | import sys 9 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 10 | 11 | DATA_SOURCE = "tests/data/data.txt" 12 | 13 | class TestMuggeo(unittest.TestCase): 14 | 15 | def test_against_muggeo_r_package_data_1(self): 16 | """ 17 | Muggeo uses slightly different packages and methods etc, so just check 18 | values are very close, not exact 19 | Starting from Muggeo's converged breakpoint values, I am iterating 20 | once, slight change 21 | The NextBreakpoint class is very vanilla, so in this example is 22 | getting in some local minima 23 | """ 24 | 25 | data = SourceFileLoader('data', DATA_SOURCE).load_module() 26 | 27 | xx = np.array(data.MUGGEO_1_XX) 28 | yy = np.array(data.MUGGEO_1_YY) 29 | 30 | # Choose some bps values from Muggeo converged values 31 | bps = np.array([7, 13]) 32 | 33 | fit = Muggeo(xx, yy, n_breakpoints=2, start_values=bps, verbose=False) 34 | 35 | best_fit = fit.best_fit 36 | 37 | # Check statistics from breakpoints etc found by Muggeo 38 | # 1. MUggeo rss for these brreakpoints are 39 | muggeo_rss = 190.02 40 | 41 | self.assertAlmostEqual( 42 | muggeo_rss, best_fit.residual_sum_squares, places=1) 43 | 44 | muggeo_c = 100.64655 45 | muggeo_c_se = 0.23081 46 | muggeo_c_t = 436.07 47 | 48 | muggeo_alpha = -4.18526 49 | muggeo_alpha_se = 0.05583 50 | muggeo_alpha_t = -74.97 51 | 52 | muggeo_beta1 = -3.65462 53 | muggeo_beta1_se = 0.11405 54 | muggeo_beta1_t = -32.05 55 | 56 | muggeo_beta2 = 3.81336 57 | muggeo_beta2_se = 0.11405 58 | muggeo_beta2_t = 34.45 59 | 60 | muggeo_bp1 = 7.152 61 | muggeo_bp1_se = 0.101 62 | 63 | muggeo_bp2 = 12.161 64 | muggeo_bp2_se = 0.095 65 | 66 | estimates = best_fit.estimates 67 | 68 | self.assertAlmostEqual( 69 | muggeo_c, estimates["const"]["estimate"], places=1) 70 | self.assertAlmostEqual( 71 | muggeo_alpha, estimates["alpha1"]["estimate"], places=1) 72 | self.assertAlmostEqual( 73 | muggeo_beta1, estimates["beta1"]["estimate"], places=1) 74 | self.assertAlmostEqual( 75 | muggeo_beta2, estimates["beta2"]["estimate"], places=1) 76 | self.assertAlmostEqual( 77 | muggeo_bp1, estimates["breakpoint1"]["estimate"], places=1) 78 | self.assertAlmostEqual( 79 | muggeo_bp2, estimates["breakpoint2"]["estimate"], places=1) 80 | 81 | self.assertAlmostEqual(muggeo_c_se, estimates["const"]["se"], places=1) 82 | self.assertAlmostEqual( 83 | muggeo_alpha_se, estimates["alpha1"]["se"], places=1) 84 | self.assertAlmostEqual( 85 | muggeo_beta1_se, estimates["beta1"]["se"], places=1) 86 | self.assertAlmostEqual( 87 | muggeo_beta2_se, estimates["beta2"]["se"], places=1) 88 | self.assertAlmostEqual( 89 | muggeo_bp1_se, estimates["breakpoint1"]["se"], places=1) 90 | self.assertAlmostEqual( 91 | muggeo_bp2_se, estimates["breakpoint2"]["se"], places=1) 92 | 93 | print(estimates) 94 | 95 | self.assertAlmostEqual( 96 | muggeo_c_t, estimates["const"]["t_stat"], delta=1) 97 | self.assertAlmostEqual( 98 | muggeo_alpha_t, estimates["alpha1"]["t_stat"], delta=1) 99 | self.assertAlmostEqual( 100 | muggeo_beta1_t, estimates["beta1"]["t_stat"], delta=1) 101 | self.assertAlmostEqual( 102 | muggeo_beta2_t, estimates["beta2"]["t_stat"], delta=1) 103 | 104 | muggeo_r_squared = 0.9991 105 | muggeo_adj_r_squared = 0.999 106 | 107 | self.assertAlmostEqual(muggeo_r_squared, best_fit.r_squared, places=2) 108 | self.assertAlmostEqual(muggeo_adj_r_squared, 109 | best_fit.adjusted_r_squared, places=2) 110 | 111 | def test_against_muggeo_r_package_data_2(self): 112 | """ 113 | Muggeo uses slightly different packages and methods etc, so just check 114 | values are very close, not exact 115 | Starting from Muggeo's converged breakpoint values, I am iterating 116 | once, slight change 117 | The NextBreakpoint class is very vanilla, so in this example is 118 | getting in some local minima 119 | """ 120 | 121 | data = SourceFileLoader('data', DATA_SOURCE).load_module() 122 | 123 | xx = np.array(data.MUGGEO_2_XX) 124 | yy = np.array(data.MUGGEO_2_YY) 125 | 126 | # Choose some bps values from Muggeo converged values 127 | bps = np.array([1.608]) 128 | 129 | fit = Muggeo(xx, yy, n_breakpoints=1, start_values=bps, verbose=False) 130 | 131 | best_fit = fit.best_fit 132 | 133 | # Check statistics from breakpoints etc found by Muggeo 134 | # 1. MUggeo rss for these brreakpoints are 135 | muggeo_rss = 34.70127 136 | 137 | self.assertAlmostEqual( 138 | muggeo_rss, best_fit.residual_sum_squares, places=1) 139 | 140 | muggeo_c = 4.86206 141 | muggeo_c_se = 0.31020 142 | muggeo_c_t = 15.67 143 | 144 | muggeo_alpha = 0.94472 145 | muggeo_alpha_se = 0.06744 146 | muggeo_alpha_t = 14.01 147 | 148 | muggeo_beta1 = 1.95719 149 | muggeo_beta1_se = 0.11008 150 | muggeo_beta1_t = 17.78 151 | 152 | muggeo_bp1 = 1.608 153 | muggeo_bp1_se = 0.291 154 | 155 | estimates = best_fit.estimates 156 | 157 | self.assertAlmostEqual( 158 | muggeo_c, estimates["const"]["estimate"], places=1) 159 | self.assertAlmostEqual( 160 | muggeo_alpha, estimates["alpha1"]["estimate"], places=1) 161 | self.assertAlmostEqual( 162 | muggeo_beta1, estimates["beta1"]["estimate"], places=1) 163 | self.assertAlmostEqual( 164 | muggeo_bp1, estimates["breakpoint1"]["estimate"], places=1) 165 | 166 | self.assertAlmostEqual(muggeo_c_se, estimates["const"]["se"], places=1) 167 | self.assertAlmostEqual( 168 | muggeo_alpha_se, estimates["alpha1"]["se"], places=1) 169 | self.assertAlmostEqual( 170 | muggeo_beta1_se, estimates["beta1"]["se"], places=1) 171 | self.assertAlmostEqual( 172 | muggeo_bp1_se, estimates["breakpoint1"]["se"], places=1) 173 | 174 | self.assertAlmostEqual( 175 | muggeo_c_t, estimates["const"]["t_stat"], delta=1) 176 | self.assertAlmostEqual( 177 | muggeo_alpha_t, estimates["alpha1"]["t_stat"], delta=1) 178 | self.assertAlmostEqual( 179 | muggeo_beta1_t, estimates["beta1"]["t_stat"], delta=1) 180 | 181 | muggeo_r_squared = 0.9911 182 | muggeo_adj_r_squared = 0.9903 183 | 184 | self.assertAlmostEqual(muggeo_r_squared, best_fit.r_squared, places=2) 185 | self.assertAlmostEqual(muggeo_adj_r_squared, 186 | best_fit.adjusted_r_squared, places=2) 187 | 188 | def test_with_inconsistent_start_values_and_n_breakpoints(self): 189 | 190 | xx = np.linspace(0, 10) 191 | yy = np.linspace(0, 10) 192 | 193 | self.assertRaises( 194 | ValueError, Muggeo, xx, yy, 195 | start_values=[3, 6], n_breakpoints=1) 196 | 197 | if __name__ == '__main__': 198 | unittest.main() 199 | -------------------------------------------------------------------------------- /tests/test_next_breakpoint.py: -------------------------------------------------------------------------------- 1 | from piecewise_regression.main import NextBreakpoints 2 | 3 | import numpy as np 4 | import unittest 5 | from importlib.machinery import SourceFileLoader 6 | 7 | import os 8 | import sys 9 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 10 | 11 | DATA_SOURCE = "tests/data/data.txt" 12 | 13 | 14 | class TestNextBreakpoint(unittest.TestCase): 15 | 16 | def test_against_muggeo_r_package_data_1(self): 17 | """ 18 | Muggeo uses slightly different packages and methods etc, so just check 19 | values are very close, not exact 20 | Starting from Muggeo's converged breakpoint values, I am iterating 21 | once, slight change 22 | The NextBreakpoint class is very vanilla, so in this example is getting 23 | in some local minima 24 | """ 25 | 26 | data = SourceFileLoader('data', DATA_SOURCE).load_module() 27 | 28 | xx = np.array(data.MUGGEO_1_XX) 29 | yy = np.array(data.MUGGEO_1_YY) 30 | 31 | # Choose some bps values from Muggeo converged values 32 | bps = np.array([7.152, 12.161]) 33 | 34 | next_fit = NextBreakpoints(xx, yy, bps) 35 | 36 | # Check statistics from breakpoints etc found by Muggeo 37 | # 1. MUggeo rss for these brreakpoints are 38 | muggeo_rss = 190.02 39 | 40 | self.assertAlmostEqual( 41 | muggeo_rss, next_fit.residual_sum_squares, places=1) 42 | 43 | muggeo_c = 100.64655 44 | muggeo_c_se = 0.23081 45 | muggeo_c_t = 436.07 46 | 47 | muggeo_alpha = -4.18526 48 | muggeo_alpha_se = 0.05583 49 | muggeo_alpha_t = -74.97 50 | 51 | muggeo_beta1 = -3.65462 52 | muggeo_beta1_se = 0.11405 53 | muggeo_beta1_t = -32.05 54 | 55 | muggeo_beta2 = 3.81336 56 | muggeo_beta2_se = 0.11405 57 | muggeo_beta2_t = 34.45 58 | 59 | muggeo_bp1 = 7.152 60 | muggeo_bp1_se = 0.101 61 | 62 | muggeo_bp2 = 12.161 63 | muggeo_bp2_se = 0.095 64 | 65 | estimates = next_fit.estimates 66 | 67 | self.assertAlmostEqual( 68 | muggeo_c, estimates["const"]["estimate"], places=1) 69 | self.assertAlmostEqual( 70 | muggeo_alpha, estimates["alpha1"]["estimate"], places=1) 71 | self.assertAlmostEqual( 72 | muggeo_beta1, estimates["beta1"]["estimate"], places=1) 73 | self.assertAlmostEqual( 74 | muggeo_beta2, estimates["beta2"]["estimate"], places=1) 75 | self.assertAlmostEqual( 76 | muggeo_bp1, estimates["breakpoint1"]["estimate"], places=1) 77 | self.assertAlmostEqual( 78 | muggeo_bp2, estimates["breakpoint2"]["estimate"], places=1) 79 | 80 | self.assertAlmostEqual(muggeo_c_se, estimates["const"]["se"], places=1) 81 | self.assertAlmostEqual( 82 | muggeo_alpha_se, estimates["alpha1"]["se"], places=1) 83 | self.assertAlmostEqual( 84 | muggeo_beta1_se, estimates["beta1"]["se"], places=1) 85 | self.assertAlmostEqual( 86 | muggeo_beta2_se, estimates["beta2"]["se"], places=1) 87 | self.assertAlmostEqual( 88 | muggeo_bp1_se, estimates["breakpoint1"]["se"], places=1) 89 | self.assertAlmostEqual( 90 | muggeo_bp2_se, estimates["breakpoint2"]["se"], places=1) 91 | 92 | self.assertAlmostEqual( 93 | muggeo_c_t, estimates["const"]["t_stat"], delta=1) 94 | self.assertAlmostEqual( 95 | muggeo_alpha_t, estimates["alpha1"]["t_stat"], delta=1) 96 | self.assertAlmostEqual( 97 | muggeo_beta1_t, estimates["beta1"]["t_stat"], delta=1) 98 | self.assertAlmostEqual( 99 | muggeo_beta2_t, estimates["beta2"]["t_stat"], delta=1) 100 | 101 | muggeo_r_squared = 0.9991 102 | muggeo_adj_r_squared = 0.999 103 | 104 | self.assertAlmostEqual(muggeo_r_squared, next_fit.r_squared, places=2) 105 | self.assertAlmostEqual(muggeo_adj_r_squared, 106 | next_fit.adjusted_r_squared, places=2) 107 | 108 | def test_against_muggeo_r_package_data_2(self): 109 | """ 110 | Muggeo uses slightly different packages and methods etc, so just check 111 | values are very close, not exact 112 | Starting from Muggeo's converged breakpoint values, I am iterating 113 | once, slight change 114 | The NextBreakpoint class is very vanilla, so in this example is 115 | getting in some local minima 116 | """ 117 | 118 | data = SourceFileLoader('data', DATA_SOURCE).load_module() 119 | 120 | xx = np.array(data.MUGGEO_2_XX) 121 | yy = np.array(data.MUGGEO_2_YY) 122 | 123 | # Choose some bps values from Muggeo converged values 124 | bps = np.array([1.608]) 125 | 126 | next_fit = NextBreakpoints(xx, yy, bps) 127 | 128 | # Check statistics from breakpoints etc found by Muggeo 129 | # 1. MUggeo rss for these brreakpoints are 130 | muggeo_rss = 34.70127 131 | 132 | self.assertAlmostEqual( 133 | muggeo_rss, next_fit.residual_sum_squares, places=1) 134 | 135 | muggeo_c = 4.86206 136 | muggeo_c_se = 0.31020 137 | muggeo_c_t = 15.67 138 | 139 | muggeo_alpha = 0.94472 140 | muggeo_alpha_se = 0.06744 141 | muggeo_alpha_t = 14.01 142 | 143 | muggeo_beta1 = 1.95719 144 | muggeo_beta1_se = 0.11008 145 | muggeo_beta1_t = 17.78 146 | 147 | muggeo_bp1 = 1.608 148 | muggeo_bp1_se = 0.291 149 | 150 | estimates = next_fit.estimates 151 | 152 | self.assertAlmostEqual( 153 | muggeo_c, estimates["const"]["estimate"], places=1) 154 | self.assertAlmostEqual( 155 | muggeo_alpha, estimates["alpha1"]["estimate"], places=1) 156 | self.assertAlmostEqual( 157 | muggeo_beta1, estimates["beta1"]["estimate"], places=1) 158 | self.assertAlmostEqual( 159 | muggeo_bp1, estimates["breakpoint1"]["estimate"], places=1) 160 | 161 | self.assertAlmostEqual(muggeo_c_se, estimates["const"]["se"], places=1) 162 | self.assertAlmostEqual( 163 | muggeo_alpha_se, estimates["alpha1"]["se"], places=1) 164 | self.assertAlmostEqual( 165 | muggeo_beta1_se, estimates["beta1"]["se"], places=1) 166 | self.assertAlmostEqual( 167 | muggeo_bp1_se, estimates["breakpoint1"]["se"], places=1) 168 | 169 | self.assertAlmostEqual( 170 | muggeo_c_t, estimates["const"]["t_stat"], delta=1) 171 | self.assertAlmostEqual( 172 | muggeo_alpha_t, estimates["alpha1"]["t_stat"], delta=1) 173 | self.assertAlmostEqual( 174 | muggeo_beta1_t, estimates["beta1"]["t_stat"], delta=1) 175 | 176 | muggeo_r_squared = 0.9911 177 | muggeo_adj_r_squared = 0.9903 178 | 179 | self.assertAlmostEqual(muggeo_r_squared, next_fit.r_squared, places=2) 180 | self.assertAlmostEqual(muggeo_adj_r_squared, 181 | next_fit.adjusted_r_squared, places=2) 182 | 183 | 184 | if __name__ == '__main__': 185 | unittest.main() 186 | -------------------------------------------------------------------------------- /tests/test_r_squared.py: -------------------------------------------------------------------------------- 1 | import piecewise_regression.r_squared_calc as r_squared_calc 2 | import numpy as np 3 | import unittest 4 | from importlib.machinery import SourceFileLoader 5 | 6 | import os 7 | import sys 8 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 9 | 10 | DATA_SOURCE = "tests/data/data.txt" 11 | 12 | 13 | class TestRSquared(unittest.TestCase): 14 | 15 | def test_some_data(self): 16 | 17 | ff = np.linspace(0, 10) 18 | yy = np.linspace(0, 10) 19 | 20 | rss, tss, r_2, adjusted_r_2 = r_squared_calc.get_r_squared(yy, ff, 1) 21 | 22 | self.assertEqual(r_2, 1) 23 | self.assertEqual(adjusted_r_2, 1) 24 | 25 | ff = [0.1, 0.8, 0.4, -4, 6, 12, 14, 1] 26 | yy = [1, 2, 3, 4, 5, 6, 6, 6] 27 | 28 | rss, tss, r_2, adjusted_r_2 = r_squared_calc.get_r_squared(yy, ff, 1) 29 | 30 | # Value calculated from sklearn's r2_score function 31 | r_2_from_sklearn = -6.405023255813953 32 | self.assertEqual(r_2_from_sklearn, r_2) 33 | 34 | data = SourceFileLoader('data', DATA_SOURCE).load_module() 35 | 36 | ff = np.array(data.BP_1_FF) 37 | yy = np.array(data.BP_1_YY) 38 | 39 | rss, tss, r_2, adjusted_r_2 = r_squared_calc.get_r_squared(yy, ff, 6) 40 | # Value calculated from sklearn's r2_score function 41 | r_2_from_sklearn = 0.9990626123719015 42 | self.assertEqual(r_2_from_sklearn, r_2) 43 | 44 | 45 | if __name__ == '__main__': 46 | unittest.main() 47 | --------------------------------------------------------------------------------