Repository: cjekel/piecewise_linear_fit_py Branch: master Commit: 45441d8b4f18 Files: 112 Total size: 1.0 MB Directory structure: gitextract_642gn0fy/ ├── .github/ │ └── workflows/ │ ├── ci.yml │ └── cron.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.rst ├── convert_README_to_RST.sh ├── docs/ │ ├── .buildinfo │ ├── .nojekyll │ ├── _sources/ │ │ ├── about.rst.txt │ │ ├── examples.rst.txt │ │ ├── how_it_works.rst.txt │ │ ├── index.rst.txt │ │ ├── installation.rst.txt │ │ ├── license.rst.txt │ │ ├── modules.rst.txt │ │ ├── pwlf.rst.txt │ │ ├── requirements.rst.txt │ │ └── stubs/ │ │ └── pwlf.PiecewiseLinFit.rst.txt │ ├── _static/ │ │ ├── alabaster.css │ │ ├── basic.css │ │ ├── custom.css │ │ ├── doctools.js │ │ ├── documentation_options.js │ │ ├── language_data.js │ │ ├── pygments.css │ │ ├── searchtools.js │ │ └── sphinx_highlight.js │ ├── about.html │ ├── examples.html │ ├── genindex.html │ ├── how_it_works.html │ ├── index.html │ ├── installation.html │ ├── license.html │ ├── modules.html │ ├── objects.inv │ ├── pwlf.html │ ├── requirements.html │ ├── search.html │ ├── searchindex.js │ └── stubs/ │ └── pwlf.PiecewiseLinFit.html ├── doctrees/ │ ├── about.doctree │ ├── environment.pickle │ ├── examples.doctree │ ├── how_it_works.doctree │ ├── index.doctree │ ├── installation.doctree │ ├── license.doctree │ ├── modules.doctree │ ├── pwlf.doctree │ ├── requirements.doctree │ └── stubs/ │ └── pwlf.PiecewiseLinFit.doctree ├── examples/ │ ├── EGO_integer_only.ipynb │ ├── README.md │ ├── README.rst │ ├── ex_data/ │ │ └── saved_parameters.npy │ ├── experiment_with_batch_process.py.ipynb │ ├── fitForSpecifiedNumberOfLineSegments.py │ ├── fitForSpecifiedNumberOfLineSegments_passDiffEvoKeywords.py │ ├── fitForSpecifiedNumberOfLineSegments_standard_deviation.py │ ├── fitWithKnownLineSegmentLocations.py │ ├── fit_begin_and_end.py │ ├── min_length_demo.ipynb │ ├── mixed_degree.py │ ├── mixed_degree_forcing_slope.py │ ├── model_persistence.py │ ├── model_persistence_prediction.py │ ├── prediction_variance.py │ ├── prediction_variance_degree2.py │ ├── robust_regression.py │ ├── run_opt_to_find_best_number_of_line_segments.py │ ├── sineWave.py │ ├── sineWave_custom_opt_bounds.py │ ├── sineWave_degrees.py │ ├── sineWave_time_compare.py │ ├── slope_constraint_demo.ipynb │ ├── stack_overflow_example.py │ ├── standard_errrors_and_p-values.py │ ├── standard_errrors_and_p-values_non-linear.py │ ├── test0.py │ ├── test_for_model_significance.py │ ├── test_if_breaks_exact.py │ ├── tf/ │ │ ├── fit_begin_and_end.py │ │ ├── sine_benchmark_fixed_20_break_points.py │ │ ├── sine_benchmark_six_segments.py │ │ └── test_fit.py │ ├── understanding_higher_degrees/ │ │ └── polynomials_in_pwlf.ipynb │ ├── useCustomOptimizationRoutine.py │ ├── weighted_least_squares_ex.py │ └── weighted_least_squares_ex_stats.py ├── pwlf/ │ ├── __init__.py │ ├── pwlf.py │ └── version.py ├── setup.py ├── sphinx_build_script.sh ├── sphinxdocs/ │ ├── Makefile │ ├── make.bat │ └── source/ │ ├── about.rst │ ├── conf.py │ ├── examples.rst │ ├── how_it_works.rst │ ├── index.rst │ ├── installation.rst │ ├── license.rst │ ├── modules.rst │ ├── pwlf.rst │ └── requirements.rst └── tests/ ├── __init__.py └── tests.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/ci.yml ================================================ name: pwlf ci on: push: jobs: build: runs-on: ubuntu-22.04 strategy: matrix: python-version: ['3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install flake8 coverage pytest pytest-cov - name: Install pwlf run: | python -m pip install . --no-cache-dir - name: Lint with flake8 run: | flake8 pwlf flake8 tests/tests.py - name: Test with pytest run: | pytest --cov=pwlf --cov-report=xml -p no:warnings tests/tests.py - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.xml directory: ./coverage/reports/ flags: unittests env_vars: OS,PYTHON name: codecov-umbrella fail_ci_if_error: false verbose: false ================================================ FILE: .github/workflows/cron.yml ================================================ name: pwlf cron on: schedule: # Also run every 24 hours - cron: '* */24 * * *' jobs: build: runs-on: ubuntu-22.04 strategy: matrix: python-version: ['3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install flake8 coverage pytest pytest-cov - name: Install pwlf run: | python -m pip install . --no-cache-dir - name: Lint with flake8 run: | flake8 pwlf - name: Test with pytest run: | pytest --cov=pwlf --cov-report=xml -p no:warnings tests/tests.py ================================================ FILE: .gitignore ================================================ *.pyc .coverage MANIFEST /build/ setup.cfg # Setuptools distribution folder. /dist/ # Python egg metadata, regenerated from source files by setuptools. /*.egg-info # pip stuff /temp/ # vscode folder .vscode* # vim temp file *~ *.swp *.un~ # temporary documentation folder tempdocs tempdocs/* sphinxdocs/build sphinxdocs/build/* ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [2.5.2] - 2025-07-26 ### Changed - Minor typo in documentation. Thanks to [Berend-ASML](https://github.com/Berend-ASML) for the fix! ## [2.5.1] - 2025-02-23 ### Changed - Only test float128 support if numpy allows you to create float128 numbers. This should fix issues on aarch64 builds that do not support float128 or longdouble. ## [2.5.0] - 2025-02-19 ### Changed - remove pyDOE as a dependency in favor of scipy's own latin hypercube sampling. This change will effect results of the `fitfast` method, where previous versions of the `fitfast` results will no longer be reproducible with this version. ## [2.4.0] - 2024-12-31 ### Added - Added support for mixing degree fits. You may now specify `degree=[0,1,0]` to fit a constant, then linear, then constant line. These are discontinuous piecewise linear. Currently this only supports mixing degrees of 0 and 1. Thanks to [wonch002](https://github.com/wonch002) for resurrecting an old branch. ### Changed - fixed a bug where `seed=0` would net get set. Thanks to [filippobistaffa](https://github.com/filippobistaffa) from https://github.com/cjekel/piecewise_linear_fit_py/pull/118 . ## [2.3.0] - 2024-10-26 ### Changed - You can now force fits of one line segment to go through a particular point `mypwlf.fit(1, x_c, y_c)`. Thanks to [fredrikhellman](https://github.com/fredrikhellman). - Update docs to sphinx 8 (from 4) - Update ci version to `python==3.9`, `python==3.10`, `python==3.11`, and `python==3.12` ## [2.2.1] - 2022-05-07 ### Added - You can now perform `fit` and `fitfast` for one line segment! ## [2.2.0] - 2022-05-01 ### Added - Now you can specify a numpy.random.seed to use on init to get reproducible results from the `fit` and `fitfast` methods. Simply specify an integer seed number like so `pwlf.PiecewiseLinFit(x, y, seed=123)`. Note this hijacks your current random seed. By default, not random seed is specified. - Python 3.9 is now part of the ci ### Changed - Add flake8 checks to tests - Two tests were not being checked because the method did not start with `test` ## [2.1.0] - 2022-03-31 ### Changed - All instances of `linalg.inv` now use `linalg.pinv`. All APIs are still the same, but this is potentially a backwards breaking change as previous results may be different from new results. This will mainly affect standard error calculations. - Previously `calc_slopes` was called after every least squares fit in optimization routines trying to find breakpoints. This would occasionally raise a numpy `RuntimeWarning` if two breakpoints were the same, or if a breakpoint was on the boundary. Now `calc_slopes` is not called during experimental breakpoint calculation, which should no longer raise this warning for most users. Slopes will still be calculated once optimal breakpoints are found! ## [2.0.5] - 2021-12-04 ### Added - conda forge installs are now officially mentioned on readme and in documentation ## [2.0.4] - 2020-08-27 ### Deprecated - Python 2.7 will not be supported in future releases - Python 3.5 reaches [end-of-life](https://devguide.python.org/#status-of-python-branches) on 2020-09-13, and will not be supported in future releases ### Added - ```python_requires``` in setup.py ## [2.0.3] - 2020-06-26 ### Changed - version handling does not require any dependencies ### Removed - importlib requirement ## [2.0.2] - 2020-05-25 ### Changed - Fixed an encoding bug that would not let pwlf install on windows. Thanks to h-vetinari for the [PR](https://github.com/cjekel/piecewise_linear_fit_py/pull/71)! ## [2.0.1] - 2020-05-24 ### Changed - Removed setuptools for importlib single source versioning ### Added - Requirement for importlib-metadata if Python version is less than 3.8 ### Removed - Requriement for setuptools ## [2.0.0] - 2020-04-02 ### Added - Added supports for pwlf to fit to weighted data sets! Check out [this example](https://github.com/cjekel/piecewise_linear_fit_py/tree/master/examples#weighted-least-squares-fit). ### Changed - Setup.py now grabs markdown file for long description ### Removed - Tensorflow support has been removed. It hasn't been updated in a long time. If you still require this object, check out [pwlftf](https://github.com/cjekel/piecewise_linear_fit_py_tf) ## [1.1.7] - 2020-02-05 ### Changed - Minimum SciPy version is now 1.2.0 because of issues with MacOS and the old SciPy versions. See issue https://github.com/cjekel/piecewise_linear_fit_py/issues/40 and thanks to bezineb5 ! ## [1.1.6] - 2020-01-22 ### Changed - Single source version now found in setup.py instead of pwlf/VERSION see issue https://github.com/cjekel/piecewise_linear_fit_py/issues/53 - New setuptools requirement to handle new version file - Fix bug where forcing pwlf through points didn't work with higher degrees. See issue https://github.com/cjekel/piecewise_linear_fit_py/issues/54 ## [1.1.5] - 2019-11-21 ### Changed - Fix minor typo in docstring of ```calc_slopes``` - Initialized all attributes in the ```__init__``` funciton - All attributes are now documented in the ```__init__``` function. To view this docstring, use ```pwlf.PiecewiseLinFit?```. ## [1.1.4] - 2019-10-24 ### Changed - TensorFlow 2.0.0 is not (and most probably will not) be supported. DepreciationWarning is displayed when using the ```PiecewiseLinFitTF``` object. Setup.py checks for this optional requirement. Tests are run on Tensorflow<2.0.0. - TravisCi now checks Python version 3.7 in addition to 3.6, 3.5, 2.7. - TravisCi tests should now be run daily. ## [1.1.3] - 2019-09-14 ### Changed - Make .ssr stored with fit_with_break* functions ## [1.1.2] - 2019-08-19 ### Changed - Bug fix in non-linear standard error, predict was calling y instead of x. https://github.com/cjekel/piecewise_linear_fit_py/pull/46 Thanks to @tcanders ## [1.1.1] - 2019-08-18 ### Changed - Raise the correct AttributeError when a fit has not yet been performed ## [1.1.0] - 2019-06-16 ### Added - Now you can calculate standard errors for non-linear regression using the Delta method! Check out this [example](https://github.com/cjekel/piecewise_linear_fit_py/tree/master/examples#non-linear-standard-errors-and-p-values). ## [1.0.1] - 2019-06-15 ### Added - Now you can fit constants and continuous polynomials with pwlf! Just specify the keyword ```degree=``` when initializing the ```PiecewiseLinFit``` object. Note that ```degree=0``` for constants, ```degree==1``` for linear (default), ```degree==2``` for quadratics, etc. - You can manually specify the optimization bounds for each breakpoint when calling the ```fit``` functions by using the ```bounds=``` keyword. Check out the related example. ### Changed - n_parameters is now calculated based on the shape of the regression matrix - assembly of the regression matrix now considers which degree polynomial - n_segments calculated from break points... - Greatly reduce teststf.py run time ## [1.0.0] - 2019-05-16 ### Changed - Numpy matrix assembly is now ~100x times faster, which will translate to much faster fits! See this [comment](https://github.com/cjekel/piecewise_linear_fit_py/issues/20#issuecomment-492860953) about the speed up. There should no longer be any performance benefits with using the ```PiecewiseLinFitTF``` (TensorFlow) object, so the only reason to use ```PiecewiseLinFitTF``` is if you want access to TensorFlow's optimizers. ### Removed - There are no sort or order optional parameters in ```PiecewiseLinFit```. The new matrix assembly method doesn't need sorted data. This may break backwards compatibility with your code. ## [0.5.1] - 2019-05-05 ### Changed - Fixed ```PiecewiseLinFitTF``` for Python 2. ## [0.5.0] - 2019-04-15 ### Added - New ```PiecewiseLinFitTF``` class which uses TensorFlow to accelerate pwlf. This class is nearly identical to ```PiecewiseLinFit```, with the exception of the removed ```sorted_data``` options. If you have TensorFlow installed you'll be able to use ```PiecewiseLinFitTF```. If you do not have TensorFlow installed, importing pwlf will issue a warning that ```PiecewiseLinFitTF``` is not available. See this blog [post](https://jekel.me/2019/Adding-tensorflow-to-pwlf/) for more information and benchmarks. The new class includes an option to use float32 or float64 data types. - ```lapack_driver``` option to choose between the least squares backend. For more see https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.lstsq.html and http://www.netlib.org/lapack/lug/node27.html ### Changed - Now use scipy.linalg instead of numpy.linalg because scipy is always compiled with lapack - least squares fit now defaults to scipy instead of numpy ### Removed - ```rcond``` optional parameter; was not necessary with scipy.linalg ## [0.4.3] - 2019-04-02 ### Changed - You can now manually specify ```rcond``` for the numpy least squares solver. For more see https://github.com/cjekel/piecewise_linear_fit_py/issues/21 . ## [0.4.2] - 2019-03-22 ### Changed - ```assemble_regression_matrix()``` now checks if breaks is a numpy array ## [0.4.1] - 2019-03-18 ### Changed - p_values() now uses the correct degrees of freedom (Thanks to Tong Qiu for pointing this out!) - p_values() now returns a value that can be compared to alpha; previous values would have been compared to alpha/2 ## [0.4.0] - 2019-03-14 ### Added - new ```assemble_regression_matrix()``` function that returns the linear regression matrix ```A``` which can allow you to do some more complicated [fits](https://jekel.me/2019/detect-number-of-line-segments-in-pwlf/) - test function for the linear regression matrix - new ```fit_guess()``` function to perform a fit when you have an estimate of the breakpoint locations ### Changed - consolidated the assembly of the linear regression matrix to a single function (and removed the duplicate code) ## [0.3.5] - 2019-02-25 ### Changed - minor correction to r_squared docstring example ## [0.3.4] - 2019-02-06 ### Added - Uploaded paper and citation information - Added example of what happens when you have more unknowns than data ### Changed - Examples now include figures ## [0.3.3] - 2019-01-32 ### Added - Documentation and link to documentation ### Changed - Minor changes to docstrings (spelling, formatting, attribute fixes) ## [0.3.2] - 2019-01-24 ### Added - y-intercepts are now calculated when running the calc_slopes() function. The Y-intercept for each line is stored in self.intercepts. ## [0.3.1] - 2018-12-09 ### Added - p_values() function to calculate the p-value of each beta parameter (WARNING! you may only want to use this if you specify the break point locations, as this does not account for uncertainty in break point locations) ### Changed - Now changes stored in CHANGELOG.md ## [0.3.0] - 2018-12-05 ### Added - r_squared() function to calculate the coefficent of determination after a fit has been performed ### Changed - complete docstring overhaul to match the numpydoc style - fix issues where sum-of-squares of residuals returned array_like, now should always return float ### Removed - legacy piecewise_lin_fit object has been removed ## [0.2.10] - 2018-12-04 ### Changed - Minor docstring changes - Fix spelling mistakes throughout - Fix README.rst format for PyPI ## [0.0.X - 0.2.9] - from 2017-04-01 to 2018-10-03 - 2018/10/03 Add example of bare minimum model persistance to predict for new data (see examples/model_persistence_prediction.py). Bug fix in predict function for custom parameters. Add new test function to check that predict works with custom parameters. - 2018/08/11 New function which calculates the predication variance for given array of x locations. The predication variance is the squared version of the standard error (not to be confused with the standard errors of the previous change). New example prediction_variance.py shows how to use the new function. - 2018/06/16 New function which calculates the standard error for each of the model parameters (Remember model parameters are stored as my_pwlf.beta). Standard errors are calculated by calling se = my_pwlf.standard_errors() after you have performed a fit. For more information about standard errors see [this](https://en.wikipedia.org/wiki/Standard_error). Fix docstrings for all functions. - 2018/05/11 New sorted_data key which can be used to avoided sorting already ordered data. If your data is already ordered as x[0] < x[1] < ... < x[n-1], you may consider using sorted_data=True for a slight performance increase. Additionally the predict function can take the sorted_data key if the data you want to predict at is already sorted. Thanks to [V-Kh](https://github.com/V-Kh) for the idea and PR. - 2018/04/15 Now you can find piecewise linear fits that go through specified data points! Read [this post](http://jekel.me/2018/Force-piecwise-linear-fit-through-data/) for the details. - 2018/04/09 Intelligently converts your x, y, or breaks to be numpy array. - 2018/04/06 Speed! pwlf just got better and faster! A vast majority of this library has been entirely rewritten! New naming convention. The class piecewise_lin_fit() is being depreciated, now use the class PiecewiseLinFit(). See [this post](http://jekel.me/2018/Continous-piecewise-linear-regression/) for details on the new formulation. New test function that tests predict(). - 2018/03/25 Default now hides optimization results. Use disp_res=True when initializing piecewise_lin_fit to change. The multi-start fitfast() function now defaults to the minimum population of 2. - 2018/03/11 Added try/except behavior for fitWithBreaks function such that the function could be used in an optimization routine. In general when you have a singular matrix, the function will now return np.inf. - 2018/02/16 Added new fitfast() function which uses multi-start gradient optimization instead of Differential Evolution. It may be substantially faster for your application. Also it would be a good candidate if you don't need the best solution, but just a reasonable fit. Fixed bug in tests function where assert was checking bound, not SSr. New requirement, pyDOE library. New 0.1.0 Version. - 2017/11/03 add setup.py, new tests folder and test scripts, new version tracking, initialize break0 breakN in the beginning - 2017/10/31 bug fix related to the case where break points exactly equal to x data points ( as per issue https://github.com/cjekel/piecewise_linear_fit_py/issues/1 ) and added attributes .sep_data_x, .sep_data_y, .sep_predict_data_x for troubleshooting issues related to the separation of data points to their respective regions - 2017/10/20 remove determinant calculation and use try-except instead, this will offer a larger performance boost for big problems. Change library name to something more Pythonic. Add version attribute. - 2017/08/03 gradients (slopes of the line segments) now stored as piecewise_lin_fit.slopes (or myPWLF.slopes) after they have been calculated by performing a fit or predicting - 2017/04/01 initial release ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017-2022 Charles Jekel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include LICENSE include README.rst ================================================ FILE: README.md ================================================ # About A library for fitting continuous piecewise linear functions to data. Just specify the number of line segments you desire and provide the data. ![Downloads a month](https://img.shields.io/pypi/dm/pwlf.svg) ![pwlf ci](https://github.com/cjekel/piecewise_linear_fit_py/workflows/pwlf%20ci/badge.svg) [![codecov](https://codecov.io/gh/cjekel/piecewise_linear_fit_py/branch/master/graph/badge.svg?token=AgeDFEQXed)](https://codecov.io/gh/cjekel/piecewise_linear_fit_py) ![PyPI version](https://img.shields.io/pypi/v/pwlf) [![Conda](https://img.shields.io/conda/vn/conda-forge/pwlf)](https://anaconda.org/conda-forge/pwlf) Check out the [documentation](https://jekel.me/piecewise_linear_fit_py)! Read the [blog post](http://jekel.me/2017/Fit-a-piecewise-linear-function-to-data/). ![Example of a continuous piecewise linear fit to data.](https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/examplePiecewiseFit.png) ![Example of a continuous piecewise linear fit to a sine wave.](https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/sinWaveFit.png) Now you can perform segmented constant fitting and piecewise polynomials! ![Example of multiple degree fits to a sine wave.](https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/multi_degree.png) # Features For a specified number of line segments, you can determine (and predict from) the optimal continuous piecewise linear function f(x). See [this example](https://github.com/cjekel/piecewise_linear_fit_py/blob/master/examples/fitForSpecifiedNumberOfLineSegments.py). You can fit and predict a continuous piecewise linear function f(x) if you know the specific x locations where the line segments terminate. See [this example](https://github.com/cjekel/piecewise_linear_fit_py/blob/master/examples/fitWithKnownLineSegmentLocations.py). If you want to pass different keywords for the SciPy differential evolution algorithm see [this example](https://github.com/cjekel/piecewise_linear_fit_py/blob/master/examples/fitForSpecifiedNumberOfLineSegments_passDiffEvoKeywords.py). You can use a different optimization algorithm to find the optimal location for line segments by using the objective function that minimizes the sum of square of residuals. See [this example](https://github.com/cjekel/piecewise_linear_fit_py/blob/master/examples/useCustomOptimizationRoutine.py). Instead of using differential evolution, you can now use a multi-start gradient optimization with fitfast() function. You can specify the number of starting points to use. The default is 2. This means that a latin hyper cube sampling (space filling DOE) of 2 is used to run 2 L-BFGS-B optimizations. See [this example](https://github.com/cjekel/piecewise_linear_fit_py/blob/master/examples/sineWave_time_compare.py) which runs fit() function, then runs the fitfast() to compare the runtime differences! # Installation ## Python Package Index (PyPI) You can now install with pip. ``` python -m pip install pwlf ``` ## Conda If you have conda, you can also install from conda-forge. ``` conda install -c conda-forge pwlf ``` ## From source Or clone the repo ``` git clone https://github.com/cjekel/piecewise_linear_fit_py.git ``` then install with pip ``` python -m pip install ./piecewise_linear_fit_py ``` # How it works This [paper](https://github.com/cjekel/piecewise_linear_fit_py/raw/master/paper/pwlf_Jekel_Venter_v2.pdf) explains how this library works in detail. This is based on a formulation of a piecewise linear least squares fit, where the user must specify the location of break points. See [this post](http://jekel.me/2018/Continous-piecewise-linear-regression/) which goes through the derivation of a least squares regression problem if the break point locations are known. Alternatively check out [Golovchenko (2004)](http://golovchenko.org/docs/ContinuousPiecewiseLinearFit.pdf). Global optimization is used to find the best location for the user defined number of line segments. I specifically use the [differential evolution](https://docs.scipy.org/doc/scipy-0.17.0/reference/generated/scipy.optimize.differential_evolution.html) algorithm in SciPy. I default the differential evolution algorithm to be aggressive, and it is probably overkill for your problem. So feel free to pass your own differential evolution keywords to the library. See [this example](https://github.com/cjekel/piecewise_linear_fit_py/blob/master/examples/fitForSpecifiedNumberOfLineSegments_passDiffEvoKeywords.py). # Changelog All changes now stored in [CHANGELOG.md](https://github.com/cjekel/piecewise_linear_fit_py/blob/master/CHANGELOG.md) New ```weights=``` keyword allows you to perform weighted pwlf fits! Removed TensorFlow code which can now be found [here](https://github.com/cjekel/piecewise_linear_fit_py_tf). # Requirements NumPy >= 1.14.0 SciPy >= 1.8.0 # License MIT License # Citation ```bibtex @Manual{pwlf, author = {Jekel, Charles F. and Venter, Gerhard}, title = {{pwlf:} A Python Library for Fitting 1D Continuous Piecewise Linear Functions}, year = {2019}, url = {https://github.com/cjekel/piecewise_linear_fit_py} } ``` ================================================ FILE: README.rst ================================================ About ===== A library for fitting continuous piecewise linear functions to data. Just specify the number of line segments you desire and provide the data. |Downloads a month| |pwlf ci| |codecov| |PyPI version| |Conda| Check out the `documentation `__! Read the `blog post `__. .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/examplePiecewiseFit.png :alt: Example of a continuous piecewise linear fit to data. Example of a continuous piecewise linear fit to data. .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/sinWaveFit.png :alt: Example of a continuous piecewise linear fit to a sine wave. Example of a continuous piecewise linear fit to a sine wave. Now you can perform segmented constant fitting and piecewise polynomials! |Example of multiple degree fits to a sine wave.| Features ======== For a specified number of line segments, you can determine (and predict from) the optimal continuous piecewise linear function f(x). See `this example `__. You can fit and predict a continuous piecewise linear function f(x) if you know the specific x locations where the line segments terminate. See `this example `__. If you want to pass different keywords for the SciPy differential evolution algorithm see `this example `__. You can use a different optimization algorithm to find the optimal location for line segments by using the objective function that minimizes the sum of square of residuals. See `this example `__. Instead of using differential evolution, you can now use a multi-start gradient optimization with fitfast() function. You can specify the number of starting points to use. The default is 2. This means that a latin hyper cube sampling (space filling DOE) of 2 is used to run 2 L-BFGS-B optimizations. See `this example `__ which runs fit() function, then runs the fitfast() to compare the runtime differences! Installation ============ Python Package Index (PyPI) --------------------------- You can now install with pip. :: python -m pip install pwlf Conda ----- If you have conda, you can also install from conda-forge. :: conda install -c conda-forge pwlf From source ----------- Or clone the repo :: git clone https://github.com/cjekel/piecewise_linear_fit_py.git then install with pip :: python -m pip install ./piecewise_linear_fit_py How it works ============ This `paper `__ explains how this library works in detail. This is based on a formulation of a piecewise linear least squares fit, where the user must specify the location of break points. See `this post `__ which goes through the derivation of a least squares regression problem if the break point locations are known. Alternatively check out `Golovchenko (2004) `__. Global optimization is used to find the best location for the user defined number of line segments. I specifically use the `differential evolution `__ algorithm in SciPy. I default the differential evolution algorithm to be aggressive, and it is probably overkill for your problem. So feel free to pass your own differential evolution keywords to the library. See `this example `__. Changelog ========= All changes now stored in `CHANGELOG.md `__ New ``weights=`` keyword allows you to perform weighted pwlf fits! Removed TensorFlow code which can now be found `here `__. Requirements ============ NumPy >= 1.14.0 SciPy >= 1.8.0 License ======= MIT License Citation ======== .. code:: bibtex @Manual{pwlf, author = {Jekel, Charles F. and Venter, Gerhard}, title = {{pwlf:} A Python Library for Fitting 1D Continuous Piecewise Linear Functions}, year = {2019}, url = {https://github.com/cjekel/piecewise_linear_fit_py} } .. |Downloads a month| image:: https://img.shields.io/pypi/dm/pwlf.svg .. |pwlf ci| image:: https://github.com/cjekel/piecewise_linear_fit_py/workflows/pwlf%20ci/badge.svg .. |codecov| image:: https://codecov.io/gh/cjekel/piecewise_linear_fit_py/branch/master/graph/badge.svg?token=AgeDFEQXed :target: https://codecov.io/gh/cjekel/piecewise_linear_fit_py .. |PyPI version| image:: https://img.shields.io/pypi/v/pwlf .. |Conda| image:: https://img.shields.io/conda/vn/conda-forge/pwlf :target: https://anaconda.org/conda-forge/pwlf .. |Example of multiple degree fits to a sine wave.| image:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/multi_degree.png ================================================ FILE: convert_README_to_RST.sh ================================================ #!/usr/bin/env bash pandoc --from=markdown --to=rst --output=README.rst README.md ================================================ FILE: docs/.buildinfo ================================================ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. config: 24f4d9dbb6c120d783c7146d16a6ffbd tags: 645f666f9bcd5a90fca523b33c5a78b7 ================================================ FILE: docs/.nojekyll ================================================ ================================================ FILE: docs/_sources/about.rst.txt ================================================ About ============ A library for fitting continuous piecewise linear functions to data. Just specify the number of line segments you desire and provide the data. .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/examplePiecewiseFit.png :alt: Example of a continuous piecewise linear fit to data. Example of a continuous piecewise linear fit to data. All changes now stored in `CHANGELOG.md `__ Please cite pwlf in your publications if it helps your research. .. code:: bibtex @Manual{pwlf, author = {Jekel, Charles F. and Venter, Gerhard}, title = {{pwlf:} A Python Library for Fitting 1D Continuous Piecewise Linear Functions}, year = {2019}, url = {https://github.com/cjekel/piecewise_linear_fit_py} } ================================================ FILE: docs/_sources/examples.rst.txt ================================================ Examples ======== All of these examples will use the following data and imports. .. code:: python import numpy as np import matplotlib.pyplot as plt import pwlf # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) 1. `fit with known breakpoint locations <#fit-with-known-breakpoint-locations>`__ 2. `fit for specified number of line segments <#fit-for-specified-number-of-line-segments>`__ 3. `fitfast for specified number of line segments <#fitfast-for-specified-number-of-line-segments>`__ 4. `force a fit through data points <#force-a-fit-through-data-points>`__ 5. `use custom optimization routine <#use-custom-optimization-routine>`__ 6. `pass differential evolution keywords <#pass-differential-evolution-keywords>`__ 7. `find the best number of line segments <#find-the-best-number-of-line-segments>`__ 8. `model persistence <#model-persistence>`__ 9. `bad fits when you have more unknowns than data <#bad-fits-when-you-have-more-unknowns-than-data>`__ 10. `fit with a breakpoint guess <#fit-with-a-breakpoint-guess>`__ 11. `get the linear regression matrix <#get-the-linear-regression-matrix>`__ 12. `use of TensorFlow <#use-of-tensorflow>`__ 13. `fit constants or polynomials <#fit-constants-or-polynomials>`__ 14. `specify breakpoint bounds <#specify-breakpoint-bounds>`__ 15. `non-linear standard errors and p-values <#non-linear-standard-errors-and-p-values>`__ 16. `obtain the equations of fitted pwlf <#obtain-the-equations-of-fitted-pwlf>`__ 17. `weighted least squares fit <#weighted-least-squares-fit>`__ 18. `reproducible results <#reproducible results>`__ fit with known breakpoint locations ----------------------------------- You can perform a least squares fit if you know the breakpoint locations. .. code:: python # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data with the specified break points # (ie the x locations of where the line segments # will terminate) my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/fit_breaks.png :alt: fit with known breakpoint locations fit with known breakpoint locations fit for specified number of line segments ----------------------------------------- Use a global optimization to find the breakpoint locations that minimize the sum of squares error. This uses `Differential Evolution `__ from scipy. .. code:: python # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments res = my_pwlf.fit(4) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/numberoflines.png :alt: fit for specified number of line segments fit for specified number of line segments fitfast for specified number of line segments --------------------------------------------- This performs a fit for a specified number of line segments with a multi-start gradient based optimization. This should be faster than `Differential Evolution `__ for a small number of starting points. .. code:: python # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments # this performs 3 multi-start optimizations res = my_pwlf.fitfast(4, pop=3) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/fitfast.png :alt: fitfast for specified number of line segments fitfast for specified number of line segments force a fit through data points ------------------------------- Sometimes it’s necessary to force the piecewise continuous model through a particular data point, or a set of data points. The following example finds the best 4 line segments that go through two data points. .. code:: python # initialize piecewise linear fit with your x and y data myPWLF = pwlf.PiecewiseLinFit(x, y) # fit the function with four line segments # force the function to go through the data points # (0.0, 0.0) and (0.19, 0.16) # where the data points are of the form (x, y) x_c = [0.0, 0.19] y_c = [0.0, 0.2] res = myPWLF.fit(4, x_c, y_c) # predict for the determined points xHat = np.linspace(min(x), 0.19, num=10000) yHat = myPWLF.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/force.png :alt: force a fit through data points force a fit through data points use custom optimization routine ------------------------------- You can use your favorite optimization routine to find the breakpoint locations. The following example uses scipy’s `minimize `__ function. .. code:: python from scipy.optimize import minimize # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # initialize custom optimization number_of_line_segments = 3 my_pwlf.use_custom_opt(number_of_line_segments) # i have number_of_line_segments - 1 number of variables # let's guess the correct location of the two unknown variables # (the program defaults to have end segments at x0= min(x) # and xn=max(x) xGuess = np.zeros(number_of_line_segments - 1) xGuess[0] = 0.02 xGuess[1] = 0.10 res = minimize(my_pwlf.fit_with_breaks_opt, xGuess) # set up the break point locations x0 = np.zeros(number_of_line_segments + 1) x0[0] = np.min(x) x0[-1] = np.max(x) x0[1:-1] = res.x # calculate the parameters based on the optimal break point locations my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() pass differential evolution keywords ------------------------------------ You can pass keyword arguments from the ``fit`` function into scipy’s `Differential Evolution `__. .. code:: python # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments # this sets DE to have an absolute tolerance of 0.1 res = my_pwlf.fit(4, atol=0.1) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() find the best number of line segments ------------------------------------- This example uses EGO (bayesian optimization) and a penalty function to find the best number of line segments. This will require careful use of the penalty parameter ``l``. Use this template to automatically find the best number of line segments. .. code:: python from GPyOpt.methods import BayesianOptimization # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # define your objective function def my_obj(x): # define some penalty parameter l # you'll have to arbitrarily pick this # it depends upon the noise in your data, # and the value of your sum of square of residuals l = y.mean()*0.001 f = np.zeros(x.shape[0]) for i, j in enumerate(x): my_pwlf.fit(j[0]) f[i] = my_pwlf.ssr + (l*j[0]) return f # define the lower and upper bound for the number of line segments bounds = [{'name': 'var_1', 'type': 'discrete', 'domain': np.arange(2, 40)}] np.random.seed(12121) myBopt = BayesianOptimization(my_obj, domain=bounds, model_type='GP', initial_design_numdata=10, initial_design_type='latin', exact_feval=True, verbosity=True, verbosity_model=False) max_iter = 30 # perform the bayesian optimization to find the optimum number # of line segments myBopt.run_optimization(max_iter=max_iter, verbosity=True) print('\n \n Opt found \n') print('Optimum number of line segments:', myBopt.x_opt) print('Function value:', myBopt.fx_opt) myBopt.plot_acquisition() myBopt.plot_convergence() # perform the fit for the optimum my_pwlf.fit(myBopt.x_opt) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() model persistence ----------------- You can save fitted models with pickle. Alternatively see `joblib `__. .. code:: python # if you use Python 2.x you should import cPickle # import cPickle as pickle # if you use Python 3.x you can just use pickle import pickle # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data with the specified break points my_pwlf.fit_with_breaks(x0) # save the fitted model with open('my_fit.pkl', 'wb') as f: pickle.dump(my_pwlf, f, pickle.HIGHEST_PROTOCOL) # load the fitted model with open('my_fit.pkl', 'rb') as f: my_pwlf = pickle.load(f) bad fits when you have more unknowns than data ---------------------------------------------- You can get very bad fits with pwlf when you have more unknowns than data points. The following example will fit 99 line segments to the 59 data points. While this will result in an error of zero, the model will have very weird predictions within the data. You should not fit more unknowns than you have data with pwlf! .. code:: python break_locations = np.linspace(min(x), max(x), num=100) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) my_pwlf.fit_with_breaks(break_locations) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/badfit.png :alt: bad fits when you have more unknowns than data bad fits when you have more unknowns than data fit with a breakpoint guess --------------------------- In this example we see two distinct linear regions, and we believe a breakpoint occurs at 6.0. We’ll use the fit_guess() function to find the best breakpoint location starting with this guess. These fits should be much faster than the ``fit`` or ``fitfast`` function when you have a reasonable idea where the breakpoints occur. .. code:: python import numpy as np import pwlf x = np.array([4., 5., 6., 7., 8.]) y = np.array([11., 13., 16., 28.92, 42.81]) my_pwlf = pwlf.PiecewiseLinFit(x, y) breaks = my_pwlf.fit_guess([6.0]) Note specifying one breakpoint will result in two line segments. If we wanted three line segments, we’ll have to specify two breakpoints. .. code:: python breaks = my_pwlf.fit_guess([5.5, 6.0]) get the linear regression matrix -------------------------------- In some cases it may be desirable to work with the linear regression matrix directly. The following example grabs the linear regression matrix ``A`` for a specific set of breakpoints. In this case we assume that the breakpoints occur at each of the data points. Please see the `paper `__ for details about the regression matrix ``A``. .. code:: python import numpy as np import pwlf # select random seed for reproducibility np.random.seed(123) # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) ytrue = y.copy() # add noise to the data y = np.random.normal(0, 0.05, 100) + ytrue my_pwlf_en = pwlf.PiecewiseLinFit(x, y) # copy the x data to use as break points breaks = my_pwlf_en.x_data.copy() # create the linear regression matrix A A = my_pwlf_en.assemble_regression_matrix(breaks, my_pwlf_en.x_data) We can perform fits that are more complicated than a least squares fit when we have the regression matrix. The following uses the Elastic Net regularizer to perform an interesting fit with the regression matrix. .. code:: python from sklearn.linear_model import ElasticNetCV # set up the elastic net en_model = ElasticNetCV(cv=5, l1_ratio=[.1, .5, .7, .9, .95, .99, 1], fit_intercept=False, max_iter=1000000, n_jobs=-1) # fit the model using the elastic net en_model.fit(A, my_pwlf_en.y_data) # predict from the elastic net parameters xhat = np.linspace(x.min(), x.max(), 1000) yhat_en = my_pwlf_en.predict(xhat, breaks=breaks, beta=en_model.coef_) .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/sin_en_net_fit.png :alt: interesting elastic net fit interesting elastic net fit use of tensorflow ----------------- You need to install `pwlftf `__ which will have the ``PiecewiseLinFitTF`` class. For performance benchmarks (these benchmarks are outdated! and the regular pwlf may be faster in many applications) see this blog `post `__. The use of the TF class is nearly identical to the original class, however note the following exceptions. ``PiecewiseLinFitTF`` does: - not have a ``lapack_driver`` option - have an optional parameter ``dtype``, so you can choose between the float64 and float32 data types - have an optional parameter ``fast`` to switch between Cholesky decomposition (default ``fast=True``), and orthogonal decomposition (``fast=False``) .. code:: python import pwlftf as pwlf # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # initialize TF piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFitTF(x, y, dtype='float32) # fit the data with the specified break points # (ie the x locations of where the line segments # will terminate) my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) fit constants or polynomials ---------------------------- You can use pwlf to fit segmented constant models, or piecewise polynomials. The following example fits a segmented constant model, piecewise linear, and a piecewise quadratic model to a sine wave. .. code:: python # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, 100) + y # initialize piecewise linear fit with your x and y data # pwlf lets you fit continuous model for many degree polynomials # degree=0 constant # degree=1 linear (default) # degree=2 quadratic my_pwlf_0 = pwlf.PiecewiseLinFit(x, y, degree=0) my_pwlf_1 = pwlf.PiecewiseLinFit(x, y, degree=1) # default my_pwlf_2 = pwlf.PiecewiseLinFit(x, y, degree=2) # fit the data for four line segments res0 = my_pwlf_0.fitfast(5, pop=50) res1 = my_pwlf_1.fitfast(5, pop=50) res2 = my_pwlf_2.fitfast(5, pop=50) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat0 = my_pwlf_0.predict(xHat) yHat1 = my_pwlf_1.predict(xHat) yHat2 = my_pwlf_2.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o', label='Data') plt.plot(xHat, yHat0, '-', label='degree=0') plt.plot(xHat, yHat1, '--', label='degree=1') plt.plot(xHat, yHat2, ':', label='degree=2') plt.legend() plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/multi_degree.png :alt: Example of multiple degree fits to a sine wave. Example of multiple degree fits to a sine wave. specify breakpoint bounds ------------------------- You may want extra control over the search space for feasible breakpoints. One way to do this is to specify the bounds for each breakpoint location. .. code:: python # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, 100) + y # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # define custom bounds for the interior break points n_segments = 4 bounds = np.zeros((n_segments-1, 2)) # first breakpoint bounds[0, 0] = 0.0 # lower bound bounds[0, 1] = 3.5 # upper bound # second breakpoint bounds[1, 0] = 3.0 # lower bound bounds[1, 1] = 7.0 # upper bound # third breakpoint bounds[2, 0] = 6.0 # lower bound bounds[2, 1] = 10.0 # upper bound res = my_pwlf.fit(n_segments, bounds=bounds) non-linear standard errors and p-values --------------------------------------- You can calculate non-linear standard errors using the Delta method. This will calculate the standard errors of the piecewise linear parameters (intercept + slopes) and the breakpoint locations! First let us generate true piecewise linear data. .. code:: python from __future__ import print_function # generate a true piecewise linear data np.random.seed(5) n_data = 100 x = np.linspace(0, 1, num=n_data) y = np.random.random(n_data) my_pwlf_gen = pwlf.PiecewiseLinFit(x, y) true_beta = np.random.normal(size=5) true_breaks = np.array([0.0, 0.2, 0.5, 0.75, 1.0]) y = my_pwlf_gen.predict(x, beta=true_beta, breaks=true_breaks) plt.figure() plt.title('True piecewise linear data') plt.plot(x, y) plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/true_pwlf.png :alt: True piecewise linear data. True piecewise linear data. Now we can perform a fit, calculate the standard errors, and p-values. The non-linear method uses a first order taylor series expansion to linearize the non-linear regression problem. A positive step_size performs a forward difference, and a negative step_size would perform a backwards difference. .. code:: python my_pwlf = pwlf.PiecewiseLinFit(x, y) res = my_pwlf.fitfast(4, pop=100) p = my_pwlf.p_values(method='non-linear', step_size=1e-4) se = my_pwlf.se # standard errors The standard errors and p-values correspond to each model parameter. First the beta parameters (intercept + slopes) and then the breakpoints. We can assemble the parameters, and print a table of the result with the following code. .. code:: python parameters = np.concatenate((my_pwlf.beta, my_pwlf.fit_breaks[1:-1])) header = ['Parameter type', 'Parameter value', 'Standard error', 't', 'P > np.abs(t) (p-value)'] print(*header, sep=' | ') values = np.zeros((parameters.size, 5), dtype=np.object_) values[:, 1] = np.around(parameters, decimals=3) values[:, 2] = np.around(se, decimals=3) values[:, 3] = np.around(parameters / se, decimals=3) values[:, 4] = np.around(p, decimals=3) for i, row in enumerate(values): if i < my_pwlf.beta.size: row[0] = 'Beta' print(*row, sep=' | ') else: row[0] = 'Breakpoint' print(*row, sep=' | ') ============== =============== ============== ============== ======================= Parameter type Parameter value Standard error t P > np.abs(t) (p-value) ============== =============== ============== ============== ======================= Beta 1.821 0.0 1763191476.046 0.0 Beta -0.427 0.0 -46404554.493 0.0 Beta -1.165 0.0 -111181494.162 0.0 Beta -1.397 0.0 -168954500.421 0.0 Beta 0.873 0.0 93753841.242 0.0 Breakpoint 0.2 0.0 166901856.885 0.0 Breakpoint 0.5 0.0 537785803.646 0.0 Breakpoint 0.75 0.0 482311769.159 0.0 ============== =============== ============== ============== ======================= obtain the equations of fitted pwlf ----------------------------------- Sometimes you may want the mathematical equations that represent your fitted model. This is easy to perform if you don’t mind using sympy. The following code will fit 5 line segments of degree=2 to a sin wave. .. code:: python import numpy as np import pwlf # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, 100) + y my_pwlf_2 = pwlf.PiecewiseLinFit(x, y, degree=2) res2 = my_pwlf_2.fitfast(5, pop=50) Given this fit, the following code will print the mathematical equation for each line segment. .. code:: python from sympy import Symbol from sympy.utilities import lambdify x = Symbol('x') def get_symbolic_eqn(pwlf_, segment_number): if pwlf_.degree < 1: raise ValueError('Degree must be at least 1') if segment_number < 1 or segment_number > pwlf_.n_segments: raise ValueError('segment_number not possible') # assemble degree = 1 first for line in range(segment_number): if line == 0: my_eqn = pwlf_.beta[0] + (pwlf_.beta[1])*(x-pwlf_.fit_breaks[0]) else: my_eqn += (pwlf_.beta[line+1])*(x-pwlf_.fit_breaks[line]) # assemble all other degrees if pwlf_.degree > 1: for k in range(2, pwlf_.degree + 1): for line in range(segment_number): beta_index = pwlf_.n_segments*(k-1) + line + 1 my_eqn += (pwlf_.beta[beta_index])*(x-pwlf_.fit_breaks[line])**k return my_eqn.simplify() eqn_list = [] f_list = [] for i in range(my_pwlf_2.n_segments): eqn_list.append(get_symbolic_eqn(my_pwlf_2, i + 1)) print('Equation number: ', i + 1) print(eqn_list[-1]) f_list.append(lambdify(x, eqn_list[-1])) which should print out something like the following: .. code:: python Equation number: 1 -0.953964059782599*x**2 + 1.89945177490653*x + 0.00538634182565454 Equation number: 2 0.951561315686298*x**2 - 5.69747505830914*x + 7.5772216545711 Equation number: 3 -0.949735350431857*x**2 + 9.48218236957122*x - 22.720785454735 Equation number: 4 0.926850298824217*x**2 - 12.9824424358344*x + 44.5102742956827 Equation number: 5 -1.03016230425747*x**2 + 18.5306546317065*x - 82.3508513333073 For more information on how this works, see `this `__ jupyter notebook. weighted least squares fit -------------------------- Sometimes your data will not have a constant variance (heteroscedasticity), and you need to perform a weighted least squares fit. The following example will perform a standard and weighted fit so you can compare the differences. First we need to generate a data set which will be a good candidate to use for weighted least squares fits. .. code:: python # generate data with heteroscedasticity n = 100 n_data_sets = 100 n_segments = 6 # generate sine data x = np.linspace(0, 10, n) y = np.zeros((n_data_sets, n)) sigma_change = np.linspace(0.001, 0.05, 100) for i in range(n_data_sets): y[i] = np.sin(x * np.pi / 2) # add noise to the data y[i] = np.random.normal(0, sigma_change, 100) + y[i] X = np.tile(x, n_data_sets) The individual weights in pwlf are the reciprocal of the standard deviation for each data point. Here weights[i] corresponds to one over the standard deviation of the ith data point. The result of this is that data points with higher variance are less important to the overall pwlf than data point with small variance. Let’s perform a standard pwlf fit and a weighted fit. .. code:: python # perform an ordinary pwlf fit to the entire data my_pwlf = pwlf.PiecewiseLinFit(X.flatten(), y.flatten()) my_pwlf.fit(n_segments) # compute the standard deviation in y y_std = np.std(y, axis=0) # set the weights to be one over the standard deviation weights = 1.0 / y_std # perform a weighted least squares to the data my_pwlf_w = pwlf.PiecewiseLinFit(x, y.mean(axis=0), weights=weights) my_pwlf_w.fit(n_segments) # compare the fits xhat = np.linspace(0, 10, 1000) yhat = my_pwlf.predict(xhat) yhat_w = my_pwlf_w.predict(xhat) plt.figure() plt.plot(X.flatten(), y.flatten(), '.') plt.plot(xhat, yhat, '-', label='Ordinary LS') plt.plot(xhat, yhat_w, '-', label='Weighted LS') plt.legend() plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/weighted_least_squares_example.png :alt: Weighted pwlf fit. Weighted pwlf fit. We can see that the weighted pwlf fit tries fit data with low variance better than data with high variance, however the ordinary pwlf fits the data assuming a uniform variance. reproducible results -------------------- The `fit` and `fitfast` methods are stochastic and may not give the same result every time the program is run. To have reproducible results you can manually specify a numpy.random.seed on init. Now everytime the following program is run, the results of the fit(2) should be the same. .. code:: python # initialize piecewise linear fit with a random seed my_pwlf = pwlf.PiecewiseLinFit(x, y, seed=123) # Now the fit() method will be reproducible my_pwlf.fit(2) ================================================ FILE: docs/_sources/how_it_works.rst.txt ================================================ How it works ============ This `paper `__ explains how this library works in detail. This is based on a formulation of a piecewise linear least squares fit, where the user must specify the location of break points. See `this post `__ which goes through the derivation of a least squares regression problem if the break point locations are known. Alternatively check out `Golovchenko (2004) `__. Global optimization is used to find the best location for the user defined number of line segments. I specifically use the `differential evolution `__ algorithm in SciPy. I default the differential evolution algorithm to be aggressive, and it is probably overkill for your problem. So feel free to pass your own differential evolution keywords to the library. See `this example `__. ================================================ FILE: docs/_sources/index.rst.txt ================================================ pwlf: piecewise linear fitting ================================ Fit piecewise linear functions to data! .. toctree:: :maxdepth: 2 installation how_it_works examples pwlf about requirements license Indices and tables ================== * :ref:`genindex` * :ref:`search` ================================================ FILE: docs/_sources/installation.rst.txt ================================================ Installation ============ Python Package Index (PyPI) --------------------------- You can now install with pip. :: python -m pip install pwlf Conda ----- If you have conda, you can also install from conda-forge. :: conda install -c conda-forge pwlf From source ----------- Or clone the repo :: git clone https://github.com/cjekel/piecewise_linear_fit_py.git then install with pip :: python -m pip install ./piecewise_linear_fit_py ================================================ FILE: docs/_sources/license.rst.txt ================================================ License ======= MIT License Copyright (c) 2017-2020 Charles Jekel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: docs/_sources/modules.rst.txt ================================================ pwlf ==== .. toctree:: :maxdepth: 4 pwlf ================================================ FILE: docs/_sources/pwlf.rst.txt ================================================ pwlf package contents ============ .. autosummary:: :toctree: stubs pwlf.PiecewiseLinFit .. autoclass:: pwlf.PiecewiseLinFit :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/_sources/requirements.rst.txt ================================================ Requirements ============ `NumPy `__ (>= 1.14.0) `SciPy `__ (>= 1.2.0) `pyDOE `__ ( >= 0.3.8) ================================================ FILE: docs/_sources/stubs/pwlf.PiecewiseLinFit.rst.txt ================================================ pwlf.PiecewiseLinFit ==================== .. currentmodule:: pwlf .. autoclass:: PiecewiseLinFit .. automethod:: __init__ .. rubric:: Methods .. autosummary:: ~PiecewiseLinFit.__init__ ~PiecewiseLinFit.assemble_regression_matrix ~PiecewiseLinFit.calc_slopes ~PiecewiseLinFit.conlstsq ~PiecewiseLinFit.fit ~PiecewiseLinFit.fit_force_points_opt ~PiecewiseLinFit.fit_guess ~PiecewiseLinFit.fit_with_breaks ~PiecewiseLinFit.fit_with_breaks_force_points ~PiecewiseLinFit.fit_with_breaks_opt ~PiecewiseLinFit.fitfast ~PiecewiseLinFit.lstsq ~PiecewiseLinFit.p_values ~PiecewiseLinFit.predict ~PiecewiseLinFit.prediction_variance ~PiecewiseLinFit.r_squared ~PiecewiseLinFit.standard_errors ~PiecewiseLinFit.use_custom_opt ================================================ FILE: docs/_static/alabaster.css ================================================ /* -- page layout ----------------------------------------------------------- */ body { font-family: Georgia, serif; font-size: 17px; background-color: #fff; color: #000; margin: 0; padding: 0; } div.document { width: 940px; margin: 30px auto 0 auto; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 0 0 0 220px; } div.sphinxsidebar { width: 220px; font-size: 14px; line-height: 1.5; } hr { border: 1px solid #B1B4B6; } div.body { background-color: #fff; color: #3E4349; padding: 0 30px 0 30px; } div.body > .section { text-align: left; } div.footer { width: 940px; margin: 20px auto 30px auto; font-size: 14px; color: #888; text-align: right; } div.footer a { color: #888; } p.caption { font-family: inherit; font-size: inherit; } div.relations { display: none; } div.sphinxsidebar { max-height: 100%; overflow-y: auto; } div.sphinxsidebar a { color: #444; text-decoration: none; border-bottom: 1px dotted #999; } div.sphinxsidebar a:hover { border-bottom: 1px solid #999; } div.sphinxsidebarwrapper { padding: 18px 10px; } div.sphinxsidebarwrapper p.logo { padding: 0; margin: -10px 0 0 0px; text-align: center; } div.sphinxsidebarwrapper h1.logo { margin-top: -10px; text-align: center; margin-bottom: 5px; text-align: left; } div.sphinxsidebarwrapper h1.logo-name { margin-top: 0px; } div.sphinxsidebarwrapper p.blurb { margin-top: 0; font-style: normal; } div.sphinxsidebar h3, div.sphinxsidebar h4 { font-family: Georgia, serif; color: #444; font-size: 24px; font-weight: normal; margin: 0 0 5px 0; padding: 0; } div.sphinxsidebar h4 { font-size: 20px; } div.sphinxsidebar h3 a { color: #444; } div.sphinxsidebar p.logo a, div.sphinxsidebar h3 a, div.sphinxsidebar p.logo a:hover, div.sphinxsidebar h3 a:hover { border: none; } div.sphinxsidebar p { color: #555; margin: 10px 0; } div.sphinxsidebar ul { margin: 10px 0; padding: 0; color: #000; } div.sphinxsidebar ul li.toctree-l1 > a { font-size: 120%; } div.sphinxsidebar ul li.toctree-l2 > a { font-size: 110%; } div.sphinxsidebar input { border: 1px solid #CCC; font-family: Georgia, serif; font-size: 1em; } div.sphinxsidebar #searchbox { margin: 1em 0; } div.sphinxsidebar .search > div { display: table-cell; } div.sphinxsidebar hr { border: none; height: 1px; color: #AAA; background: #AAA; text-align: left; margin-left: 0; width: 50%; } div.sphinxsidebar .badge { border-bottom: none; } div.sphinxsidebar .badge:hover { border-bottom: none; } /* To address an issue with donation coming after search */ div.sphinxsidebar h3.donation { margin-top: 10px; } /* -- body styles ----------------------------------------------------------- */ a { color: #004B6B; text-decoration: underline; } a:hover { color: #6D4100; text-decoration: underline; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: Georgia, serif; font-weight: normal; margin: 30px 0px 10px 0px; padding: 0; } div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } div.body h2 { font-size: 180%; } div.body h3 { font-size: 150%; } div.body h4 { font-size: 130%; } div.body h5 { font-size: 100%; } div.body h6 { font-size: 100%; } a.headerlink { color: #DDD; padding: 0 4px; text-decoration: none; } a.headerlink:hover { color: #444; background: #EAEAEA; } div.body p, div.body dd, div.body li { line-height: 1.4em; } div.admonition { margin: 20px 0px; padding: 10px 30px; background-color: #EEE; border: 1px solid #CCC; } div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { background-color: #FBFBFB; border-bottom: 1px solid #fafafa; } div.admonition p.admonition-title { font-family: Georgia, serif; font-weight: normal; font-size: 24px; margin: 0 0 10px 0; padding: 0; line-height: 1; } div.admonition p.last { margin-bottom: 0; } dt:target, .highlight { background: #FAF3E8; } div.warning { background-color: #FCC; border: 1px solid #FAA; } div.danger { background-color: #FCC; border: 1px solid #FAA; -moz-box-shadow: 2px 2px 4px #D52C2C; -webkit-box-shadow: 2px 2px 4px #D52C2C; box-shadow: 2px 2px 4px #D52C2C; } div.error { background-color: #FCC; border: 1px solid #FAA; -moz-box-shadow: 2px 2px 4px #D52C2C; -webkit-box-shadow: 2px 2px 4px #D52C2C; box-shadow: 2px 2px 4px #D52C2C; } div.caution { background-color: #FCC; border: 1px solid #FAA; } div.attention { background-color: #FCC; border: 1px solid #FAA; } div.important { background-color: #EEE; border: 1px solid #CCC; } div.note { background-color: #EEE; border: 1px solid #CCC; } div.tip { background-color: #EEE; border: 1px solid #CCC; } div.hint { background-color: #EEE; border: 1px solid #CCC; } div.seealso { background-color: #EEE; border: 1px solid #CCC; } div.topic { background-color: #EEE; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre, tt, code { font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.9em; } .hll { background-color: #FFC; margin: 0 -12px; padding: 0 12px; display: block; } img.screenshot { } tt.descname, tt.descclassname, code.descname, code.descclassname { font-size: 0.95em; } tt.descname, code.descname { padding-right: 0.08em; } img.screenshot { -moz-box-shadow: 2px 2px 4px #EEE; -webkit-box-shadow: 2px 2px 4px #EEE; box-shadow: 2px 2px 4px #EEE; } table.docutils { border: 1px solid #888; -moz-box-shadow: 2px 2px 4px #EEE; -webkit-box-shadow: 2px 2px 4px #EEE; box-shadow: 2px 2px 4px #EEE; } table.docutils td, table.docutils th { border: 1px solid #888; padding: 0.25em 0.7em; } table.field-list, table.footnote { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } table.footnote { margin: 15px 0; width: 100%; border: 1px solid #EEE; background: #FDFDFD; font-size: 0.9em; } table.footnote + table.footnote { margin-top: -15px; border-top: none; } table.field-list th { padding: 0 0.8em 0 0; } table.field-list td { padding: 0; } table.field-list p { margin-bottom: 0.8em; } /* Cloned from * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 */ .field-name { -moz-hyphens: manual; -ms-hyphens: manual; -webkit-hyphens: manual; hyphens: manual; } table.footnote td.label { width: .1px; padding: 0.3em 0 0.3em 0.5em; } table.footnote td { padding: 0.3em 0.5em; } dl { margin-left: 0; margin-right: 0; margin-top: 0; padding: 0; } dl dd { margin-left: 30px; } blockquote { margin: 0 0 0 30px; padding: 0; } ul, ol { /* Matches the 30px from the narrow-screen "li > ul" selector below */ margin: 10px 0 10px 30px; padding: 0; } pre { background: unset; padding: 7px 30px; margin: 15px 0px; line-height: 1.3em; } div.viewcode-block:target { background: #ffd; } dl pre, blockquote pre, li pre { margin-left: 0; padding-left: 30px; } tt, code { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ } tt.xref, code.xref, a tt { background-color: #FBFBFB; border-bottom: 1px solid #fff; } a.reference { text-decoration: none; border-bottom: 1px dotted #004B6B; } a.reference:hover { border-bottom: 1px solid #6D4100; } /* Don't put an underline on images */ a.image-reference, a.image-reference:hover { border-bottom: none; } a.footnote-reference { text-decoration: none; font-size: 0.7em; vertical-align: top; border-bottom: 1px dotted #004B6B; } a.footnote-reference:hover { border-bottom: 1px solid #6D4100; } a:hover tt, a:hover code { background: #EEE; } @media screen and (max-width: 940px) { body { margin: 0; padding: 20px 30px; } div.documentwrapper { float: none; background: #fff; margin-left: 0; margin-top: 0; margin-right: 0; margin-bottom: 0; } div.sphinxsidebar { display: block; float: none; width: unset; margin: 50px -30px -20px -30px; padding: 10px 20px; background: #333; color: #FFF; } div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, div.sphinxsidebar h3 a { color: #fff; } div.sphinxsidebar a { color: #AAA; } div.sphinxsidebar p.logo { display: none; } div.document { width: 100%; margin: 0; } div.footer { display: none; } div.bodywrapper { margin: 0; } div.body { min-height: 0; min-width: auto; /* fixes width on small screens, breaks .hll */ padding: 0; } .hll { /* "fixes" the breakage */ width: max-content; } .rtd_doc_footer { display: none; } .document { width: auto; } .footer { width: auto; } .github { display: none; } ul { margin-left: 0; } li > ul { /* Matches the 30px from the "ul, ol" selector above */ margin-left: 30px; } } /* misc. */ .revsys-inline { display: none!important; } /* Hide ugly table cell borders in ..bibliography:: directive output */ table.docutils.citation, table.docutils.citation td, table.docutils.citation th { border: none; /* Below needed in some edge cases; if not applied, bottom shadows appear */ -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } /* relbar */ .related { line-height: 30px; width: 100%; font-size: 0.9rem; } .related.top { border-bottom: 1px solid #EEE; margin-bottom: 20px; } .related.bottom { border-top: 1px solid #EEE; } .related ul { padding: 0; margin: 0; list-style: none; } .related li { display: inline; } nav#rellinks { float: right; } nav#rellinks li+li:before { content: "|"; } nav#breadcrumbs li+li:before { content: "\00BB"; } /* Hide certain items when printing */ @media print { div.related { display: none; } } img.github { position: absolute; top: 0; border: 0; right: 0; } ================================================ FILE: docs/_static/basic.css ================================================ /* * Sphinx stylesheet -- basic theme. */ /* -- main layout ----------------------------------------------------------- */ div.clearer { clear: both; } div.section::after { display: block; content: ''; clear: left; } /* -- relbar ---------------------------------------------------------------- */ div.related { width: 100%; font-size: 90%; } div.related h3 { display: none; } div.related ul { margin: 0; padding: 0 0 0 10px; list-style: none; } div.related li { display: inline; } div.related li.right { float: right; margin-right: 5px; } /* -- sidebar --------------------------------------------------------------- */ div.sphinxsidebarwrapper { padding: 10px 5px 0 10px; } div.sphinxsidebar { float: left; width: 230px; margin-left: -100%; font-size: 90%; word-wrap: break-word; overflow-wrap : break-word; } div.sphinxsidebar ul { list-style: none; } div.sphinxsidebar ul ul, div.sphinxsidebar ul.want-points { margin-left: 20px; list-style: square; } div.sphinxsidebar ul ul { margin-top: 0; margin-bottom: 0; } div.sphinxsidebar form { margin-top: 10px; } div.sphinxsidebar input { border: 1px solid #98dbcc; font-family: sans-serif; font-size: 1em; } div.sphinxsidebar #searchbox form.search { overflow: hidden; } div.sphinxsidebar #searchbox input[type="text"] { float: left; width: 80%; padding: 0.25em; box-sizing: border-box; } div.sphinxsidebar #searchbox input[type="submit"] { float: left; width: 20%; border-left: none; padding: 0.25em; box-sizing: border-box; } img { border: 0; max-width: 100%; } /* -- search page ----------------------------------------------------------- */ ul.search { margin-top: 10px; } ul.search li { padding: 5px 0; } ul.search li a { font-weight: bold; } ul.search li p.context { color: #888; margin: 2px 0 0 30px; text-align: left; } ul.keywordmatches li.goodmatch a { font-weight: bold; } /* -- index page ------------------------------------------------------------ */ table.contentstable { width: 90%; margin-left: auto; margin-right: auto; } table.contentstable p.biglink { line-height: 150%; } a.biglink { font-size: 1.3em; } span.linkdescr { font-style: italic; padding-top: 5px; font-size: 90%; } /* -- general index --------------------------------------------------------- */ table.indextable { width: 100%; } table.indextable td { text-align: left; vertical-align: top; } table.indextable ul { margin-top: 0; margin-bottom: 0; list-style-type: none; } table.indextable > tbody > tr > td > ul { padding-left: 0em; } table.indextable tr.pcap { height: 10px; } table.indextable tr.cap { margin-top: 10px; background-color: #f2f2f2; } img.toggler { margin-right: 3px; margin-top: 3px; cursor: pointer; } div.modindex-jumpbox { border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; margin: 1em 0 1em 0; padding: 0.4em; } div.genindex-jumpbox { border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; margin: 1em 0 1em 0; padding: 0.4em; } /* -- domain module index --------------------------------------------------- */ table.modindextable td { padding: 2px; border-collapse: collapse; } /* -- general body styles --------------------------------------------------- */ div.body { min-width: inherit; max-width: 800px; } div.body p, div.body dd, div.body li, div.body blockquote { -moz-hyphens: auto; -ms-hyphens: auto; -webkit-hyphens: auto; hyphens: auto; } a.headerlink { visibility: hidden; } a:visited { color: #551A8B; } h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, h4:hover > a.headerlink, h5:hover > a.headerlink, h6:hover > a.headerlink, dt:hover > a.headerlink, caption:hover > a.headerlink, p.caption:hover > a.headerlink, div.code-block-caption:hover > a.headerlink { visibility: visible; } div.body p.caption { text-align: inherit; } div.body td { text-align: left; } .first { margin-top: 0 !important; } p.rubric { margin-top: 30px; font-weight: bold; } img.align-left, figure.align-left, .figure.align-left, object.align-left { clear: left; float: left; margin-right: 1em; } img.align-right, figure.align-right, .figure.align-right, object.align-right { clear: right; float: right; margin-left: 1em; } img.align-center, figure.align-center, .figure.align-center, object.align-center { display: block; margin-left: auto; margin-right: auto; } img.align-default, figure.align-default, .figure.align-default { display: block; margin-left: auto; margin-right: auto; } .align-left { text-align: left; } .align-center { text-align: center; } .align-default { text-align: center; } .align-right { text-align: right; } /* -- sidebars -------------------------------------------------------------- */ div.sidebar, aside.sidebar { margin: 0 0 0.5em 1em; border: 1px solid #ddb; padding: 7px; background-color: #ffe; width: 40%; float: right; clear: right; overflow-x: auto; } p.sidebar-title { font-weight: bold; } nav.contents, aside.topic, div.admonition, div.topic, blockquote { clear: left; } /* -- topics ---------------------------------------------------------------- */ nav.contents, aside.topic, div.topic { border: 1px solid #ccc; padding: 7px; margin: 10px 0 10px 0; } p.topic-title { font-size: 1.1em; font-weight: bold; margin-top: 10px; } /* -- admonitions ----------------------------------------------------------- */ div.admonition { margin-top: 10px; margin-bottom: 10px; padding: 7px; } div.admonition dt { font-weight: bold; } p.admonition-title { margin: 0px 10px 5px 0px; font-weight: bold; } div.body p.centered { text-align: center; margin-top: 25px; } /* -- content of sidebars/topics/admonitions -------------------------------- */ div.sidebar > :last-child, aside.sidebar > :last-child, nav.contents > :last-child, aside.topic > :last-child, div.topic > :last-child, div.admonition > :last-child { margin-bottom: 0; } div.sidebar::after, aside.sidebar::after, nav.contents::after, aside.topic::after, div.topic::after, div.admonition::after, blockquote::after { display: block; content: ''; clear: both; } /* -- tables ---------------------------------------------------------------- */ table.docutils { margin-top: 10px; margin-bottom: 10px; border: 0; border-collapse: collapse; } table.align-center { margin-left: auto; margin-right: auto; } table.align-default { margin-left: auto; margin-right: auto; } table caption span.caption-number { font-style: italic; } table caption span.caption-text { } table.docutils td, table.docutils th { padding: 1px 8px 1px 5px; border-top: 0; border-left: 0; border-right: 0; border-bottom: 1px solid #aaa; } th { text-align: left; padding-right: 5px; } table.citation { border-left: solid 1px gray; margin-left: 1px; } table.citation td { border-bottom: none; } th > :first-child, td > :first-child { margin-top: 0px; } th > :last-child, td > :last-child { margin-bottom: 0px; } /* -- figures --------------------------------------------------------------- */ div.figure, figure { margin: 0.5em; padding: 0.5em; } div.figure p.caption, figcaption { padding: 0.3em; } div.figure p.caption span.caption-number, figcaption span.caption-number { font-style: italic; } div.figure p.caption span.caption-text, figcaption span.caption-text { } /* -- field list styles ----------------------------------------------------- */ table.field-list td, table.field-list th { border: 0 !important; } .field-list ul { margin: 0; padding-left: 1em; } .field-list p { margin: 0; } .field-name { -moz-hyphens: manual; -ms-hyphens: manual; -webkit-hyphens: manual; hyphens: manual; } /* -- hlist styles ---------------------------------------------------------- */ table.hlist { margin: 1em 0; } table.hlist td { vertical-align: top; } /* -- object description styles --------------------------------------------- */ .sig { font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; } .sig-name, code.descname { background-color: transparent; font-weight: bold; } .sig-name { font-size: 1.1em; } code.descname { font-size: 1.2em; } .sig-prename, code.descclassname { background-color: transparent; } .optional { font-size: 1.3em; } .sig-paren { font-size: larger; } .sig-param.n { font-style: italic; } /* C++ specific styling */ .sig-inline.c-texpr, .sig-inline.cpp-texpr { font-family: unset; } .sig.c .k, .sig.c .kt, .sig.cpp .k, .sig.cpp .kt { color: #0033B3; } .sig.c .m, .sig.cpp .m { color: #1750EB; } .sig.c .s, .sig.c .sc, .sig.cpp .s, .sig.cpp .sc { color: #067D17; } /* -- other body styles ----------------------------------------------------- */ ol.arabic { list-style: decimal; } ol.loweralpha { list-style: lower-alpha; } ol.upperalpha { list-style: upper-alpha; } ol.lowerroman { list-style: lower-roman; } ol.upperroman { list-style: upper-roman; } :not(li) > ol > li:first-child > :first-child, :not(li) > ul > li:first-child > :first-child { margin-top: 0px; } :not(li) > ol > li:last-child > :last-child, :not(li) > ul > li:last-child > :last-child { margin-bottom: 0px; } ol.simple ol p, ol.simple ul p, ul.simple ol p, ul.simple ul p { margin-top: 0; } ol.simple > li:not(:first-child) > p, ul.simple > li:not(:first-child) > p { margin-top: 0; } ol.simple p, ul.simple p { margin-bottom: 0; } aside.footnote > span, div.citation > span { float: left; } aside.footnote > span:last-of-type, div.citation > span:last-of-type { padding-right: 0.5em; } aside.footnote > p { margin-left: 2em; } div.citation > p { margin-left: 4em; } aside.footnote > p:last-of-type, div.citation > p:last-of-type { margin-bottom: 0em; } aside.footnote > p:last-of-type:after, div.citation > p:last-of-type:after { content: ""; clear: both; } dl.field-list { display: grid; grid-template-columns: fit-content(30%) auto; } dl.field-list > dt { font-weight: bold; word-break: break-word; padding-left: 0.5em; padding-right: 5px; } dl.field-list > dd { padding-left: 0.5em; margin-top: 0em; margin-left: 0em; margin-bottom: 0em; } dl { margin-bottom: 15px; } dd > :first-child { margin-top: 0px; } dd ul, dd table { margin-bottom: 10px; } dd { margin-top: 3px; margin-bottom: 10px; margin-left: 30px; } .sig dd { margin-top: 0px; margin-bottom: 0px; } .sig dl { margin-top: 0px; margin-bottom: 0px; } dl > dd:last-child, dl > dd:last-child > :last-child { margin-bottom: 0; } dt:target, span.highlighted { background-color: #fbe54e; } rect.highlighted { fill: #fbe54e; } dl.glossary dt { font-weight: bold; font-size: 1.1em; } .versionmodified { font-style: italic; } .system-message { background-color: #fda; padding: 5px; border: 3px solid red; } .footnote:target { background-color: #ffa; } .line-block { display: block; margin-top: 1em; margin-bottom: 1em; } .line-block .line-block { margin-top: 0; margin-bottom: 0; margin-left: 1.5em; } .guilabel, .menuselection { font-family: sans-serif; } .accelerator { text-decoration: underline; } .classifier { font-style: oblique; } .classifier:before { font-style: normal; margin: 0 0.5em; content: ":"; display: inline-block; } abbr, acronym { border-bottom: dotted 1px; cursor: help; } .translated { background-color: rgba(207, 255, 207, 0.2) } .untranslated { background-color: rgba(255, 207, 207, 0.2) } /* -- code displays --------------------------------------------------------- */ pre { overflow: auto; overflow-y: hidden; /* fixes display issues on Chrome browsers */ } pre, div[class*="highlight-"] { clear: both; } span.pre { -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; white-space: nowrap; } div[class*="highlight-"] { margin: 1em 0; } td.linenos pre { border: 0; background-color: transparent; color: #aaa; } table.highlighttable { display: block; } table.highlighttable tbody { display: block; } table.highlighttable tr { display: flex; } table.highlighttable td { margin: 0; padding: 0; } table.highlighttable td.linenos { padding-right: 0.5em; } table.highlighttable td.code { flex: 1; overflow: hidden; } .highlight .hll { display: block; } div.highlight pre, table.highlighttable pre { margin: 0; } div.code-block-caption + div { margin-top: 0; } div.code-block-caption { margin-top: 1em; padding: 2px 5px; font-size: small; } div.code-block-caption code { background-color: transparent; } table.highlighttable td.linenos, span.linenos, div.highlight span.gp { /* gp: Generic.Prompt */ user-select: none; -webkit-user-select: text; /* Safari fallback only */ -webkit-user-select: none; /* Chrome/Safari */ -moz-user-select: none; /* Firefox */ -ms-user-select: none; /* IE10+ */ } div.code-block-caption span.caption-number { padding: 0.1em 0.3em; font-style: italic; } div.code-block-caption span.caption-text { } div.literal-block-wrapper { margin: 1em 0; } code.xref, a code { background-color: transparent; font-weight: bold; } h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { background-color: transparent; } .viewcode-link { float: right; } .viewcode-back { float: right; font-family: sans-serif; } div.viewcode-block:target { margin: -1px -10px; padding: 0 10px; } /* -- math display ---------------------------------------------------------- */ img.math { vertical-align: middle; } div.body div.math p { text-align: center; } span.eqno { float: right; } span.eqno a.headerlink { position: absolute; z-index: 1; } div.math:hover a.headerlink { visibility: visible; } /* -- printout stylesheet --------------------------------------------------- */ @media print { div.document, div.documentwrapper, div.bodywrapper { margin: 0 !important; width: 100%; } div.sphinxsidebar, div.related, div.footer, #top-link { display: none; } } ================================================ FILE: docs/_static/custom.css ================================================ /* This file intentionally left blank. */ ================================================ FILE: docs/_static/doctools.js ================================================ /* * Base JavaScript utilities for all Sphinx HTML documentation. */ "use strict"; const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ "TEXTAREA", "INPUT", "SELECT", "BUTTON", ]); const _ready = (callback) => { if (document.readyState !== "loading") { callback(); } else { document.addEventListener("DOMContentLoaded", callback); } }; /** * Small JavaScript module for the documentation. */ const Documentation = { init: () => { Documentation.initDomainIndexTable(); Documentation.initOnKeyListeners(); }, /** * i18n support */ TRANSLATIONS: {}, PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), LOCALE: "unknown", // gettext and ngettext don't access this so that the functions // can safely bound to a different name (_ = Documentation.gettext) gettext: (string) => { const translated = Documentation.TRANSLATIONS[string]; switch (typeof translated) { case "undefined": return string; // no translation case "string": return translated; // translation exists default: return translated[0]; // (singular, plural) translation tuple exists } }, ngettext: (singular, plural, n) => { const translated = Documentation.TRANSLATIONS[singular]; if (typeof translated !== "undefined") return translated[Documentation.PLURAL_EXPR(n)]; return n === 1 ? singular : plural; }, addTranslations: (catalog) => { Object.assign(Documentation.TRANSLATIONS, catalog.messages); Documentation.PLURAL_EXPR = new Function( "n", `return (${catalog.plural_expr})` ); Documentation.LOCALE = catalog.locale; }, /** * helper function to focus on search bar */ focusSearchBar: () => { document.querySelectorAll("input[name=q]")[0]?.focus(); }, /** * Initialise the domain index toggle buttons */ initDomainIndexTable: () => { const toggler = (el) => { const idNumber = el.id.substr(7); const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); if (el.src.substr(-9) === "minus.png") { el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; toggledRows.forEach((el) => (el.style.display = "none")); } else { el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; toggledRows.forEach((el) => (el.style.display = "")); } }; const togglerElements = document.querySelectorAll("img.toggler"); togglerElements.forEach((el) => el.addEventListener("click", (event) => toggler(event.currentTarget)) ); togglerElements.forEach((el) => (el.style.display = "")); if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); }, initOnKeyListeners: () => { // only install a listener if it is really needed if ( !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS ) return; document.addEventListener("keydown", (event) => { // bail for input elements if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; // bail with special keys if (event.altKey || event.ctrlKey || event.metaKey) return; if (!event.shiftKey) { switch (event.key) { case "ArrowLeft": if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; const prevLink = document.querySelector('link[rel="prev"]'); if (prevLink && prevLink.href) { window.location.href = prevLink.href; event.preventDefault(); } break; case "ArrowRight": if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; const nextLink = document.querySelector('link[rel="next"]'); if (nextLink && nextLink.href) { window.location.href = nextLink.href; event.preventDefault(); } break; } } // some keyboard layouts may need Shift to get / switch (event.key) { case "/": if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; Documentation.focusSearchBar(); event.preventDefault(); } }); }, }; // quick alias for translations const _ = Documentation.gettext; _ready(Documentation.init); ================================================ FILE: docs/_static/documentation_options.js ================================================ const DOCUMENTATION_OPTIONS = { VERSION: '2.5.2', LANGUAGE: 'en', COLLAPSE_INDEX: false, BUILDER: 'html', FILE_SUFFIX: '.html', LINK_SUFFIX: '.html', HAS_SOURCE: true, SOURCELINK_SUFFIX: '.txt', NAVIGATION_WITH_KEYS: false, SHOW_SEARCH_SUMMARY: true, ENABLE_SEARCH_SHORTCUTS: true, }; ================================================ FILE: docs/_static/language_data.js ================================================ /* * This script contains the language-specific data used by searchtools.js, * namely the list of stopwords, stemmer, scorer and splitter. */ var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; /* Non-minified version is copied as a separate JS file, if available */ /** * Porter Stemmer */ var Stemmer = function() { var step2list = { ational: 'ate', tional: 'tion', enci: 'ence', anci: 'ance', izer: 'ize', bli: 'ble', alli: 'al', entli: 'ent', eli: 'e', ousli: 'ous', ization: 'ize', ation: 'ate', ator: 'ate', alism: 'al', iveness: 'ive', fulness: 'ful', ousness: 'ous', aliti: 'al', iviti: 'ive', biliti: 'ble', logi: 'log' }; var step3list = { icate: 'ic', ative: '', alize: 'al', iciti: 'ic', ical: 'ic', ful: '', ness: '' }; var c = "[^aeiou]"; // consonant var v = "[aeiouy]"; // vowel var C = c + "[^aeiouy]*"; // consonant sequence var V = v + "[aeiou]*"; // vowel sequence var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 var s_v = "^(" + C + ")?" + v; // vowel in stem this.stemWord = function (w) { var stem; var suffix; var firstch; var origword = w; if (w.length < 3) return w; var re; var re2; var re3; var re4; firstch = w.substr(0,1); if (firstch == "y") w = firstch.toUpperCase() + w.substr(1); // Step 1a re = /^(.+?)(ss|i)es$/; re2 = /^(.+?)([^s])s$/; if (re.test(w)) w = w.replace(re,"$1$2"); else if (re2.test(w)) w = w.replace(re2,"$1$2"); // Step 1b re = /^(.+?)eed$/; re2 = /^(.+?)(ed|ing)$/; if (re.test(w)) { var fp = re.exec(w); re = new RegExp(mgr0); if (re.test(fp[1])) { re = /.$/; w = w.replace(re,""); } } else if (re2.test(w)) { var fp = re2.exec(w); stem = fp[1]; re2 = new RegExp(s_v); if (re2.test(stem)) { w = stem; re2 = /(at|bl|iz)$/; re3 = new RegExp("([^aeiouylsz])\\1$"); re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); if (re2.test(w)) w = w + "e"; else if (re3.test(w)) { re = /.$/; w = w.replace(re,""); } else if (re4.test(w)) w = w + "e"; } } // Step 1c re = /^(.+?)y$/; if (re.test(w)) { var fp = re.exec(w); stem = fp[1]; re = new RegExp(s_v); if (re.test(stem)) w = stem + "i"; } // Step 2 re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; if (re.test(w)) { var fp = re.exec(w); stem = fp[1]; suffix = fp[2]; re = new RegExp(mgr0); if (re.test(stem)) w = stem + step2list[suffix]; } // Step 3 re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; if (re.test(w)) { var fp = re.exec(w); stem = fp[1]; suffix = fp[2]; re = new RegExp(mgr0); if (re.test(stem)) w = stem + step3list[suffix]; } // Step 4 re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; re2 = /^(.+?)(s|t)(ion)$/; if (re.test(w)) { var fp = re.exec(w); stem = fp[1]; re = new RegExp(mgr1); if (re.test(stem)) w = stem; } else if (re2.test(w)) { var fp = re2.exec(w); stem = fp[1] + fp[2]; re2 = new RegExp(mgr1); if (re2.test(stem)) w = stem; } // Step 5 re = /^(.+?)e$/; if (re.test(w)) { var fp = re.exec(w); stem = fp[1]; re = new RegExp(mgr1); re2 = new RegExp(meq1); re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) w = stem; } re = /ll$/; re2 = new RegExp(mgr1); if (re.test(w) && re2.test(w)) { re = /.$/; w = w.replace(re,""); } // and turn initial Y back to y if (firstch == "y") w = firstch.toLowerCase() + w.substr(1); return w; } } ================================================ FILE: docs/_static/pygments.css ================================================ pre { line-height: 125%; } td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } .highlight .hll { background-color: #ffffcc } .highlight { background: #f8f8f8; } .highlight .c { color: #8f5902; font-style: italic } /* Comment */ .highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */ .highlight .g { color: #000000 } /* Generic */ .highlight .k { color: #004461; font-weight: bold } /* Keyword */ .highlight .l { color: #000000 } /* Literal */ .highlight .n { color: #000000 } /* Name */ .highlight .o { color: #582800 } /* Operator */ .highlight .x { color: #000000 } /* Other */ .highlight .p { color: #000000; font-weight: bold } /* Punctuation */ .highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */ .highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ .highlight .cp { color: #8f5902 } /* Comment.Preproc */ .highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */ .highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ .highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */ .highlight .gd { color: #a40000 } /* Generic.Deleted */ .highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ .highlight .ges { color: #000000 } /* Generic.EmphStrong */ .highlight .gr { color: #ef2929 } /* Generic.Error */ .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ .highlight .gi { color: #00A000 } /* Generic.Inserted */ .highlight .go { color: #888888 } /* Generic.Output */ .highlight .gp { color: #745334 } /* Generic.Prompt */ .highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */ .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ .highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ .highlight .kc { color: #004461; font-weight: bold } /* Keyword.Constant */ .highlight .kd { color: #004461; font-weight: bold } /* Keyword.Declaration */ .highlight .kn { color: #004461; font-weight: bold } /* Keyword.Namespace */ .highlight .kp { color: #004461; font-weight: bold } /* Keyword.Pseudo */ .highlight .kr { color: #004461; font-weight: bold } /* Keyword.Reserved */ .highlight .kt { color: #004461; font-weight: bold } /* Keyword.Type */ .highlight .ld { color: #000000 } /* Literal.Date */ .highlight .m { color: #990000 } /* Literal.Number */ .highlight .s { color: #4e9a06 } /* Literal.String */ .highlight .na { color: #c4a000 } /* Name.Attribute */ .highlight .nb { color: #004461 } /* Name.Builtin */ .highlight .nc { color: #000000 } /* Name.Class */ .highlight .no { color: #000000 } /* Name.Constant */ .highlight .nd { color: #888888 } /* Name.Decorator */ .highlight .ni { color: #ce5c00 } /* Name.Entity */ .highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ .highlight .nf { color: #000000 } /* Name.Function */ .highlight .nl { color: #f57900 } /* Name.Label */ .highlight .nn { color: #000000 } /* Name.Namespace */ .highlight .nx { color: #000000 } /* Name.Other */ .highlight .py { color: #000000 } /* Name.Property */ .highlight .nt { color: #004461; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #000000 } /* Name.Variable */ .highlight .ow { color: #004461; font-weight: bold } /* Operator.Word */ .highlight .pm { color: #000000; font-weight: bold } /* Punctuation.Marker */ .highlight .w { color: #f8f8f8 } /* Text.Whitespace */ .highlight .mb { color: #990000 } /* Literal.Number.Bin */ .highlight .mf { color: #990000 } /* Literal.Number.Float */ .highlight .mh { color: #990000 } /* Literal.Number.Hex */ .highlight .mi { color: #990000 } /* Literal.Number.Integer */ .highlight .mo { color: #990000 } /* Literal.Number.Oct */ .highlight .sa { color: #4e9a06 } /* Literal.String.Affix */ .highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */ .highlight .sc { color: #4e9a06 } /* Literal.String.Char */ .highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */ .highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ .highlight .s2 { color: #4e9a06 } /* Literal.String.Double */ .highlight .se { color: #4e9a06 } /* Literal.String.Escape */ .highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */ .highlight .si { color: #4e9a06 } /* Literal.String.Interpol */ .highlight .sx { color: #4e9a06 } /* Literal.String.Other */ .highlight .sr { color: #4e9a06 } /* Literal.String.Regex */ .highlight .s1 { color: #4e9a06 } /* Literal.String.Single */ .highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */ .highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #000000 } /* Name.Function.Magic */ .highlight .vc { color: #000000 } /* Name.Variable.Class */ .highlight .vg { color: #000000 } /* Name.Variable.Global */ .highlight .vi { color: #000000 } /* Name.Variable.Instance */ .highlight .vm { color: #000000 } /* Name.Variable.Magic */ .highlight .il { color: #990000 } /* Literal.Number.Integer.Long */ ================================================ FILE: docs/_static/searchtools.js ================================================ /* * Sphinx JavaScript utilities for the full-text search. */ "use strict"; /** * Simple result scoring code. */ if (typeof Scorer === "undefined") { var Scorer = { // Implement the following function to further tweak the score for each result // The function takes a result array [docname, title, anchor, descr, score, filename] // and returns the new score. /* score: result => { const [docname, title, anchor, descr, score, filename, kind] = result return score }, */ // query matches the full name of an object objNameMatch: 11, // or matches in the last dotted part of the object name objPartialMatch: 6, // Additive scores depending on the priority of the object objPrio: { 0: 15, // used to be importantResults 1: 5, // used to be objectResults 2: -5, // used to be unimportantResults }, // Used when the priority is not in the mapping. objPrioDefault: 0, // query found in title title: 15, partialTitle: 7, // query found in terms term: 5, partialTerm: 2, }; } // Global search result kind enum, used by themes to style search results. class SearchResultKind { static get index() { return "index"; } static get object() { return "object"; } static get text() { return "text"; } static get title() { return "title"; } } const _removeChildren = (element) => { while (element && element.lastChild) element.removeChild(element.lastChild); }; /** * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping */ const _escapeRegExp = (string) => string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string const _displayItem = (item, searchTerms, highlightTerms) => { const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; const contentRoot = document.documentElement.dataset.content_root; const [docName, title, anchor, descr, score, _filename, kind] = item; let listItem = document.createElement("li"); // Add a class representing the item's type: // can be used by a theme's CSS selector for styling // See SearchResultKind for the class names. listItem.classList.add(`kind-${kind}`); let requestUrl; let linkUrl; if (docBuilder === "dirhtml") { // dirhtml builder let dirname = docName + "/"; if (dirname.match(/\/index\/$/)) dirname = dirname.substring(0, dirname.length - 6); else if (dirname === "index/") dirname = ""; requestUrl = contentRoot + dirname; linkUrl = requestUrl; } else { // normal html builders requestUrl = contentRoot + docName + docFileSuffix; linkUrl = docName + docLinkSuffix; } let linkEl = listItem.appendChild(document.createElement("a")); linkEl.href = linkUrl + anchor; linkEl.dataset.score = score; linkEl.innerHTML = title; if (descr) { listItem.appendChild(document.createElement("span")).innerHTML = " (" + descr + ")"; // highlight search terms in the description if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); } else if (showSearchSummary) fetch(requestUrl) .then((responseData) => responseData.text()) .then((data) => { if (data) listItem.appendChild( Search.makeSearchSummary(data, searchTerms, anchor) ); // highlight search terms in the summary if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); }); Search.output.appendChild(listItem); }; const _finishSearch = (resultCount) => { Search.stopPulse(); Search.title.innerText = _("Search Results"); if (!resultCount) Search.status.innerText = Documentation.gettext( "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." ); else Search.status.innerText = Documentation.ngettext( "Search finished, found one page matching the search query.", "Search finished, found ${resultCount} pages matching the search query.", resultCount, ).replace('${resultCount}', resultCount); }; const _displayNextItem = ( results, resultCount, searchTerms, highlightTerms, ) => { // results left, load the summary and display it // this is intended to be dynamic (don't sub resultsCount) if (results.length) { _displayItem(results.pop(), searchTerms, highlightTerms); setTimeout( () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), 5 ); } // search finished, update title and status message else _finishSearch(resultCount); }; // Helper function used by query() to order search results. // Each input is an array of [docname, title, anchor, descr, score, filename, kind]. // Order the results by score (in opposite order of appearance, since the // `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. const _orderResultsByScoreThenName = (a, b) => { const leftScore = a[4]; const rightScore = b[4]; if (leftScore === rightScore) { // same score: sort alphabetically const leftTitle = a[1].toLowerCase(); const rightTitle = b[1].toLowerCase(); if (leftTitle === rightTitle) return 0; return leftTitle > rightTitle ? -1 : 1; // inverted is intentional } return leftScore > rightScore ? 1 : -1; }; /** * Default splitQuery function. Can be overridden in ``sphinx.search`` with a * custom function per language. * * The regular expression works by splitting the string on consecutive characters * that are not Unicode letters, numbers, underscores, or emoji characters. * This is the same as ``\W+`` in Python, preserving the surrogate pair area. */ if (typeof splitQuery === "undefined") { var splitQuery = (query) => query .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) .filter(term => term) // remove remaining empty strings } /** * Search Module */ const Search = { _index: null, _queued_query: null, _pulse_status: -1, htmlToText: (htmlString, anchor) => { const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); for (const removalQuery of [".headerlink", "script", "style"]) { htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); } if (anchor) { const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); if (anchorContent) return anchorContent.textContent; console.warn( `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` ); } // if anchor not specified or not found, fall back to main content const docContent = htmlElement.querySelector('[role="main"]'); if (docContent) return docContent.textContent; console.warn( "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." ); return ""; }, init: () => { const query = new URLSearchParams(window.location.search).get("q"); document .querySelectorAll('input[name="q"]') .forEach((el) => (el.value = query)); if (query) Search.performSearch(query); }, loadIndex: (url) => (document.body.appendChild(document.createElement("script")).src = url), setIndex: (index) => { Search._index = index; if (Search._queued_query !== null) { const query = Search._queued_query; Search._queued_query = null; Search.query(query); } }, hasIndex: () => Search._index !== null, deferQuery: (query) => (Search._queued_query = query), stopPulse: () => (Search._pulse_status = -1), startPulse: () => { if (Search._pulse_status >= 0) return; const pulse = () => { Search._pulse_status = (Search._pulse_status + 1) % 4; Search.dots.innerText = ".".repeat(Search._pulse_status); if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); }; pulse(); }, /** * perform a search for something (or wait until index is loaded) */ performSearch: (query) => { // create the required interface elements const searchText = document.createElement("h2"); searchText.textContent = _("Searching"); const searchSummary = document.createElement("p"); searchSummary.classList.add("search-summary"); searchSummary.innerText = ""; const searchList = document.createElement("ul"); searchList.setAttribute("role", "list"); searchList.classList.add("search"); const out = document.getElementById("search-results"); Search.title = out.appendChild(searchText); Search.dots = Search.title.appendChild(document.createElement("span")); Search.status = out.appendChild(searchSummary); Search.output = out.appendChild(searchList); const searchProgress = document.getElementById("search-progress"); // Some themes don't use the search progress node if (searchProgress) { searchProgress.innerText = _("Preparing search..."); } Search.startPulse(); // index already loaded, the browser was quick! if (Search.hasIndex()) Search.query(query); else Search.deferQuery(query); }, _parseQuery: (query) => { // stem the search terms and add them to the correct list const stemmer = new Stemmer(); const searchTerms = new Set(); const excludedTerms = new Set(); const highlightTerms = new Set(); const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); splitQuery(query.trim()).forEach((queryTerm) => { const queryTermLower = queryTerm.toLowerCase(); // maybe skip this "word" // stopwords array is from language_data.js if ( stopwords.indexOf(queryTermLower) !== -1 || queryTerm.match(/^\d+$/) ) return; // stem the word let word = stemmer.stemWord(queryTermLower); // select the correct list if (word[0] === "-") excludedTerms.add(word.substr(1)); else { searchTerms.add(word); highlightTerms.add(queryTermLower); } }); if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) } // console.debug("SEARCH: searching for:"); // console.info("required: ", [...searchTerms]); // console.info("excluded: ", [...excludedTerms]); return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; }, /** * execute search (requires search index to be loaded) */ _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { const filenames = Search._index.filenames; const docNames = Search._index.docnames; const titles = Search._index.titles; const allTitles = Search._index.alltitles; const indexEntries = Search._index.indexentries; // Collect multiple result groups to be sorted separately and then ordered. // Each is an array of [docname, title, anchor, descr, score, filename, kind]. const normalResults = []; const nonMainIndexResults = []; _removeChildren(document.getElementById("search-progress")); const queryLower = query.toLowerCase().trim(); for (const [title, foundTitles] of Object.entries(allTitles)) { if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { for (const [file, id] of foundTitles) { const score = Math.round(Scorer.title * queryLower.length / title.length); const boost = titles[file] === title ? 1 : 0; // add a boost for document titles normalResults.push([ docNames[file], titles[file] !== title ? `${titles[file]} > ${title}` : title, id !== null ? "#" + id : "", null, score + boost, filenames[file], SearchResultKind.title, ]); } } } // search for explicit entries in index directives for (const [entry, foundEntries] of Object.entries(indexEntries)) { if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { for (const [file, id, isMain] of foundEntries) { const score = Math.round(100 * queryLower.length / entry.length); const result = [ docNames[file], titles[file], id ? "#" + id : "", null, score, filenames[file], SearchResultKind.index, ]; if (isMain) { normalResults.push(result); } else { nonMainIndexResults.push(result); } } } } // lookup as object objectTerms.forEach((term) => normalResults.push(...Search.performObjectSearch(term, objectTerms)) ); // lookup as search terms in fulltext normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); // let the scorer override scores with a custom scoring function if (Scorer.score) { normalResults.forEach((item) => (item[4] = Scorer.score(item))); nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); } // Sort each group of results by score and then alphabetically by name. normalResults.sort(_orderResultsByScoreThenName); nonMainIndexResults.sort(_orderResultsByScoreThenName); // Combine the result groups in (reverse) order. // Non-main index entries are typically arbitrary cross-references, // so display them after other results. let results = [...nonMainIndexResults, ...normalResults]; // remove duplicate search results // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept let seen = new Set(); results = results.reverse().reduce((acc, result) => { let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); if (!seen.has(resultStr)) { acc.push(result); seen.add(resultStr); } return acc; }, []); return results.reverse(); }, query: (query) => { const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); // for debugging //Search.lastresults = results.slice(); // a copy // console.info("search results:", Search.lastresults); // print the results _displayNextItem(results, results.length, searchTerms, highlightTerms); }, /** * search for object names */ performObjectSearch: (object, objectTerms) => { const filenames = Search._index.filenames; const docNames = Search._index.docnames; const objects = Search._index.objects; const objNames = Search._index.objnames; const titles = Search._index.titles; const results = []; const objectSearchCallback = (prefix, match) => { const name = match[4] const fullname = (prefix ? prefix + "." : "") + name; const fullnameLower = fullname.toLowerCase(); if (fullnameLower.indexOf(object) < 0) return; let score = 0; const parts = fullnameLower.split("."); // check for different match types: exact matches of full name or // "last name" (i.e. last dotted part) if (fullnameLower === object || parts.slice(-1)[0] === object) score += Scorer.objNameMatch; else if (parts.slice(-1)[0].indexOf(object) > -1) score += Scorer.objPartialMatch; // matches in last name const objName = objNames[match[1]][2]; const title = titles[match[0]]; // If more than one term searched for, we require other words to be // found in the name/title/description const otherTerms = new Set(objectTerms); otherTerms.delete(object); if (otherTerms.size > 0) { const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); if ( [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) ) return; } let anchor = match[3]; if (anchor === "") anchor = fullname; else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; const descr = objName + _(", in ") + title; // add custom score for some objects according to scorer if (Scorer.objPrio.hasOwnProperty(match[2])) score += Scorer.objPrio[match[2]]; else score += Scorer.objPrioDefault; results.push([ docNames[match[0]], fullname, "#" + anchor, descr, score, filenames[match[0]], SearchResultKind.object, ]); }; Object.keys(objects).forEach((prefix) => objects[prefix].forEach((array) => objectSearchCallback(prefix, array) ) ); return results; }, /** * search for full-text terms in the index */ performTermsSearch: (searchTerms, excludedTerms) => { // prepare search const terms = Search._index.terms; const titleTerms = Search._index.titleterms; const filenames = Search._index.filenames; const docNames = Search._index.docnames; const titles = Search._index.titles; const scoreMap = new Map(); const fileMap = new Map(); // perform the search on the required terms searchTerms.forEach((word) => { const files = []; const arr = [ { files: terms[word], score: Scorer.term }, { files: titleTerms[word], score: Scorer.title }, ]; // add support for partial matches if (word.length > 2) { const escapedWord = _escapeRegExp(word); if (!terms.hasOwnProperty(word)) { Object.keys(terms).forEach((term) => { if (term.match(escapedWord)) arr.push({ files: terms[term], score: Scorer.partialTerm }); }); } if (!titleTerms.hasOwnProperty(word)) { Object.keys(titleTerms).forEach((term) => { if (term.match(escapedWord)) arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); }); } } // no match but word was a required one if (arr.every((record) => record.files === undefined)) return; // found search word in contents arr.forEach((record) => { if (record.files === undefined) return; let recordFiles = record.files; if (recordFiles.length === undefined) recordFiles = [recordFiles]; files.push(...recordFiles); // set score for the word in each file recordFiles.forEach((file) => { if (!scoreMap.has(file)) scoreMap.set(file, {}); scoreMap.get(file)[word] = record.score; }); }); // create the mapping files.forEach((file) => { if (!fileMap.has(file)) fileMap.set(file, [word]); else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); }); }); // now check if the files don't contain excluded terms const results = []; for (const [file, wordList] of fileMap) { // check if all requirements are matched // as search terms with length < 3 are discarded const filteredTermCount = [...searchTerms].filter( (term) => term.length > 2 ).length; if ( wordList.length !== searchTerms.size && wordList.length !== filteredTermCount ) continue; // ensure that none of the excluded terms is in the search result if ( [...excludedTerms].some( (term) => terms[term] === file || titleTerms[term] === file || (terms[term] || []).includes(file) || (titleTerms[term] || []).includes(file) ) ) break; // select one (max) score for the file. const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); // add result to the result list results.push([ docNames[file], titles[file], "", null, score, filenames[file], SearchResultKind.text, ]); } return results; }, /** * helper function to return a node containing the * search summary for a given text. keywords is a list * of stemmed words. */ makeSearchSummary: (htmlText, keywords, anchor) => { const text = Search.htmlToText(htmlText, anchor); if (text === "") return null; const textLower = text.toLowerCase(); const actualStartPosition = [...keywords] .map((k) => textLower.indexOf(k.toLowerCase())) .filter((i) => i > -1) .slice(-1)[0]; const startWithContext = Math.max(actualStartPosition - 120, 0); const top = startWithContext === 0 ? "" : "..."; const tail = startWithContext + 240 < text.length ? "..." : ""; let summary = document.createElement("p"); summary.classList.add("context"); summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; return summary; }, }; _ready(Search.init); ================================================ FILE: docs/_static/sphinx_highlight.js ================================================ /* Highlighting utilities for Sphinx HTML documentation. */ "use strict"; const SPHINX_HIGHLIGHT_ENABLED = true /** * highlight a given string on a node by wrapping it in * span elements with the given class name. */ const _highlight = (node, addItems, text, className) => { if (node.nodeType === Node.TEXT_NODE) { const val = node.nodeValue; const parent = node.parentNode; const pos = val.toLowerCase().indexOf(text); if ( pos >= 0 && !parent.classList.contains(className) && !parent.classList.contains("nohighlight") ) { let span; const closestNode = parent.closest("body, svg, foreignObject"); const isInSVG = closestNode && closestNode.matches("svg"); if (isInSVG) { span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); } else { span = document.createElement("span"); span.classList.add(className); } span.appendChild(document.createTextNode(val.substr(pos, text.length))); const rest = document.createTextNode(val.substr(pos + text.length)); parent.insertBefore( span, parent.insertBefore( rest, node.nextSibling ) ); node.nodeValue = val.substr(0, pos); /* There may be more occurrences of search term in this node. So call this * function recursively on the remaining fragment. */ _highlight(rest, addItems, text, className); if (isInSVG) { const rect = document.createElementNS( "http://www.w3.org/2000/svg", "rect" ); const bbox = parent.getBBox(); rect.x.baseVal.value = bbox.x; rect.y.baseVal.value = bbox.y; rect.width.baseVal.value = bbox.width; rect.height.baseVal.value = bbox.height; rect.setAttribute("class", className); addItems.push({ parent: parent, target: rect }); } } } else if (node.matches && !node.matches("button, select, textarea")) { node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); } }; const _highlightText = (thisNode, text, className) => { let addItems = []; _highlight(thisNode, addItems, text, className); addItems.forEach((obj) => obj.parent.insertAdjacentElement("beforebegin", obj.target) ); }; /** * Small JavaScript module for the documentation. */ const SphinxHighlight = { /** * highlight the search words provided in localstorage in the text */ highlightSearchWords: () => { if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight // get and clear terms from localstorage const url = new URL(window.location); const highlight = localStorage.getItem("sphinx_highlight_terms") || url.searchParams.get("highlight") || ""; localStorage.removeItem("sphinx_highlight_terms") url.searchParams.delete("highlight"); window.history.replaceState({}, "", url); // get individual terms from highlight string const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); if (terms.length === 0) return; // nothing to do // There should never be more than one element matching "div.body" const divBody = document.querySelectorAll("div.body"); const body = divBody.length ? divBody[0] : document.querySelector("body"); window.setTimeout(() => { terms.forEach((term) => _highlightText(body, term, "highlighted")); }, 10); const searchBox = document.getElementById("searchbox"); if (searchBox === null) return; searchBox.appendChild( document .createRange() .createContextualFragment( '" ) ); }, /** * helper function to hide the search marks again */ hideSearchWords: () => { document .querySelectorAll("#searchbox .highlight-link") .forEach((el) => el.remove()); document .querySelectorAll("span.highlighted") .forEach((el) => el.classList.remove("highlighted")); localStorage.removeItem("sphinx_highlight_terms") }, initEscapeListener: () => { // only install a listener if it is really needed if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; document.addEventListener("keydown", (event) => { // bail for input elements if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; // bail with special keys if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { SphinxHighlight.hideSearchWords(); event.preventDefault(); } }); }, }; _ready(() => { /* Do not call highlightSearchWords() when we are on the search page. * It will highlight words from the *previous* search query. */ if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); SphinxHighlight.initEscapeListener(); }); ================================================ FILE: docs/about.html ================================================ About — pwlf 2.5.2 documentation

About

A library for fitting continuous piecewise linear functions to data. Just specify the number of line segments you desire and provide the data.

Example of a continuous piecewise linear fit to data.

Example of a continuous piecewise linear fit to data.

All changes now stored in CHANGELOG.md

Please cite pwlf in your publications if it helps your research.

@Manual{pwlf,
    author = {Jekel, Charles F. and Venter, Gerhard},
    title = {{pwlf:} A Python Library for Fitting 1D Continuous Piecewise Linear Functions},
    year = {2019},
    url = {https://github.com/cjekel/piecewise_linear_fit_py}
}
================================================ FILE: docs/examples.html ================================================ Examples — pwlf 2.5.2 documentation

Examples

All of these examples will use the following data and imports.

import numpy as np
import matplotlib.pyplot as plt
import pwlf

# your data
y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02,
              4.39052750e-02, 5.45343950e-02, 6.74104940e-02,
              8.34831790e-02, 1.02580042e-01, 1.22767939e-01,
              1.42172312e-01, 0.00000000e+00, 8.58600000e-06,
              8.31543400e-03, 2.34184100e-02, 3.39709150e-02,
              4.03581990e-02, 4.53545600e-02, 5.02345260e-02,
              5.55253360e-02, 6.14750770e-02, 6.82125120e-02,
              7.55892510e-02, 8.38356810e-02, 9.26413070e-02,
              1.02039790e-01, 1.11688258e-01, 1.21390666e-01,
              1.31196948e-01, 0.00000000e+00, 1.56706510e-02,
              3.54628780e-02, 4.63739040e-02, 5.61442590e-02,
              6.78542550e-02, 8.16388310e-02, 9.77756110e-02,
              1.16531753e-01, 1.37038283e-01, 0.00000000e+00,
              1.16951050e-02, 3.12089850e-02, 4.41776550e-02,
              5.42877590e-02, 6.63321350e-02, 8.07655920e-02,
              9.70363280e-02, 1.15706975e-01, 1.36687642e-01,
              0.00000000e+00, 1.50144640e-02, 3.44519970e-02,
              4.55907760e-02, 5.59556700e-02, 6.88450940e-02,
              8.41374060e-02, 1.01254006e-01, 1.20605073e-01,
              1.41881288e-01, 1.62618058e-01])
x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02,
              5.66106800e-02, 7.95549800e-02, 1.00936330e-01,
              1.20351520e-01, 1.37442010e-01, 1.51858250e-01,
              1.64433570e-01, 0.00000000e+00, -2.12600000e-05,
              7.03872000e-03, 1.85494500e-02, 3.00926700e-02,
              4.17617000e-02, 5.37279600e-02, 6.54941000e-02,
              7.68092100e-02, 8.76596300e-02, 9.80525800e-02,
              1.07961810e-01, 1.17305210e-01, 1.26063930e-01,
              1.34180360e-01, 1.41725010e-01, 1.48629710e-01,
              1.55374770e-01, 0.00000000e+00, 1.65610200e-02,
              3.91016100e-02, 6.18679400e-02, 8.30997400e-02,
              1.02132890e-01, 1.19011260e-01, 1.34620080e-01,
              1.49429370e-01, 1.63539960e-01, -0.00000000e+00,
              1.01980300e-02, 3.28642800e-02, 5.59461900e-02,
              7.81388400e-02, 9.84458400e-02, 1.16270210e-01,
              1.31279040e-01, 1.45437090e-01, 1.59627540e-01,
              0.00000000e+00, 1.63404300e-02, 4.00086000e-02,
              6.34390200e-02, 8.51085900e-02, 1.04787860e-01,
              1.22120350e-01, 1.36931660e-01, 1.50958760e-01,
              1.65299640e-01, 1.79942720e-01])
  1. fit with known breakpoint locations

  2. fit for specified number of line segments

  3. fitfast for specified number of line segments

  4. force a fit through data points

  5. use custom optimization routine

  6. pass differential evolution keywords

  7. find the best number of line segments

  8. model persistence

  9. bad fits when you have more unknowns than data

  10. fit with a breakpoint guess

  11. get the linear regression matrix

  12. use of TensorFlow

  13. fit constants or polynomials

  14. specify breakpoint bounds

  15. non-linear standard errors and p-values

  16. obtain the equations of fitted pwlf

  17. weighted least squares fit

  18. reproducible results

fit with known breakpoint locations

You can perform a least squares fit if you know the breakpoint locations.

# your desired line segment end locations
x0 = np.array([min(x), 0.039, 0.10, max(x)])

# initialize piecewise linear fit with your x and y data
my_pwlf = pwlf.PiecewiseLinFit(x, y)

# fit the data with the specified break points
# (ie the x locations of where the line segments
# will terminate)
my_pwlf.fit_with_breaks(x0)

# predict for the determined points
xHat = np.linspace(min(x), max(x), num=10000)
yHat = my_pwlf.predict(xHat)

# plot the results
plt.figure()
plt.plot(x, y, 'o')
plt.plot(xHat, yHat, '-')
plt.show()
fit with known breakpoint locations

fit with known breakpoint locations

fit for specified number of line segments

Use a global optimization to find the breakpoint locations that minimize the sum of squares error. This uses Differential Evolution from scipy.

# initialize piecewise linear fit with your x and y data
my_pwlf = pwlf.PiecewiseLinFit(x, y)

# fit the data for four line segments
res = my_pwlf.fit(4)

# predict for the determined points
xHat = np.linspace(min(x), max(x), num=10000)
yHat = my_pwlf.predict(xHat)

# plot the results
plt.figure()
plt.plot(x, y, 'o')
plt.plot(xHat, yHat, '-')
plt.show()
fit for specified number of line segments

fit for specified number of line segments

fitfast for specified number of line segments

This performs a fit for a specified number of line segments with a multi-start gradient based optimization. This should be faster than Differential Evolution for a small number of starting points.

# initialize piecewise linear fit with your x and y data
my_pwlf = pwlf.PiecewiseLinFit(x, y)

# fit the data for four line segments
# this performs 3 multi-start optimizations
res = my_pwlf.fitfast(4, pop=3)

# predict for the determined points
xHat = np.linspace(min(x), max(x), num=10000)
yHat = my_pwlf.predict(xHat)

# plot the results
plt.figure()
plt.plot(x, y, 'o')
plt.plot(xHat, yHat, '-')
plt.show()
fitfast for specified number of line segments

fitfast for specified number of line segments

force a fit through data points

Sometimes it’s necessary to force the piecewise continuous model through a particular data point, or a set of data points. The following example finds the best 4 line segments that go through two data points.

# initialize piecewise linear fit with your x and y data
myPWLF = pwlf.PiecewiseLinFit(x, y)

# fit the function with four line segments
# force the function to go through the data points
# (0.0, 0.0) and (0.19, 0.16)
# where the data points are of the form (x, y)
x_c = [0.0, 0.19]
y_c = [0.0, 0.2]
res = myPWLF.fit(4, x_c, y_c)

# predict for the determined points
xHat = np.linspace(min(x), 0.19, num=10000)
yHat = myPWLF.predict(xHat)

# plot the results
plt.figure()
plt.plot(x, y, 'o')
plt.plot(xHat, yHat, '-')
plt.show()
force a fit through data points

force a fit through data points

use custom optimization routine

You can use your favorite optimization routine to find the breakpoint locations. The following example uses scipy’s minimize function.

from scipy.optimize import minimize
# initialize piecewise linear fit with your x and y data
my_pwlf = pwlf.PiecewiseLinFit(x, y)

# initialize custom optimization
number_of_line_segments = 3
my_pwlf.use_custom_opt(number_of_line_segments)

# i have number_of_line_segments - 1 number of variables
# let's guess the correct location of the two unknown variables
# (the program defaults to have end segments at x0= min(x)
# and xn=max(x)
xGuess = np.zeros(number_of_line_segments - 1)
xGuess[0] = 0.02
xGuess[1] = 0.10

res = minimize(my_pwlf.fit_with_breaks_opt, xGuess)

# set up the break point locations
x0 = np.zeros(number_of_line_segments + 1)
x0[0] = np.min(x)
x0[-1] = np.max(x)
x0[1:-1] = res.x

# calculate the parameters based on the optimal break point locations
my_pwlf.fit_with_breaks(x0)

# predict for the determined points
xHat = np.linspace(min(x), max(x), num=10000)
yHat = my_pwlf.predict(xHat)

plt.figure()
plt.plot(x, y, 'o')
plt.plot(xHat, yHat, '-')
plt.show()

pass differential evolution keywords

You can pass keyword arguments from the fit function into scipy’s Differential Evolution.

# initialize piecewise linear fit with your x and y data
my_pwlf = pwlf.PiecewiseLinFit(x, y)

# fit the data for four line segments
# this sets DE to have an absolute tolerance of 0.1
res = my_pwlf.fit(4, atol=0.1)

# predict for the determined points
xHat = np.linspace(min(x), max(x), num=10000)
yHat = my_pwlf.predict(xHat)

# plot the results
plt.figure()
plt.plot(x, y, 'o')
plt.plot(xHat, yHat, '-')
plt.show()

find the best number of line segments

This example uses EGO (bayesian optimization) and a penalty function to find the best number of line segments. This will require careful use of the penalty parameter l. Use this template to automatically find the best number of line segments.

from GPyOpt.methods import BayesianOptimization
# initialize piecewise linear fit with your x and y data
my_pwlf = pwlf.PiecewiseLinFit(x, y)

# define your objective function


def my_obj(x):
    # define some penalty parameter l
    # you'll have to arbitrarily pick this
    # it depends upon the noise in your data,
    # and the value of your sum of square of residuals
    l = y.mean()*0.001
    f = np.zeros(x.shape[0])
    for i, j in enumerate(x):
        my_pwlf.fit(j[0])
        f[i] = my_pwlf.ssr + (l*j[0])
    return f


# define the lower and upper bound for the number of line segments
bounds = [{'name': 'var_1', 'type': 'discrete',
           'domain': np.arange(2, 40)}]

np.random.seed(12121)

myBopt = BayesianOptimization(my_obj, domain=bounds, model_type='GP',
                              initial_design_numdata=10,
                              initial_design_type='latin',
                              exact_feval=True, verbosity=True,
                              verbosity_model=False)
max_iter = 30

# perform the bayesian optimization to find the optimum number
# of line segments
myBopt.run_optimization(max_iter=max_iter, verbosity=True)

print('\n \n Opt found \n')
print('Optimum number of line segments:', myBopt.x_opt)
print('Function value:', myBopt.fx_opt)
myBopt.plot_acquisition()
myBopt.plot_convergence()

# perform the fit for the optimum
my_pwlf.fit(myBopt.x_opt)
# predict for the determined points
xHat = np.linspace(min(x), max(x), num=10000)
yHat = my_pwlf.predict(xHat)

# plot the results
plt.figure()
plt.plot(x, y, 'o')
plt.plot(xHat, yHat, '-')
plt.show()

model persistence

You can save fitted models with pickle. Alternatively see joblib.

# if you use Python 2.x you should import cPickle
# import cPickle as pickle
# if you use Python 3.x you can just use pickle
import pickle

# your desired line segment end locations
x0 = np.array([min(x), 0.039, 0.10, max(x)])

# initialize piecewise linear fit with your x and y data
my_pwlf = pwlf.PiecewiseLinFit(x, y)

# fit the data with the specified break points
my_pwlf.fit_with_breaks(x0)

# save the fitted model
with open('my_fit.pkl', 'wb') as f:
    pickle.dump(my_pwlf, f, pickle.HIGHEST_PROTOCOL)

# load the fitted model
with open('my_fit.pkl', 'rb') as f:
    my_pwlf = pickle.load(f)

bad fits when you have more unknowns than data

You can get very bad fits with pwlf when you have more unknowns than data points. The following example will fit 99 line segments to the 59 data points. While this will result in an error of zero, the model will have very weird predictions within the data. You should not fit more unknowns than you have data with pwlf!

break_locations = np.linspace(min(x), max(x), num=100)
# initialize piecewise linear fit with your x and y data
my_pwlf = pwlf.PiecewiseLinFit(x, y)
my_pwlf.fit_with_breaks(break_locations)

# predict for the determined points
xHat = np.linspace(min(x), max(x), num=10000)
yHat = my_pwlf.predict(xHat)

# plot the results
plt.figure()
plt.plot(x, y, 'o')
plt.plot(xHat, yHat, '-')
plt.show()
bad fits when you have more unknowns than data

bad fits when you have more unknowns than data

fit with a breakpoint guess

In this example we see two distinct linear regions, and we believe a breakpoint occurs at 6.0. We’ll use the fit_guess() function to find the best breakpoint location starting with this guess. These fits should be much faster than the fit or fitfast function when you have a reasonable idea where the breakpoints occur.

import numpy as np
import pwlf
x = np.array([4., 5., 6., 7., 8.])
y = np.array([11., 13., 16., 28.92, 42.81])
my_pwlf = pwlf.PiecewiseLinFit(x, y)
breaks = my_pwlf.fit_guess([6.0])

Note specifying one breakpoint will result in two line segments. If we wanted three line segments, we’ll have to specify two breakpoints.

breaks = my_pwlf.fit_guess([5.5, 6.0])

get the linear regression matrix

In some cases it may be desirable to work with the linear regression matrix directly. The following example grabs the linear regression matrix A for a specific set of breakpoints. In this case we assume that the breakpoints occur at each of the data points. Please see the paper for details about the regression matrix A.

import numpy as np
import pwlf
# select random seed for reproducibility
np.random.seed(123)
# generate sin wave data
x = np.linspace(0, 10, num=100)
y = np.sin(x * np.pi / 2)
ytrue = y.copy()
# add noise to the data
y = np.random.normal(0, 0.05, 100) + ytrue

my_pwlf_en = pwlf.PiecewiseLinFit(x, y)
# copy the x data to use as break points
breaks = my_pwlf_en.x_data.copy()
# create the linear regression matrix A
A = my_pwlf_en.assemble_regression_matrix(breaks, my_pwlf_en.x_data)

We can perform fits that are more complicated than a least squares fit when we have the regression matrix. The following uses the Elastic Net regularizer to perform an interesting fit with the regression matrix.

from sklearn.linear_model import ElasticNetCV
# set up the elastic net
en_model = ElasticNetCV(cv=5,
                        l1_ratio=[.1, .5, .7, .9,
                                  .95, .99, 1],
                        fit_intercept=False,
                        max_iter=1000000, n_jobs=-1)
# fit the model using the elastic net
en_model.fit(A, my_pwlf_en.y_data)

# predict from the elastic net parameters
xhat = np.linspace(x.min(), x.max(), 1000)
yhat_en = my_pwlf_en.predict(xhat, breaks=breaks,
                             beta=en_model.coef_)
interesting elastic net fit

interesting elastic net fit

use of tensorflow

You need to install pwlftf which will have the PiecewiseLinFitTF class. For performance benchmarks (these benchmarks are outdated! and the regular pwlf may be faster in many applications) see this blog post.

The use of the TF class is nearly identical to the original class, however note the following exceptions. PiecewiseLinFitTF does:

  • not have a lapack_driver option

  • have an optional parameter dtype, so you can choose between the float64 and float32 data types

  • have an optional parameter fast to switch between Cholesky decomposition (default fast=True), and orthogonal decomposition (fast=False)

import pwlftf as pwlf
# your desired line segment end locations
x0 = np.array([min(x), 0.039, 0.10, max(x)])

# initialize TF piecewise linear fit with your x and y data
my_pwlf = pwlf.PiecewiseLinFitTF(x, y, dtype='float32)

# fit the data with the specified break points
# (ie the x locations of where the line segments
# will terminate)
my_pwlf.fit_with_breaks(x0)

# predict for the determined points
xHat = np.linspace(min(x), max(x), num=10000)
yHat = my_pwlf.predict(xHat)

fit constants or polynomials

You can use pwlf to fit segmented constant models, or piecewise polynomials. The following example fits a segmented constant model, piecewise linear, and a piecewise quadratic model to a sine wave.

# generate sin wave data
x = np.linspace(0, 10, num=100)
y = np.sin(x * np.pi / 2)
# add noise to the data
y = np.random.normal(0, 0.05, 100) + y

# initialize piecewise linear fit with your x and y data
# pwlf lets you fit continuous model for many degree polynomials
# degree=0 constant
# degree=1 linear (default)
# degree=2 quadratic
my_pwlf_0 = pwlf.PiecewiseLinFit(x, y, degree=0)
my_pwlf_1 = pwlf.PiecewiseLinFit(x, y, degree=1)  # default
my_pwlf_2 = pwlf.PiecewiseLinFit(x, y, degree=2)

# fit the data for four line segments
res0 = my_pwlf_0.fitfast(5, pop=50)
res1 = my_pwlf_1.fitfast(5, pop=50)
res2 = my_pwlf_2.fitfast(5, pop=50)

# predict for the determined points
xHat = np.linspace(min(x), max(x), num=10000)
yHat0 = my_pwlf_0.predict(xHat)
yHat1 = my_pwlf_1.predict(xHat)
yHat2 = my_pwlf_2.predict(xHat)

# plot the results
plt.figure()
plt.plot(x, y, 'o', label='Data')
plt.plot(xHat, yHat0, '-', label='degree=0')
plt.plot(xHat, yHat1, '--', label='degree=1')
plt.plot(xHat, yHat2, ':', label='degree=2')
plt.legend()
plt.show()
Example of multiple degree fits to a sine wave.

Example of multiple degree fits to a sine wave.

specify breakpoint bounds

You may want extra control over the search space for feasible breakpoints. One way to do this is to specify the bounds for each breakpoint location.

# generate sin wave data
x = np.linspace(0, 10, num=100)
y = np.sin(x * np.pi / 2)
# add noise to the data
y = np.random.normal(0, 0.05, 100) + y

# initialize piecewise linear fit with your x and y data
my_pwlf = pwlf.PiecewiseLinFit(x, y)

# define custom bounds for the interior break points
n_segments = 4
bounds = np.zeros((n_segments-1, 2))
# first breakpoint
bounds[0, 0] = 0.0  # lower bound
bounds[0, 1] = 3.5  # upper bound
# second breakpoint
bounds[1, 0] = 3.0  # lower bound
bounds[1, 1] = 7.0  # upper bound
# third breakpoint
bounds[2, 0] = 6.0  # lower bound
bounds[2, 1] = 10.0  # upper bound
res = my_pwlf.fit(n_segments, bounds=bounds)

non-linear standard errors and p-values

You can calculate non-linear standard errors using the Delta method. This will calculate the standard errors of the piecewise linear parameters (intercept + slopes) and the breakpoint locations!

First let us generate true piecewise linear data.

from __future__ import print_function
# generate a true piecewise linear data
np.random.seed(5)
n_data = 100
x = np.linspace(0, 1, num=n_data)
y = np.random.random(n_data)
my_pwlf_gen = pwlf.PiecewiseLinFit(x, y)
true_beta = np.random.normal(size=5)
true_breaks = np.array([0.0, 0.2, 0.5, 0.75, 1.0])
y = my_pwlf_gen.predict(x, beta=true_beta, breaks=true_breaks)

plt.figure()
plt.title('True piecewise linear data')
plt.plot(x, y)
plt.show()
True piecewise linear data.

True piecewise linear data.

Now we can perform a fit, calculate the standard errors, and p-values. The non-linear method uses a first order taylor series expansion to linearize the non-linear regression problem. A positive step_size performs a forward difference, and a negative step_size would perform a backwards difference.

my_pwlf = pwlf.PiecewiseLinFit(x, y)
res = my_pwlf.fitfast(4, pop=100)

p = my_pwlf.p_values(method='non-linear', step_size=1e-4)
se = my_pwlf.se  # standard errors

The standard errors and p-values correspond to each model parameter. First the beta parameters (intercept + slopes) and then the breakpoints. We can assemble the parameters, and print a table of the result with the following code.

parameters = np.concatenate((my_pwlf.beta,
                             my_pwlf.fit_breaks[1:-1]))

header = ['Parameter type', 'Parameter value', 'Standard error', 't',
          'P > np.abs(t) (p-value)']
print(*header, sep=' | ')
values = np.zeros((parameters.size, 5), dtype=np.object_)
values[:, 1] = np.around(parameters, decimals=3)
values[:, 2] = np.around(se, decimals=3)
values[:, 3] = np.around(parameters / se, decimals=3)
values[:, 4] = np.around(p, decimals=3)

for i, row in enumerate(values):
    if i < my_pwlf.beta.size:
        row[0] = 'Beta'
        print(*row, sep=' | ')
    else:
        row[0] = 'Breakpoint'
        print(*row, sep=' | ')

Parameter type

Parameter value

Standard error

t

P > np.abs(t) (p-value)

Beta

1.821

0.0

1763191476.046

0.0

Beta

-0.427

0.0

-46404554.493

0.0

Beta

-1.165

0.0

-111181494.162

0.0

Beta

-1.397

0.0

-168954500.421

0.0

Beta

0.873

0.0

93753841.242

0.0

Breakpoint

0.2

0.0

166901856.885

0.0

Breakpoint

0.5

0.0

537785803.646

0.0

Breakpoint

0.75

0.0

482311769.159

0.0

obtain the equations of fitted pwlf

Sometimes you may want the mathematical equations that represent your fitted model. This is easy to perform if you don’t mind using sympy.

The following code will fit 5 line segments of degree=2 to a sin wave.

import numpy as np
import pwlf
# generate sin wave data
x = np.linspace(0, 10, num=100)
y = np.sin(x * np.pi / 2)
# add noise to the data
y = np.random.normal(0, 0.05, 100) + y
my_pwlf_2 = pwlf.PiecewiseLinFit(x, y, degree=2)
res2 = my_pwlf_2.fitfast(5, pop=50)

Given this fit, the following code will print the mathematical equation for each line segment.

from sympy import Symbol
from sympy.utilities import lambdify
x = Symbol('x')


def get_symbolic_eqn(pwlf_, segment_number):
    if pwlf_.degree < 1:
        raise ValueError('Degree must be at least 1')
    if segment_number < 1 or segment_number > pwlf_.n_segments:
        raise ValueError('segment_number not possible')
    # assemble degree = 1 first
    for line in range(segment_number):
        if line == 0:
            my_eqn = pwlf_.beta[0] + (pwlf_.beta[1])*(x-pwlf_.fit_breaks[0])
        else:
            my_eqn += (pwlf_.beta[line+1])*(x-pwlf_.fit_breaks[line])
    # assemble all other degrees
    if pwlf_.degree > 1:
        for k in range(2, pwlf_.degree + 1):
            for line in range(segment_number):
                beta_index = pwlf_.n_segments*(k-1) + line + 1
                my_eqn += (pwlf_.beta[beta_index])*(x-pwlf_.fit_breaks[line])**k
    return my_eqn.simplify()


eqn_list = []
f_list = []
for i in range(my_pwlf_2.n_segments):
    eqn_list.append(get_symbolic_eqn(my_pwlf_2, i + 1))
    print('Equation number: ', i + 1)
    print(eqn_list[-1])
    f_list.append(lambdify(x, eqn_list[-1]))

which should print out something like the following:

Equation number:  1
-0.953964059782599*x**2 + 1.89945177490653*x + 0.00538634182565454
Equation number:  2
0.951561315686298*x**2 - 5.69747505830914*x + 7.5772216545711
Equation number:  3
-0.949735350431857*x**2 + 9.48218236957122*x - 22.720785454735
Equation number:  4
0.926850298824217*x**2 - 12.9824424358344*x + 44.5102742956827
Equation number:  5
-1.03016230425747*x**2 + 18.5306546317065*x - 82.3508513333073

For more information on how this works, see this jupyter notebook.

weighted least squares fit

Sometimes your data will not have a constant variance (heteroscedasticity), and you need to perform a weighted least squares fit. The following example will perform a standard and weighted fit so you can compare the differences. First we need to generate a data set which will be a good candidate to use for weighted least squares fits.

# generate data with heteroscedasticity
n = 100
n_data_sets = 100
n_segments = 6
# generate sine data
x = np.linspace(0, 10, n)
y = np.zeros((n_data_sets, n))
sigma_change = np.linspace(0.001, 0.05, 100)
for i in range(n_data_sets):
    y[i] = np.sin(x * np.pi / 2)
    # add noise to the data
    y[i] = np.random.normal(0, sigma_change, 100) + y[i]
X = np.tile(x, n_data_sets)

The individual weights in pwlf are the reciprocal of the standard deviation for each data point. Here weights[i] corresponds to one over the standard deviation of the ith data point. The result of this is that data points with higher variance are less important to the overall pwlf than data point with small variance. Let’s perform a standard pwlf fit and a weighted fit.

# perform an ordinary pwlf fit to the entire data
my_pwlf = pwlf.PiecewiseLinFit(X.flatten(), y.flatten())
my_pwlf.fit(n_segments)

# compute the standard deviation in y
y_std = np.std(y, axis=0)
# set the weights to be one over the standard deviation
weights = 1.0 / y_std

# perform a weighted least squares to the data
my_pwlf_w = pwlf.PiecewiseLinFit(x, y.mean(axis=0), weights=weights)
my_pwlf_w.fit(n_segments)

# compare the fits
xhat = np.linspace(0, 10, 1000)
yhat = my_pwlf.predict(xhat)
yhat_w = my_pwlf_w.predict(xhat)

plt.figure()
plt.plot(X.flatten(), y.flatten(), '.')
plt.plot(xhat, yhat, '-', label='Ordinary LS')
plt.plot(xhat, yhat_w, '-', label='Weighted LS')
plt.legend()
plt.show()
Weighted pwlf fit.

Weighted pwlf fit.

We can see that the weighted pwlf fit tries fit data with low variance better than data with high variance, however the ordinary pwlf fits the data assuming a uniform variance.

reproducible results

The fit and fitfast methods are stochastic and may not give the same result every time the program is run. To have reproducible results you can manually specify a numpy.random.seed on init. Now everytime the following program is run, the results of the fit(2) should be the same.

# initialize piecewise linear fit with a random seed
my_pwlf = pwlf.PiecewiseLinFit(x, y, seed=123)

# Now the fit() method will be reproducible
my_pwlf.fit(2)
================================================ FILE: docs/genindex.html ================================================ Index — pwlf 2.5.2 documentation ================================================ FILE: docs/how_it_works.html ================================================ How it works — pwlf 2.5.2 documentation

How it works

This paper explains how this library works in detail.

This is based on a formulation of a piecewise linear least squares fit, where the user must specify the location of break points. See this post which goes through the derivation of a least squares regression problem if the break point locations are known. Alternatively check out Golovchenko (2004).

Global optimization is used to find the best location for the user defined number of line segments. I specifically use the differential evolution algorithm in SciPy. I default the differential evolution algorithm to be aggressive, and it is probably overkill for your problem. So feel free to pass your own differential evolution keywords to the library. See this example.

================================================ FILE: docs/index.html ================================================ pwlf: piecewise linear fitting — pwlf 2.5.2 documentation ================================================ FILE: docs/installation.html ================================================ Installation — pwlf 2.5.2 documentation

Installation

Python Package Index (PyPI)

You can now install with pip.

python -m pip install pwlf

Conda

If you have conda, you can also install from conda-forge.

conda install -c conda-forge pwlf

From source

Or clone the repo

git clone https://github.com/cjekel/piecewise_linear_fit_py.git

then install with pip

python -m pip install ./piecewise_linear_fit_py
================================================ FILE: docs/license.html ================================================ License — pwlf 2.5.2 documentation

License

MIT License

Copyright (c) 2017-2020 Charles Jekel

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

================================================ FILE: docs/modules.html ================================================ pwlf — pwlf 2.5.2 documentation ================================================ FILE: docs/pwlf.html ================================================ pwlf package contents — pwlf 2.5.2 documentation

pwlf package contents

pwlf.PiecewiseLinFit(x, y[, disp_res, ...])

class pwlf.PiecewiseLinFit(x, y, disp_res=False, lapack_driver='gelsd', degree=1, weights=None, seed=None)

Bases: object

Methods

assemble_regression_matrix(breaks, x)

Assemble the linear regression matrix A

calc_slopes()

Calculate the slopes of the lines after a piecewise linear function has been fitted.

conlstsq(A[, calc_slopes])

Perform a constrained least squares fit for A matrix.

fit(n_segments[, x_c, y_c, bounds])

Fit a continuous piecewise linear function for a specified number of line segments.

fit_force_points_opt(var)

The objective function to perform a continuous piecewise linear fit for a specified number of breakpoints.

fit_guess(guess_breakpoints[, bounds])

Uses L-BFGS-B optimization to find the location of breakpoints from a guess of where breakpoint locations should be.

fit_with_breaks(breaks)

A function which fits a continuous piecewise linear function for specified breakpoint locations.

fit_with_breaks_force_points(breaks, x_c, y_c)

A function which fits a continuous piecewise linear function for specified breakpoint locations, where you force the fit to go through the data points at x_c and y_c.

fit_with_breaks_opt(var)

The objective function to perform a continuous piecewise linear fit for a specified number of breakpoints.

fitfast(n_segments[, pop, bounds])

Uses multi start LBFGSB optimization to find the location of breakpoints for a given number of line segments by minimizing the sum of the square of the errors.

lstsq(A[, calc_slopes])

Perform the least squares fit for A matrix.

p_values([method, step_size])

Calculate the p-values for each beta parameter.

predict(x[, beta, breaks])

Evaluate the fitted continuous piecewise linear function at untested points.

prediction_variance(x)

Calculate the prediction variance for each specified x location.

r_squared()

Calculate the coefficient of determination ("R squared", R^2) value after a fit has been performed.

standard_errors([method, step_size])

Calculate the standard errors for each beta parameter determined from the piecewise linear fit.

use_custom_opt(n_segments[, x_c, y_c])

Provide the number of line segments you want to use with your custom optimization routine.

assemble_regression_matrix(breaks, x)

Assemble the linear regression matrix A

Parameters:
breaksarray_like

The x locations where each line segment terminates. These are referred to as breakpoints for each line segment. This should be structured as a 1-D numpy array.

xndarray (1-D)

The x locations which the linear regression matrix is assembled on. This must be a numpy array!

Returns:
Andarray (2-D)

The assembled linear regression matrix.

Examples

Assemble the linear regression matrix on the x data for some set of breakpoints.

>>> import pwlf
>>> my_pwlf = pwlf.PiecewiseLinFit(x, y)
>>> breaks = [0.0, 0.5, 1.0]
>>> A = assemble_regression_matrix(breaks, self.x_data)
calc_slopes()

Calculate the slopes of the lines after a piecewise linear function has been fitted.

This will also calculate the y-intercept from each line in the form y = mx + b. The intercepts are stored at self.intercepts.

Returns:
slopesndarray(1-D)

The slope of each ling segment as a 1-D numpy array. This assumes that x[0] <= x[1] <= … <= x[n]. Thus, slopes[0] is the slope of the first line segment.

Examples

Calculate the slopes after performing a simple fit

>>> import pwlf
>>> x = np.linspace(0.0, 1.0, 10)
>>> y = np.random.random(10)
>>> my_pwlf = pwlf.PiecewiseLinFit(x, y)
>>> breaks = my_pwlf.fit(3)
>>> slopes = my_pwlf.calc_slopes()
conlstsq(A, calc_slopes=True)

Perform a constrained least squares fit for A matrix.

Parameters:
Andarray (2-D)

The regression matrix you want to fit in the linear system of equations Ab=y.

calc_slopesboolean, optional

Whether to calculate slopes after performing a fit. Default is calc_slopes=True.

fit(n_segments, x_c=None, y_c=None, bounds=None, **kwargs)

Fit a continuous piecewise linear function for a specified number of line segments. Uses differential evolution to finds the optimum location of breakpoints for a given number of line segments by minimizing the sum of the square error.

Parameters:
n_segmentsint

The desired number of line segments.

x_carray_like, optional

The x locations of the data points that the piecewise linear function will be forced to go through.

y_carray_like, optional

The x locations of the data points that the piecewise linear function will be forced to go through.

boundsarray_like, optional

Bounds for each breakpoint location within the optimization. This should have the shape of (n_segments, 2).

**kwargsoptional

Directly passed into scipy.optimize.differential_evolution(). This will override any pwlf defaults when provided. See Note for more information.

Returns:
fit_breaksfloat

breakpoint locations stored as a 1-D numpy array.

Raises:
ValueError

You probably provided x_c without y_c (or vice versa). You must provide both x_c and y_c if you plan to force the model through data point(s).

ValueError

You can’t specify weights with x_c and y_c.

Notes

All **kwargs are passed into sicpy.optimize.differential_evolution. If any **kwargs is used, it will override my differential_evolution, defaults. This allows advanced users to tweak their own optimization. For me information see: https://github.com/cjekel/piecewise_linear_fit_py/issues/15#issuecomment-434717232

Examples

This example shows you how to fit three continuous piecewise lines to a dataset. This assumes that x is linearly spaced from [0, 1), and y is random.

>>> import pwlf
>>> x = np.linspace(0.0, 1.0, 10)
>>> y = np.random.random(10)
>>> my_pwlf = pwlf.PiecewiseLinFit(x, y)
>>> breaks = my_pwlf.fit(3)

Additionally you desired that the piecewise linear function go through the point (0.0, 0.0).

>>> x_c = [0.0]
>>> y_c = [0.0]
>>> breaks = my_pwlf.fit(3, x_c=x_c, y_c=y_c)

Additionally you desired that the piecewise linear function go through the points (0.0, 0.0) and (1.0, 1.0).

>>> x_c = [0.0, 1.0]
>>> y_c = [0.0, 1.0]
>>> breaks = my_pwlf.fit(3, x_c=x_c, y_c=y_c)
fit_force_points_opt(var)

The objective function to perform a continuous piecewise linear fit for a specified number of breakpoints. This is to be used with a custom optimization routine, and after use_custom_opt has been called.

Use this function if you intend to be force the model through x_c and y_c, while performing a custom optimization.

This was intended for advanced users only. See the following example https://github.com/cjekel/piecewise_linear_fit_py/blob/master/examples/useCustomOptimizationRoutine.py

Parameters:
vararray_like

The breakpoint locations, or variable, in a custom optimization routine.

Returns:
ssrfloat

The sum of square of the residuals.

Raises:
LinAlgError

This typically means your regression problem is ill-conditioned.

Notes

You should run use_custom_opt to initialize necessary object attributes first.

Unlike fit_with_breaks_force_points, fit_force_points_opt automatically assumes that the first and last breakpoints occur at the min and max values of x.

fit_guess(guess_breakpoints, bounds=None, **kwargs)

Uses L-BFGS-B optimization to find the location of breakpoints from a guess of where breakpoint locations should be.

In some cases you may have a good idea where the breakpoint locations occur. It generally won’t be necessary to run a full global optimization to search the entire domain for the breakpoints when you have a good idea where the breakpoints occur. Here a local optimization is run from a guess of the breakpoint locations.

Parameters:
guess_breakpointsarray_like

Guess where the breakpoints occur. This should be a list or numpy array containing the locations where it appears breakpoints occur.

boundsarray_like, optional

Bounds for each breakpoint location within the optimization. This should have the shape of (n_segments, 2).

**kwargsoptional

Directly passed into scipy.optimize.fmin_l_bfgs_b(). This will override any pwlf defaults when provided. See Note for more information.

Returns:
fit_breaksfloat

breakpoint locations stored as a 1-D numpy array.

Notes

All **kwargs are passed into sicpy.optimize.fmin_l_bfgs_b. If any **kwargs is used, it will override my defaults. This allows advanced users to tweak their own optimization. For me information see: https://github.com/cjekel/piecewise_linear_fit_py/issues/15#issuecomment-434717232

You do not need to specify the x.min() or x.max() in geuss_breakpoints!

Examples

In this example we see two distinct linear regions, and we believe a breakpoint occurs at 6.0. We’ll use the fit_guess() function to find the best breakpoint location starting with this guess.

>>> import pwlf
>>> x = np.array([4., 5., 6., 7., 8.])
>>> y = np.array([11., 13., 16., 28.92, 42.81])
>>> my_pwlf = pwlf.PiecewiseLinFit(x, y)
>>> breaks = my_pwlf.fit_guess([6.0])

Note specifying one breakpoint will result in two line segments. If we wanted three line segments, we’ll have to specify two breakpoints.

>>> breaks = my_pwlf.fit_guess([5.5, 6.0])
fit_with_breaks(breaks)

A function which fits a continuous piecewise linear function for specified breakpoint locations.

The function minimizes the sum of the square of the residuals for the x y data.

If you want to understand the math behind this read https://jekel.me/2018/Continous-piecewise-linear-regression/

Other useful resources: http://golovchenko.org/docs/ContinuousPiecewiseLinearFit.pdf https://www.mathworks.com/matlabcentral/fileexchange/40913-piecewise-linear-least-square-fittic http://www.regressionist.com/2018/02/07/continuous-piecewise-linear-fitting/

Parameters:
breaksarray_like

The x locations where each line segment terminates. These are referred to as breakpoints for each line segment. This should be structured as a 1-D numpy array.

Returns:
ssrfloat

Returns the sum of squares of the residuals.

Raises:
LinAlgError

This typically means your regression problem is ill-conditioned.

Examples

If your x data exists from 0 <= x <= 1 and you want three piecewise linear lines where the lines terminate at x = 0.0, 0.3, 0.6, and 1.0. This assumes that x is linearly spaced from [0, 1), and y is random.

>>> import pwlf
>>> x = np.linspace(0.0, 1.0, 10)
>>> y = np.random.random(10)
>>> my_pwlf = pwlf.PiecewiseLinFit(x, y)
>>> breaks = [0.0, 0.3, 0.6, 1.0]
>>> ssr = my_pwlf.fit_with_breaks(breaks)
fit_with_breaks_force_points(breaks, x_c, y_c)

A function which fits a continuous piecewise linear function for specified breakpoint locations, where you force the fit to go through the data points at x_c and y_c.

The function minimizes the sum of the square of the residuals for the pair of x, y data points. If you want to understand the math behind this read https://jekel.me/2018/Force-piecwise-linear-fit-through-data/

Parameters:
breaksarray_like

The x locations where each line segment terminates. These are referred to as breakpoints for each line segment. This should be structured as a 1-D numpy array.

x_carray_like

The x locations of the data points that the piecewise linear function will be forced to go through.

y_carray_like

The x locations of the data points that the piecewise linear function will be forced to go through.

Returns:
Lfloat

Returns the Lagrangian function value. This is the sum of squares of the residuals plus the constraint penalty.

Raises:
LinAlgError

This typically means your regression problem is ill-conditioned.

ValueError

You can’t specify weights with x_c and y_c.

Examples

If your x data exists from 0 <= x <= 1 and you want three piecewise linear lines where the lines terminate at x = 0.0, 0.3, 0.6, and 1.0. This assumes that x is linearly spaced from [0, 1), and y is random. Additionally you desired that the piecewise linear function go through the point (0.0, 0.0)

>>> import pwlf
>>> x = np.linspace(0.0, 1.0, 10)
>>> y = np.random.random(10)
>>> x_c = [0.0]
>>> y_c = [0.0]
>>> my_pwlf = pwlf.PiecewiseLinFit(x, y)
>>> breaks = [0.0, 0.3, 0.6, 1.0]
>>> L = my_pwlf.fit_with_breaks_force_points(breaks, x_c, y_c)
fit_with_breaks_opt(var)

The objective function to perform a continuous piecewise linear fit for a specified number of breakpoints. This is to be used with a custom optimization routine, and after use_custom_opt has been called.

This was intended for advanced users only.

See the following example https://github.com/cjekel/piecewise_linear_fit_py/blob/master/examples/useCustomOptimizationRoutine.py

Parameters:
vararray_like

The breakpoint locations, or variable, in a custom optimization routine.

Returns:
ssrfloat

The sum of square of the residuals.

Raises:
LinAlgError

This typically means your regression problem is ill-conditioned.

Notes

You should run use_custom_opt to initialize necessary object attributes first.

Unlike fit_with_breaks, fit_with_breaks_opt automatically assumes that the first and last breakpoints occur at the min and max values of x.

fitfast(n_segments, pop=2, bounds=None, **kwargs)

Uses multi start LBFGSB optimization to find the location of breakpoints for a given number of line segments by minimizing the sum of the square of the errors.

The idea is that we generate n random latin hypercube samples and run LBFGSB optimization on each one. This isn’t guaranteed to find the global optimum. It’s suppose to be a reasonable compromise between speed and quality of fit. Let me know how it works.

Since this is based on random sampling, you might want to run it multiple times and save the best version… The best version will have the lowest self.ssr (sum of square of residuals).

There is no guarantee that this will be faster than fit(), however you may find it much faster sometimes.

Parameters:
n_segmentsint

The desired number of line segments.

popint, optional

The number of latin hypercube samples to generate. Default pop=2.

boundsarray_like, optional

Bounds for each breakpoint location within the optimization. This should have the shape of (n_segments, 2).

**kwargsoptional

Directly passed into scipy.optimize.fmin_l_bfgs_b(). This will override any pwlf defaults when provided. See Note for more information.

Returns:
fit_breaksfloat

breakpoint locations stored as a 1-D numpy array.

Notes

The default number of multi start optimizations is 2.
  • Decreasing this number will result in a faster run time.

  • Increasing this number will improve the likelihood of finding

    good results

  • You can specify the number of starts using the following call

  • Minimum value of pop is 2

All **kwargs are passed into sicpy.optimize.fmin_l_bfgs_b. If any **kwargs is used, it will override my defaults. This allows advanced users to tweak their own optimization. For me information see: https://github.com/cjekel/piecewise_linear_fit_py/issues/15#issuecomment-434717232

Examples

This example shows you how to fit three continuous piecewise lines to a dataset. This assumes that x is linearly spaced from [0, 1), and y is random.

>>> import pwlf
>>> x = np.linspace(0.0, 1.0, 10)
>>> y = np.random.random(10)
>>> my_pwlf = pwlf.PiecewiseLinFit(x, y)
>>> breaks = my_pwlf.fitfast(3)

You can change the number of latin hypercube samples (or starting point, locations) to use with pop. The following example will use 50 samples.

>>> breaks = my_pwlf.fitfast(3, pop=50)
lstsq(A, calc_slopes=True)

Perform the least squares fit for A matrix.

Parameters:
Andarray (2-D)

The regression matrix you want to fit in the linear system of equations Ab=y.

calc_slopesboolean, optional

Whether to calculate slopes after performing a fit. Default is calc_slopes=True.

p_values(method='linear', step_size=0.0001)

Calculate the p-values for each beta parameter.

This calculates the p-values for the beta parameters under the assumption that your breakpoint locations are known. Section 2.4.2 of [2] defines how to calculate the p-value of individual parameters. This is really a marginal test since each parameter is dependent upon the other parameters.

These values are typically compared to some confidence level alpha for significance. A 95% confidence level would have alpha = 0.05.

Parameters:
methodstring, optional

Calculate the standard errors for a linear or non-linear regression problem. The default is method=’linear’. A taylor- series expansion is performed when method=’non-linear’ (which is commonly referred to as the Delta method).

step_sizefloat, optional

The step size to perform forward differences for the taylor- series expansion when method=’non-linear’. Default is step_size=1e-4.

Returns:
pndarray (1-D)

p-values for each beta parameter where p-value[0] corresponds to beta[0] and so forth

Raises:
AttributeError

You have probably not performed a fit yet.

ValueError

You supplied an unsupported method.

Notes

The linear regression problem is when you know the breakpoint locations (e.g. when using the fit_with_breaks function).

The non-linear regression problem is when you don’t know the breakpoint locations (e.g. when using the fit, fitfast, and fit_guess functions).

See https://github.com/cjekel/piecewise_linear_fit_py/issues/14

References

[2]

Myers RH, Montgomery DC, Anderson-Cook CM. Response surface methodology . Hoboken. New Jersey: John Wiley & Sons, Inc. 2009;20:38-44.

Examples

After performing a fit, one can calculate the p-value for each beta parameter

>>> import pwlf
>>> x = np.linspace(0.0, 1.0, 10)
>>> y = np.random.random(10)
>>> my_pwlf = pwlf.PiecewiseLinFit(x, y)
>>> breaks = my_pwlf.fitfast(3)
>>> x_new = np.linspace(0.0, 1.0, 100)
>>> p = my_pwlf.p_values(x_new)

see also examples/standard_errrors_and_p-values.py

predict(x, beta=None, breaks=None)

Evaluate the fitted continuous piecewise linear function at untested points.

You can manfully specify the breakpoints and calculated values for beta if you want to quickly predict from different models and the same data set.

Parameters:
xarray_like

The x locations where you want to predict the output of the fitted continuous piecewise linear function.

betanone or ndarray (1-D), optional

The model parameters for the continuous piecewise linear fit. Default is None.

breaksnone or array_like, optional

The x locations where each line segment terminates. These are referred to as breakpoints for each line segment. This should be structured as a 1-D numpy array. Default is None.

Returns:
y_hatndarray (1-D)

The predicted values at x.

Examples

Fits a simple model, then predict at x_new locations which are linearly spaced.

>>> import pwlf
>>> x = np.linspace(0.0, 1.0, 10)
>>> y = np.random.random(10)
>>> my_pwlf = pwlf.PiecewiseLinFit(x, y)
>>> breaks = [0.0, 0.3, 0.6, 1.0]
>>> ssr = my_pwlf.fit_with_breaks(breaks)
>>> x_new = np.linspace(0.0, 1.0, 100)
>>> yhat = my_pwlf.predict(x_new)
prediction_variance(x)

Calculate the prediction variance for each specified x location. The prediction variance is the uncertainty of the model due to the lack of data. This can be used to find a 95% confidence interval of possible piecewise linear models based on the current data. This would be done typically as y_hat +- 1.96*np.sqrt(pre_var). The prediction_variance needs to be calculated at various x locations. For more information see: www2.mae.ufl.edu/haftka/vvuq/lectures/Regression-accuracy.pptx

Parameters:
xarray_like

The x locations where you want the prediction variance from the fitted continuous piecewise linear function.

Returns:
pre_varndarray (1-D)

Numpy array (floats) of prediction variance at each x location.

Raises:
AttributeError

You have probably not performed a fit yet.

LinAlgError

This typically means your regression problem is ill-conditioned.

Notes

This assumes that your breakpoint locations are exact! and does not consider the uncertainty with your breakpoint locations.

Examples

Calculate the prediction variance at x_new after performing a simple fit.

>>> import pwlf
>>> x = np.linspace(0.0, 1.0, 10)
>>> y = np.random.random(10)
>>> my_pwlf = pwlf.PiecewiseLinFit(x, y)
>>> breaks = my_pwlf.fitfast(3)
>>> x_new = np.linspace(0.0, 1.0, 100)
>>> pre_var = my_pwlf.prediction_variance(x_new)

see also examples/prediction_variance.py

r_squared()

Calculate the coefficient of determination (“R squared”, R^2) value after a fit has been performed. For more information see: https://en.wikipedia.org/wiki/Coefficient_of_determination

Returns:
rsqfloat

Coefficient of determination, or ‘R squared’ value.

Raises:
AttributeError

You have probably not performed a fit yet.

LinAlgError

This typically means your regression problem is ill-conditioned.

Examples

Calculate the R squared value after performing a simple fit.

>>> import pwlf
>>> x = np.linspace(0.0, 1.0, 10)
>>> y = np.random.random(10)
>>> my_pwlf = pwlf.PiecewiseLinFit(x, y)
>>> breaks = my_pwlf.fitfast(3)
>>> rsq = my_pwlf.r_squared()
standard_errors(method='linear', step_size=0.0001)

Calculate the standard errors for each beta parameter determined from the piecewise linear fit. Typically +- 1.96*se will yield the center of a 95% confidence region around your parameters. This assumes the parmaters follow a normal distribution. For more information see: https://en.wikipedia.org/wiki/Standard_error

This calculation follows the derivation provided in [1].

Parameters:
methodstring, optional

Calculate the standard errors for a linear or non-linear regression problem. The default is method=’linear’. A taylor- series expansion is performed when method=’non-linear’ (which is commonly referred to as the Delta method).

step_sizefloat, optional

The step size to perform forward differences for the taylor- series expansion when method=’non-linear’. Default is step_size=1e-4.

Returns:
sendarray (1-D)

Standard errors associated with each beta parameter. Specifically se[0] correspounds to the standard error for beta[0], and so forth.

Raises:
AttributeError

You have probably not performed a fit yet.

ValueError

You supplied an unsupported method.

LinAlgError

This typically means your regression problem is ill-conditioned.

Notes

The linear regression problem is when you know the breakpoint locations (e.g. when using the fit_with_breaks function).

The non-linear regression problem is when you don’t know the breakpoint locations (e.g. when using the fit, fitfast, and fit_guess functions).

References

[1]

Coppe, A., Haftka, R. T., and Kim, N. H., “Uncertainty Identification of Damage Growth Parameters Using Nonlinear Regression,” AIAA Journal, Vol. 49, No. 12, dec 2011, pp. 2818–2821.

Examples

Calculate the standard errors after performing a simple fit.

>>> import pwlf
>>> x = np.linspace(0.0, 1.0, 10)
>>> y = np.random.random(10)
>>> my_pwlf = pwlf.PiecewiseLinFit(x, y)
>>> breaks = my_pwlf.fitfast(3)
>>> se = my_pwlf.standard_errors()
use_custom_opt(n_segments, x_c=None, y_c=None)

Provide the number of line segments you want to use with your custom optimization routine.

Run this function first to initialize necessary attributes!!!

This was intended for advanced users only.

See the following example https://github.com/cjekel/piecewise_linear_fit_py/blob/master/examples/useCustomOptimizationRoutine.py

Parameters:
n_segmentsint

The x locations where each line segment terminates. These are referred to as breakpoints for each line segment. This should be structured as a 1-D numpy array.

x_cnone or array_like, optional

The x locations of the data points that the piecewise linear function will be forced to go through.

y_cnone or array_like, optional

The x locations of the data points that the piecewise linear function will be forced to go through.

Raises:
ValueError

You can’t specify weights with x_c and y_c.

Notes

Optimize fit_with_breaks_opt(var) where var is a 1D array containing the x locations of your variables var has length n_segments - 1, because the two breakpoints are always defined (1. the min of x, 2. the max of x).

fit_with_breaks_opt(var) will return the sum of the square of the residuals which you’ll want to minimize with your optimization routine.

================================================ FILE: docs/requirements.html ================================================ Requirements — pwlf 2.5.2 documentation

Requirements

NumPy (>= 1.14.0)

SciPy (>= 1.2.0)

pyDOE ( >= 0.3.8)

================================================ FILE: docs/search.html ================================================ Search — pwlf 2.5.2 documentation

Search

Searching for multiple words only shows matches that contain all words.

================================================ FILE: docs/searchindex.js ================================================ Search.setIndex({"alltitles": {"About": [[0, null]], "Conda": [[4, "conda"]], "Examples": [[1, null]], "From source": [[4, "from-source"]], "How it works": [[2, null]], "Indices and tables": [[3, "indices-and-tables"]], "Installation": [[4, null]], "License": [[5, null]], "Python Package Index (PyPI)": [[4, "python-package-index-pypi"]], "Requirements": [[8, null]], "bad fits when you have more unknowns than data": [[1, "bad-fits-when-you-have-more-unknowns-than-data"]], "find the best number of line segments": [[1, "find-the-best-number-of-line-segments"]], "fit constants or polynomials": [[1, "fit-constants-or-polynomials"]], "fit for specified number of line segments": [[1, "fit-for-specified-number-of-line-segments"]], "fit with a breakpoint guess": [[1, "fit-with-a-breakpoint-guess"]], "fit with known breakpoint locations": [[1, "fit-with-known-breakpoint-locations"]], "fitfast for specified number of line segments": [[1, "fitfast-for-specified-number-of-line-segments"]], "force a fit through data points": [[1, "force-a-fit-through-data-points"]], "get the linear regression matrix": [[1, "get-the-linear-regression-matrix"]], "model persistence": [[1, "model-persistence"]], "non-linear standard errors and p-values": [[1, "non-linear-standard-errors-and-p-values"]], "obtain the equations of fitted pwlf": [[1, "obtain-the-equations-of-fitted-pwlf"]], "pass differential evolution keywords": [[1, "pass-differential-evolution-keywords"]], "pwlf": [[6, null]], "pwlf package contents": [[7, null]], "pwlf.PiecewiseLinFit": [[9, null]], "pwlf: piecewise linear fitting": [[3, null]], "reproducible results": [[1, "reproducible-results"]], "specify breakpoint bounds": [[1, "specify-breakpoint-bounds"]], "use custom optimization routine": [[1, "use-custom-optimization-routine"]], "use of tensorflow": [[1, "use-of-tensorflow"]], "weighted least squares fit": [[1, "weighted-least-squares-fit"]]}, "docnames": ["about", "examples", "how_it_works", "index", "installation", "license", "modules", "pwlf", "requirements", "stubs/pwlf.PiecewiseLinFit"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.intersphinx": 1}, "filenames": ["about.rst", "examples.rst", "how_it_works.rst", "index.rst", "installation.rst", "license.rst", "modules.rst", "pwlf.rst", "requirements.rst", "stubs/pwlf.PiecewiseLinFit.rst"], "indexentries": {"__init__() (pwlf.piecewiselinfit method)": [[9, "pwlf.PiecewiseLinFit.__init__", false]], "assemble_regression_matrix() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.assemble_regression_matrix", false]], "calc_slopes() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.calc_slopes", false]], "conlstsq() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.conlstsq", false]], "fit() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.fit", false]], "fit_force_points_opt() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.fit_force_points_opt", false]], "fit_guess() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.fit_guess", false]], "fit_with_breaks() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.fit_with_breaks", false]], "fit_with_breaks_force_points() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.fit_with_breaks_force_points", false]], "fit_with_breaks_opt() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.fit_with_breaks_opt", false]], "fitfast() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.fitfast", false]], "lstsq() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.lstsq", false]], "p_values() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.p_values", false]], "piecewiselinfit (class in pwlf)": [[7, "pwlf.PiecewiseLinFit", false], [9, "pwlf.PiecewiseLinFit", false]], "predict() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.predict", false]], "prediction_variance() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.prediction_variance", false]], "r_squared() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.r_squared", false]], "standard_errors() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.standard_errors", false]], "use_custom_opt() (pwlf.piecewiselinfit method)": [[7, "pwlf.PiecewiseLinFit.use_custom_opt", false]]}, "objects": {"pwlf": [[9, 0, 1, "", "PiecewiseLinFit"]], "pwlf.PiecewiseLinFit": [[9, 1, 1, "", "__init__"], [7, 1, 1, "", "assemble_regression_matrix"], [7, 1, 1, "", "calc_slopes"], [7, 1, 1, "", "conlstsq"], [7, 1, 1, "", "fit"], [7, 1, 1, "", "fit_force_points_opt"], [7, 1, 1, "", "fit_guess"], [7, 1, 1, "", "fit_with_breaks"], [7, 1, 1, "", "fit_with_breaks_force_points"], [7, 1, 1, "", "fit_with_breaks_opt"], [7, 1, 1, "", "fitfast"], [7, 1, 1, "", "lstsq"], [7, 1, 1, "", "p_values"], [7, 1, 1, "", "predict"], [7, 1, 1, "", "prediction_variance"], [7, 1, 1, "", "r_squared"], [7, 1, 1, "", "standard_errors"], [7, 1, 1, "", "use_custom_opt"]]}, "objnames": {"0": ["py", "class", "Python class"], "1": ["py", "method", "Python method"]}, "objtypes": {"0": "py:class", "1": "py:method"}, "terms": {"": [1, 7], "0": [1, 7, 8, 9], "00": 1, "00000000e": 1, "0001": 7, "00086000e": 1, "001": 1, "00538634182565454": 1, "00926700e": 1, "00936330e": 1, "01": 1, "01254006e": 1, "01980300e": 1, "02": [1, 7], "02039790e": 1, "02132890e": 1, "02345260e": 1, "02580042e": 1, "03": 1, "03016230425747": 1, "03581990e": 1, "03872000e": 1, "039": 1, "046": 1, "04787860e": 1, "05": [1, 7], "06": 1, "07": 7, "07655920e": 1, "07961810e": 1, "1": [1, 7, 8, 9], "10": [1, 7], "100": [1, 7], "1000": 1, "10000": 1, "1000000": 1, "11": [1, 7], "111181494": 1, "11688258e": 1, "12": [1, 7], "12089850e": 1, "12121": 1, "123": 1, "12600000e": 1, "13": [1, 7], "14": [7, 8], "14750770e": 1, "15": 7, "15706975e": 1, "159": 1, "16": [1, 7], "162": 1, "16270210e": 1, "16388310e": 1, "165": 1, "16531753e": 1, "166901856": 1, "168954500": 1, "16951050e": 1, "17305210e": 1, "17617000e": 1, "1763191476": 1, "18": 1, "18679400e": 1, "19": 1, "19011260e": 1, "1d": [0, 7], "1e": [1, 7], "2": [1, 7, 8, 9], "20": 7, "2004": 2, "2009": 7, "2011": 7, "2017": 5, "2018": 7, "2019": 0, "2020": 5, "20351520e": 1, "20605073e": 1, "21390666e": 1, "22": 1, "22120350e": 1, "22767939e": 1, "242": 1, "25615100e": 1, "26063930e": 1, "26413070e": 1, "28": [1, 7], "2818": 7, "2821": 7, "28642800e": 1, "3": [1, 7, 8], "30": 1, "30997400e": 1, "31196948e": 1, "31279040e": 1, "31543400e": 1, "34180360e": 1, "34184100e": 1, "34390200e": 1, "34620080e": 1, "34831790e": 1, "3508513333073": 1, "36687642e": 1, "36931660e": 1, "37038283e": 1, "37279600e": 1, "37442010e": 1, "38": 7, "38356810e": 1, "39052750e": 1, "397": 1, "39709150e": 1, "4": [1, 7], "40": 1, "40913": 7, "41374060e": 1, "41725010e": 1, "41776550e": 1, "41881288e": 1, "42": [1, 7], "421": 1, "42172312e": 1, "427": 1, "42877590e": 1, "434717232": 7, "44": [1, 7], "44519970e": 1, "45343950e": 1, "45437090e": 1, "46404554": 1, "48218236957122": 1, "482311769": 1, "48629710e": 1, "49": 7, "493": 1, "49429370e": 1, "5": [1, 7], "50": [1, 7], "50144640e": 1, "50958760e": 1, "5102742956827": 1, "51085900e": 1, "51858250e": 1, "5306546317065": 1, "53545600e": 1, "537785803": 1, "54628780e": 1, "54941000e": 1, "55253360e": 1, "55374770e": 1, "55892510e": 1, "55907760e": 1, "56706510e": 1, "5772216545711": 1, "58600000e": 1, "59": 1, "59461900e": 1, "59556700e": 1, "59627540e": 1, "6": [1, 7], "61442590e": 1, "62618058e": 1, "63321350e": 1, "63404300e": 1, "63539960e": 1, "63739040e": 1, "64433570e": 1, "646": 1, "65299640e": 1, "65610200e": 1, "66106800e": 1, "68092100e": 1, "69747505830914": 1, "69801700e": 1, "7": [1, 7], "70363280e": 1, "720785454735": 1, "74104940e": 1, "75": 1, "76596300e": 1, "77756110e": 1, "78542550e": 1, "79942720e": 1, "8": [1, 7, 8], "80525800e": 1, "81": [1, 7], "81388400e": 1, "82": 1, "821": 1, "82125120e": 1, "82678000e": 1, "84458400e": 1, "85494500e": 1, "873": 1, "88450940e": 1, "885": 1, "89945177490653": 1, "9": 1, "91016100e": 1, "92": [1, 7], "926850298824217": 1, "93753841": 1, "94350340e": 1, "949735350431857": 1, "95": [1, 7], "951561315686298": 1, "953964059782599": 1, "95549800e": 1, "96": 7, "9824424358344": 1, "99": 1, "A": [0, 1, 5, 7], "AND": 5, "AS": 5, "BE": 5, "BUT": 5, "FOR": 5, "For": [1, 7, 9], "IN": 5, "If": [1, 4, 7], "In": [1, 7], "It": 7, "NO": 5, "NOT": 5, "No": 7, "OF": 5, "OR": 5, "One": 1, "Or": 4, "THE": 5, "TO": 5, "The": [1, 5, 7, 9], "There": 7, "These": [1, 7], "To": 1, "WITH": 5, "__future__": 1, "__init__": [6, 9], "ab": [1, 7], "about": [1, 3], "abov": 5, "absolut": 1, "accuraci": 7, "action": 5, "add": 1, "addition": 7, "advanc": 7, "after": [7, 9], "aggress": 2, "aiaa": 7, "algorithm": 2, "all": [0, 1, 5, 7], "allow": [7, 9], "alpha": 7, "also": [4, 7], "altern": [1, 2], "alwai": 7, "an": [1, 5, 7, 9], "anderson": 7, "ani": [5, 7], "appear": 7, "append": 1, "applic": 1, "ar": [1, 2, 7, 9], "arang": 1, "arbitrarili": 1, "argument": 1, "aris": 5, "around": [1, 7], "arrai": [1, 7, 9], "array_lik": [7, 9], "assembl": [1, 7], "assemble_regression_matrix": [1, 6, 7], "associ": [5, 7, 9], "assum": [1, 7, 9], "assumpt": 7, "atol": 1, "attribut": [7, 9], "attributeerror": 7, "author": [0, 5], "automat": [1, 7], "axi": 1, "b": 7, "backward": 1, "bad": 3, "base": [1, 2, 7], "bayesian": 1, "bayesianoptim": 1, "becaus": 7, "been": [7, 9], "behavior": 9, "behind": 7, "believ": [1, 7], "benchmark": 1, "best": [2, 3, 7], "beta": [1, 7, 9], "beta_index": 1, "better": 1, "between": [1, 7], "bfg": 7, "blob": 7, "blog": 1, "bool": 9, "boolean": 7, "both": 7, "bound": [3, 7], "break": [1, 2, 7, 9], "break_0": 9, "break_loc": 1, "break_n": 9, "breakpoint": [3, 7, 9], "c": [4, 5], "c_n": 9, "calc_slop": [6, 7, 9], "calcul": [1, 7, 9], "call": [7, 9], "can": [1, 4, 7], "candid": 1, "care": 1, "case": [1, 7], "center": 7, "chang": [0, 7], "changelog": 0, "charg": 5, "charl": [0, 5], "check": 2, "choleski": 1, "choos": 1, "cite": 0, "cjekel": [0, 4, 7], "claim": 5, "class": [1, 7, 9], "clone": 4, "cm": 7, "code": 1, "coef_": 1, "coeffici": [7, 9], "coefficient_of_determin": 7, "com": [0, 4, 7], "commonli": 7, "compar": [1, 7], "complic": 1, "compromis": 7, "comput": 1, "concaten": 1, "conda": 3, "condit": [5, 7], "confid": 7, "conlstsq": [6, 7], "connect": 5, "consid": 7, "constant": [3, 9], "constrain": 7, "constraint": [7, 9], "contain": 7, "content": [3, 6], "contin": 7, "continu": [0, 1, 7, 9], "continuouspiecewiselinearfit": 7, "contract": 5, "control": 1, "cook": 7, "copi": [1, 5], "copp": 7, "copyright": 5, "correct": 1, "correspond": [1, 7, 9], "correspound": [7, 9], "cpickl": 1, "creat": 1, "current": 7, "custom": [3, 7, 9], "cv": 1, "d": [7, 9], "damag": [5, 7], "data": [0, 3, 7, 9], "dataset": 7, "dc": 7, "de": 1, "deal": 5, "dec": 7, "decim": 1, "decomposit": 1, "decreas": 7, "def": 1, "default": [1, 2, 7, 9], "defin": [1, 2, 7], "degre": [1, 7, 9], "delta": [1, 7], "depend": [1, 7, 9], "deriv": [2, 7], "desir": [0, 1, 7], "detail": [1, 2], "determin": [1, 7, 9], "deviat": [1, 9], "differ": [1, 7], "differenti": [2, 3, 7], "differential_evolut": 7, "dimension": 9, "directli": [1, 7], "discret": 1, "disp_r": [7, 9], "distinct": [1, 7], "distribut": [5, 7], "do": [1, 5, 7], "doc": [7, 9], "document": 5, "doe": [1, 7], "domain": [1, 7], "don": [1, 7], "done": 7, "driver": 9, "dtype": 1, "due": 7, "dump": 1, "e": 7, "each": [1, 7, 9], "easi": 1, "edu": 7, "ego": 1, "elast": 1, "elasticnetcv": 1, "els": 1, "en": 7, "en_model": 1, "end": 1, "entir": [1, 7], "enumer": 1, "eqn_list": 1, "equat": [3, 7], "equival": 9, "error": [3, 7, 9], "evalu": [7, 9], "event": 5, "everi": 1, "everytim": 1, "evolut": [2, 3, 7], "exact": 7, "exact_fev": 1, "exampl": [0, 2, 3, 7, 9], "except": [1, 9], "exist": 7, "expans": [1, 7], "explain": 2, "express": 5, "extra": 1, "f": [0, 1], "f_list": 1, "fals": [1, 7, 9], "fast": 1, "faster": [1, 7, 9], "favorit": 1, "feasibl": 1, "feel": 2, "figur": 1, "file": 5, "fileexchang": 7, "find": [2, 3, 7], "first": [1, 7, 9], "fit": [0, 2, 5, 6, 7, 9], "fit_break": [1, 7, 9], "fit_force_points_opt": [6, 7, 9], "fit_guess": [1, 6, 7], "fit_intercept": 1, "fit_with_break": [1, 6, 7, 9], "fit_with_breaks_force_point": [6, 7, 9], "fit_with_breaks_opt": [1, 6, 7, 9], "fitfast": [3, 6, 7, 9], "fittic": 7, "flatten": 1, "float": [7, 9], "float32": 1, "float64": 1, "fmin_l_bfgs_b": 7, "follow": [1, 5, 7], "forc": [3, 7, 9], "forg": 4, "form": [1, 7], "formul": 2, "forth": [7, 9], "forward": [1, 7], "found": 1, "four": 1, "free": [2, 5], "from": [1, 3, 5, 7, 9], "full": 7, "function": [0, 1, 3, 7, 9], "furnish": 5, "fx_opt": 1, "g": 7, "gelsd": [7, 9], "gelsi": 9, "gelss": 9, "gener": [1, 7, 9], "gerhard": 0, "get": 3, "get_symbolic_eqn": 1, "geuss_breakpoint": 7, "git": 4, "github": [0, 4, 7], "give": 1, "given": [1, 7], "global": [1, 2, 7, 9], "go": [1, 7, 9], "goe": 2, "golovchenko": [2, 7], "good": [1, 7], "gp": 1, "gpyopt": 1, "grab": 1, "gradient": 1, "grant": 5, "growth": 7, "guarante": 7, "guess": [3, 7], "guess_breakpoint": 7, "h": 7, "ha": [7, 9], "haftka": 7, "have": [3, 4, 7], "header": 1, "help": 0, "here": [1, 7], "herebi": 5, "heteroscedast": 1, "high": 1, "higher": 1, "highest_protocol": 1, "hoboken": 7, "holder": 5, "how": [1, 3, 7], "howev": [1, 7], "html": 9, "http": [0, 4, 7, 9], "hypercub": 7, "i": [1, 2, 5, 7, 9], "idea": [1, 7], "ident": 1, "identif": 7, "ie": 1, "ill": 7, "impli": 5, "import": [1, 7, 9], "improv": 7, "inc": 7, "includ": 5, "increas": 7, "independ": 9, "index": 3, "individu": [1, 7, 9], "inform": [1, 7], "init": [1, 9], "initi": [1, 7, 9], "initial_design_numdata": 1, "initial_design_typ": 1, "input": 9, "instal": [1, 3], "int": [7, 9], "integ": 9, "intend": 7, "intercept": [1, 7, 9], "interest": 1, "interior": 1, "interv": 7, "isn": 7, "issu": 7, "issuecom": 7, "ith": [1, 9], "j": 1, "jekel": [0, 5, 7], "jersei": 7, "joblib": 1, "john": 7, "journal": 7, "jupyt": 1, "just": [0, 1], "k": 1, "keyword": [2, 3], "kim": 7, "kind": 5, "know": [1, 7], "known": [2, 3, 7, 9], "kwarg": [7, 9], "l": [1, 7], "l1_ratio": 1, "label": 1, "lack": 7, "lagrangian": 7, "lambdifi": 1, "lapack": 9, "lapack_driv": [1, 7, 9], "larg": 9, "largest": 9, "last": 7, "latin": [1, 7], "lbfgsb": 7, "least": [2, 3, 7, 9], "lectur": 7, "left": 9, "legend": 1, "len": 9, "length": 7, "less": 1, "let": [1, 7], "level": 7, "liabil": 5, "liabl": 5, "librari": [0, 2, 9], "licens": 3, "like": 1, "likelihood": 7, "limit": 5, "linalg": 9, "linalgerror": 7, "line": [0, 2, 3, 7, 9], "linear": [0, 2, 7, 9], "linear_model": 1, "linearli": 7, "ling": [7, 9], "linspac": [1, 7], "list": [7, 9], "ll": [1, 7, 9], "load": 1, "local": 7, "locat": [2, 3, 7, 9], "low": 1, "lower": [1, 9], "lowest": 7, "lstsq": [6, 7, 9], "lug": 9, "m": 4, "mae": 7, "mai": [1, 7, 9], "make": 9, "manfulli": 7, "mani": 1, "manual": [0, 1], "margin": 7, "master": 7, "math": 7, "mathemat": 1, "mathwork": 7, "matlabcentr": 7, "matplotlib": 1, "matrix": [3, 7], "max": [1, 7], "max_it": 1, "md": 0, "me": 7, "mean": [1, 7], "merchant": 5, "merg": 5, "method": [1, 7, 9], "methodologi": 7, "middl": 9, "might": 7, "min": [1, 7], "mind": 1, "minim": [1, 7], "minimum": 7, "mit": 5, "mix": 9, "model": [3, 7, 9], "model_typ": 1, "modifi": 5, "montgomeri": 7, "more": [3, 7, 9], "most": 9, "much": [1, 7], "multi": [1, 7], "multipl": [1, 7], "must": [1, 2, 7, 9], "mx": 7, "my": 7, "my_eqn": 1, "my_fit": 1, "my_obj": 1, "my_pwlf": [1, 7, 9], "my_pwlf_0": 1, "my_pwlf_1": 1, "my_pwlf_2": 1, "my_pwlf_en": 1, "my_pwlf_gen": 1, "my_pwlf_w": 1, "mybopt": 1, "myer": 7, "mypwlf": 1, "n": [1, 7, 9], "n_data": [1, 9], "n_data_set": 1, "n_job": 1, "n_paramet": 9, "n_segment": [1, 7, 9], "name": 1, "ndarrai": [7, 9], "nearli": 1, "necessari": [1, 7, 9], "need": [1, 7], "neg": 1, "net": 1, "netlib": 9, "new": [7, 9], "node27": 9, "nois": 1, "non": [3, 7], "none": [7, 9], "noninfring": 5, "nonlinear": 7, "normal": [1, 7], "note": [1, 7, 9], "notebook": 1, "notic": 5, "now": [0, 1, 4], "np": [1, 7], "num": 1, "number": [0, 2, 3, 7, 9], "number_of_line_seg": 1, "numpi": [1, 7, 8, 9], "nvar": 9, "o": 1, "object": [1, 7, 9], "object_": 1, "obtain": [3, 5], "occur": [1, 7], "one": [1, 7, 9], "onli": [7, 9], "open": 1, "opt": 1, "optim": [2, 3, 7, 9], "optimum": [1, 7], "option": [1, 7, 9], "order": 1, "ordinari": 1, "org": [7, 9], "origin": 1, "orthogon": 1, "other": [1, 5, 7], "otherwis": 5, "out": [1, 2, 5], "outdat": 1, "output": 7, "over": [1, 9], "overal": 1, "overkil": 2, "overrid": 7, "own": [2, 7], "p": [3, 7], "p_valu": [1, 6, 7], "packag": [3, 6], "page": 3, "pair": 7, "paper": [1, 2], "paramet": [1, 7, 9], "parmat": 7, "particular": [1, 5], "pass": [2, 3, 7], "pdf": 7, "penalti": [1, 7], "perform": [1, 7, 9], "permiss": 5, "permit": 5, "persist": 3, "person": 5, "pi": 1, "pick": [1, 9], "pickl": 1, "piecewis": [0, 1, 2, 7, 9], "piecewise_linear_fit_pi": [0, 4, 7], "piecewiselinfit": [1, 3, 6, 7], "piecewiselinfittf": 1, "piecwis": 7, "pip": 4, "pkl": 1, "plan": 7, "pleas": [0, 1], "plot": 1, "plot_acquisit": 1, "plot_converg": 1, "plt": 1, "plu": 7, "point": [2, 3, 7, 9], "polynomi": [3, 9], "pop": [1, 7, 9], "portion": 5, "posit": 1, "possibl": [1, 7], "post": [1, 2], "pp": 7, "pptx": 7, "pre_var": 7, "predict": [1, 6, 7, 9], "prediction_vari": [6, 7, 9], "print": [1, 9], "print_funct": 1, "prior": 9, "probabl": [2, 7], "problem": [1, 2, 7, 9], "program": 1, "provid": [0, 5, 7, 9], "public": 0, "publish": 5, "purpos": 5, "pwlf": [0, 4], "pwlf_": 1, "pwlftf": 1, "py": 7, "pydo": 8, "pypi": 3, "pyplot": 1, "python": [0, 1, 3], "quadrat": 1, "qualiti": [7, 9], "quickli": 7, "r": [7, 9], "r_squar": [6, 7, 9], "rais": [1, 7], "random": [1, 7, 9], "rang": 1, "rb": 1, "re": 1, "read": [7, 9], "realli": 7, "reason": [1, 7], "reciproc": [1, 9], "refer": [7, 9], "region": [1, 7], "regress": [2, 3, 7], "regressionist": 7, "regular": 1, "reli": 9, "repo": 4, "repres": 1, "reproduc": [3, 9], "requir": [1, 3], "res0": 1, "res1": 1, "res2": 1, "research": 0, "residu": [1, 7], "resourc": 7, "respons": 7, "restrict": 5, "result": [3, 7, 9], "return": [1, 7], "rh": 7, "right": [5, 9], "routin": [3, 7, 9], "row": 1, "rsq": 7, "run": [1, 7], "run_optim": 1, "same": [1, 7, 9], "sampl": 7, "save": [1, 7], "scipi": [1, 2, 7, 8, 9], "se": [1, 7, 9], "search": [1, 3, 7], "second": 1, "section": 7, "see": [1, 2, 7, 9], "seed": [1, 7, 9], "segment": [0, 2, 3, 7, 9], "segment_numb": 1, "select": 1, "self": 7, "sell": 5, "sep": 1, "seri": [1, 7], "set": [1, 7, 9], "shall": 5, "shape": [1, 7], "should": [1, 7, 9], "show": [1, 7], "sicpi": 7, "sigma_chang": 1, "signific": 7, "simpl": 7, "simplifi": 1, "sin": 1, "sinc": 7, "sine": 1, "size": [1, 7], "sklearn": 1, "slope": [1, 7, 9], "small": 1, "smallest": 9, "so": [1, 2, 5, 7, 9], "softwar": 5, "solv": 9, "some": [1, 7], "someth": 1, "sometim": [1, 7], "son": 7, "sourc": 3, "space": [1, 7], "special": 9, "specif": [1, 2, 7, 9], "specifi": [0, 2, 3, 7, 9], "speed": 7, "sqrt": 7, "squar": [2, 3, 7, 9], "ssr": [1, 7, 9], "standard": [3, 7, 9], "standard_error": [6, 7, 9], "standard_errrors_and_p": 7, "start": [1, 7], "std": 1, "step": 7, "step_siz": [1, 7], "stochast": [1, 9], "store": [0, 7, 9], "str": 9, "string": 7, "structur": 7, "subject": 5, "sublicens": 5, "substanti": 5, "sum": [1, 7, 9], "suppli": [7, 9], "support": 9, "suppos": 7, "surfac": 7, "switch": 1, "symbol": 1, "sympi": 1, "system": 7, "t": [1, 7, 9], "tabl": 1, "taylor": [1, 7], "templat": 1, "tensorflow": 3, "termin": [1, 7], "test": 7, "tf": 1, "than": [3, 7, 9], "thi": [1, 2, 5, 7, 9], "third": 1, "three": [1, 7], "through": [2, 3, 7, 9], "thu": [7, 9], "tile": 1, "time": [1, 7], "titl": [0, 1], "toler": 1, "tort": 5, "tradeoff": 9, "tri": 1, "true": [1, 7, 9], "true_beta": 1, "true_break": 1, "tweak": 7, "two": [1, 7], "type": 1, "typic": [7, 9], "u": 1, "ufl": 7, "uncertainti": [7, 9], "under": 7, "understand": 7, "uniform": 1, "unknown": 3, "unlik": 7, "unsupport": 7, "untest": [7, 9], "up": 1, "upon": [1, 7], "upper": 1, "url": 0, "us": [2, 3, 5, 7, 9], "use_custom_opt": [1, 6, 7, 9], "usecustomoptimizationroutin": 7, "user": [2, 7, 9], "util": 1, "valu": [3, 7, 9], "valueerror": [1, 7], "var": [7, 9], "var_1": 1, "variabl": [1, 7, 9], "varianc": [1, 7, 9], "variou": 7, "vector": 9, "venter": 0, "verbos": 1, "verbosity_model": 1, "veri": 1, "versa": 7, "version": 7, "vice": 7, "vol": 7, "vvuq": 7, "wa": 7, "wai": 1, "want": [1, 7], "warranti": 5, "wave": 1, "wb": 1, "we": [1, 7], "weight": [3, 7, 9], "weird": 1, "when": [3, 7], "where": [1, 2, 7, 9], "whether": [5, 7, 9], "which": [1, 2, 7, 9], "while": [1, 7], "whom": 5, "wiki": 7, "wikipedia": 7, "wilei": 7, "within": [1, 7], "without": [5, 7], "won": [7, 9], "work": [1, 3, 7], "would": [1, 7], "www": [7, 9], "www2": 7, "x": [1, 7, 9], "x0": 1, "x_c": [1, 7, 9], "x_data": [1, 7, 9], "x_new": 7, "x_opt": 1, "xguess": 1, "xhat": 1, "xn": 1, "y": [1, 7, 9], "y_c": [1, 7, 9], "y_data": [1, 9], "y_hat": 7, "y_std": 1, "y_w": 9, "year": 0, "yet": 7, "yhat": [1, 7], "yhat0": 1, "yhat1": 1, "yhat2": 1, "yhat_en": 1, "yhat_w": 1, "yield": 7, "you": [0, 3, 4, 7, 9], "your": [0, 1, 2, 7], "ytrue": 1, "zero": 1, "zeta": 9}, "titles": ["About", "Examples", "How it works", "pwlf: piecewise linear fitting", "Installation", "License", "pwlf", "pwlf package contents", "Requirements", "pwlf.PiecewiseLinFit"], "titleterms": {"about": 0, "bad": 1, "best": 1, "bound": 1, "breakpoint": 1, "conda": 4, "constant": 1, "content": 7, "custom": 1, "data": 1, "differenti": 1, "equat": 1, "error": 1, "evolut": 1, "exampl": 1, "find": 1, "fit": [1, 3], "fitfast": 1, "forc": 1, "from": 4, "get": 1, "guess": 1, "have": 1, "how": 2, "index": 4, "indic": 3, "instal": 4, "keyword": 1, "known": 1, "least": 1, "licens": 5, "line": 1, "linear": [1, 3], "locat": 1, "matrix": 1, "model": 1, "more": 1, "non": 1, "number": 1, "obtain": 1, "optim": 1, "p": 1, "packag": [4, 7], "pass": 1, "persist": 1, "piecewis": 3, "piecewiselinfit": 9, "point": 1, "polynomi": 1, "pwlf": [1, 3, 6, 7, 9], "pypi": 4, "python": 4, "regress": 1, "reproduc": 1, "requir": 8, "result": 1, "routin": 1, "segment": 1, "sourc": 4, "specifi": 1, "squar": 1, "standard": 1, "tabl": 3, "tensorflow": 1, "than": 1, "through": 1, "unknown": 1, "us": 1, "valu": 1, "weight": 1, "when": 1, "work": 2, "you": 1}}) ================================================ FILE: docs/stubs/pwlf.PiecewiseLinFit.html ================================================ pwlf.PiecewiseLinFit — pwlf 2.5.2 documentation

pwlf.PiecewiseLinFit

class pwlf.PiecewiseLinFit(x, y, disp_res=False, lapack_driver='gelsd', degree=1, weights=None, seed=None)

Methods

assemble_regression_matrix(breaks, x)

Assemble the linear regression matrix A

calc_slopes()

Calculate the slopes of the lines after a piecewise linear function has been fitted.

conlstsq(A[, calc_slopes])

Perform a constrained least squares fit for A matrix.

fit(n_segments[, x_c, y_c, bounds])

Fit a continuous piecewise linear function for a specified number of line segments.

fit_force_points_opt(var)

The objective function to perform a continuous piecewise linear fit for a specified number of breakpoints.

fit_guess(guess_breakpoints[, bounds])

Uses L-BFGS-B optimization to find the location of breakpoints from a guess of where breakpoint locations should be.

fit_with_breaks(breaks)

A function which fits a continuous piecewise linear function for specified breakpoint locations.

fit_with_breaks_force_points(breaks, x_c, y_c)

A function which fits a continuous piecewise linear function for specified breakpoint locations, where you force the fit to go through the data points at x_c and y_c.

fit_with_breaks_opt(var)

The objective function to perform a continuous piecewise linear fit for a specified number of breakpoints.

fitfast(n_segments[, pop, bounds])

Uses multi start LBFGSB optimization to find the location of breakpoints for a given number of line segments by minimizing the sum of the square of the errors.

lstsq(A[, calc_slopes])

Perform the least squares fit for A matrix.

p_values([method, step_size])

Calculate the p-values for each beta parameter.

predict(x[, beta, breaks])

Evaluate the fitted continuous piecewise linear function at untested points.

prediction_variance(x)

Calculate the prediction variance for each specified x location.

r_squared()

Calculate the coefficient of determination ("R squared", R^2) value after a fit has been performed.

standard_errors([method, step_size])

Calculate the standard errors for each beta parameter determined from the piecewise linear fit.

use_custom_opt(n_segments[, x_c, y_c])

Provide the number of line segments you want to use with your custom optimization routine.

__init__(x, y, disp_res=False, lapack_driver='gelsd', degree=1, weights=None, seed=None)

An object to fit a continuous piecewise linear function to data.

Initiate the library with the supplied x and y data. Supply the x and y data of which you’ll be fitting a continuous piecewise linear model to where y(x). by default pwlf won’t print the optimization results.;

Parameters:
xarray_like

The x or independent data point locations as list or 1 dimensional numpy array.

yarray_like

The y or dependent data point locations as list or 1 dimensional numpy array.

disp_resbool, optional

Whether the optimization results should be printed. Default is False.

lapack_driverstr, optional

Which LAPACK driver is used to solve the least-squares problem. Default lapack_driver=’gelsd’. Options are ‘gelsd’, ‘gelsy’, ‘gelss’. For more see https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.lstsq.html http://www.netlib.org/lapack/lug/node27.html

degreeint, list, optional

The degree of polynomial to use. The default is degree=1 for linear models. Use degree=0 for constant models. Use a list for mixed degrees (only supports degrees 1 or 0). List should be read from left to right, degree=[1,0,1] corresponds to a mixed degree model, where the left most segment has degree 1, the middle segment degree 0, and the right most segment degree 1.

weightsNone, or array_like

The individual weights are typically the reciprocal of the standard deviation for each data point, where weights[i] corresponds to one over the standard deviation of the ith data point. Default weights=None.

seedNone, or int

Pick an integer which will set the numpy.random.seed on init. The fit and fitfast methods rely on stochastic methods and setting this value will make the results reproducible. The default behavior is to not specify a seed.

Attributes:
betandarray (1-D)

The model parameters for the continuous piecewise linear fit.

break_0float

The smallest x value.

break_nfloat

The largest x value.

c_nint

The number of constraint points. This is the same as len(x_c).

degree: int, list

The degree of polynomial to use. The default is degree=1 for linear models. Use degree=0 for constant models. This will be a list if the user provided a list.

fit_breaksndarray (1-D)

breakpoint locations stored as a 1-D numpy array.

interceptsndarray (1-D)

The y-intercept of each line segment as a 1-D numpy array.

lapack_driverstr

Which LAPACK driver is used to solve the least-squares problem.

printbool

Whether the optimization results should be printed. Default is False.

n_dataint

The number of data points.

n_parametersint

The number of model parameters. This is equivalent to the len(beta).

n_segmentsint

The number of line segments.

nVarint

The number of variables in the global optimization problem.

sendarray (1-D)

Standard errors associated with each beta parameter. Specifically se[0] correspounds to the standard error for beta[0], and so forth.

seedint

Numpy random seed number set on init.

slopesndarray (1-D)

The slope of each ling segment as a 1-D numpy array. This assumes that x[0] <= x[1] <= … <= x[n]. Thus, slopes[0] is the slope of the first line segment.

ssrfloat

Optimal sum of square error.

x_cndarray (1-D)

The x locations of the data points that the piecewise linear function will be forced to go through.

y_cndarray (1-D)

The y locations of the data points that the piecewise linear function will be forced to go through.

x_datandarray (1-D)

The inputted parameter x from the 1-D data set.

y_datandarray (1-D)

The inputted parameter y from the 1-D data set.

y_wndarray (1-D)

The weighted y data vector.

zetandarray (1-D)

The model parameters associated with the constraint function.

Methods

fit(n_segments, x_c=None, y_c=None, **kwargs)

Fit a continuous piecewise linear function for a specified number of line segments.

fitfast(n_segments, pop=2, **kwargs)

Fit a continuous piecewise linear function for a specified number of line segments using a specialized optimization routine that should be faster than fit() for large problems. The tradeoff may be that fitfast() results in a lower quality model.

fit_with_breaks(breaks)

Fit a continuous piecewise linear function where the breakpoint locations are known.

fit_with_breaks_force_points(breaks, x_c, y_c)

Fit a continuous piecewise linear function where the breakpoint locations are known, and force the fit to go through points at x_c and y_c.

predict(x, beta=None, breaks=None)

Evaluate the continuous piecewise linear function at new untested points.

fit_with_breaks_opt(var)

The objective function to perform a continuous piecewise linear fit for a specified number of breakpoints. This is to be used with a custom optimization routine, and after use_custom_opt has been called.

fit_force_points_opt(var)’

Same as fit_with_breaks_opt(var), except this allows for points to be forced through x_c and y_c.

use_custom_opt(n_segments, x_c=None, y_c=None)

Function to initialize the attributes necessary to use a custom optimization routine. Must be used prior to calling fit_with_breaks_opt() or fit_force_points_opt().

calc_slopes()

Calculate the slopes of the lines after a piecewise linear function has been fitted.

standard_errors()

Calculate the standard error of each model parameter in the fitted piecewise linear function. Note, this assumes no uncertainty in breakpoint locations.

prediction_variance(x)

Calculate the prediction variance at x locations for the fitted piecewise linear function. Note, assumes no uncertainty in break point locations.

r_squared()

Calculate the coefficient of determination, or ‘R-squared’ value for a fitted piecewise linear function.

Examples

Initialize for x, y data

>>> import pwlf
>>> my_pwlf = pwlf.PiecewiseLinFit(x, y)

Initialize for x,y data and print optimization results

>>> my_pwlf = pwlf.PiecewiseLinFit(x, y, disp_res=True)

Methods

__init__(x, y[, disp_res, lapack_driver, ...])

An object to fit a continuous piecewise linear function to data.

assemble_regression_matrix(breaks, x)

Assemble the linear regression matrix A

calc_slopes()

Calculate the slopes of the lines after a piecewise linear function has been fitted.

conlstsq(A[, calc_slopes])

Perform a constrained least squares fit for A matrix.

fit(n_segments[, x_c, y_c, bounds])

Fit a continuous piecewise linear function for a specified number of line segments.

fit_force_points_opt(var)

The objective function to perform a continuous piecewise linear fit for a specified number of breakpoints.

fit_guess(guess_breakpoints[, bounds])

Uses L-BFGS-B optimization to find the location of breakpoints from a guess of where breakpoint locations should be.

fit_with_breaks(breaks)

A function which fits a continuous piecewise linear function for specified breakpoint locations.

fit_with_breaks_force_points(breaks, x_c, y_c)

A function which fits a continuous piecewise linear function for specified breakpoint locations, where you force the fit to go through the data points at x_c and y_c.

fit_with_breaks_opt(var)

The objective function to perform a continuous piecewise linear fit for a specified number of breakpoints.

fitfast(n_segments[, pop, bounds])

Uses multi start LBFGSB optimization to find the location of breakpoints for a given number of line segments by minimizing the sum of the square of the errors.

lstsq(A[, calc_slopes])

Perform the least squares fit for A matrix.

p_values([method, step_size])

Calculate the p-values for each beta parameter.

predict(x[, beta, breaks])

Evaluate the fitted continuous piecewise linear function at untested points.

prediction_variance(x)

Calculate the prediction variance for each specified x location.

r_squared()

Calculate the coefficient of determination ("R squared", R^2) value after a fit has been performed.

standard_errors([method, step_size])

Calculate the standard errors for each beta parameter determined from the piecewise linear fit.

use_custom_opt(n_segments[, x_c, y_c])

Provide the number of line segments you want to use with your custom optimization routine.

================================================ FILE: examples/EGO_integer_only.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import pwlf" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[]" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "x = np.linspace(0, 1024, 100)\n", "y = np.sin(.01*x)\n", "plt.figure()\n", "plt.plot(x, y)" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "from GPyOpt.methods import BayesianOptimization" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "my_pwlf = pwlf.PiecewiseLinFit(x, y, degree=2)\n", "number_of_line_segments = 4\n", "my_pwlf.use_custom_opt(number_of_line_segments)" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "# define domain for possible breakpoints\n", "bounds = [{'name': 'break_1', 'type': 'discrete',\n", " 'domain': np.arange(1, 1023)},\n", " {'name': 'break_2', 'type': 'discrete',\n", " 'domain': np.arange(1, 1023)},\n", " {'name': 'break_3', 'type': 'discrete',\n", " 'domain': np.arange(1, 1023)}]\n", "max_iter = 120" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "def my_obj(x):\n", " f = np.zeros(x.shape[0])\n", " for i, j in enumerate(x):\n", " f[i] = my_pwlf.fit_with_breaks_opt(j)\n", " return f" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "myBopt = BayesianOptimization(my_obj,\n", " domain=bounds, model_type='GP',\n", " initial_design_numdata=20,\n", " initial_design_type='latin',\n", " exact_feval=True, verbosity=True,\n", " verbosity_model=False)" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "num acquisition: 1, time elapsed: 0.20s\n", "num acquisition: 2, time elapsed: 0.40s\n", "num acquisition: 3, time elapsed: 0.60s\n", "num acquisition: 4, time elapsed: 0.79s\n", "num acquisition: 5, time elapsed: 0.97s\n", "num acquisition: 6, time elapsed: 1.15s\n", "num acquisition: 7, time elapsed: 1.35s\n", "num acquisition: 8, time elapsed: 1.53s\n", "num acquisition: 9, time elapsed: 1.73s\n", "num acquisition: 10, time elapsed: 1.93s\n", "num acquisition: 11, time elapsed: 2.11s\n", "num acquisition: 12, time elapsed: 2.30s\n", "num acquisition: 13, time elapsed: 2.49s\n", "num acquisition: 14, time elapsed: 2.76s\n", "num acquisition: 15, time elapsed: 3.01s\n", "num acquisition: 16, time elapsed: 3.28s\n", "num acquisition: 17, time elapsed: 3.51s\n", "num acquisition: 18, time elapsed: 3.79s\n", "num acquisition: 19, time elapsed: 4.08s\n", "num acquisition: 20, time elapsed: 4.32s\n", "num acquisition: 21, time elapsed: 4.57s\n", "num acquisition: 22, time elapsed: 4.83s\n", "num acquisition: 23, time elapsed: 5.08s\n", "num acquisition: 24, time elapsed: 5.34s\n", "num acquisition: 25, time elapsed: 5.66s\n", "num acquisition: 26, time elapsed: 5.95s\n", "num acquisition: 27, time elapsed: 6.20s\n", "num acquisition: 28, time elapsed: 6.49s\n", "num acquisition: 29, time elapsed: 6.74s\n", "num acquisition: 30, time elapsed: 6.99s\n", "num acquisition: 31, time elapsed: 7.21s\n", "num acquisition: 32, time elapsed: 7.47s\n", "num acquisition: 33, time elapsed: 7.70s\n", "num acquisition: 34, time elapsed: 7.96s\n", "num acquisition: 35, time elapsed: 8.18s\n", "num acquisition: 36, time elapsed: 8.44s\n", "num acquisition: 37, time elapsed: 8.70s\n", "num acquisition: 38, time elapsed: 8.94s\n", "num acquisition: 39, time elapsed: 9.21s\n", "num acquisition: 40, time elapsed: 9.49s\n", "num acquisition: 41, time elapsed: 9.77s\n", "num acquisition: 42, time elapsed: 10.02s\n", "num acquisition: 43, time elapsed: 10.31s\n", "num acquisition: 44, time elapsed: 10.56s\n", "num acquisition: 45, time elapsed: 10.80s\n", "num acquisition: 46, time elapsed: 11.07s\n", "num acquisition: 47, time elapsed: 11.34s\n", "num acquisition: 48, time elapsed: 11.61s\n", "num acquisition: 49, time elapsed: 11.91s\n", "num acquisition: 50, time elapsed: 12.18s\n", "num acquisition: 51, time elapsed: 12.47s\n", "num acquisition: 52, time elapsed: 12.74s\n", "num acquisition: 53, time elapsed: 13.04s\n", "num acquisition: 54, time elapsed: 13.39s\n", "num acquisition: 55, time elapsed: 13.70s\n", "num acquisition: 56, time elapsed: 13.96s\n", "num acquisition: 57, time elapsed: 14.25s\n", "num acquisition: 58, time elapsed: 14.52s\n", "num acquisition: 59, time elapsed: 14.80s\n", "num acquisition: 60, time elapsed: 15.10s\n", "num acquisition: 61, time elapsed: 15.38s\n", "num acquisition: 62, time elapsed: 15.65s\n", "num acquisition: 63, time elapsed: 15.93s\n", "num acquisition: 64, time elapsed: 16.23s\n", "num acquisition: 65, time elapsed: 16.54s\n", "num acquisition: 66, time elapsed: 16.82s\n", "num acquisition: 67, time elapsed: 17.16s\n", "num acquisition: 68, time elapsed: 17.45s\n", "num acquisition: 69, time elapsed: 17.73s\n", "num acquisition: 70, time elapsed: 18.02s\n", "num acquisition: 71, time elapsed: 18.33s\n", "num acquisition: 72, time elapsed: 18.61s\n", "num acquisition: 73, time elapsed: 18.88s\n", "num acquisition: 74, time elapsed: 19.17s\n", "num acquisition: 75, time elapsed: 19.46s\n", "num acquisition: 76, time elapsed: 19.76s\n", "num acquisition: 77, time elapsed: 20.07s\n", "num acquisition: 78, time elapsed: 20.38s\n", "num acquisition: 79, time elapsed: 20.67s\n", "num acquisition: 80, time elapsed: 20.95s\n", "num acquisition: 81, time elapsed: 21.30s\n", "num acquisition: 82, time elapsed: 21.58s\n", "num acquisition: 83, time elapsed: 21.88s\n", "num acquisition: 84, time elapsed: 22.17s\n", "num acquisition: 85, time elapsed: 22.46s\n", "num acquisition: 86, time elapsed: 22.75s\n", "num acquisition: 87, time elapsed: 23.07s\n", "num acquisition: 88, time elapsed: 23.40s\n", "num acquisition: 89, time elapsed: 23.72s\n", "num acquisition: 90, time elapsed: 24.02s\n", "num acquisition: 91, time elapsed: 24.34s\n", "num acquisition: 92, time elapsed: 24.67s\n", "num acquisition: 93, time elapsed: 24.99s\n", "num acquisition: 94, time elapsed: 25.34s\n", "num acquisition: 95, time elapsed: 25.69s\n", "num acquisition: 96, time elapsed: 26.03s\n", "num acquisition: 97, time elapsed: 26.32s\n", "num acquisition: 98, time elapsed: 26.65s\n", "num acquisition: 99, time elapsed: 27.02s\n", "num acquisition: 100, time elapsed: 27.36s\n", "num acquisition: 101, time elapsed: 27.78s\n", "num acquisition: 102, time elapsed: 28.11s\n", "num acquisition: 103, time elapsed: 28.47s\n", "num acquisition: 104, time elapsed: 28.80s\n", "num acquisition: 105, time elapsed: 29.13s\n", "num acquisition: 106, time elapsed: 29.48s\n", "num acquisition: 107, time elapsed: 29.80s\n", "num acquisition: 108, time elapsed: 30.19s\n", "num acquisition: 109, time elapsed: 30.55s\n", "num acquisition: 110, time elapsed: 30.84s\n", "num acquisition: 111, time elapsed: 31.25s\n", "num acquisition: 112, time elapsed: 31.58s\n", "num acquisition: 113, time elapsed: 31.91s\n", "num acquisition: 114, time elapsed: 32.26s\n", "num acquisition: 115, time elapsed: 32.65s\n", "num acquisition: 116, time elapsed: 33.02s\n", "num acquisition: 117, time elapsed: 33.36s\n", "num acquisition: 118, time elapsed: 33.72s\n", "num acquisition: 119, time elapsed: 34.06s\n", "num acquisition: 120, time elapsed: 34.47s\n" ] } ], "source": [ "myBopt.run_optimization(max_iter=max_iter, verbosity=True)" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", " \n", " Opt found \n", "\n", "Optimum number of line segments: [612. 310. 842.]\n", "Function value: 0.058342840166060575\n" ] } ], "source": [ "print('\\n \\n Opt found \\n')\n", "print('Optimum number of line segments:', myBopt.x_opt)\n", "print('Function value:', myBopt.fx_opt)" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "myBopt.plot_convergence()" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# perform the fit for the optimum\n", "x_opt = list(myBopt.x_opt)\n", "x_opt.append(x.min())\n", "x_opt.append(x.max())\n", "ssr = my_pwlf.fit_with_breaks(x_opt)\n", "# predict for the determined points\n", "xHat = np.linspace(min(x), max(x), num=10000)\n", "yHat = my_pwlf.predict(xHat)\n", "\n", "# plot the results\n", "plt.figure()\n", "plt.plot(x, y, 'o')\n", "plt.plot(xHat, yHat, '-')\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.058342840166060575" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ssr" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.8" } }, "nbformat": 4, "nbformat_minor": 2 } ================================================ FILE: examples/README.md ================================================ # Examples All of these examples will use the following data and imports. ```python import numpy as np import matplotlib.pyplot as plt import pwlf # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) ``` 1. [fit with known breakpoint locations](#fit-with-known-breakpoint-locations) 2. [fit for specified number of line segments](#fit-for-specified-number-of-line-segments) 3. [fitfast for specified number of line segments](#fitfast-for-specified-number-of-line-segments) 4. [force a fit through data points](#force-a-fit-through-data-points) 5. [use custom optimization routine](#use-custom-optimization-routine) 6. [pass differential evolution keywords](#pass-differential-evolution-keywords) 7. [find the best number of line segments](#find-the-best-number-of-line-segments) 8. [model persistence](#model-persistence) 9. [bad fits when you have more unknowns than data](#bad-fits-when-you-have-more-unknowns-than-data) 10. [fit with a breakpoint guess](#fit-with-a-breakpoint-guess) 11. [get the linear regression matrix](#get-the-linear-regression-matrix) 12. [use of TensorFlow](#use-of-tensorflow) 13. [fit constants or polynomials](#fit-constants-or-polynomials) 14. [specify breakpoint bounds](#specify-breakpoint-bounds) 15. [non-linear standard errors and p-values](#non-linear-standard-errors-and-p-values) 16. [obtain the equations of fitted pwlf](#obtain-the-equations-of-fitted-pwlf) 17. [weighted least squares fit](#weighted-least-squares-fit) 18. [reproducible results](#reproducible-resultss) ## fit with known breakpoint locations You can perform a least squares fit if you know the breakpoint locations. ```python # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data with the specified break points # (ie the x locations of where the line segments # will terminate) my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ``` ![fit with known breakpoint locations](https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/fit_breaks.png) ## fit for specified number of line segments Use a global optimization to find the breakpoint locations that minimize the sum of squares error. This uses [Differential Evolution](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.differential_evolution.html) from scipy. ```python # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments res = my_pwlf.fit(4) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ``` ![fit for specified number of line segments](https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/numberoflines.png) ## fitfast for specified number of line segments This performs a fit for a specified number of line segments with a multi-start gradient based optimization. This should be faster than [Differential Evolution](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.differential_evolution.html) for a small number of starting points. ```python # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments # this performs 3 multi-start optimizations res = my_pwlf.fitfast(4, pop=3) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ``` ![fitfast for specified number of line segments](https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/fitfast.png) ## force a fit through data points Sometimes it's necessary to force the piecewise continuous model through a particular data point, or a set of data points. The following example finds the best 4 line segments that go through two data points. ```python # initialize piecewise linear fit with your x and y data myPWLF = pwlf.PiecewiseLinFit(x, y) # fit the function with four line segments # force the function to go through the data points # (0.0, 0.0) and (0.19, 0.16) # where the data points are of the form (x, y) x_c = [0.0, 0.19] y_c = [0.0, 0.2] res = myPWLF.fit(4, x_c, y_c) # predict for the determined points xHat = np.linspace(min(x), 0.19, num=10000) yHat = myPWLF.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ``` ![force a fit through data points](https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/force.png) ## use custom optimization routine You can use your favorite optimization routine to find the breakpoint locations. The following example uses scipy's [minimize](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html) function. ```python from scipy.optimize import minimize # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # initialize custom optimization number_of_line_segments = 3 my_pwlf.use_custom_opt(number_of_line_segments) # i have number_of_line_segments - 1 number of variables # let's guess the correct location of the two unknown variables # (the program defaults to have end segments at x0= min(x) # and xn=max(x) xGuess = np.zeros(number_of_line_segments - 1) xGuess[0] = 0.02 xGuess[1] = 0.10 res = minimize(my_pwlf.fit_with_breaks_opt, xGuess) # set up the break point locations x0 = np.zeros(number_of_line_segments + 1) x0[0] = np.min(x) x0[-1] = np.max(x) x0[1:-1] = res.x # calculate the parameters based on the optimal break point locations my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ``` ## pass differential evolution keywords You can pass keyword arguments from the ```fit``` function into scipy's [Differential Evolution](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.differential_evolution.html). ```python # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments # this sets DE to have an absolute tolerance of 0.1 res = my_pwlf.fit(4, atol=0.1) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ``` ## find the best number of line segments This example uses EGO (bayesian optimization) and a penalty function to find the best number of line segments. This will require careful use of the penalty parameter ```l```. Use this template to automatically find the best number of line segments. ```python from GPyOpt.methods import BayesianOptimization # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # define your objective function def my_obj(x): # define some penalty parameter l # you'll have to arbitrarily pick this # it depends upon the noise in your data, # and the value of your sum of square of residuals l = y.mean()*0.001 f = np.zeros(x.shape[0]) for i, j in enumerate(x): my_pwlf.fit(j[0]) f[i] = my_pwlf.ssr + (l*j[0]) return f # define the lower and upper bound for the number of line segments bounds = [{'name': 'var_1', 'type': 'discrete', 'domain': np.arange(2, 40)}] np.random.seed(12121) myBopt = BayesianOptimization(my_obj, domain=bounds, model_type='GP', initial_design_numdata=10, initial_design_type='latin', exact_feval=True, verbosity=True, verbosity_model=False) max_iter = 30 # perform the bayesian optimization to find the optimum number # of line segments myBopt.run_optimization(max_iter=max_iter, verbosity=True) print('\n \n Opt found \n') print('Optimum number of line segments:', myBopt.x_opt) print('Function value:', myBopt.fx_opt) myBopt.plot_acquisition() myBopt.plot_convergence() # perform the fit for the optimum my_pwlf.fit(myBopt.x_opt) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ``` ## model persistence You can save fitted models with pickle. Alternatively see [joblib](https://joblib.readthedocs.io/en/latest/). ```python # if you use Python 2.x you should import cPickle # import cPickle as pickle # if you use Python 3.x you can just use pickle import pickle # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data with the specified break points my_pwlf.fit_with_breaks(x0) # save the fitted model with open('my_fit.pkl', 'wb') as f: pickle.dump(my_pwlf, f, pickle.HIGHEST_PROTOCOL) # load the fitted model with open('my_fit.pkl', 'rb') as f: my_pwlf = pickle.load(f) ``` ## bad fits when you have more unknowns than data You can get very bad fits with pwlf when you have more unknowns than data points. The following example will fit 99 line segments to the 59 data points. While this will result in an error of zero, the model will have very weird predictions within the data. You should not fit more unknowns than you have data with pwlf! ```python break_locations = np.linspace(min(x), max(x), num=100) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) my_pwlf.fit_with_breaks(break_locations) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ``` ![bad fits when you have more unknowns than data](https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/badfit.png) ## fit with a breakpoint guess In this example we see two distinct linear regions, and we believe a breakpoint occurs at 6.0. We'll use the fit_guess() function to find the best breakpoint location starting with this guess. These fits should be much faster than the ```fit``` or ```fitfast``` function when you have a reasonable idea where the breakpoints occur. ```python import numpy as np import pwlf x = np.array([4., 5., 6., 7., 8.]) y = np.array([11., 13., 16., 28.92, 42.81]) my_pwlf = pwlf.PiecewiseLinFit(x, y) breaks = my_pwlf.fit_guess([6.0]) ``` Note specifying one breakpoint will result in two line segments. If we wanted three line segments, we'll have to specify two breakpoints. ```python breaks = my_pwlf.fit_guess([5.5, 6.0]) ``` ## get the linear regression matrix In some cases it may be desirable to work with the linear regression matrix directly. The following example grabs the linear regression matrix ```A``` for a specific set of breakpoints. In this case we assume that the breakpoints occur at each of the data points. Please see the [paper](https://github.com/cjekel/piecewise_linear_fit_py/tree/master/paper) for details about the regression matrix ```A```. ```python import numpy as np import pwlf # select random seed for reproducibility np.random.seed(123) # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) ytrue = y.copy() # add noise to the data y = np.random.normal(0, 0.05, 100) + ytrue my_pwlf_en = pwlf.PiecewiseLinFit(x, y) # copy the x data to use as break points breaks = my_pwlf_en.x_data.copy() # create the linear regression matrix A A = my_pwlf_en.assemble_regression_matrix(breaks, my_pwlf_en.x_data) ``` We can perform fits that are more complicated than a least squares fit when we have the regression matrix. The following uses the Elastic Net regularizer to perform an interesting fit with the regression matrix. ```python from sklearn.linear_model import ElasticNetCV # set up the elastic net en_model = ElasticNetCV(cv=5, l1_ratio=[.1, .5, .7, .9, .95, .99, 1], fit_intercept=False, max_iter=1000000, n_jobs=-1) # fit the model using the elastic net en_model.fit(A, my_pwlf_en.y_data) # predict from the elastic net parameters xhat = np.linspace(x.min(), x.max(), 1000) yhat_en = my_pwlf_en.predict(xhat, breaks=breaks, beta=en_model.coef_) ``` ![interesting elastic net fit](https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/sin_en_net_fit.png) ## use of tensorflow You need to install [pwlftf](https://github.com/cjekel/piecewise_linear_fit_py_tf) which will have the ```PiecewiseLinFitTF``` class. For performance benchmarks (these benchmarks are outdated! and the regular pwlf may be faster in many applications) see this blog [post](https://jekel.me/2019/Adding-tensorflow-to-pwlf/). The use of the TF class is nearly identical to the original class, however note the following exceptions. ```PiecewiseLinFitTF``` does: - not have a ```lapack_driver``` option - have an optional parameter ```dtype```, so you can choose between the float64 and float32 data types - have an optional parameter ```fast``` to switch between Cholesky decomposition (default ```fast=True```), and orthogonal decomposition (```fast=False```) ```python import pwlftf as pwlf # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # initialize TF piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFitTF(x, y, dtype='float32) # fit the data with the specified break points # (ie the x locations of where the line segments # will terminate) my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) ``` ## fit constants or polynomials You can use pwlf to fit segmented constant models, or piecewise polynomials. The following example fits a segmented constant model, piecewise linear, and a piecewise quadratic model to a sine wave. ```python # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, 100) + y # initialize piecewise linear fit with your x and y data # pwlf lets you fit continuous model for many degree polynomials # degree=0 constant # degree=1 linear (default) # degree=2 quadratic my_pwlf_0 = pwlf.PiecewiseLinFit(x, y, degree=0) my_pwlf_1 = pwlf.PiecewiseLinFit(x, y, degree=1) # default my_pwlf_2 = pwlf.PiecewiseLinFit(x, y, degree=2) # fit the data for four line segments res0 = my_pwlf_0.fitfast(5, pop=50) res1 = my_pwlf_1.fitfast(5, pop=50) res2 = my_pwlf_2.fitfast(5, pop=50) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat0 = my_pwlf_0.predict(xHat) yHat1 = my_pwlf_1.predict(xHat) yHat2 = my_pwlf_2.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o', label='Data') plt.plot(xHat, yHat0, '-', label='degree=0') plt.plot(xHat, yHat1, '--', label='degree=1') plt.plot(xHat, yHat2, ':', label='degree=2') plt.legend() plt.show() ``` ![Example of multiple degree fits to a sine wave.](https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/multi_degree.png) ## specify breakpoint bounds You may want extra control over the search space for feasible breakpoints. One way to do this is to specify the bounds for each breakpoint location. ```python # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, 100) + y # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # define custom bounds for the interior break points n_segments = 4 bounds = np.zeros((n_segments-1, 2)) # first breakpoint bounds[0, 0] = 0.0 # lower bound bounds[0, 1] = 3.5 # upper bound # second breakpoint bounds[1, 0] = 3.0 # lower bound bounds[1, 1] = 7.0 # upper bound # third breakpoint bounds[2, 0] = 6.0 # lower bound bounds[2, 1] = 10.0 # upper bound res = my_pwlf.fit(n_segments, bounds=bounds) ``` ## non-linear standard errors and p-values You can calculate non-linear standard errors using the Delta method. This will calculate the standard errors of the piecewise linear parameters (intercept + slopes) and the breakpoint locations! First let us generate true piecewise linear data. ```python from __future__ import print_function # generate a true piecewise linear data np.random.seed(5) n_data = 100 x = np.linspace(0, 1, num=n_data) y = np.random.random(n_data) my_pwlf_gen = pwlf.PiecewiseLinFit(x, y) true_beta = np.random.normal(size=5) true_breaks = np.array([0.0, 0.2, 0.5, 0.75, 1.0]) y = my_pwlf_gen.predict(x, beta=true_beta, breaks=true_breaks) plt.figure() plt.title('True piecewise linear data') plt.plot(x, y) plt.show() ``` ![True piecewise linear data.](https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/true_pwlf.png) Now we can perform a fit, calculate the standard errors, and p-values. The non-linear method uses a first order taylor series expansion to linearize the non-linear regression problem. A positive step_size performs a forward difference, and a negative step_size would perform a backwards difference. ```python my_pwlf = pwlf.PiecewiseLinFit(x, y) res = my_pwlf.fitfast(4, pop=100) p = my_pwlf.p_values(method='non-linear', step_size=1e-4) se = my_pwlf.se # standard errors ``` The standard errors and p-values correspond to each model parameter. First the beta parameters (intercept + slopes) and then the breakpoints. We can assemble the parameters, and print a table of the result with the following code. ```python parameters = np.concatenate((my_pwlf.beta, my_pwlf.fit_breaks[1:-1])) header = ['Parameter type', 'Parameter value', 'Standard error', 't', 'P > np.abs(t) (p-value)'] print(*header, sep=' | ') values = np.zeros((parameters.size, 5), dtype=np.object_) values[:, 1] = np.around(parameters, decimals=3) values[:, 2] = np.around(se, decimals=3) values[:, 3] = np.around(parameters / se, decimals=3) values[:, 4] = np.around(p, decimals=3) for i, row in enumerate(values): if i < my_pwlf.beta.size: row[0] = 'Beta' print(*row, sep=' | ') else: row[0] = 'Breakpoint' print(*row, sep=' | ') ``` | Parameter type | Parameter value | Standard error | t | P > np.abs(t) (p-value) | | ------------- | --------------- | -------------- |---| ----------------------- | | Beta | 1.821 | 0.0 | 1763191476.046 | 0.0 | | Beta | -0.427 | 0.0 | -46404554.493 | 0.0 | | Beta | -1.165 | 0.0 | -111181494.162 | 0.0 | | Beta | -1.397 | 0.0 | -168954500.421 | 0.0 | | Beta | 0.873 | 0.0 | 93753841.242 | 0.0 | | Breakpoint | 0.2 | 0.0 | 166901856.885 | 0.0 | | Breakpoint | 0.5 | 0.0 | 537785803.646 | 0.0 | | Breakpoint | 0.75 | 0.0 | 482311769.159 | 0.0 | ## obtain the equations of fitted pwlf Sometimes you may want the mathematical equations that represent your fitted model. This is easy to perform if you don't mind using sympy. The following code will fit 5 line segments of degree=2 to a sin wave. ```python import numpy as np import pwlf # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, 100) + y my_pwlf_2 = pwlf.PiecewiseLinFit(x, y, degree=2) res2 = my_pwlf_2.fitfast(5, pop=50) ``` Given this fit, the following code will print the mathematical equation for each line segment. ```python from sympy import Symbol from sympy.utilities import lambdify x = Symbol('x') def get_symbolic_eqn(pwlf_, segment_number): if pwlf_.degree < 1: raise ValueError('Degree must be at least 1') if segment_number < 1 or segment_number > pwlf_.n_segments: raise ValueError('segment_number not possible') # assemble degree = 1 first for line in range(segment_number): if line == 0: my_eqn = pwlf_.beta[0] + (pwlf_.beta[1])*(x-pwlf_.fit_breaks[0]) else: my_eqn += (pwlf_.beta[line+1])*(x-pwlf_.fit_breaks[line]) # assemble all other degrees if pwlf_.degree > 1: for k in range(2, pwlf_.degree + 1): for line in range(segment_number): beta_index = pwlf_.n_segments*(k-1) + line + 1 my_eqn += (pwlf_.beta[beta_index])*(x-pwlf_.fit_breaks[line])**k return my_eqn.simplify() eqn_list = [] f_list = [] for i in range(my_pwlf_2.n_segments): eqn_list.append(get_symbolic_eqn(my_pwlf_2, i + 1)) print('Equation number: ', i + 1) print(eqn_list[-1]) f_list.append(lambdify(x, eqn_list[-1])) ``` which should print out something like the following: ```python Equation number: 1 -0.953964059782599*x**2 + 1.89945177490653*x + 0.00538634182565454 Equation number: 2 0.951561315686298*x**2 - 5.69747505830914*x + 7.5772216545711 Equation number: 3 -0.949735350431857*x**2 + 9.48218236957122*x - 22.720785454735 Equation number: 4 0.926850298824217*x**2 - 12.9824424358344*x + 44.5102742956827 Equation number: 5 -1.03016230425747*x**2 + 18.5306546317065*x - 82.3508513333073 ``` For more information on how this works, see [this](https://github.com/cjekel/piecewise_linear_fit_py/blob/master/examples/understanding_higher_degrees/polynomials_in_pwlf.ipynb) jupyter notebook. ## weighted least squares fit Sometimes your data will not have a constant variance (heteroscedasticity), and you need to perform a weighted least squares fit. The following example will perform a standard and weighted fit so you can compare the differences. First we need to generate a data set which will be a good candidate to use for weighted least squares fits. ```python # generate data with heteroscedasticity n = 100 n_data_sets = 100 n_segments = 6 # generate sine data x = np.linspace(0, 10, n) y = np.zeros((n_data_sets, n)) sigma_change = np.linspace(0.001, 0.05, 100) for i in range(n_data_sets): y[i] = np.sin(x * np.pi / 2) # add noise to the data y[i] = np.random.normal(0, sigma_change, 100) + y[i] X = np.tile(x, n_data_sets) ``` The individual weights in pwlf are the reciprocal of the standard deviation for each data point. Here weights[i] corresponds to one over the standard deviation of the ith data point. The result of this is that data points with higher variance are less important to the overall pwlf than data point with small variance. Let's perform a standard pwlf fit and a weighted fit. ```python # perform an ordinary pwlf fit to the entire data my_pwlf = pwlf.PiecewiseLinFit(X.flatten(), y.flatten()) my_pwlf.fit(n_segments) # compute the standard deviation in y y_std = np.std(y, axis=0) # set the weights to be one over the standard deviation weights = 1.0 / y_std # perform a weighted least squares to the data my_pwlf_w = pwlf.PiecewiseLinFit(x, y.mean(axis=0), weights=weights) my_pwlf_w.fit(n_segments) # compare the fits xhat = np.linspace(0, 10, 1000) yhat = my_pwlf.predict(xhat) yhat_w = my_pwlf_w.predict(xhat) plt.figure() plt.plot(X.flatten(), y.flatten(), '.') plt.plot(xhat, yhat, '-', label='Ordinary LS') plt.plot(xhat, yhat_w, '-', label='Weighted LS') plt.legend() plt.show() ``` ![Weighted pwlf fit.](https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/weighted_least_squares_example.png) We can see that the weighted pwlf fit tries fit data with low variance better than data with high variance, however the ordinary pwlf fits the data assuming a uniform variance. ## reproducible results The `fit` and `fitfast` methods are stochastic and may not give the same result every time the program is run. To have reproducible results you can manually specify a numpy.random.seed on init. Now everytime the following program is run, the results of the fit(2) should be the same. ```python # initialize piecewise linear fit with a random seed my_pwlf = pwlf.PiecewiseLinFit(x, y, seed=123) # Now the fit() method will be reproducible my_pwlf.fit(2) ``` ================================================ FILE: examples/README.rst ================================================ Examples ======== All of these examples will use the following data and imports. .. code:: python import numpy as np import matplotlib.pyplot as plt import pwlf # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) 1. `fit with known breakpoint locations <#fit-with-known-breakpoint-locations>`__ 2. `fit for specified number of line segments <#fit-for-specified-number-of-line-segments>`__ 3. `fitfast for specified number of line segments <#fitfast-for-specified-number-of-line-segments>`__ 4. `force a fit through data points <#force-a-fit-through-data-points>`__ 5. `use custom optimization routine <#use-custom-optimization-routine>`__ 6. `pass differential evolution keywords <#pass-differential-evolution-keywords>`__ 7. `find the best number of line segments <#find-the-best-number-of-line-segments>`__ 8. `model persistence <#model-persistence>`__ 9. `bad fits when you have more unknowns than data <#bad-fits-when-you-have-more-unknowns-than-data>`__ 10. `fit with a breakpoint guess <#fit-with-a-breakpoint-guess>`__ 11. `get the linear regression matrix <#get-the-linear-regression-matrix>`__ 12. `use of TensorFlow <#use-of-tensorflow>`__ 13. `fit constants or polynomials <#fit-constants-or-polynomials>`__ 14. `specify breakpoint bounds <#specify-breakpoint-bounds>`__ 15. `non-linear standard errors and p-values <#non-linear-standard-errors-and-p-values>`__ 16. `obtain the equations of fitted pwlf <#obtain-the-equations-of-fitted-pwlf>`__ 17. `weighted least squares fit <#weighted-least-squares-fit>`__ 18. `reproducible results <#reproducible results>`__ fit with known breakpoint locations ----------------------------------- You can perform a least squares fit if you know the breakpoint locations. .. code:: python # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data with the specified break points # (ie the x locations of where the line segments # will terminate) my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/fit_breaks.png :alt: fit with known breakpoint locations fit with known breakpoint locations fit for specified number of line segments ----------------------------------------- Use a global optimization to find the breakpoint locations that minimize the sum of squares error. This uses `Differential Evolution `__ from scipy. .. code:: python # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments res = my_pwlf.fit(4) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/numberoflines.png :alt: fit for specified number of line segments fit for specified number of line segments fitfast for specified number of line segments --------------------------------------------- This performs a fit for a specified number of line segments with a multi-start gradient based optimization. This should be faster than `Differential Evolution `__ for a small number of starting points. .. code:: python # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments # this performs 3 multi-start optimizations res = my_pwlf.fitfast(4, pop=3) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/fitfast.png :alt: fitfast for specified number of line segments fitfast for specified number of line segments force a fit through data points ------------------------------- Sometimes it’s necessary to force the piecewise continuous model through a particular data point, or a set of data points. The following example finds the best 4 line segments that go through two data points. .. code:: python # initialize piecewise linear fit with your x and y data myPWLF = pwlf.PiecewiseLinFit(x, y) # fit the function with four line segments # force the function to go through the data points # (0.0, 0.0) and (0.19, 0.16) # where the data points are of the form (x, y) x_c = [0.0, 0.19] y_c = [0.0, 0.2] res = myPWLF.fit(4, x_c, y_c) # predict for the determined points xHat = np.linspace(min(x), 0.19, num=10000) yHat = myPWLF.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/force.png :alt: force a fit through data points force a fit through data points use custom optimization routine ------------------------------- You can use your favorite optimization routine to find the breakpoint locations. The following example uses scipy’s `minimize `__ function. .. code:: python from scipy.optimize import minimize # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # initialize custom optimization number_of_line_segments = 3 my_pwlf.use_custom_opt(number_of_line_segments) # i have number_of_line_segments - 1 number of variables # let's guess the correct location of the two unknown variables # (the program defaults to have end segments at x0= min(x) # and xn=max(x) xGuess = np.zeros(number_of_line_segments - 1) xGuess[0] = 0.02 xGuess[1] = 0.10 res = minimize(my_pwlf.fit_with_breaks_opt, xGuess) # set up the break point locations x0 = np.zeros(number_of_line_segments + 1) x0[0] = np.min(x) x0[-1] = np.max(x) x0[1:-1] = res.x # calculate the parameters based on the optimal break point locations my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() pass differential evolution keywords ------------------------------------ You can pass keyword arguments from the ``fit`` function into scipy’s `Differential Evolution `__. .. code:: python # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments # this sets DE to have an absolute tolerance of 0.1 res = my_pwlf.fit(4, atol=0.1) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() find the best number of line segments ------------------------------------- This example uses EGO (bayesian optimization) and a penalty function to find the best number of line segments. This will require careful use of the penalty parameter ``l``. Use this template to automatically find the best number of line segments. .. code:: python from GPyOpt.methods import BayesianOptimization # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # define your objective function def my_obj(x): # define some penalty parameter l # you'll have to arbitrarily pick this # it depends upon the noise in your data, # and the value of your sum of square of residuals l = y.mean()*0.001 f = np.zeros(x.shape[0]) for i, j in enumerate(x): my_pwlf.fit(j[0]) f[i] = my_pwlf.ssr + (l*j[0]) return f # define the lower and upper bound for the number of line segments bounds = [{'name': 'var_1', 'type': 'discrete', 'domain': np.arange(2, 40)}] np.random.seed(12121) myBopt = BayesianOptimization(my_obj, domain=bounds, model_type='GP', initial_design_numdata=10, initial_design_type='latin', exact_feval=True, verbosity=True, verbosity_model=False) max_iter = 30 # perform the bayesian optimization to find the optimum number # of line segments myBopt.run_optimization(max_iter=max_iter, verbosity=True) print('\n \n Opt found \n') print('Optimum number of line segments:', myBopt.x_opt) print('Function value:', myBopt.fx_opt) myBopt.plot_acquisition() myBopt.plot_convergence() # perform the fit for the optimum my_pwlf.fit(myBopt.x_opt) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() model persistence ----------------- You can save fitted models with pickle. Alternatively see `joblib `__. .. code:: python # if you use Python 2.x you should import cPickle # import cPickle as pickle # if you use Python 3.x you can just use pickle import pickle # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data with the specified break points my_pwlf.fit_with_breaks(x0) # save the fitted model with open('my_fit.pkl', 'wb') as f: pickle.dump(my_pwlf, f, pickle.HIGHEST_PROTOCOL) # load the fitted model with open('my_fit.pkl', 'rb') as f: my_pwlf = pickle.load(f) bad fits when you have more unknowns than data ---------------------------------------------- You can get very bad fits with pwlf when you have more unknowns than data points. The following example will fit 99 line segments to the 59 data points. While this will result in an error of zero, the model will have very weird predictions within the data. You should not fit more unknowns than you have data with pwlf! .. code:: python break_locations = np.linspace(min(x), max(x), num=100) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) my_pwlf.fit_with_breaks(break_locations) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/badfit.png :alt: bad fits when you have more unknowns than data bad fits when you have more unknowns than data fit with a breakpoint guess --------------------------- In this example we see two distinct linear regions, and we believe a breakpoint occurs at 6.0. We’ll use the fit_guess() function to find the best breakpoint location starting with this guess. These fits should be much faster than the ``fit`` or ``fitfast`` function when you have a reasonable idea where the breakpoints occur. .. code:: python import numpy as np import pwlf x = np.array([4., 5., 6., 7., 8.]) y = np.array([11., 13., 16., 28.92, 42.81]) my_pwlf = pwlf.PiecewiseLinFit(x, y) breaks = my_pwlf.fit_guess([6.0]) Note specifying one breakpoint will result in two line segments. If we wanted three line segments, we’ll have to specify two breakpoints. .. code:: python breaks = my_pwlf.fit_guess([5.5, 6.0]) get the linear regression matrix -------------------------------- In some cases it may be desirable to work with the linear regression matrix directly. The following example grabs the linear regression matrix ``A`` for a specific set of breakpoints. In this case we assume that the breakpoints occur at each of the data points. Please see the `paper `__ for details about the regression matrix ``A``. .. code:: python import numpy as np import pwlf # select random seed for reproducibility np.random.seed(123) # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) ytrue = y.copy() # add noise to the data y = np.random.normal(0, 0.05, 100) + ytrue my_pwlf_en = pwlf.PiecewiseLinFit(x, y) # copy the x data to use as break points breaks = my_pwlf_en.x_data.copy() # create the linear regression matrix A A = my_pwlf_en.assemble_regression_matrix(breaks, my_pwlf_en.x_data) We can perform fits that are more complicated than a least squares fit when we have the regression matrix. The following uses the Elastic Net regularizer to perform an interesting fit with the regression matrix. .. code:: python from sklearn.linear_model import ElasticNetCV # set up the elastic net en_model = ElasticNetCV(cv=5, l1_ratio=[.1, .5, .7, .9, .95, .99, 1], fit_intercept=False, max_iter=1000000, n_jobs=-1) # fit the model using the elastic net en_model.fit(A, my_pwlf_en.y_data) # predict from the elastic net parameters xhat = np.linspace(x.min(), x.max(), 1000) yhat_en = my_pwlf_en.predict(xhat, breaks=breaks, beta=en_model.coef_) .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/sin_en_net_fit.png :alt: interesting elastic net fit interesting elastic net fit use of tensorflow ----------------- You need to install `pwlftf `__ which will have the ``PiecewiseLinFitTF`` class. For performance benchmarks (these benchmarks are outdated! and the regular pwlf may be faster in many applications) see this blog `post `__. The use of the TF class is nearly identical to the original class, however note the following exceptions. ``PiecewiseLinFitTF`` does: - not have a ``lapack_driver`` option - have an optional parameter ``dtype``, so you can choose between the float64 and float32 data types - have an optional parameter ``fast`` to switch between Cholesky decomposition (default ``fast=True``), and orthogonal decomposition (``fast=False``) .. code:: python import pwlftf as pwlf # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # initialize TF piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFitTF(x, y, dtype='float32) # fit the data with the specified break points # (ie the x locations of where the line segments # will terminate) my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) fit constants or polynomials ---------------------------- You can use pwlf to fit segmented constant models, or piecewise polynomials. The following example fits a segmented constant model, piecewise linear, and a piecewise quadratic model to a sine wave. .. code:: python # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, 100) + y # initialize piecewise linear fit with your x and y data # pwlf lets you fit continuous model for many degree polynomials # degree=0 constant # degree=1 linear (default) # degree=2 quadratic my_pwlf_0 = pwlf.PiecewiseLinFit(x, y, degree=0) my_pwlf_1 = pwlf.PiecewiseLinFit(x, y, degree=1) # default my_pwlf_2 = pwlf.PiecewiseLinFit(x, y, degree=2) # fit the data for four line segments res0 = my_pwlf_0.fitfast(5, pop=50) res1 = my_pwlf_1.fitfast(5, pop=50) res2 = my_pwlf_2.fitfast(5, pop=50) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat0 = my_pwlf_0.predict(xHat) yHat1 = my_pwlf_1.predict(xHat) yHat2 = my_pwlf_2.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o', label='Data') plt.plot(xHat, yHat0, '-', label='degree=0') plt.plot(xHat, yHat1, '--', label='degree=1') plt.plot(xHat, yHat2, ':', label='degree=2') plt.legend() plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/multi_degree.png :alt: Example of multiple degree fits to a sine wave. Example of multiple degree fits to a sine wave. specify breakpoint bounds ------------------------- You may want extra control over the search space for feasible breakpoints. One way to do this is to specify the bounds for each breakpoint location. .. code:: python # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, 100) + y # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # define custom bounds for the interior break points n_segments = 4 bounds = np.zeros((n_segments-1, 2)) # first breakpoint bounds[0, 0] = 0.0 # lower bound bounds[0, 1] = 3.5 # upper bound # second breakpoint bounds[1, 0] = 3.0 # lower bound bounds[1, 1] = 7.0 # upper bound # third breakpoint bounds[2, 0] = 6.0 # lower bound bounds[2, 1] = 10.0 # upper bound res = my_pwlf.fit(n_segments, bounds=bounds) non-linear standard errors and p-values --------------------------------------- You can calculate non-linear standard errors using the Delta method. This will calculate the standard errors of the piecewise linear parameters (intercept + slopes) and the breakpoint locations! First let us generate true piecewise linear data. .. code:: python from __future__ import print_function # generate a true piecewise linear data np.random.seed(5) n_data = 100 x = np.linspace(0, 1, num=n_data) y = np.random.random(n_data) my_pwlf_gen = pwlf.PiecewiseLinFit(x, y) true_beta = np.random.normal(size=5) true_breaks = np.array([0.0, 0.2, 0.5, 0.75, 1.0]) y = my_pwlf_gen.predict(x, beta=true_beta, breaks=true_breaks) plt.figure() plt.title('True piecewise linear data') plt.plot(x, y) plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/true_pwlf.png :alt: True piecewise linear data. True piecewise linear data. Now we can perform a fit, calculate the standard errors, and p-values. The non-linear method uses a first order taylor series expansion to linearize the non-linear regression problem. A positive step_size performs a forward difference, and a negative step_size would perform a backwards difference. .. code:: python my_pwlf = pwlf.PiecewiseLinFit(x, y) res = my_pwlf.fitfast(4, pop=100) p = my_pwlf.p_values(method='non-linear', step_size=1e-4) se = my_pwlf.se # standard errors The standard errors and p-values correspond to each model parameter. First the beta parameters (intercept + slopes) and then the breakpoints. We can assemble the parameters, and print a table of the result with the following code. .. code:: python parameters = np.concatenate((my_pwlf.beta, my_pwlf.fit_breaks[1:-1])) header = ['Parameter type', 'Parameter value', 'Standard error', 't', 'P > np.abs(t) (p-value)'] print(*header, sep=' | ') values = np.zeros((parameters.size, 5), dtype=np.object_) values[:, 1] = np.around(parameters, decimals=3) values[:, 2] = np.around(se, decimals=3) values[:, 3] = np.around(parameters / se, decimals=3) values[:, 4] = np.around(p, decimals=3) for i, row in enumerate(values): if i < my_pwlf.beta.size: row[0] = 'Beta' print(*row, sep=' | ') else: row[0] = 'Breakpoint' print(*row, sep=' | ') ============== =============== ============== ============== ======================= Parameter type Parameter value Standard error t P > np.abs(t) (p-value) ============== =============== ============== ============== ======================= Beta 1.821 0.0 1763191476.046 0.0 Beta -0.427 0.0 -46404554.493 0.0 Beta -1.165 0.0 -111181494.162 0.0 Beta -1.397 0.0 -168954500.421 0.0 Beta 0.873 0.0 93753841.242 0.0 Breakpoint 0.2 0.0 166901856.885 0.0 Breakpoint 0.5 0.0 537785803.646 0.0 Breakpoint 0.75 0.0 482311769.159 0.0 ============== =============== ============== ============== ======================= obtain the equations of fitted pwlf ----------------------------------- Sometimes you may want the mathematical equations that represent your fitted model. This is easy to perform if you don’t mind using sympy. The following code will fit 5 line segments of degree=2 to a sin wave. .. code:: python import numpy as np import pwlf # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, 100) + y my_pwlf_2 = pwlf.PiecewiseLinFit(x, y, degree=2) res2 = my_pwlf_2.fitfast(5, pop=50) Given this fit, the following code will print the mathematical equation for each line segment. .. code:: python from sympy import Symbol from sympy.utilities import lambdify x = Symbol('x') def get_symbolic_eqn(pwlf_, segment_number): if pwlf_.degree < 1: raise ValueError('Degree must be at least 1') if segment_number < 1 or segment_number > pwlf_.n_segments: raise ValueError('segment_number not possible') # assemble degree = 1 first for line in range(segment_number): if line == 0: my_eqn = pwlf_.beta[0] + (pwlf_.beta[1])*(x-pwlf_.fit_breaks[0]) else: my_eqn += (pwlf_.beta[line+1])*(x-pwlf_.fit_breaks[line]) # assemble all other degrees if pwlf_.degree > 1: for k in range(2, pwlf_.degree + 1): for line in range(segment_number): beta_index = pwlf_.n_segments*(k-1) + line + 1 my_eqn += (pwlf_.beta[beta_index])*(x-pwlf_.fit_breaks[line])**k return my_eqn.simplify() eqn_list = [] f_list = [] for i in range(my_pwlf_2.n_segments): eqn_list.append(get_symbolic_eqn(my_pwlf_2, i + 1)) print('Equation number: ', i + 1) print(eqn_list[-1]) f_list.append(lambdify(x, eqn_list[-1])) which should print out something like the following: .. code:: python Equation number: 1 -0.953964059782599*x**2 + 1.89945177490653*x + 0.00538634182565454 Equation number: 2 0.951561315686298*x**2 - 5.69747505830914*x + 7.5772216545711 Equation number: 3 -0.949735350431857*x**2 + 9.48218236957122*x - 22.720785454735 Equation number: 4 0.926850298824217*x**2 - 12.9824424358344*x + 44.5102742956827 Equation number: 5 -1.03016230425747*x**2 + 18.5306546317065*x - 82.3508513333073 For more information on how this works, see `this `__ jupyter notebook. weighted least squares fit -------------------------- Sometimes your data will not have a constant variance (heteroscedasticity), and you need to perform a weighted least squares fit. The following example will perform a standard and weighted fit so you can compare the differences. First we need to generate a data set which will be a good candidate to use for weighted least squares fits. .. code:: python # generate data with heteroscedasticity n = 100 n_data_sets = 100 n_segments = 6 # generate sine data x = np.linspace(0, 10, n) y = np.zeros((n_data_sets, n)) sigma_change = np.linspace(0.001, 0.05, 100) for i in range(n_data_sets): y[i] = np.sin(x * np.pi / 2) # add noise to the data y[i] = np.random.normal(0, sigma_change, 100) + y[i] X = np.tile(x, n_data_sets) The individual weights in pwlf are the reciprocal of the standard deviation for each data point. Here weights[i] corresponds to one over the standard deviation of the ith data point. The result of this is that data points with higher variance are less important to the overall pwlf than data point with small variance. Let’s perform a standard pwlf fit and a weighted fit. .. code:: python # perform an ordinary pwlf fit to the entire data my_pwlf = pwlf.PiecewiseLinFit(X.flatten(), y.flatten()) my_pwlf.fit(n_segments) # compute the standard deviation in y y_std = np.std(y, axis=0) # set the weights to be one over the standard deviation weights = 1.0 / y_std # perform a weighted least squares to the data my_pwlf_w = pwlf.PiecewiseLinFit(x, y.mean(axis=0), weights=weights) my_pwlf_w.fit(n_segments) # compare the fits xhat = np.linspace(0, 10, 1000) yhat = my_pwlf.predict(xhat) yhat_w = my_pwlf_w.predict(xhat) plt.figure() plt.plot(X.flatten(), y.flatten(), '.') plt.plot(xhat, yhat, '-', label='Ordinary LS') plt.plot(xhat, yhat_w, '-', label='Weighted LS') plt.legend() plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/weighted_least_squares_example.png :alt: Weighted pwlf fit. Weighted pwlf fit. We can see that the weighted pwlf fit tries fit data with low variance better than data with high variance, however the ordinary pwlf fits the data assuming a uniform variance. reproducible results -------------------- The `fit` and `fitfast` methods are stochastic and may not give the same result every time the program is run. To have reproducible results you can manually specify a numpy.random.seed on init. Now everytime the following program is run, the results of the fit(2) should be the same. .. code:: python # initialize piecewise linear fit with a random seed my_pwlf = pwlf.PiecewiseLinFit(x, y, seed=123) # Now the fit() method will be reproducible my_pwlf.fit(2) ================================================ FILE: examples/experiment_with_batch_process.py.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import pwlf\n", "%matplotlib inline" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "np.random.seed(123)\n", "n = 100\n", "n_data_sets = 100\n", "n_segments = 6\n", "# generate sine data\n", "x = np.linspace(0, 10, n)\n", "y = np.zeros((n_data_sets, n))\n", "for i in range(n_data_sets):\n", " y[i] = np.sin(x * np.pi / 2)\n", " # add noise to the data\n", " y[i] = np.random.normal(0, 0.05, 100) + y[i]\n", "X = np.tile(x, n_data_sets)" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# plot all of the data\n", "plt.figure()\n", "plt.plot(X.flatten(), y.flatten(), '.')\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([ 0. , 1.05164435, 2.98570544, 5.00010778, 7.01335991,\n", " 8.94940776, 10. ])" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# perform a single fit to all of the data\n", "my_pwlf_global = pwlf.PiecewiseLinFit(X.flatten(), y.flatten())\n", "my_pwlf_global.fit(n_segments)" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# plot all of the data with global fit\n", "yhat = my_pwlf_global.predict(x)\n", "plt.figure()\n", "plt.plot(X.flatten(), y.flatten(), '.')\n", "plt.plot(x, yhat, '-')\n", "plt.plot()\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "# for each data set, fit a pwlf model using the global breaks\n", "my_pwlf_models = []\n", "slopes = []\n", "for i in range(n_data_sets):\n", " temp_pwlf = pwlf.PiecewiseLinFit(x, y[i])\n", " temp_pwlf.fit_with_breaks(my_pwlf_global.fit_breaks)\n", " my_pwlf_models.append(temp_pwlf)\n", " slopes.append(temp_pwlf.slopes)" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[ 1.01504066 -1.24340053 1.2084571 -1.21045573 1.24507127 -1.02114751]\n", "[ 1.01504066 -1.24340053 1.2084571 -1.21045573 1.24507127 -1.02114751]\n", "[0.03299628 0.01432216 0.01273798 0.01173331 0.01313007 0.03130695]\n" ] } ], "source": [ "slopes = np.array(slopes)\n", "print(my_pwlf_global.slopes)\n", "print(slopes.mean(axis=0))\n", "print(slopes.std(axis=0))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.10" } }, "nbformat": 4, "nbformat_minor": 4 } ================================================ FILE: examples/fitForSpecifiedNumberOfLineSegments.py ================================================ # fit for a specified number of line segments # you specify the number of line segments you want, the library does the rest # import our libraries import numpy as np import matplotlib.pyplot as plt import pwlf # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments res = my_pwlf.fit(4) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # Get the slopes my_slopes = my_pwlf.slopes # Get my model parameters beta = my_pwlf.beta # calculate the standard errors associated with each beta parameter se = my_pwlf.standard_errors() # calcualte the R^2 value Rsquared = my_pwlf.r_squared() # calculate the piecewise R^2 value R2values = np.zeros(my_pwlf.n_segments) for i in range(my_pwlf.n_segments): # segregate the data based on break point locations xmin = my_pwlf.fit_breaks[i] xmax = my_pwlf.fit_breaks[i+1] xtemp = my_pwlf.x_data ytemp = my_pwlf.y_data indtemp = np.where(xtemp >= xmin) xtemp = my_pwlf.x_data[indtemp] ytemp = my_pwlf.y_data[indtemp] indtemp = np.where(xtemp <= xmax) xtemp = xtemp[indtemp] ytemp = ytemp[indtemp] # predict for the new data yhattemp = my_pwlf.predict(xtemp) # calcualte ssr e = yhattemp - ytemp ssr = np.dot(e, e) # calculate sst ybar = np.ones(ytemp.size) * np.mean(ytemp) ydiff = ytemp - ybar sst = np.dot(ydiff, ydiff) R2values[i] = 1.0 - (ssr/sst) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ================================================ FILE: examples/fitForSpecifiedNumberOfLineSegments_passDiffEvoKeywords.py ================================================ # fit for a specified number of line segments # you specify the number of line segments you want, the library does the rest # same as fitForSpecifiedNumberOfLineSegments.py, with the exception of # passing custom keywords directly to the scipy differential evolution algo # see # https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.differential_evolution.html # import our libraries import numpy as np import matplotlib.pyplot as plt import pwlf # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments # My differential evolutions keywords are tuned to be very aggressive # it's probably overkill for your application # so you can pass your custom keywords here # for example I'll pass the only keyword that I want differential evolution # to print the iterations with disp=True # if any keyword is passed, all of my aggressively tuned differential evolution # keywords reset to the default res = my_pwlf.fit(4, disp=True) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ================================================ FILE: examples/fitForSpecifiedNumberOfLineSegments_standard_deviation.py ================================================ # fit for a specified number of line segments # you specify the number of line segments you want, the library does the rest # import our libraries import numpy as np import matplotlib.pyplot as plt import pwlf # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments res = my_pwlf.fit(4) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # Get the slopes my_slopes = my_pwlf.slopes # Get my model parameters beta = my_pwlf.beta # calculate the standard errors associated with each beta parameter se = my_pwlf.standard_errors() # calculate the sum of the square of the residuals ssr = my_pwlf.fit_with_breaks(my_pwlf.fit_breaks) # calculate the unbiased standard deviation sigma = np.sqrt(ssr / (my_pwlf.n_data - my_pwlf.n_parameters)) # sigma can be used as a prediction variance # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.fill_between(xHat, yHat - (sigma*1.96), yHat + (sigma*1.96), alpha=0.1, color="r") plt.show() ================================================ FILE: examples/fitWithKnownLineSegmentLocations.py ================================================ # fit and predict with known line segment x locations # import our libraries import numpy as np import matplotlib.pyplot as plt import pwlf # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data with the specified break points (ie the x locations of where # the line segments should end my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ================================================ FILE: examples/fit_begin_and_end.py ================================================ # fit and predict between a known begging and known ending # import our libraries import numpy as np import matplotlib.pyplot as plt import pwlf from scipy.optimize import differential_evolution # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) # initialize piecewise linear fit with your x and y data myPWLF = pwlf.PiecewiseLinFit(x, y, disp_res=True) # fit the function with four line segments # force the function to go through the data points # (0.0, 0.0) and (0.19, 0.16) # where the data points are of the form (x, y) x_c = [0.0, 0.19] y_c = [0.0, 0.2] res = myPWLF.fit(4, x_c, y_c) # predict for the determined points xHat = np.linspace(min(x), 0.19, num=10000) yHat = myPWLF.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ================================================ FILE: examples/min_length_demo.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import pwlf\n", "pwlf.__version__\n", "%matplotlib inline" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Force minimum length of segments to be greater than x\n", "\n", "[Issue 66](https://github.com/cjekel/piecewise_linear_fit_py/issues/66)\n", "\n", "This jupyter notebook uses SLSQP to apply a constraint function to force a minimum line segment length. You'll need to supply a starting point (or guess) to start the optimization. If you don't know what is a good starting point, check out how I use Latin Hypercube random sampling to run multiple optimizations in the ```fitfast``` function. \n", "\n", "\n", "A constraint function could look like:\n", "```python\n", "def my_con(var):\n", " var = np.sort(var)\n", " distances = np.zeros(number_of_line_segments)\n", " distances[0] = var[0] - my_pwlf.break_0\n", " distances[-1] = my_pwlf.break_n - var[-1]\n", " for i in range(number_of_line_segments - 2):\n", " distances[i+1] = var[i+1] - var[i]\n", " # element must be greater or equal to 0.0\n", " # in a successfully optimized problem\n", " return np.array((distances.min() - min_length))\n", "```\n", "\n", "This is a single constraint for the minimum length of all segments. It's possible that the ```min()``` in this function will create issues with the gradient of the constraint. If you run into issues with this, you may want to investigate using a separate constraint for each line segment. That could be done by changing:\n", "```python\n", " return np.array((distances.min() - min_length))\n", "```\n", "to\n", "```python\n", " return distances - min_length\n", "```" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "# your data\n", "y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02,\n", " 4.39052750e-02, 5.45343950e-02, 6.74104940e-02,\n", " 8.34831790e-02, 1.02580042e-01, 1.22767939e-01,\n", " 1.42172312e-01, 0.00000000e+00, 8.58600000e-06,\n", " 8.31543400e-03, 2.34184100e-02, 3.39709150e-02,\n", " 4.03581990e-02, 4.53545600e-02, 5.02345260e-02,\n", " 5.55253360e-02, 6.14750770e-02, 6.82125120e-02,\n", " 7.55892510e-02, 8.38356810e-02, 9.26413070e-02,\n", " 1.02039790e-01, 1.11688258e-01, 1.21390666e-01,\n", " 1.31196948e-01, 0.00000000e+00, 1.56706510e-02,\n", " 3.54628780e-02, 4.63739040e-02, 5.61442590e-02,\n", " 6.78542550e-02, 8.16388310e-02, 9.77756110e-02,\n", " 1.16531753e-01, 1.37038283e-01, 0.00000000e+00,\n", " 1.16951050e-02, 3.12089850e-02, 4.41776550e-02,\n", " 5.42877590e-02, 6.63321350e-02, 8.07655920e-02,\n", " 9.70363280e-02, 1.15706975e-01, 1.36687642e-01,\n", " 0.00000000e+00, 1.50144640e-02, 3.44519970e-02,\n", " 4.55907760e-02, 5.59556700e-02, 6.88450940e-02,\n", " 8.41374060e-02, 1.01254006e-01, 1.20605073e-01,\n", " 1.41881288e-01, 1.62618058e-01])\n", "x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02,\n", " 5.66106800e-02, 7.95549800e-02, 1.00936330e-01,\n", " 1.20351520e-01, 1.37442010e-01, 1.51858250e-01,\n", " 1.64433570e-01, 0.00000000e+00, -2.12600000e-05,\n", " 7.03872000e-03, 1.85494500e-02, 3.00926700e-02,\n", " 4.17617000e-02, 5.37279600e-02, 6.54941000e-02,\n", " 7.68092100e-02, 8.76596300e-02, 9.80525800e-02,\n", " 1.07961810e-01, 1.17305210e-01, 1.26063930e-01,\n", " 1.34180360e-01, 1.41725010e-01, 1.48629710e-01,\n", " 1.55374770e-01, 0.00000000e+00, 1.65610200e-02,\n", " 3.91016100e-02, 6.18679400e-02, 8.30997400e-02,\n", " 1.02132890e-01, 1.19011260e-01, 1.34620080e-01,\n", " 1.49429370e-01, 1.63539960e-01, -0.00000000e+00,\n", " 1.01980300e-02, 3.28642800e-02, 5.59461900e-02,\n", " 7.81388400e-02, 9.84458400e-02, 1.16270210e-01,\n", " 1.31279040e-01, 1.45437090e-01, 1.59627540e-01,\n", " 0.00000000e+00, 1.63404300e-02, 4.00086000e-02,\n", " 6.34390200e-02, 8.51085900e-02, 1.04787860e-01,\n", " 1.22120350e-01, 1.36931660e-01, 1.50958760e-01,\n", " 1.65299640e-01, 1.79942720e-01])\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "# initialize piecewise linear fit with your x and y data\n", "my_pwlf = pwlf.PiecewiseLinFit(x, y)\n", "\n", "# initialize custom optimization\n", "number_of_line_segments = 3\n", "my_pwlf.use_custom_opt(number_of_line_segments)\n", "\n", "# minium length of a segment\n", "min_length = 0.05" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "def my_con(var):\n", " var = np.sort(var)\n", " distances = np.zeros(number_of_line_segments)\n", " distances[0] = var[0] - my_pwlf.break_0\n", " distances[-1] = my_pwlf.break_n - var[-1]\n", " for i in range(number_of_line_segments - 2):\n", " distances[i+1] = var[i+1] - var[i]\n", " # element must be greater or equal to 0.0\n", " # in a successfully optimized problem\n", " return np.array((distances.min() - min_length))\n" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Optimization terminated successfully. (Exit mode 0)\n", " Current function value: 0.00035834090548162784\n", " Iterations: 4\n", " Function evaluations: 18\n", " Gradient evaluations: 4\n" ] } ], "source": [ "from scipy.optimize import fmin_slsqp\n", "# i have number_of_line_segments - 1 number of variables\n", "# let's guess the correct location of the two unknown variables\n", "# (the program defaults to have end segments at x0= min(x)\n", "# and xn=max(x)\n", "xGuess = np.zeros(number_of_line_segments - 1)\n", "xGuess[0] = 0.06\n", "xGuess[1] = 0.13\n", "bounds = np.zeros((number_of_line_segments - 1, 2))\n", "bounds[:, 0] = my_pwlf.break_0\n", "bounds[:, 1] = my_pwlf.break_n\n", "\n", "res = fmin_slsqp(my_pwlf.fit_with_breaks_opt, xGuess, f_ieqcons=my_con,\n", " bounds=bounds, iter=100, acc=1e-06, iprint=1,\n", " epsilon=1.4901161193847656e-08)\n" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXoAAAD4CAYAAADiry33AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nO3de3xT9f3H8denaQoFxkVBJwUEB6IgClIu6nTiVPAGTFFAVLwNf3O4qziYN0Q3VObQDaeyeUNURAVEROpdNwWlgIAFqxURWlArUC5S2jT5/P44JzWkKU1p0qTp5/l49EFyzvecfhrCu4dvvuf7FVXFGGNM6kpLdAHGGGPiy4LeGGNSnAW9McakOAt6Y4xJcRb0xhiT4tITXUC4tm3baufOnRNdhjHGNCgrVqz4TlXbRdqXdEHfuXNncnNzE12GMcY0KCLyVXX7rOvGGGNSnAW9McakOAt6Y4xJcRb0xhiT4izojTEmxSXdqBtjjGlsFqwqYlpOPltKSmnfOpMJg7szvE9WzM5vQW+MMQm0YFURk+atpdTnB6CopJRJ89YCxCzsrevGGGMSaFpOfmXIB5X6/EzLyY/Z94gq6EVkiIjki0iBiEyMsP80EVkpIhUiMiJsXycReU1E1ovIOhHpHJvSjTGm4dtSUlqr7QejxqAXEQ/wIHAO0AMYLSI9wpptAq4EnolwilnANFU9FugPfFuXgo0xJpW0b51Zq+0HI5or+v5AgapuUNVyYA4wLLSBqm5U1TVAIHS7+wshXVVfd9vtUdW9sSndGGMavgmDu5Pp9fB/noXcmP4cAJleDxMGd4/Z94gm6LOAzSHPC91t0TgaKBGReSKySkSmuf9D2I+IjBORXBHJLS4ujvLUxhjT8A3v3Z6Xur/GRO8cOsm3dGyVwdQLe9X7qBuJsC3ahWbTgVOBPjjdO8/hdPE8ut/JVGcCMwGys7NtEVtjTOMQ8MOi33F0wSzIvoah505jaFqVa+E6i+aKvhDoGPK8A7AlyvMXAqvcbp8KYAFwYu1KNMaYFFRRBi9cBStnwWkT4Lz7IA4hD9EF/XKgm4h0EZEMYBSwMMrzLwfaiEhwjuQzgHW1L9MYY1JI2R54ZiSsewkG/xXOuAUkUudJbNQY9O6V+HggB1gPzFXVPBGZIiJDAUSkn4gUAhcDj4hInnusH7gReFNE1uJ0A/07Pj+KMcY0AHu3w6xh8OW7MOxfcNKv4/4tRTW5usSzs7PVFh4xxqSkXVth9oWwrQBGPA7Hnh+zU4vIClXNjrTPpkAwxpj6sH0DzBoOe7fBmOfhqNPr7Vtb0BtjTLx9/YlzJe/3wdiFkNW3Xr+9zXVjjDHxtOlDeOJcEA9c9Wq9hzxY0BtjTPx8/obzwWuztnBNDhx2TELKsKA3xph4+ORFeHYUtO0KVy+B1p0SVooFvTHGxFruY/DCNdChH1z5CrQ4LKHlWNAbY0ysqMJ/74NFv4duZ8FlL0LTVomuykbdGGNMTKjC67fCB/+EXhfD8IfA4010VYAFvTHG1J2/Ahb9FlbNhn6/hHPuhbTk6TCxoDfGmLqoKIMXr4H1L8NpN8GgP8d13pqDYUFvjDEHq2w3zBnjzFszeCqcdH2iK4rIgt4YYw7G3u3w9AjY8jEMfxh6j050RdWyoDfGmNratQWe+gVs/xJGzoZjzk10RQdkQW+MMdVYsKqIyQvzKCn1AdCmmZd7B7XgrNzroHSHM3yyy6kJrrJmFvTGGBPBglVFTHh+Nb7AD1O5/7i0gN5v3E1ZE6HJ2IWQ1TAWzEue8T/GGJNEpuXk7xfyfSWf5zLuxIeHq5jSYEIeogx6ERkiIvkiUiAiEyPsP01EVopIhYiMiLC/pYgUiciMWBRtjDHxtqWktPLx6WkfMztjKsXaihFlk1m6q20CK6u9GoNeRDzAg8A5QA9gtIj0CGu2CbgSeKaa09wJvHvwZRpjTP1q3zoTgKFpH/Bv7318oe25pPw2ttC2cl9DEc0VfX+gQFU3qGo5MAcYFtpAVTeq6hogEH6wiPQFDgdei0G9xhhTLyYM7s7Y9De43/sgK7Ubo8tvYRut8HqECYO7J7q8Wokm6LOAzSHPC91tNRKRNOA+YEIN7caJSK6I5BYXF0dzamOMiZsFKwv55pW7uCP9Md4O9OaK8onsphltmnmZNuIEhveJKgKTRjSjbiLdyxvtiuLXA4tVdbMc4JZgVZ0JzARncfAoz22MMXWyYFUR03Ly2VJSSvvWmc6VugYoWfAnrkt7hXn+n3KTbxxebxPuv7BXgwv4oGiCvhDoGPK8A7AlyvOfBJwqItcDLYAMEdmjqlU+0DXGmPq0YFURk+atpdTnB6CopJRb5n3MXzwzuTLtHR6vGMyUistR0qjw+ZmWk5/SQb8c6CYiXYAiYBRwaTQnV9UxwcciciWQbSFvjEkG03LyK0MeoAnl3McMBpPL/RUXcn/FRYR2aISOwmloauyjV9UKYDyQA6wH5qpqnohMEZGhACLST0QKgYuBR0QkL55FG2NMXYUGd3NKecw7jcGeXCb7ruD+ihGE91o3tJE2oaK6M1ZVFwOLw7bdFvJ4OU6XzoHO8QTwRK0rNMaYOGjfOpOiklLasIvHM+7lONnI78t/xTtNzyDTF9jvaj/T62lwI21C2Z2xxphGacLg7nT2ljA3406Olc1c5/s9Szync/sFPZl6YS+yWmciQFbrTKY24A9iwea6McaksEijaoKBPbxjKWe3uBMtLWFs2UQ2tzqRqaH7G3Cwh7OgN8akpEijaibNWwvA8B9/B09dSDMBxi1hTvveCaw0/qzrxhiTksJH1QCU+vy89up8eOJ8SG8KVy+BFA95sCt6Y0yKijQcclDaKv5edj+07QxXLIBWBxxDkjLsit4Yk5LCh0MOTXufmd6/szGto3Ml30hCHizojTEpasLg7mR6PQBc7nmN+73/YhXdKThnDjRvWNMM15V13RhjUtLwPlmgSvHiu/hlxbP8N60fO859hKHZP0l0afXOgt4Yk5oCAYZ/MwMqnoUTRnPq0BngaZyR1zh/amNMavNXwMLxsPpZGPArGPxXSGu8PdUW9MaY1OLbBy9cBfmLYdDNcNoEOMA06Y2BBb0xJnXs2wVzLoWN/4VzpsGAcYmuKClY0BtjUsLiZWs5KmcsXQNf8peM33FCxnkMT3RRScKC3hjToESavyazdCvdcy4ni2LG+f7AW2Unkhmc7iCF5qw5WI330wljTIMTnL+mqKQUxZm/5l/Pv8pxOZfQjh1cUT6RtwInAs50B9Ny8hNbcJKIKuhFZIiI5ItIgYhUWSFKRE4TkZUiUiEiI0K29xaRpSKSJyJrRGRkLIs3xjQu4fPX9JQvecZ7B03wMbr8Vj7SY/dr35BXhYqlGoNeRDzAg8A5QA9gtIj0CGu2CbgSeCZs+17gClXtCQwB7heR1nUt2hjTOIUG9wBZz5yMu9hHBheX306edq7SviGvChVL0VzR9wcKVHWDqpYDc4BhoQ1UdaOqrgECYds/U9XP3cdbgG+BdjGp3BjT6ASD++dpK3gy426+0TaMKLudL/WIKm0b+qpQsRRN0GcBm0OeF7rbakVE+gMZwBcR9o0TkVwRyS0uLq7tqY0xjcSEwd25JOMDHvFOJ187cnH5bXzNoVXaeUQa/KpQsRRN0Ee600Br801E5AjgKeAqVQ2E71fVmaqararZ7drZBb8xJrLh5Yu4N20GK+VYLi2/mR20rNIm0+vhvktOsJAPEc3wykKgY8jzDsCWaL+BiLQEXgFuUdVltSvPGNOY/TCUci83t3iZayvmsPXHZ/DLoqv4Hk9lO8G5+swKWy7QOKIJ+uVANxHpAhQBo4BLozm5iGQA84FZqvr8QVdpjGl0gkMp9/l83Jb+FFdV5DAv8DP+8vU4dvr27xgIhvz7E89ITLFJrsauG1WtAMYDOcB6YK6q5onIFBEZCiAi/USkELgYeERE8tzDLwFOA64UkY/dr9Rft8sYU2fTcvLx+cq4z/swV6Xn8J+Kc/hj+S/Ztq9K7y9gQykPJKo7Y1V1MbA4bNttIY+X43TphB83G5hdxxqNMY3QtpKdPOT9B2d5VjLNdwkP+ocR+SNDhw2lrJ5NgWCMST77dvJs5r2cEFjPLb6rmO0/q3JXm2Ze9vkC+904ZUMpD8yC3hiTUOFz19x8elvO/fjXnMBnTNAbeNE/sLJtptfD7Rf0BKgy3419AFs9C3pjTMIEP3ANXp1ryWaOffV6/J7teEY/y6nf92RZNYFuwR49C3pjTMKEzl3zEyniqYyptGAf49Nv46Gjz2Y4FuixYEFvjEmY4EiZ42QDT2bcQwBhVPktrC/rnNjCUowFvTEmYdq3zqTjrhX823sfO2nOZeWT2KhHkGUjaGLKgt4YkzD3HV9Enw/v4Ss9jMvLJ/ENh9gImjiwoDfGJMbHzzJw+W/Z3qYHvy29kW/LvTaFQZxY0Btj6t+yh2DJROjyMw4Z9TSvNvlRoitKaRb0xpj6owrvTIV374FjzocRj0F6k0RXlfIs6I0x9SMQgCV/go9mQu/L4IIHwGMRVB/sVTbGxJ/fBwuuh7Vz4aTxcPZdINXPW2NiK6rFwY0x5qD5Svl65kWwdi73+kZyysozWPBx1EtamBiwK3pjTEyFzl3TrVWAmenT6LRnDTdXXM3T/jNh5z4mzVsL2F2v9cWu6I0xMROcu6aopJRD2cn00pvJ2vMJv/GNd0LeVerzMy0nP4GVNi5RBb2IDBGRfBEpEJGJEfafJiIrRaRCREaE7RsrIp+7X2NjVbgxJvkE567Jopi5GXdwlGzlWt+NLAqcVKWtLRRSf2rsuhERD/AgcBbO+rHLRWShqq4LabYJuBK4MezYQ4DbgWyc1b5WuMfuiE35xphksqWklK5SyFMZd9OMfVxWPokVGvkuV1sopP5Ec0XfHyhQ1Q2qWg7MAYaFNlDVjaq6Bghf42sw8LqqbnfD/XVgSAzqNsYkoTNaFjI3Ywrp+BlZflu1IQ/YNAf1KJqgzwI2hzwvdLdFI6pjRWSciOSKSG5xcXGUpzbGJJUv3+Nh/2S+J5MR5bfzqXaqtmmbZl77ILYeRTPqJtJgV43y/FEdq6ozgZkA2dnZ0Z7bGJNgwRE2PXe9xz8zZlDStAOj991Iobap9pjQVaJM/Yjmir4Q6BjyvAMQ7SDYuhxrjEliwRE2J+1ewkPe+1kXOJLBJZMo9FcNeY8IAmS1zmTqhb3sar6eRXNFvxzoJiJdgCJgFHBplOfPAf4qIsG/+bOBSbWu0hiTdKbl5HNp4GVu9c7mv/7juM73B/bSNGLbgCpf3n1ePVdogmq8olfVCmA8TmivB+aqap6ITBGRoQAi0k9ECoGLgUdEJM89djtwJ84vi+XAFHebMaYhU2X0nie51Tubxf7+XOObUG3Ig42wSTRRTa4u8ezsbM3NzU10GcaY6gQCsPhGyH2UORWn8+eKawmEXDMK+38Ql+n1WHdNPRCRFaqaHWmfTYFgjIkodCqD9sEFQY4/DOb/H3zyAp93vZo78s8mEDKqOtPr4aK+Wbz9afH+x1nIJ5QFvTGmiuAHraU+PwBFJaXcMS+Xgcv+w4+/eQ/OnEy3n/6eqZF+GVioJx0LemNMFcGpDIJa8j2PyN847JvP4Pz7IfsqwJmUzII9+dmkZsaYKkLnoWnLTuZk3EVvKeA35TdUhrxpOCzojTFVBEfJdJBins+YTGf5mmt9N7Kq5aAEV2YOhgW9MaaKCYO7c5x3Ky9kTOYQ2c1l5ZNY7jnR5qdpoKyP3hhTxfB2X3Nu0yns8qUxsuw2drfqzlT7oLXBsqA3xuzvi7dhzhgymrel7RUvseSQLomuyNSRdd0YY36wbiE8cwm0ORKuzgEL+ZRgQW+Mcax8Cp4fC0ecAFe+Ai2PSHRFJkYs6I0x8ME/YeF4OOp0uOIlaHZIoisyMWR99MaksEjTGACV21o1TWc8z3It83kz7WS+P/bvDM1onuCqTaxZ0BuToiJNYzDhhdWg4AsoaQSYUPEIY9Lf5JmKQdxScQ1NXson4Mmw0TUpxrpujElR4dMYAPj8ii+geKngAe8MxqS/yb8qhlbOQFnq8zMtJz9BFZt4sSt6Y1JU6DQGoTLZx0PeBzjds5q/+kYz039BVMeZhsuC3pgUtGBVEWki+MPWm2jJHh7L+Bt95HP+5Pslz/mrTmlgi4Sknqi6bkRkiIjki0iBiEyMsL+JiDzn7v9QRDq7270i8qSIrBWR9SJiywgaE2fBvvnwkG9HCc9l3MXx8gXjfb+JGPKZXo9Nc5CCagx6EfEADwLnAD2A0SLSI6zZNcAOVe0KTAfucbdfDDRR1V5AX+C64C8BY0x8ROqb7yDf8nzGHXSSb7jadxOvBgZU7mvTzGsLd6e4aLpu+gMFqroBQETmAMOAdSFthgGT3ccvADNEJLiiWHMRSQcygXJgV2xKN8ZEEt7HfrRs5qmMqTTBx2Xlf2aVdqvcl9U6k/cnnlHfJZp6Fk3XTRawOeR5obstYht3MfGdwKE4of89sBXYBPwt0uLgIjJORHJFJLe4uLjWP4Qx5gehfex95HPmZkwBYGT5bfuFvHXTNB7RBL1E2Ba+onh1bfoDfqA90AX4o4gcVaWh6kxVzVbV7Hbt2kVRkjGmOhMGdyfT6+GnaWuZnfFXSrQFYwJ30m/AKWS1zrRumkYomq6bQqBjyPMOwJZq2hS63TStgO3ApcASVfUB34rI+0A2sKGuhRvTGERcoLuGcB7eJ4v2W3Los3waBYH23JQ5mfFDBlqoN2LRBP1yoJuIdAGKgFE4AR5qITAWWAqMAN5SVRWRTcAZIjIbaAYMBO6PVfHGpLJId7ZOmrcW4MChveJJ+uf+ETr249hLn+PlzDb1Ua5JYjV23bh97uOBHGA9MFdV80RkiogMdZs9ChwqIgXAH4DgEMwHgRbAJzi/MB5X1TUx/hmMSUmRRs/UeOfq+w/Ay7+BowbB5fPBQt4Q5Q1TqroYWBy27baQx/twhlKGH7cn0nZjTM2qu0M14nZVePMO+N906Hkh/OIRSM+Ic4WmobC5boxJUtXdoVple8APi37nhHzfq+Ci/1jIm/1Y0BuTpIKjZ0JVGRJZUQ4vXA0rnoBT/wjnT4e0/Y8xxua6MSZJBT9wjTjqZt8uyF8Myx+Fwo/g7Lvg5BsSXLFJVhb0xiSx4X2yfhhhU7YHPlsCc+bD56+DvwxaZsHwh6H36MQWapKaBb0xSSLimPmebeDz1yBvHnz2GlSUwo+OgOyroecvoEM/SLMeWHNgFvTGxFF1NzyFbx90TDteXFFEqc9PE8rpuWs53vnTqFi0inR/KTRvB33GOCNqOp1k4W5qRVTDZzNIrOzsbM3NzU10GcbUWfgNTwBej5CeJpT6Avu1bYKPU9LWcr5nGWelreBHUsp2bcF76SczfMx46PxT+5DVHJCIrFDV7Ej77IremDipdik/v3NxlU4Fp6TlcX7aUgZ7cmkpeynR5iz2D2BRYCBLAz3wl6Uz/KifJaJ8k0Is6I2Jk6IINzZ58HNS2jrOS1vGEM9y2sgedmkmrweyedk/kPcDvfCF/LPMstWeTAxY0BsTJx53Kb80AgxIW18Z7m1lF3u0KW8ETmSR/yTeCxxPOc7iH6EdqTaNsIkVC3pj4iEQoA/rOT99Ged6PuIwKWGvNuGtQB9e9g/knUBvyvjh7tVMr4eL+mbx9qfFtZqp0phoWNAbEyuqUJjrDIXMW8ALTbawT728HejNIv9JvBXoTSlNqxzWppmX2y/oaaFu4saC3pi6UIUtqyrDnZ2bwZMBXc8kt8XvuO6jw9jm++HK3esRmmeks7PUZ1ftpt5Y0BsTwQEX/FCFr9e64T4fdmyEtHT4yRkw6GY45lxo2ops4NYOtV84xJhYs3H0xoSJNP490+thxplN+bn/f064bysA8cBRP3NuYjrmPGh2SAKrNo1dncfRi8gQ4AHAA/xHVe8O298EmAX0BbYBI1V1o7vveOARoCUQAPq589cbk5RCx7//RIo4P20Z58kyjn67CCTNuXnppPFw7FBofmiCqzWmZjUGvYh4cFaKOgtnbdjlIrJQVdeFNLsG2KGqXUVkFHAPMNJdP3Y2cLmqrhaRQwFfzH8KY2LIu/NLfu1ZxvmeZRybtomACsu1O7f6ruLOSX+GFoclukRjaiWaK/r+QIGqbgAQkTnAMCA06IcBk93HLwAzRESAs4E1qroaQFW3xahuY2Jrx0anSyZvPu80WQ1AbuBoJvuuYLF/AN/ShqzWmdxpIW8aoGiCPgvYHPK8EBhQXRtVrRCRncChwNGAikgO0A6Yo6r3hn8DERkHjAPo1KlTbX8GYw7OzsLKcKdohbMtqy9re97Eb9ccyQbfD+ut2s1LpiGLJuglwrbwT3Cra5MO/BToB+wF3nQ/MHhzv4aqM4GZ4HwYG0VNxhycXVth3QIn3Dd/6Gw74gQ48w7oORzadKYX8JuuNlrGpI5ogr4Q6BjyvAOwpZo2hW6/fCtgu7v9XVX9DkBEFgMnAm9iTBxEHBbZzQvrXnLC/asPAGVny+7MSR/DnO9PpHzHUQz6rh1vP7KBLSV5lce9P/GMRP84xsRENEG/HOgmIl2AImAUcGlYm4XAWGApMAJ4S1WDXTY3iUgzoBz4GTA9VsUbEyp0WGQbdnHa7jc5fMGtqKxDCEC7Y+D0SbyRdjI3vP79D8MnS0qZvWxT5XmKSkqZNG8tgF3Fm5RQY9C7fe7jgRyc4ZWPqWqeiEwBclV1IfAo8JSIFOBcyY9yj90hIn/H+WWhwGJVfSVOP4tp5B5eksv5gf9xgXcpJ6flkS4BNgR+zD/9Q1nkP4nvdx/NhFbdI04fHK7U52daTr4FvUkJdsOUadj27YRPF0PePMo/e5MM8fNV4DBeCQxkkX8g6/RIQj9CyvR6agz5IAG+vPu8+NRtTIzZwiMmtZTthvwlzhQEBW+AvxxadeL59AuYszebtdqFyOMDnCv14PTBNWlvc8GbFGFBbxqG8u/hsxwn3D9/HSr2wY/aQ79fuotkZ9P84y0UzFsLNVyx+1VrvLK34ZQmlVjQm+TlK3VCPW+eE/K+vdDicDhxrBPuHQfst0h2sD/9j3NXH/CKPcsdVRO+OLfNBW9SlQW9Sajw4ZB/OrMLQ1usd8I9/1Uo3wPN2sIJo5zJw448+YCLZAfDOXxSsqDglfrwPlkW5KbRsKA3CRMcDlnhK+NnaWu54PtlDHo5F6QUMtvAcRc64d75VPBE/1YNBvi0nHyKSkor++Sz7ErdNFIW9CYx/BW8u/g5Juu7DG6ynNbyPbu0GUv8/Via+TP+fuNvweM96NPbFbsxP7CgN/Un4IeN/3O6Zda/zHTfNnZ7Mnk90JdF/oH8L9DLWSR7N/y9DiFvjNmfBb2Jr0AANi11ph9Y9xJ8/y14m0P3IUzM78b83cfst0g22LBGY2LNgt7EXiAAhcvdcF8Au7dS4WnKe9qHF8pHs5J+lK1rwo69viqj3W1YozGxZ0Fvai3ixGG920PRyh8Wyd5VCJ4m0O0sljc/nf/7qB3bfG53zD4Irj+jOLc2KdiHpcbEiQW9qZX911NVWu9cx9YXH2PT/GV0SismIOmkdTsTfn4bdD8Hmrbkd3e/xTZfabXnDIa8zRZpTHxY0JtambbkU46s+JLz0pdxftpSuqR9g089vB84jn/4LuR1f18uaNaDu07oVXnMlpLqQ742bYwxB8eC3kSnOB8+mceTpbPo2mQLfhU+CPTkYd9QcvzZlPCjyqZPL9tE9pGHVHbBtG+dSVENQW4fwBoTPxb0pnrle+GjmbDmOfh2HSDsSu/JzWVDWOLvxzZaRTxMYb8pficM7l7tnapgH8AaE28W9KYqVVj/MuT8GXZuho4D4Zx7occwNhX4mTdvLaX+A08cFtoVE3qn6paSUlplehGBkr0+m1fGmHoQVdCLyBDgAZyFR/6jqneH7W8CzAL6AtuAkaq6MWR/J2AdMFlV/xab0k00Io6QOVCoFn8Gr94EG96Gw4+DXzwCnU+p3D28j/NncHqB6oR3xdidqsYkTo1BLyIe4EHgLJw1YJeLyEJVXRfS7Bpgh6p2FZFRwD3AyJD904FXY1e2icb+I2RqWCKvbDe8ey8s+5dzQ9M50yD76ohzzISG9i0L1vL0sk37rRZvXTHGJJe0mpvQHyhQ1Q2qWg7MAYaFtRkGPOk+fgH4uYgIgIgMBzYAebEp2UQr0pJ5wSXyKqnCmufhn9nwwT/ghNFwwwoYMC6qicTuGt6L6SN7k9U6E8EZJjn1wl529W5MEomm6yYL2BzyvBAYUF0bd43ZncChIlIK/AnnfwM3VvcNRGQcMA6gU6dOURdvDqy6IYuV27/+BBZPgE0fQPs+MOpp6BBxJbIDsm4ZY5JbNEEfaU228FUdqmtzBzBdVfe4F/gRqepMYCY4a8ZGUZOJQnXDGo9uFYDFN8Hyf0PT1nDBA9Dniv0W8TDGpI5ogr4Q6BjyvAOwpZo2hSKSDrQCtuNc+Y8QkXuB1kBARPap6ow6V25qFD6sUQhwacb/uE3nwvISpw9+0M3Q7JAEV2qMiadogn450E1EugBFwCjg0rA2C4GxwFJgBPCWqipwarCBiEwG9ljIx1f4KJuL+mbx9qfFHLozj6lNn6Snfg6HD4Bzp8ERJyS6XGNMPagx6N0+9/FADs7wysdUNU9EpgC5qroQeBR4SkQKcK7kR8WzaBNZpFE2i5d9wiNZr9Bv3yJo1g7OfgSOHwkH6EozxqQW0QMsopwI2dnZmpubm+gyEqrWY99dp9z9VmWffBoBLvW8yY3pc2lBKRu7Xk7Xi++Cpi3jXb4xJgFEZIWqRhxNYXfGJplajX0PExxN01fymeJ9gp5pX/G+vyeTK8ayt6gb71vIG9MoWdAnmQONfa8p6I9rtY8r9z7ORZ7/skUP4fry37A4MAAQxGaHNKbRsqBPMtWNfS8qKaXLxFcid+X4ffDRTOb5/0IgrYwZFcN4sGIYpTStbGKzQxrTeFnQJ5kDTemrROjK2fCuMzdN8ad4u57FdO/V/GOV2pQExphKdodMkpkwuDuZXs8B25T6/Dy55H14/kqYNRR8pTB6Dox5nt+PPNemJDDG7ELZxhEAAA0eSURBVMeu6JNM+JS+4WOiMvBxrWcx4/ctgHxxbng6+QbwZu53Dgt2Y0yQBX0SCg3qn0xajN8dAnt62sfcnv4kXdK+Icffj8G/fhTaHJnIUo0xDYAFfZLzq9JRvuG29Nmc5VnBF4EjuKL8T7wXOIGNFvLGmChY0Cez8r3c1nwBYyrmU0EaU32jecx/Dj7SybJRNMaYKFnQJyNV+HQRLPkzV/s3sUhP4c7y0XyDM/mYjaIxxtSGBX2y+e5zZ7jkF2/BYT1g7CIqSo4iPScfqeWUCMYYAxb0MXGwc9Psp2wPvDcNlj7ojKAZcjf0uxY8XoZT8/QHxhhTHQv6OqrL3DSA003zyYvw2q2wewv0HgNnToYWh8WvaGNMo2JBX0d1mZuGb9Y5S/l99T9nbvhLnoSO/eNYrTGmMbKgr6Ma12WNpLQE3rkbPprpTBt8/nQ4cSykHfiOWGOMORhRTYEgIkNEJF9ECkRkYoT9TUTkOXf/hyLS2d1+loisEJG17p9nxLb8xKtusrCI2wMBWPU0zMiGDx+GvmPhhpXOkn4W8saYOKkx6EXEAzwInAP0AEaLSI+wZtcAO1S1KzAduMfd/h1wgar2wllq8KlYFZ4sIs1NE3H445aP4bHB8NL10KYzjHvHuZK39VqNMXEWTddNf6BAVTcAiMgcYBiwLqTNMGCy+/gFYIaIiKquCmmTBzQVkSaqWlbnypNE+Nw0VUbd7N0Ob90JuY9D87Yw/CE4fhSk2Xxyxpj6EU3QZwGbQ54XAgOqa+OuMbsTOBTnij7oImBVKoV8UMRJxAJ+WPkkvDkF9u2Cgb+C0ydC01aJKdIY02hFE/SRVpEOn1TxgG1EpCdOd87ZEb+ByDhgHECnTp2iKCnJbf4IFt8IW1fDkT+Fc6fB4eG9XcYYUz+iCfpCoGPI8w7AlmraFIpIOtAK2A4gIh2A+cAVqvpFpG+gqjOBmeAsDl6bHyCp7PkW3pgMHz8NP2oPIx6DnheCRPo9aIwx9SOaoF8OdBORLkARMAq4NKzNQpwPW5cCI4C3VFVFpDXwCjBJVd+PXdmJF3o3bMdWGczotoLjP3/QWQTklN/BaROgSYtEl2mMMTUHvdvnPh7IATzAY6qaJyJTgFxVXQg8CjwlIgU4V/Kj3MPHA12BW0XkVnfb2ar6bax/kPp0y4K1PL1sEwoMTFvH5NInOeaTzXzT7hQOH/kAtO2W6BKNMaZSVDdMqepiYHHYtttCHu8DLo5w3F3AXXWsMaksWFXE08s2cTjb+LP3GYZ6llKobRlX/nvydp3K+xbyxpgkY3fG1tL0JZ9wnWchN6TPJ50A91dcyEMVQykjA9m5L9HlGWNMFRb0tVHwBo+X/pajvFt5zd+XOysuY7MeXrm7urtkjTEmkSzoQ1Q73fCOryDnz/DpItI9R3Bl2U28E+i937ECthiIMSYpWdC7Ik03PHneCrrn/4tjC/4DkgY/v52PM3/Bhy/lOzdEuQQYM7CTzRlvjElKFvQ4If/Huavxa3AIv3Jm2kpuk1l0+rTYGQt/9p3QqgNDgYAno+4LjRhjTD1p9EEfvJIPhnxn2crt6bMY5FnNZ4EsRpffzLMX37TfMRGnPDDGmCTVaIM+2B9f5M4bn8k+xqcv4FrPYsrwMsV3ObP8Z3F46x8luFJjjKmbRhn0+/fHK+elfcjN3tm0l+286D+Vu32jKaZ15OmGjTGmgWmUQR9c/q+bFHJH+hOc7FlHXuBIbii/gRXqBLtHhKkX9rIuGmNMg9cog353yTZuSX+RKz057CGTW3xX8Yz/5wTcdVgyvR4LeWNMymhcQR8IwJrneLvpRNroTub4BzGt4hJ20LKySZaNojHGpJjGE/RbV8PiCbD5Q6TN8Vzy3ShyKzpX7rareGNMqkr9oN+7Hd66C1Y8DpmHwNAZHNJ7DJet3spWGwtvjGkEUjfoA35YOctdyq8E+v0SBk2CzDaAjYU3xjQeqRn0hbnOUn5bVkGnk52l/H58XKKrMsaYhEitoN9TDG9OhlWzocWP4cL/QK8RtpSfMaZRS4umkYgMEZF8ESkQkYkR9jcRkefc/R+KSOeQfZPc7fkiMjh2pe/vgtsfY9e04/GtfJaHK85n4O674fiLLeSNMY1ejUEvIh7gQeAcoAcwWkR6hDW7Btihql2B6cA97rE9cJYV7AkMAf7lni+mjr99CWvLDuNZ/yCGlN/N3RWX8nVZBsffviTW38oYYxqcaK7o+wMFqrpBVcuBOcCwsDbDgCfdxy8APxcRcbfPUdUyVf0SKHDPF1O7yvyAMLViDF9oVth2Y4xp3KIJ+ixgc8jzQndbxDaqWgHsBA6N8lhEZJyI5IpIbnFxcfTVG2OMqVE0QR+pk1ujbBPNsajqTFXNVtXsdu3aRVGSMcaYaEUT9IVAx5DnHYAt1bURkXSgFbA9ymPrrGWTyN3+1W03xpjGJJqgXw50E5EuIpKB8+HqwrA2C4Gx7uMRwFuqqu72Ue6onC5AN+Cj2JT+gzV3DKkS6i2beFhzx5BYfytjjGlwahxHr6oVIjIeyAE8wGOqmiciU4BcVV0IPAo8JSIFOFfyo9xj80RkLrAOqAB+rapx+YTUQt0YYyIT1Spd5gmVnZ2tubm5iS7DGGMaFBFZoarZkfZFdcOUMcaYhsuC3hhjUpwFvTHGpDgLemOMSXFJ92GsiBQDX9XhFG2B72JUTjxZnbHXUGq1OmOrodQJ8a31SFWNeMdp0gV9XYlIbnWfPCcTqzP2GkqtVmdsNZQ6IXG1WteNMcakOAt6Y4xJcakY9DMTXUCUrM7Yayi1Wp2x1VDqhATVmnJ99MYYY/aXilf0xhhjQljQG2NMikvqoI/HouQ1nbM+6xSRs0RkhYisdf88I+SYd9xzfux+HZbgWjuLSGlIPQ+HHNPX/RkKROQf7jKSiapzTEiNH4tIQER6u/ti/ppGUedpIrJSRCpEZETYvrEi8rn7NTZkeyJez4h1ikhvEVkqInkiskZERobse0JEvgx5PXvXtc661Oru84fUszBkexf3ffK5+77JSFSdIjIo7D26T0SGu/vi8pqiqkn5hTMl8hfAUUAGsBroEdbmeuBh9/Eo4Dn3cQ+3fROgi3seTzTnrOc6+wDt3cfHAUUhx7wDZCfRa9oZ+KSa834EnISzotirwDmJqjOsTS9gQ7xe0yjr7AwcD8wCRoRsPwTY4P7Zxn3cJoGvZ3V1Hg10cx+3B7YCrd3nT4S2TfRr6u7bU8155wKj3McPA79KZJ1h74PtQLN4vaaqmtRX9PFYlDyac9Zbnaq6SlWDK27lAU1FpEkd64lLrdWdUESOAFqq6lJ13qmzgOFJUudo4Nk61lKnOlV1o6quAQJhxw4GXlfV7aq6A3gdGJKo17O6OlX1M1X93H28BfgWiOd6n3V5TSNy3xdn4LxPwHnfJOw1DTMCeFVV99axngNK5qCPx6LkUS1WXo91hroIWKWqZSHbHnf/+3ZrLP77HoNau4jIKhF5V0RODWlfWMM567vOoJFUDfpYvqZ1eT8d6D2aiNezRiLSH+fq9YuQzX9xu3Smx+gipa61NhWRXBFZFuwOwXlflLjvk4M5ZzzqDBpF1fdorF/TpA76eCxKHtVi5bVUlzqdnSI9gXuA60L2j1HVXsCp7tfldayzxjpqaLMV6KSqfYA/AM+ISMsoz1lbsXhNBwB7VfWTkP2xfk3r8rMn23v0wCdw/qfxFHCVqgavUCcBxwD9cLog/lSXIoPfKsK22tTaSZ0pBi4F7heRn8TgnJHE6jXthbN6X1A8XtOkDvp4LEoej8XK61InItIBmA9coaqVV0qqWuT+uRt4Bue/inV10LW63WDb3JpW4FzVHe2271DDOeutzpD9Va6U4vCa1uX9dKD3aCJez2q5v9BfAW5R1WXB7aq6VR1lwOPU33u0WsGuUFXdgPOZTB+cScRau++TWp8zHnW6LgHmq6ovuCFOr2lSB308FiWP5pz1VqeItMb5BzRJVd8PNhaRdBFp6z72AucDn1B3dam1nYh43JqOwnlNN6jqVmC3iAx0u0KuAF5KVJ1ufWnAxTj9prjb4vGa1uX9lAOcLSJtRKQNcDaQk8DXMyK3/Xxglqo+H7bvCPdPwenzrq/3aHW1tgl2dbh/16cA69z3xds47xNw3jcJe01DVPkMKU6vafKOunH/zZ4LfIZz9Xizu20KMNR93BR4HufD1o+Ao0KOvdk9Lp+QUQuRzpmoOoFbgO+Bj0O+DgOaAyuANTgf0j4AeBJc60VuLauBlcAFIefMxnlDfgHMwL3jOoF/96cDy8LOF5fXNIo6++Fc/X0PbAPyQo692q2/AKdLJJGvZ8Q6gcsAX9h7tLe77y1grVvrbKBFPb1Hq6v1ZLee1e6f14Sc8yj3fVLgvm+aJPjvvjNQBKSFnTMur6lNgWCMMSkumbtujDHGxIAFvTHGpDgLemOMSXEW9MYYk+Is6I0xJsVZ0BtjTIqzoDfGmBT3/yiIrHHUj+R7AAAAAElFTkSuQmCC\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# set up the break point locations\n", "x0 = np.zeros(number_of_line_segments + 1)\n", "x0[0] = np.min(x)\n", "x0[-1] = np.max(x)\n", "x0[1:-1] = res\n", "\n", "# calculate the parameters based on the optimal break point locations\n", "my_pwlf.fit_with_breaks(x0)\n", "\n", "# predict for the determined points\n", "xHat = np.linspace(min(x), max(x), num=10000)\n", "yHat = my_pwlf.predict(xHat)\n", "\n", "plt.figure()\n", "plt.plot(x, y, 'o')\n", "plt.plot(xHat, yHat, '-')\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.10" } }, "nbformat": 4, "nbformat_minor": 4 } ================================================ FILE: examples/mixed_degree.py ================================================ import numpy as np import matplotlib.pyplot as plt import pwlf x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] y = [1, 2, 3, 4, 4.25, 3.75, 4, 5, 6, 7] degree_list = [1, 0, 1] my_pwlf = pwlf.PiecewiseLinFit(x, y, degree=degree_list) breaks = my_pwlf.fit(3) # generate predictions x_hat = np.linspace(min(x), max(x), 1000) y_hat = my_pwlf.predict(x_hat) plt.figure() plt.plot(x, y, 'o') plt.plot(x_hat, y_hat) plt.show() ================================================ FILE: examples/mixed_degree_forcing_slope.py ================================================ import numpy as np from scipy.optimize import differential_evolution import matplotlib.pyplot as plt import pwlf x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] y = [1, 2, 3, 4, 4.25, 3.75, 4, 5, 6, 7] my_pwlf = pwlf.PiecewiseLinFit(x, y, degree=1) # perform initial fit breaks = my_pwlf.fit(2) def my_fun(beta): # assing variables to the pwlf object my_pwlf.beta[0] = beta[0] # first line offset my_pwlf.beta[1] = beta[1] # first line slope my_pwlf.beta[2] = -1*beta[1] my_pwlf.fit_breaks[1] = beta[2] # breakpoint # generate predictions y_temp = my_pwlf.predict(my_pwlf.x_data) # compute ssr e = y_temp - my_pwlf.y_data return np.dot(e, e) bounds = np.zeros((3, 2)) # first line offset bounds[0, 0] = -100.0 # lower bound bounds[0, 1] = 100.0 # upper bound # first line slope bounds[1, 0] = -100.0 # lower bound bounds[1, 1] = 100.0 # upper bound # breakpont bounds[2, 0] = 2. # lower bound bounds[2, 1] = 6. # upper bound res = differential_evolution(my_fun, bounds, maxiter=1000, popsize=30, disp=True) # assign optimum to my_pwlf object my_fun(res.x) # generate predictions x_hat = np.linspace(min(x), max(x), 1000) y_hat = my_pwlf.predict(x_hat) plt.figure() plt.plot(x, y, 'o') plt.plot(x_hat, y_hat) plt.show() ================================================ FILE: examples/model_persistence.py ================================================ # import our libraries import numpy as np import pwlf # if you use Python 2.x you should import cPickle # import cPickle as pickle # if you use Python 3.x you can just use pickle import pickle # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data with the specified break points (ie the x locations of where # the line segments should end my_pwlf.fit_with_breaks(x0) # save the fitted model with open('my_fit.pkl', 'wb') as f: pickle.dump(my_pwlf, f, pickle.HIGHEST_PROTOCOL) # load the fitted model with open('my_fit.pkl', 'rb') as f: my_pwlf = pickle.load(f) ================================================ FILE: examples/model_persistence_prediction.py ================================================ # import our libraries import numpy as np import pwlf # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data with for 4 line segments my_pwlf.fit(4) # save only the parameters necessary to predict for new data np.save('ex_data/saved_parameters.npy', [my_pwlf.beta, my_pwlf.fit_breaks]) # load the parameters necessary for prediction my_prev_model = np.load('ex_data/saved_parameters.npy') # initialize new object my_pwlf_new = pwlf.PiecewiseLinFit(x, y) # predict with the saved parameters y_hat = my_pwlf_new.predict(x, beta=my_prev_model[0], breaks=my_prev_model[1]) ================================================ FILE: examples/prediction_variance.py ================================================ # fit for a specified number of line segments # you specify the number of line segments you want, the library does the rest # import our libraries import numpy as np import matplotlib.pyplot as plt import pwlf # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments res = my_pwlf.fit(4) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # Get the slopes my_slopes = my_pwlf.slopes # Get my model parameters beta = my_pwlf.beta # calculate the prediction variance at the xHat locations pre_var = my_pwlf.prediction_variance(xHat) # turn variance into standard deviation pre_sigma = np.sqrt(pre_var) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.fill_between(xHat, yHat - (pre_sigma*1.96), yHat + (pre_sigma*1.96), alpha=0.1, color="r") plt.show() ================================================ FILE: examples/prediction_variance_degree2.py ================================================ # fit for a specified number of line segments # you specify the number of line segments you want, the library does the rest # import our libraries import numpy as np import matplotlib.pyplot as plt import pwlf # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y, degree=2) # fit the data for four line segments res = my_pwlf.fit(4) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # Get the slopes my_slopes = my_pwlf.slopes # Get my model parameters beta = my_pwlf.beta # calculate the prediction variance at the xHat locations pre_var = my_pwlf.prediction_variance(xHat) # turn variance into standard deviation pre_sigma = np.sqrt(pre_var) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.fill_between(xHat, yHat - (pre_sigma*1.96), yHat + (pre_sigma*1.96), alpha=0.1, color="r") plt.show() ================================================ FILE: examples/robust_regression.py ================================================ # import our libraries import numpy as np import matplotlib.pyplot as plt import pwlf from scipy.optimize import least_squares # generate sin wave data np.random.seed(1213) x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.5, 100) + y # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) num_of_line_segments = 5 # change this value num_in_breaks = num_of_line_segments - 1 n_beta = num_of_line_segments + 1 def my_fun(breaks_and_beta): """ Returns the residuals given an array of breaks and beta """ inner_breaks = breaks_and_beta[:num_in_breaks] # break point locations # breaks code is taken from pwlf... breaks = np.zeros(len(inner_breaks) + 2) breaks[1:-1] = inner_breaks.copy() breaks[0] = my_pwlf.break_0 # smallest x value breaks[-1] = my_pwlf.break_n # largest x value beta = breaks_and_beta[num_in_breaks:] # beta paramters (slopes and int) A = my_pwlf.assemble_regression_matrix(breaks, my_pwlf.x_data) y_hat = np.dot(A, beta) resids = y_hat - my_pwlf.y_data return resids n_runs = 50 # perfrom 50 robust regressions, and take the best one results = [] for i in range(n_runs): # fit the data three line segments and use this result as a starting point res = my_pwlf.fitfast(num_of_line_segments) breaks_and_beta_guess = np.zeros(num_in_breaks + n_beta) breaks_and_beta_guess[:num_in_breaks] = res[1:num_of_line_segments] breaks_and_beta_guess[num_in_breaks:] = my_pwlf.beta # use the result from pwlf to start a robust regresion # notes on soft_l1: from documentation # https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.least_squares.html # ‘soft_l1’ : rho(z) = 2 * ((1 + z)**0.5 - 1). The smooth approximation of l1 # (absolute value) loss. Usually a good choice for robust least squares. result = least_squares(my_fun, breaks_and_beta_guess, # initial guess loss='soft_l1', f_scale=0.1) # inlier residuals less than 0.1 results.append(result) # find the best result costs = [] for r in results: costs.append(r['cost']) # the objective function from each Robust Reg. index_best_result = np.argmin(costs) result = results[index_best_result] # least squares result xhat = np.linspace(0, 10, 1000) yhat_ols = my_pwlf.predict(xhat) # initial guess # put the results back into my_pwlf breaks = np.zeros(num_of_line_segments+1) breaks[0] = my_pwlf.break_0 breaks[-1] = my_pwlf.break_n breaks[1:num_in_breaks+1] = result.x[:num_in_breaks] my_pwlf.fit_breaks = breaks beta = result.x[num_in_breaks:] my_pwlf.beta = beta yhat_robo = my_pwlf.predict(xhat) plt.figure() plt.plot(x, y, 'o') plt.plot(xhat, yhat_ols, '-', label='OLS') plt.plot(xhat, yhat_robo, '-', label='Robust Reg.') plt.legend() plt.show() ================================================ FILE: examples/run_opt_to_find_best_number_of_line_segments.py ================================================ # Running an optimization to find the best number of line segments # import our libraries import numpy as np import matplotlib.pyplot as plt import pwlf from GPyOpt.methods import BayesianOptimization # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # define your objective function def my_obj(x): # define some penalty parameter l # you'll have to arbitrarily pick this # it depends upon the noise in your data, # and the value of your sum of square of residuals l = y.mean()*0.001 f = np.zeros(x.shape[0]) for i, j in enumerate(x): my_pwlf.fit(j[0]) f[i] = my_pwlf.ssr + (l*j[0]) return f # define the lower and upper bound for the number of line segements bounds = [{'name': 'var_1', 'type': 'discrete', 'domain': np.arange(2, 40)}] np.random.seed(12121) myBopt = BayesianOptimization(my_obj, domain=bounds, model_type='GP', initial_design_numdata=10, initial_design_type='latin', exact_feval=True, verbosity=True, verbosity_model=False) max_iter = 30 # perform the bayesian optimization to find the optimum number of line segments myBopt.run_optimization(max_iter=max_iter, verbosity=True) print('\n \n Opt found \n') print('Optimum number of line segments:', myBopt.x_opt) print('Function value:', myBopt.fx_opt) myBopt.plot_acquisition() myBopt.plot_convergence() # perform the fit for the optimum my_pwlf.fit(myBopt.x_opt) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ================================================ FILE: examples/sineWave.py ================================================ # fit for a specified number of line segments # you specify the number of line segments you want, the library does the rest # import our libraries import numpy as np import matplotlib.pyplot as plt import pwlf # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, 100) + y # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments res = my_pwlf.fit(16, disp=True) # i'm passing the argument disp=True to see the progress of the differential # evolution so you can be sure the program isn't just hanging... # Be patient! this one takes some time - It's a difficult problem # using this differential evolution algo + bfgs can be over 500,000.0 function # evaluations # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ================================================ FILE: examples/sineWave_custom_opt_bounds.py ================================================ # fit for a specified number of line segments # you specify the number of line segments you want, the library does the rest # import our libraries import numpy as np import matplotlib.pyplot as plt import pwlf # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, 100) + y # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # define custom bounds for the interior break points n_segments = 4 bounds = np.zeros((n_segments-1, 2)) # first lower and upper bound bounds[0, 0] = 0.0 bounds[0, 1] = 3.5 # second lower and upper bound bounds[1, 0] = 3.0 bounds[1, 1] = 7.0 # third lower and upper bound bounds[2, 0] = 6.0 bounds[2, 1] = 10.0 res = my_pwlf.fit(n_segments, bounds=bounds) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ================================================ FILE: examples/sineWave_degrees.py ================================================ # import our libraries import numpy as np import matplotlib.pyplot as plt import pwlf # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, 100) + y # initialize piecewise linear fit with your x and y data # pwlf lets you fit continuous model for many degree polynomials # degree=0 constant # degree=1 linear (default) # degree=2 quadratic my_pwlf_0 = pwlf.PiecewiseLinFit(x, y, degree=0) my_pwlf_1 = pwlf.PiecewiseLinFit(x, y, degree=1) # default my_pwlf_2 = pwlf.PiecewiseLinFit(x, y, degree=2) # fit the data for four line segments res0 = my_pwlf_0.fitfast(5, pop=50) res1 = my_pwlf_1.fitfast(5, pop=50) res2 = my_pwlf_2.fitfast(5, pop=50) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat0 = my_pwlf_0.predict(xHat) yHat1 = my_pwlf_1.predict(xHat) yHat2 = my_pwlf_2.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o', label='Data') plt.plot(xHat, yHat0, '-', label='degree=0') plt.plot(xHat, yHat1, '--', label='degree=1') plt.plot(xHat, yHat2, ':', label='degree=2') plt.legend() plt.show() ================================================ FILE: examples/sineWave_time_compare.py ================================================ # fit for a specified number of line segments # you specify the number of line segments you want, the library does the rest # import our libraries import numpy as np import matplotlib.pyplot as plt import pwlf from time import time # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, 100) + y # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for sixteen line segments t0 = time() res1 = my_pwlf.fit(16, disp=True) t1 = time() # i'm passing the argument disp=True to see the progress of the differential # evolution so you can be sure the program isn't just hanging... # Be patient! this one takes some time - It's a difficult problem # using this differential evolution algo + bfgs can be over 500,000.0 function # evaluations # predict for the determined points xHat1 = np.linspace(min(x), max(x), num=10000) yHat1 = my_pwlf.predict(xHat1) # fit the data for sixteen line segments # using the default 50 number of multi starts t2 = time() res2 = my_pwlf.fitfast(16) # this is equivalent to my_pwlf.fitfast(16,50) t3 = time() # predict for the determined points xHat2 = np.linspace(min(x), max(x), num=10000) yHat2 = my_pwlf.predict(xHat2) print('Run time for differential_evolution', t1 - t0, 'seconds') print('Run time for multi-start', t3 - t2, 'seconds') # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat1, yHat1, '-', label='Diff. evolution') plt.plot(xHat2, yHat2, '-', label='Multi start') plt.legend() plt.show() ================================================ FILE: examples/slope_constraint_demo.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2.2.1\n" ] } ], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import pwlf\n", "print(pwlf.__version__)\n", "%matplotlib inline" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "# your data\n", "y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02,\n", " 4.39052750e-02, 5.45343950e-02, 6.74104940e-02,\n", " 8.34831790e-02, 1.02580042e-01, 1.22767939e-01,\n", " 1.42172312e-01, 0.00000000e+00, 8.58600000e-06,\n", " 8.31543400e-03, 2.34184100e-02, 3.39709150e-02,\n", " 4.03581990e-02, 4.53545600e-02, 5.02345260e-02,\n", " 5.55253360e-02, 6.14750770e-02, 6.82125120e-02,\n", " 7.55892510e-02, 8.38356810e-02, 9.26413070e-02,\n", " 1.02039790e-01, 1.11688258e-01, 1.21390666e-01,\n", " 1.31196948e-01, 0.00000000e+00, 1.56706510e-02,\n", " 3.54628780e-02, 4.63739040e-02, 5.61442590e-02,\n", " 6.78542550e-02, 8.16388310e-02, 9.77756110e-02,\n", " 1.16531753e-01, 1.37038283e-01, 0.00000000e+00,\n", " 1.16951050e-02, 3.12089850e-02, 4.41776550e-02,\n", " 5.42877590e-02, 6.63321350e-02, 8.07655920e-02,\n", " 9.70363280e-02, 1.15706975e-01, 1.36687642e-01,\n", " 0.00000000e+00, 1.50144640e-02, 3.44519970e-02,\n", " 4.55907760e-02, 5.59556700e-02, 6.88450940e-02,\n", " 8.41374060e-02, 1.01254006e-01, 1.20605073e-01,\n", " 1.41881288e-01, 1.62618058e-01])\n", "x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02,\n", " 5.66106800e-02, 7.95549800e-02, 1.00936330e-01,\n", " 1.20351520e-01, 1.37442010e-01, 1.51858250e-01,\n", " 1.64433570e-01, 0.00000000e+00, -2.12600000e-05,\n", " 7.03872000e-03, 1.85494500e-02, 3.00926700e-02,\n", " 4.17617000e-02, 5.37279600e-02, 6.54941000e-02,\n", " 7.68092100e-02, 8.76596300e-02, 9.80525800e-02,\n", " 1.07961810e-01, 1.17305210e-01, 1.26063930e-01,\n", " 1.34180360e-01, 1.41725010e-01, 1.48629710e-01,\n", " 1.55374770e-01, 0.00000000e+00, 1.65610200e-02,\n", " 3.91016100e-02, 6.18679400e-02, 8.30997400e-02,\n", " 1.02132890e-01, 1.19011260e-01, 1.34620080e-01,\n", " 1.49429370e-01, 1.63539960e-01, -0.00000000e+00,\n", " 1.01980300e-02, 3.28642800e-02, 5.59461900e-02,\n", " 7.81388400e-02, 9.84458400e-02, 1.16270210e-01,\n", " 1.31279040e-01, 1.45437090e-01, 1.59627540e-01,\n", " 0.00000000e+00, 1.63404300e-02, 4.00086000e-02,\n", " 6.34390200e-02, 8.51085900e-02, 1.04787860e-01,\n", " 1.22120350e-01, 1.36931660e-01, 1.50958760e-01,\n", " 1.65299640e-01, 1.79942720e-01])\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "# initialize piecewise linear fit with your x and y data\n", "my_pwlf = pwlf.PiecewiseLinFit(x, y)\n", "\n", "# initialize custom optimization\n", "number_of_line_segments = 3\n", "my_pwlf.use_custom_opt(number_of_line_segments)\n", "\n", "# maximum slope\n", "max_slope = 1.25" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "def my_con(var):\n", " # define a constraint on the maximum slope\n", " my_pwlf.fit_with_breaks_opt(var)\n", " slopes = my_pwlf.calc_slopes()\n", " # element must be greater or equal to 0.0\n", " # in a successfully optimized problem\n", " return np.array((max_slope - slopes.max()))\n" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Optimization terminated successfully (Exit mode 0)\n", " Current function value: 0.00026952124484395115\n", " Iterations: 6\n", " Function evaluations: 20\n", " Gradient evaluations: 6\n" ] } ], "source": [ "from scipy.optimize import fmin_slsqp\n", "# i have number_of_line_segments - 1 number of variables\n", "# let's guess the correct location of the two unknown variables\n", "# (the program defaults to have end segments at x0= min(x)\n", "# and xn=max(x)\n", "xGuess = np.zeros(number_of_line_segments - 1)\n", "xGuess[0] = 0.06\n", "xGuess[1] = 0.13\n", "bounds = np.zeros((number_of_line_segments - 1, 2))\n", "bounds[:, 0] = my_pwlf.break_0\n", "bounds[:, 1] = my_pwlf.break_n\n", "\n", "res = fmin_slsqp(my_pwlf.fit_with_breaks_opt, xGuess, f_ieqcons=my_con,\n", " bounds=bounds, iter=100, acc=1e-06, iprint=1,\n", " epsilon=1.4901161193847656e-08)\n" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# set up the break point locations\n", "x0 = np.zeros(number_of_line_segments + 1)\n", "x0[0] = np.min(x)\n", "x0[-1] = np.max(x)\n", "x0[1:-1] = res\n", "\n", "# calculate the parameters based on the optimal break point locations\n", "my_pwlf.fit_with_breaks(x0)\n", "\n", "# predict for the determined points\n", "xHat = np.linspace(min(x), max(x), num=10000)\n", "yHat = my_pwlf.predict(xHat)\n", "\n", "plt.figure()\n", "plt.plot(x, y, 'o')\n", "plt.plot(xHat, yHat, '-')\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([1.10704555, 0.50206765, 1.24978354])" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "my_pwlf.calc_slopes()" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "# the maximum slope is less than max_slope!\n", "# this would not be possible without the constrained fit!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.13" } }, "nbformat": 4, "nbformat_minor": 4 } ================================================ FILE: examples/stack_overflow_example.py ================================================ import numpy as np import matplotlib.pyplot as plt import pwlf # https://stackoverflow.com/questions/29382903/how-to-apply-piecewise-linear-fit-in-python x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) y = np.array([5, 7, 9, 11, 13, 15, 28.92, 42.81, 56.7, 70.59, 84.47, 98.36, 112.25, 126.14, 140.03]) # pwlf has two approaches to perform your fit: # 1. You can fit for a specified number of line segments. # 2. You can specify the x locations where the continuous piecewise lines # should terminate. # Approach 1 my_pwlf = pwlf.PiecewiseLinFit(x, y) # breaks will return the end location of each line segment breaks = my_pwlf.fit(2) print(breaks) # The gradient change point you asked for is at breaks[1] x_hat = np.linspace(x.min(), x.max(), 100) y_hat = my_pwlf.predict(x_hat) plt.figure() plt.plot(x, y, 'o') plt.plot(x_hat, y_hat, '-') plt.show() ================================================ FILE: examples/standard_errrors_and_p-values.py ================================================ # example of how to calculate standard errors and p-values from __future__ import print_function import numpy as np import pwlf # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # fit the data for our specified line segment locations res = my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # Get my model parameters beta = my_pwlf.beta # calculate the standard errors associated with each beta parameter # not that these standard errors and p-values are only meaningful if # you have specified the specific line segment end locations # at least for now... se = my_pwlf.standard_errors() # calculate my t-value t = beta / se k = len(beta) - 1 # calculate the p-values pvalues = my_pwlf.p_values() # print the results values = np.zeros((k, 4)) values[:, 0] = beta values[:, 1] = se values[:, 2] = t values[:, 3] = pvalues header = ['Beta value', 'Standard error', 't', 'P > |t| (p-value)'] print(*header, sep=' & ') for row in values: print(*row, sep=' & ') ================================================ FILE: examples/standard_errrors_and_p-values_non-linear.py ================================================ from __future__ import print_function import numpy as np import pwlf # import matplotlib.pyplot as plt # generate a true piecewise linear data np.random.seed(5) n_data = 100 x = np.linspace(0, 1, num=n_data) y = np.random.random(n_data) my_pwlf = pwlf.PiecewiseLinFit(x, y) true_beta = np.random.normal(size=5) true_breaks = np.array([0.0, 0.2, 0.5, 0.75, 1.0]) y = my_pwlf.predict(x, beta=true_beta, breaks=true_breaks) # plt.figure() # plt.title('True piecewise linear data') # plt.plot(x, y) # plt.show() # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for our specified line segment locations res = my_pwlf.fitfast(4, pop=100) # calculate the non-linear standard errors se = my_pwlf.standard_errors(method='non-linear', step_size=1e-4) # calculate p-values p = my_pwlf.p_values(method='non-linear', step_size=1e-4) parameters = np.concatenate((my_pwlf.beta, my_pwlf.fit_breaks[1:-1])) header = ['Parmater type', 'Parameter value', 'Standard error', 't', 'P > |t| (p-value)'] print(*header, sep=' & ') values = np.zeros((parameters.size, 5), dtype=np.object_) values[:, 1] = np.around(parameters, decimals=3) values[:, 2] = np.around(se, decimals=3) values[:, 3] = np.around(parameters / se, decimals=3) values[:, 4] = np.around(p, decimals=3) for i, row in enumerate(values): if i < my_pwlf.beta.size: row[0] = 'Beta' print(*row, sep=' & ') else: row[0] = 'Breakpoint' print(*row, sep=' & ') ================================================ FILE: examples/test0.py ================================================ import numpy as np import matplotlib.pyplot as plt import pwlf from scipy.optimize import minimize y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) x0 = np.array([min(x), 0.039, 0.10, max(x)]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data with the specified break points (ie the x locations of where # the line segments should end # my_pwlf.fit_with_breaks(x0) # my_pwlf.seperateData(x0) # res = my_pwlf.fit(3) # initialize custom optimization number_of_line_segments = 3 my_pwlf.use_custom_opt(number_of_line_segments) res = minimize(my_pwlf.fit_with_breaks_opt, np.array([x0[1], x0[2]])) # set up the break point locations x0 = np.zeros(number_of_line_segments + 1) x0[0] = np.min(x) x0[-1] = np.max(x) x0[1:-1] = res.x # calculate the parameters based on the optimal break point locations my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ================================================ FILE: examples/test_for_model_significance.py ================================================ # example of how to calculate standard errors and p-values from __future__ import print_function import numpy as np import pwlf from scipy.stats import f # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # fit the data for our specified line segment locations # this is a linear model res = my_pwlf.fit_with_breaks(x0) # calculate the p-value as a test for significance in Regression # H0: beta0 = 0, beta1 = 0... # H1: betaj != 00 for at least 1 j # As defined in Section 2.4.1 of Myers RH, Montgomery DC, Anderson-Cook CM. # Response surface methodology . Hoboken. New Jersey: John Wiley & Sons, Inc. # 2009;20:38-44. sse = my_pwlf.ssr # this is to follow the notation in the above textbook ybar = np.ones(my_pwlf.n_data) * np.mean(my_pwlf.y_data) ydiff = my_pwlf.y_data - ybar sst = np.dot(ydiff, ydiff) ssr = sst - sse k = my_pwlf.beta.size - 1 n = my_pwlf.n_data f0 = (ssr / k) / (sse / (n - k - 1)) p_value = f.sf(f0, k, n-k-1) print(f"Linear p-value: {p_value}") # The above case is a linear model, where we know the breakpoints # The following case is for the non-linear model, where we do not know the # break point locations res = my_pwlf.fit(2) sse = my_pwlf.ssr # to follow the Book notation ybar = np.ones(my_pwlf.n_data) * np.mean(my_pwlf.y_data) ydiff = my_pwlf.y_data - ybar sst = np.dot(ydiff, ydiff) ssr = sst - sse nb = my_pwlf.beta.size + my_pwlf.fit_breaks.size - 2 k = nb - 1 n = my_pwlf.n_data f0 = (ssr / k) / (sse / (n - k - 1)) p_value = f.sf(f0, k, n-k-1) print(f"non-linear p_value: {p_value}") # in both these cases, the p_value is very small, so we reject H0 # and thus our paramters are significant! ================================================ FILE: examples/test_if_breaks_exact.py ================================================ import numpy as np import pwlf x = np.array((0.0, 1.0, 1.5, 2.0)) y = np.array((0.0, 1.0, 1.1, 1.5)) my_fit1 = pwlf.PiecewiseLinFit(x, y) x0 = x.copy() # check that I can fit when break poitns spot on a ssr = my_fit1.fit_with_breaks(x0) # check that i can fit when I slightly modify x0 my_fit2 = pwlf.PiecewiseLinFit(x, y) x0[1] = 1.00001 x0[2] = 1.50001 ssr2 = my_fit2.fit_with_breaks(x0) # check if my duplicate is in a different location x0 = x.copy() my_fit3 = pwlf.PiecewiseLinFit(x, y) x0[1] = 0.9 ssr3 = my_fit3.fit_with_breaks(x0) # check if my duplicate is in a different location x0 = x.copy() my_fit4 = pwlf.PiecewiseLinFit(x, y) x0[1] = 1.1 ssr4 = my_fit4.fit_with_breaks(x0) # check if my duplicate is in a different location x0 = x.copy() my_fit5 = pwlf.PiecewiseLinFit(x, y) x0[2] = 1.6 ssr5 = my_fit5.fit_with_breaks(x0) # check if my duplicate is in a different location x0 = x.copy() my_fit6 = pwlf.PiecewiseLinFit(x, y) x0[2] = 1.4 ssr6 = my_fit6.fit_with_breaks(x0) ================================================ FILE: examples/tf/fit_begin_and_end.py ================================================ # fit and predict between a known begging and known ending # import our libraries import numpy as np import matplotlib.pyplot as plt import pwlf from scipy.optimize import differential_evolution # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFitTF(x, y, disp_res=True) # fit the function with four line segments # force the function to go through the data points # (0.0, 0.0) and (0.19, 0.16) # where the data points are of the form (x, y) x_c = [0.0, 0.19] y_c = [0.0, 0.2] breaks = [0.00711605, 0.12014667, 0.1799223] L = my_pwlf.fit_with_breaks_force_points(breaks, x_c, y_c) # predict for the determined points xHat = np.linspace(min(x), 0.19, num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ================================================ FILE: examples/tf/sine_benchmark_fixed_20_break_points.py ================================================ import numpy as np import pwlf from time import time import os breaks = np.linspace(0.0, 10.0, num=21) n = np.logspace(3, 7, num=15, dtype=np.int) n_repeats = 10 run_times = np.zeros((3, n.size, n_repeats)) for i, n_data in enumerate(n): # set random seed np.random.seed(256) # generate sin wave data x = np.linspace(0, 10, num=n_data) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, size=n_data) + y for j in range(n_repeats): # normal PWLF fit t0 = time() my_pwlf = pwlf.PiecewiseLinFit(x, y) ssr = my_pwlf.fit_with_breaks(breaks) t1 = time() # PWLF TF fit t2 = time() my_pwlf = pwlf.PiecewiseLinFitTF(x, y) ssr = my_pwlf.fit_with_breaks(breaks) t3 = time() run_times[0, i, j] = t1 - t0 run_times[1, i, j] = t3 - t2 np.save('bench_run_times/20_break_times.npy', run_times) np.save('bench_run_times/n.npy', n) ================================================ FILE: examples/tf/sine_benchmark_six_segments.py ================================================ import numpy as np import matplotlib.pyplot as plt import pwlf from time import time # set random seed np.random.seed(256) breaks = np.array((0.0, 0.94, 2.96, 4.93, 7.02, 9.04, 10.0)) n_data = int(1e6) # generate sin wave data x = np.linspace(0, 10, num=n_data) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, size=n_data) + y t0 = time() # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # # fit the data for four line segments # res = my_pwlf.fit(16) # breaks = my_pwlf.fit(6) ssr = my_pwlf.fit_with_breaks(breaks) t1 = time() print('run time:', t1 - t0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ================================================ FILE: examples/tf/test_fit.py ================================================ # fit and predict with known line segment x locations # import our libraries import numpy as np import matplotlib.pyplot as plt import pwlf # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFitTF(x, y) # fit the data with the specified break points (ie the x locations of where # the line segments should end ssr = my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ================================================ FILE: examples/understanding_higher_degrees/polynomials_in_pwlf.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pwlf\n", "# generate sin wave data\n", "x = np.linspace(0, 10, num=100)\n", "y = np.sin(x * np.pi / 2)\n", "# add noise to the data\n", "y = np.random.normal(0, 0.05, 100) + y\n", "my_pwlf_2 = pwlf.PiecewiseLinFit(x, y, degree=2)\n", "res2 = my_pwlf_2.fitfast(5, pop=50)" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYoAAAEGCAYAAAB7DNKzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+17YcXAAAgAElEQVR4nO3de3zU5Zn38c+VEEhEJIp4IIhARSpyCCWeoLYeqmBBjLZWa12ru5btQanuI23cttbtow+xdB9r3daWVWu7tauWKiK4Yiv6tGK1BkEEBcVzAgoqQZRATvfzx8yEyTAzOc38TvN9v155hfxmMnMNk/ld9+G675855xAREcmkyO8AREQk2JQoREQkKyUKERHJSolCRESyUqIQEZGs+vkdQK4dfPDBbuTIkX6HISISKqtWrXrPOTc03W2RSxQjR46krq7O7zBERELFzN7MdJuGnkREJCslChERyUqJQkREslKiEBGRrJQoREQkq8hVPYkE1eLVDSxYvpHNjU0MKy9j3vSxVE+u8DsskS4pUYh4YPHqBq69/wWaWtoAaGhs4tr7XwBQspDA09CTiAcWLN/YkSQSmlraWLB8o08RiXSfEoWIBzY3NvXouEiQKFGIeGBYeVmPjosEiRJFgCxe3cC02hWMqlnGtNoVLF7d4HdIkiPzpo+lrKS407GykmLmTR/rU0Qi3afJbB+kq34BNNkZYYn3UFVP0VIolWy+JgozuxOYBWx1zo1Pc7sBtwCfB3YBlzrnnvM2ytzKVP1SWlKUcbIzin94haJQTiSFKFslG0SrUeB3j+Iu4D+A32a4/SxgTPzrBOC2+PfQylT9knosQZOd4aWS2GjL9Fm+fsl69rS2R+p993WOwjn3F+CDLHc5B/iti3kaKDezw72JLj96euLXZGd4qSQ22jJ9lhubWiL3vgd9MrsCeDvp5/r4sU7MbI6Z1ZlZ3bZt2zwLrjcynfjLy0o02RkxKomNtp424sL8vvs99NQVS3PM7XPAuYXAQoCqqqp9bg+SedPHdhqOgNiLbGxqobyshNKSIhp3tURiXLPQDSsvoyHNyUG9xPBJnmsaXFaCGWzf1YKR5oSUgQOm1a4I5ec66ImiHjgi6efhwGafYsmJ5OqXhsamTn9ojU0tlJUUc/MFlaH7Q5J9pWsUqJcYPqlzTY1NLR23OehRsgjrfEXQh56WAJdYzInADufcFr+D6qvqyRWsrDmNivIyHFBEO0Y7EP6xTNm7Hubqe9cwoF8RB+5XggEV5WXMP28C1ZMrtGYmRNLNNSVzQLGlG/xIL4yfcb/LY/8bOAU42MzqgR8CJQDOuV8CDxMrjd1ErDz2Mn8izY/9drzCo/1v4RO2GYfxihvOg21T+V3jGYBKK8MoXesztZeYrhpq3h+e598eWq9hxwDqztxCm3OUlRTv03uMSjWjr4nCOfflLm53wLc8Csc7zbug/360HjCCt3YdwvL24yiinaqil6kpuYd/LPkTT/6pmGv/UhapErtCkK3SKXnYMfU+Le2O7btiQxp6r4Ml01xTsop4ck9t2CWGmNM9ZpgEfY4ieup+DSt/Cpc/xrdnTOTK+2toat170jix5FX+c/CvOXHlPzKj7Z95gJM7btMCvOBK9P4ynVCSW5DdaU3qvQ6OdHNNyRLzTtWTK9K+X1GYp1Ki8NphE2HEVOg/kOrJBwOdV3BeOP0LDDrmUlbeOIOflPySXS2lLG8/ruPXw9ZljbLk5NDVhGZyC7I7LVTQex0UqduvJKqeujNMGJWtW5QoPLL07xuYv2Jz/I/lfOaN/KCjBZLuj+a6/b5Pza6fsM0N7nQ8bF3WqEqdZ8iWJFJbkF21UBP0XgdHps9pX383LPOQQa96ioSlf99A5bJZzNp5H469Y9DZKl2unDGJuXyH59zR8SMulF3WqOqqEiYhudIpoXpyBfPPm0BFeRlGbLFlSXHnqhm919GXaGw0NDZ1+7zgF/Uo8ijRWvjWRz/j8OL3eKb9mI7buhqD7uiyPrKBiz/+NfsP6MegWTcGsrVRiLozLFRRXsbKmtPS3pbaygxLy1JypzuFD0GhRJEnidbCuNYXuWjA4/yqdSZr3FGd7tPVyabjZPLQEsCgclgeI5ae6Gqeoac9gr4MbUg4hWmLFw095cmC5RvZ3dLCD0t+y2Z3ED9t/cI+9+n2GPSsn8LZP4UeLOqR/Ep3IaLEu5NuuEkkIbHYMtO8VhDnptSjyJPNjU2cXfQ0E4te5+rmb9BEaafbe9TiNGPx6gYe+p+lfLBzN1sHT9DQhM+iUs0i3kotgkgV1Lkpi61pi46qqipXV1fndxh8Zv6fuKvpCvZQwueb5+OSOm8VPTypLF7dwPfuf57/sbk0sj+zm2/AMFwvHktE/DOtdkXGIUu/P8tmtso5V5XuNvUociwxKVm1cwWj+7/DPzdf1ZEkykqKezUksWD5Rj5ucdxafC4LShZyStEanmifDGgVr0iYZJp/MMhY+BAEmqPIob3lbrv4er+HeLm9gj+1xxJ0X8atE39cD7R9mgY3hG/0e6jT7WHcZEykEGWafwjivEQyJYocSpS7ldLM0+3juLX1XNop6iiT7G2LP/FH1Eo/7mj9PCcUbWCSbep0nyBWSohIZ+mKIII6L5FMiSKHEifr3Qzg31q/ykPtUzsd763kP657207hQ1fGZf0e6XSfoLdIRMImH1vBpy62DEuFnOYocmhYeRntjfWMsK084z5JomCyryfxzhc7gj+2fYavFP+ZG/gH3mNwKFok0jtaiOePdFvB52ouMIxrZtSjyKF508fy1f4ruLv/jQylEchdtzJxsaM3amdyxJlX0t/a+GLx/wtNi0R6LkxbPERNtlXThUiJIoeqJ1dQcfb3+Zf+P+Q9DszbSfxznzkZjpxGzaF/Z+V3T1WSiCidrPwTplXTXtDQU46dfdxRnH3cVfws30904jdh82po3QMlpV3fX0JHJyv/ZNqipVDnAtWjyKVHfxC7MJEXjpkFp/9ASSLCwlpKGQVhrU7KFyWKXGnaDs/8Et572bvnbGuBl5dDi1qYUaSTlX/CWp2ULxp6ypUXl0BbM0w437vnfHMl/P5L8KXfwrhzvHveAuZlFZL2k/JXGKuT8kWJIlfW/REO+gQMm+zdc448GS66D0af6t1zFrB8lkxmopOVBIGGnnLg4afX0vb6X7l16wSm3fS4d+WLRcVw9HTo19+b5ytwXlUh5WOhl0hfKFH00eLVDTzz8G8opp1lbSd6X+ve/DGsuAE2/dmb5ytgXlQhae2EBJESRR8tWL6R090zvN5+KBvcEYDHte79SmHVXbD6bm+er4B5UYWktRMSREoUfbSz8T1OKnqR5e3Hs/caZx7WuhcVw9jPwyuPQstub56zQHlRhaS1E94LylBfUOJIR4mij84b9CIl1sajbVM6Hfe01v2Ts6D5I3jjr949ZwHyomRSaye8FZShvqDEkYkSRR+deuJx3NN+BqvdUR3HPK91H/UZ6L8/bFjm3XMWqMSeW6/XzuzT1vGZaO2Et4Iy1BeUODJRouijz54+k9Jzb2FY+UBfFuYsXt3AtH9/iv9pOoZ3Vy1h8XP1njyv5IcWenkrKEN9QYkjE62j6IvGt6ClierKo335ICfX9a8onsxZxc/ymweWgp2tE0uIZVo7oS3Hcy8oezoFJY5M1KPopcWrG7j71h/Q/B8ncUbtw76MJSZ3V59oqwTgpPZVgemuSu4EfQw7rNIN9Rmx/18vJ5SDEkcm6lH0QuJDW95yOo8XfYJX9pD3FbrpJHdLt1HOuvaRnFL8PLc1VnsWQyEIQks+2xi2ehW91/miYE0Y4OK3ebHyPmhxZKIeRS8kPrRbGMKf22PVTn5MPKV2Sx9om8am9gqGDdaOsrkSlJZ80MewwyxRoFBRXtZxck7w8nMdlDjSUaLohc2NTZxUtJ4Li1fQj9ZOx72U2l29o20m/9v+mXkzPulpHFEWlGoUlc3mX1CScVDiSKZE0QvDysv4cvEK/qXfIlop7nTcS2krZM4dT/UY7f2UK0H50KpsNv+CkoyDEkcyJYpemHfmGD5dtI4n28eTWI3t14c2ua5/3vSxDFj6LV7/8bRATIBFQVA+tCqbzb+gJOOgxJFMk9m9UH34drCdvFg6GWshEKWKibH049tO4DAbw+bGj32fAIuCedPHdtpaHPxtFOi9zJ+gXP8jKHEkM+dSp03CraqqytXV1eX3SZ66FR79PvzLS3DAsPw+VzdNq12Rtg67oryMlTWn+RBRdASh6ikMMUm4mdkq51xVutvUo+iN1/8CQ44KTJKAzmPmw20rw+09nm4fp6qYHAhaS96PCyhJYfN1jsLMZpjZRjPbZGY1aW6/1My2mdma+NflfsTZSVsrvPm32P5KAZI8Zv7dfvdwc8kvAKeqmAgKSiWWFA7fEoWZFQM/B84CxgFfNrNxae56r3OuMv51u6dBprPleWjeCSM/7XcknSRPgD3dPo7D7QM+WbJNVTERFJRKLPFGELYf97NHcTywyTn3mnOuGbgHOMfHeLq0eHUDP7/rLgDOfohAVRUlV8U83X4MAPOnfKihiAgKSiWW5F9QFnz6mSgqgLeTfq6PH0v1BTNba2aLzOwIb0LbV+INK9q9nRfbj+SFHaWB22snUSr72P/5Ggw8hMnt6/0OSfIgiOWTkh9BGWb0M1FYmmOpJVgPASOdcxOBPwO/SftAZnPMrM7M6rZt25bjMGMSb9hNrV9mZvONQIDHhc1oGDyZd9Y+FsirZUnfaE1F4QjKMKOfVU/1QHIPYTiwOfkOzrn3k378T+CmdA/knFsILIRYeWxuw4xJfmNcUn4N4rjw4tUNrKs/nO8XvccwttHQOFRVMRETtEosyY+gbD/uZ4/iWWCMmY0ys/7AhcCS5DuY2eFJP84GXvIwvk5i23Y8xv39r2MgTZ2OB82C5Rt5quVoAI4rivV4Atv7EZGMgjLM6FuicM61AlcAy4klgPucc+vN7EdmNjt+t7lmtt7MngfmApf6E23sDWsuHsg2V87HxJJDUMeFNzc2scGN4ENX1pEoEsele4JQaSISlGFGXxfcOeceBh5OOXZd0r+vBa71Oq50Ym/M1/nR8lOxgK+GTXRXV7ePYXLRpk7HpWta0CZBEoRhRm3h0V0tu2PfS4J/rYe9F1baynb2ZzcDKCsp1oRnN2k7FClE2bbw0O6x3bVhKcwfDtte9juSLiW6q0Xlw9nDAFXFdFNiuCldkgAN3Unh0l5P3fX236G4BA4a7Xck3VI9uYLqymHw+I2xmCvVEs4mdbgpHQ3dSaFSj6K76v8OFVOgOES51Qw2PQYNq/yOJPDSLWxKFtTCBREvhOis56OWJnjnBZg61+9Ieu7yx6BI7YGuZBtWqghw4YL0jrZp7xkliu7YvAbaW2H4cX5H0nNKEt2SaWGTJrCjR1VtPaezSHc0xKuowpgodn0Av54Ja+/zO5JAC8rCJsm/oOyfFCbqUXRHfR2Uj4D9h/odSc+VlsO7L8CbK2Hil/yOJrCCePnJ7tIwSs8EZf+kMFGi6I6GVTA8bXlx8BUVxSbh6zWh3ZUgLGzqKQ2j9FxQ9k8KEw09deWjrbDj7djJNqwqpsDWF6F5l9+RSI5pGKXnNMzYc0oUXek3AGbfCkef5XckvTfsU+Da4J21fkciOaZhlJ4Lyv5JYaKhp66UDoZPXeJ3FH1T8anY94bnYMSJ/sYiOaVhlN4J4zCjn9Sj6Mqrj8P7r/odRd8MOgwOqNDCuwjSMIp4QT2KbJxj933/xGOtk7ji48vDXVEybDJsWeN3FJJjYa7WkvBQoshi8eoGbt/1r+xqdZ0ubA4hrCgZVhnb2HD3jthwmkSGhlEk3zT0lMWCR19mXcvhvOaGdRwLbUXJyJNh4gXQ/LHfkYhIyKhHkcW4D5/kpOKPWNT22U7HQ1lRMuJETWSLhJxfiyuVKLL4x9IVlLe+t0+iCG1FiXOwuxHKDvQ7ksDQqubCEfb32s/FlRp6ysQ5Jpe8xQY6X38ijBUliQvyPHDdLN768VRd/zku8cFraGzqNAel/5/oicJ77efiSiWKTHa+Q+me9/nEpGmhXpiT/AFZ2nYCC5unc+39a0P1AckXrWouHFF4r/1cXKmhpzQWr25g5cO/YwFw28aBzDsrXF3UZMkfkMfa49uQtLWzYPnG0L6mXIniquawD6/kSxTeaz8XV6pHkSLRAj9s18u0O+MvHx4Wui5qss4fBMeR9g4j7N1QfUDyJdMHLKxzUFEYXsmXKLzXfi6uVKJIkWiBjyt6kzfcoXxMWei6qMlSPwh/7H893yx+MFQfkFxLzNk0NDZhKbeFcQ4qIQrDK/kShRXsfu5RpaGnFImW9rH2BmvdJ/Y5Hjbzpo9NqpQwXmw/kgnFb4bqA5JLqZUjDrD497Bf8jQKwyv5EpUV7MmLKxPDjFffuybvr0eJIsWw8jI+bHyfEUXbuKfltE7Hwyj1A/JW/6OY5pZy7MRDfI7MH+la3YkkEfZLnmqDwOyitILd61JZDT2lmDd9LGNLttLiinnRHQmEr4uaqnpyBStrTuP12plcfM5Mittb4L2X/Q7LF1FudUdheEW6x+thRvUoUsSycTWfe+QotuzZHfrhiH0cNiH2/Z11cOix/sbigyi3uqMyvCJd87rBo0SRRpS6qPsYchQUD4hdxGjSBX5H47nOczYxUWp1R/pvVzp43eDR0FM6S6+GZ2/3O4r8KO4HhxwD7673OxJf6OpmhSVR4TaqZhnTaldEplTY62FG9ShSOQfvvQL7DfE7kvw5bDy8/KjfUfhGre7C4OfeSPnm9TCjEkUqM7h0qd9R5M3i1Q288UJ/rmrdyqz593P5jBMC+6FpaWmhvr6e3bt3+x2KpFFaWsrw4cMpKSnxO5S0sk34BvVvvie8bPAoURSQRAvrwJZKnin6Hq/stkC3sOrr6xk0aBAjR47ELHVpnHRl+65m3t2xm+a2dvoXF3Ho4FIO3K9/Th7bOcf7779PfX09o0aNyslj5lqUK9y8pjmKVH/9d7jjTGhv9zuSnEu0sDZzMH9rP5Y99A/0yt3du3czZMgQJYle2L6rmYbtTTS3xf6Om9vaadjexPZdzTl5fDNjyJAhge7tRWHbjqBQokhVvwqaGqEoev81yS2pTxe9wBlFdfscDxolid55d8du2p3rdKzdOd7dkbsTe9DfG60ryZ3onQ17KVEd8dZLf+exDw6OTHVEsuSW1Jzipcztd/8+xyUaEj2J7h6PIlW45Y4SBXvH7rc3bmdE0TZW7xkWyV03k1tY32mZwwXN12HEqkGiVDqYS8XFxVRWVnZ8vfHGG9TV1TF37txuP0ZjYyO/+MUv8hjlvvoXp/9oZzqeyRtvvMHvf//7XITki+RdCVbWnKYk0UtKFOwduz/a6gHY6I4I9Nh9byW3sN5hCE2Ukhic0JbU6ZWVlbFmzZqOr5EjR1JVVcXPfvazfe7b2tqa9jH8SBSHDi6lKGVoqMiMQweX9uhxwp4oJDeUKNg7Rn900dtALFEkH4+SRAvrk4PbmNfvHqbY3mQYxeSYD0888QSzZs0C4Prrr2fOnDmceeaZXHLJJaxfv57jjz+eyspKJk6cyCuvvEJNTQ2vvvoqlZWVzJs3r9Njffzxx8ycOZNJkyYxfvx47r33XgBWrVrFZz/7WaZMmcL06dPZsmULAM8++ywTJ07kpJNOYt68eYwfPx6Au+66i+rqas4++2xGjRrF3XcuZPF//YoLZ3yGi2efwa4PG6k4sIwPtrzNjBkzmDJlCieffDIbNmwA4NJLL2Xu3LlMnTqV0aNHs2jRIgBqamr461//SmVlJTfffLMn/78SPF2Wx5rZFcDdzrntHsTji8Ry+LFWzy43gLfd0I7jUfXmjla+PuAh9rgSVrXtndwLdHL89cyu73P0dJg2d+/9Ky+CyV+Bj9+H+y7pfN/LlnX5cE1NTVRWVgIwatQoHnjggX3us2rVKp588knKysq48sor+fa3v81XvvIVmpubaWtro7a2lnXr1rFmzZp9fveRRx5h2LBhLFsWi2XHjh20tLRw5ZVX8uCDDzJ06FDuvfdevve973HnnXdy2WWXsXDhQqZOnUpNTU2nx1q3bh2rV69m9+7dHHXUUdx00028uG4tV199NU8/upgTr7qKL86Zwy9/+UvGjBnDM888wze/+U1WrFgBwJYtW3jyySfZsGEDs2fP5otf/CK1tbX85Cc/YenS6K4tkq51Zx3FYcCzZvYccCew3LmUcopeMrMZwC1AMXC7c6425fYBwG+BKcD7wAXOuTdy8dzJEvv/HM3bvOIqcBRFvjrioPJy3tx1CGOK6iFpTVKUk2NvJIaespk9ezZlZbH/t5NOOokbb7yR+vp6zjvvPMaMGZP1dydMmMA111zDd7/7XWbNmsXJJ5/MunXrWLduHWeccQYAbW1tHH744TQ2NrJz506mTp0KwEUXXdTpBH7qqacyaNAgBg0axODBgzn77LM7nmPt2rV89NFHPPXUU5x//vkdv7Nnz56Of1dXV1NUVMS4ceN49913e/C/JFHXZaJwzn3fzH4AnAlcBvyHmd0H3OGce7W3T2xmxcDPgTOAemLJaIlz7sWku/0TsN05d5SZXQjcBOR8J7vEBNcxDzbweOuE6O0Ym8a86WN5dfERHE19x7HAJ8du9AAy3n/gkJ7/fjcNHDiw498XXXQRJ5xwAsuWLWP69OncfvvtjB49OuPvHn300axatYqHH36Ya6+9ljPPPJNzzz2XY489lr/97W+d7rt9e/ZO/YABAzr+XVRU1PFzUVERra2ttLe3U15enjHxJf9+jtqCEhHdmqOI9yDeiX+1AgcCi8zsx3147uOBTc6515xzzcA9wDkp9zkH+E3834uA0y1PxdvVEw/l4MqZnP+lSwqiOqJ6cgVHjJ3CyKJ3KaFVpYM58tprrzF69Gjmzp3L7NmzWbt2LYMGDWLnzp1p779582b2228/Lr74Yq655hqee+45xo4dy7Zt2zoSRUtLC+vXr+fAAw9k0KBBPP300wDcc889PYrtgAMOYNSoUfzhD38AYsng+eefz/o72WKXwtGdOYq5wFeB94DbgXnOuRYzKwJeAb7Ty+euAN5O+rkeOCHTfZxzrWa2AxgSjyU5xjnAHIARI0b0LpriflD98979bkiNPfdfofh6Xum/n9+hRMa9997L7373O0pKSjjssMO47rrrOOigg5g2bRrjx4/nrLPOYsGCBR33f+GFF5g3bx5FRUWUlJRw22230b9/fxYtWsTcuXPZsWMHra2tXHXVVRx77LHccccdfO1rX2PgwIGccsopDB48uEfx3X333XzjG9/ghhtuoKWlhQsvvJBJkyZlvP/EiRPp168fkyZN4tJLL+Xqq6/u9f+NhJd11cU0sx8RG2Z6M81txzjnXurVE5udD0x3zl0e//kfgOOdc1cm3Wd9/D718Z9fjd/n/UyPW1VV5erq6noTkgTMSy+9xDHHHON3GIHy0Ucfsf/++wNQW1vLli1buOWWW3yLJ4jvUeJa0oV08aZcvGYzW+Wcq0p3W3fmKK7LcluvkkRcPXBE0s/Dgc0Z7lNvZv2AwcAHfXhOkVBbtmwZ8+fPp7W1lSOPPJK77rrL75ACJcpbi2fixWv2cx3Fs8AYMxtlZv2BC4ElKfdZQmzYC+CLwIpcVVyJhNEFF1zAmjVrWLduHcuWLWPo0KF+hxQoXl9LOgi8eM2+bTMen3O4AlhOrDz2Tufc+vhQV51zbglwB/BfZraJWE/iQr/iFX845wK/+VyhCmKbrRC3FvfiNft6PQrn3MPAwynHrkv6927g/NTfk8JQWlrK+++/r63GAyhxPYrS0p5tCZJvXl9LOgi8eM26cJEE1vDhw6mvr2fbtm1+hyJpJK5wFySJxbPJQzGBXx/UR168ZiUKCaySkpKcXT2tECthkhXK6/f6WtJB4MVr7rI8NmxUHiupUqtCINbiKpQFhoX++qV7spXHavdYibxCrIRJVuivX/pOiUIirxArYZIV+uuXvlOikMjLVP0R5UqYZIX++qXvlCgk8pIvAZsQ9UqYZIX++qXvVPUkkVeIlTDJCv31S9+p6klERPq2KaCISJAVyhoRPylRCKAPm4RTIe4W6wdNZkvHh62hsQnH3g/b4tUNfocmkpXWiHhDiUL0YZPQ0hoRbyhRiD5sElpaI+INJQrRh01CS2tEvKFEIfqwSWhVT65g/nkTqCgvw4CK8jJtdpgHqnoSLciSUKueXKG/1TxTohBAHzYRyUyJQqTAaM2M9JQShUSSTobpaYGa9IYmsyVytIAwM62Zkd5QopDI0ckwM62Zkd5QopDI0ckwM62Zkd5QopDI0ckwM62Zkd5QopDI0ckwMy1Qk95Q1ZNERnKl0+CyEkpLimjc1aKqpxRaMyM9pUQh+whjaWlq2WdjUwtlJcXcfEFl4GMXCTolCukkrHX22Sqdghy39E4YGzNhpkQhnYT1hKtKp94J4wk3rI2ZMNNktnQS1hOuKp16LqwLE7VOxntKFNJJWE+4qnTqubCecMPamAkzJQrpJKwnXJV99lxYT7hhbcyEmeYopJMwX5tCZZ89M6y8jIY0SSHoJ9x508d2mqOAcDRmwkyJQvahE25hCOsJN8yNmbBSohApUGE74YaxQisqlChEClhYeo8qifWXJrNFJPDCWqEVFUoUIhJ4Ya3QigolChEJPJXE+suXRGFmB5nZn8zslfj3AzPcr83M1sS/lngdp0ghWby6gWm1KxhVs4xptSsCtUI7rOt7osKvHkUN8JhzbgzwWPzndJqcc5Xxr9nehSdSWIK+nYcWVPrLnHPeP6nZRuAU59wWMzsceMI5t0/TwMw+cs7t35PHrqqqcnV1dbkKVaQgTKtdkXbxXUV5GStrTvMhIvGama1yzlWlu82vHsWhzrktAPHvh2S4X6mZ1ZnZ02ZWnenBzGxO/H5127Zty0e8IpGmyWLJJm/rKMzsz8BhaW76Xg8eZoRzbrOZjQZWmNkLzrlXU+/knFsILIRYj6JXAYsUsLBu5yHeyFuPwjn3Oefc+DRfDwLvxoeciH/fmuExNse/vwY8AUzOV7wihUyTxZKNXyuzlwBfBWrj3+uITOcAAAnSSURBVB9MvUO8EmqXc26PmR0MTAN+7GmU0om2UIiusG3nId7yazJ7CHAfMAJ4CzjfOfeBmVUBX3fOXW5mU4FfAe3Eej4/dc7d0dVjazI7P1K3UIBYi9PvyhMlL5HcyDaZ7UuiyCclitxKnIjTjV+Dv1UxQU1eImEUxKonCYHk2vpM/KyK0f4/It7Q7rGSUboTcSo/q2JU0hl9GloMBvUoJKOuTrh+V8Vo/59oC/pq8UKiRCEZZTvhBmELBZV0RpuGFoNDQ0+SUaZLZfqdIBJU0hltGloMDiUKySgMJ+KwXKFNek6rxYNDiUKy0olY/JKpR6uhRe8pUUjoqBKmMIShR1solCgkVFIX2SUqYQCdQCJIPdpgUNWThIoqYUS8px6FhIoqYbyh4T1JpkQhoZA4cWXamUyVMLmj4T1JpUQhveZVqzPd5n/JVAmTW9mG95QoCpMShfSKl63ObHtOVWhYJOc0vCepNJktveLlpHKmE5QBK2tOU5LIMe2hJamUKKRXvGx16sTlLe2hJamUKKRXvDx568TlrerJFcw/bwIV5WUY3m8AuXh1A9NqVzCqZhnTaldot9gA0ByF9IqX2ytoha73/FropoqrYFKikF7x+uStFbqFQRVXwaREIb2mk7fkmiqugkmJQnJOq3qlt7S1eDApUUhOaYxZeiPRuGhobMKg0wp8FS74T4lCckpjzNGUz15iauPCQUey0ILKYFCikJzSGHP05LuXmK5xkUgSK2tO6/PjS99pHYXklBbHRU++V+GrcRF8ShSSU1ocFz3ZTuS5WBynxkXwKVFITvm9qldyL9MJe3BZCdfe/wINjU049g5J9TRZqHERfOZcph3+w6mqqsrV1dX5HYZIZKTb5j21MilZd+cWkifIB5eVYAaNu1pUUu0TM1vlnKtKd5sms0Ukq+RV+OnKV1N1Z24hNfk0NrVQVlLMzRdUKkEEkBKF5ERPyye1KC9cEqvwp9WuSLsgLll35hZURh0uShTSZ9nKJ2Hf/aCAbpVbKpkET1e9he7OLajSKVyUKKTPMrUOr1+ynj2t7fskhNKSoi5bk1rhHUyZttiAni2O01Yd4aJEIX2WqRXY2NSyz7GmlraMlzVNlFsmxsLT/a6GJvyVaXv5bJVt6XqGXm5TL32nRCF9lq2V2ROJcstMiQQ0NOG3bNvLp0sIkH6Ycf55E5h/3gQNLYaEymOlz9KVT5aVFFNaUsT2Xfv2KsrLSjoNSUH2cstk2tYhmHr6N6D3MXhUHit5lamVCaQ9eVw/+9iO+3en3DL5dzU0EUyZ5qmyDTNKeChRSE5ku4hRpuGF7pZbgnYRDbqenvg1aR0uShSSV11dBa875ZbaAiT4ss1T6foS4efLXk9mdr6ZrTezdjNLOyYWv98MM9toZpvMrMbLGCX30m0gl61lqX2iwiPdfk0JietLgN7TsPJlMtvMjgHagV8B1zjn9pl9NrNi4GXgDKAeeBb4snPuxWyPrcnsYMo02fmFKRX8cVVDj8otJZiylTbD3uFDVToFU7bJbF96FM65l5xzXW1mfzywyTn3mnOuGbgHOCf/0Uk+ZJrsfHzDNu02GxHVkytYWXNaR+8hVaI0tq+7zYr3gjxHUQG8nfRzPXBCujua2RxgDsCIESPyH5n0WLYtG7qax5BwyTRfUWym/Z1CKm89CjP7s5mtS/PV3V5BuoZJ2nEy59xC51yVc65q6NChvQ9a8kYXpykcma4v0ZZhmFulssGXt0ThnPucc258mq8Hu/kQ9cARST8PBzbnPlLxgi5OUzgyXbyqQo2F0Ary0NOzwBgzGwU0ABcCF/kbkvRWtq0fJHoyDSdqf6dw8iVRmNm5wK3AUGCZma1xzk03s2HA7c65zzvnWs3sCmA5UAzc6Zxb70e8khuaiyhsaiyEl/Z6EhGR4JXHiohIeChRiIhIVkoUIiKSlRKFiIhkpUQhIiJZRa7qycy2AW/24SEOBt7LUThhUWivudBeL+g1F4q+vOYjnXNpt7aIXKLoKzOry1QiFlWF9poL7fWCXnOhyNdr1tCTiIhkpUQhIiJZKVHsa6HfAfig0F5zob1e0GsuFHl5zZqjEBGRrNSjEBGRrJQoREQkKyWKODObYWYbzWyTmdX4HU++mdkRZva4mb1kZuvN7Nt+x+QVMys2s9VmttTvWLxgZuVmtsjMNsTf75P8jinfzOzq+N/1OjP7bzMr9TumXDOzO81sq5mtSzp2kJn9ycxeiX8/MBfPpURB7MQB/Bw4CxgHfNnMxvkbVd61Av/LOXcMcCLwrQJ4zQnfBl7yOwgP3QI84pz7JDCJiL92M6sA5gJVzrnxxK5nc6G/UeXFXcCMlGM1wGPOuTHAY/Gf+0yJIuZ4YJNz7jXnXDNwD9Dda3uHknNui3Puufi/dxI7eUT+CjJmNhyYCdzudyxeMLMDgM8AdwA455qdc43+RuWJfkCZmfUD9iOCl1F2zv0F+CDl8DnAb+L//g1QnYvnUqKIqQDeTvq5ngI4aSaY2UhgMvCMv5F44qfAd4B2vwPxyGhgG/Dr+HDb7WY20O+g8sk51wD8BHgL2ALscM496m9UnjnUObcFYo1B4JBcPKgSRYylOVYQdcNmtj/wR+Aq59yHfseTT2Y2C9jqnFvldywe6gd8CrjNOTcZ+JgcDUcEVXxc/hxgFDAMGGhmF/sbVbgpUcTUA0ck/TycCHZVU5lZCbEkcbdz7n6/4/HANGC2mb1BbHjxNDP7nb8h5V09UO+cS/QWFxFLHFH2OeB159w251wLcD8w1eeYvPKumR0OEP++NRcPqkQR8ywwxsxGmVl/YhNfS3yOKa/MzIiNW7/knPu/fsfjBefctc654c65kcTe4xXOuUi3NJ1z7wBvm9nY+KHTgRd9DMkLbwEnmtl+8b/z04n4BH6SJcBX4//+KvBgLh60Xy4eJOycc61mdgWwnFiFxJ3OufU+h5Vv04B/AF4wszXxY//qnHvYx5gkP64E7o43gl4DLvM5nrxyzj1jZouA54hV960mgtt5mNl/A6cAB5tZPfBDoBa4z8z+iVjCPD8nz6UtPEREJBsNPYmISFZKFCIikpUShYiIZKVEISIiWSlRiIhIVkoUIiKSlRKFiIhkpUQhkmdmdpyZrTWzUjMbGL9Owni/4xLpLi24E/GAmd0AlAJlxPZemu9zSCLdpkQh4oH49hnPAruBqc65Np9DEuk2DT2JeOMgYH9gELGehUhoqEch4gEzW0Jsa/NRwOHOuSt8Dkmk27R7rEiemdklQKtz7vfx67M/ZWanOedW+B2bSHeoRyEiIllpjkJERLJSohARkayUKEREJCslChERyUqJQkREslKiEBGRrJQoREQkq/8PftzuWgyVKsQAAAAASUVORK5CYII=\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "import matplotlib.pyplot as plt\n", "%matplotlib inline\n", "# first line\n", "xhat = np.linspace(my_pwlf_2.fit_breaks[0], my_pwlf_2.fit_breaks[1], 100)\n", "yhat = my_pwlf_2.beta[0] + my_pwlf_2.beta[1]*(xhat-my_pwlf_2.fit_breaks[0]) + my_pwlf_2.beta[6]*(xhat-my_pwlf_2.fit_breaks[0])**2\n", "plt.figure()\n", "plt.plot(x, y, 'o')\n", "plt.plot(xhat, yhat, '-.', label='First segment')\n", "plt.xlabel('x')\n", "plt.ylabel('y')\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "image/png": "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d2bMmDF8/fXXtG7dmmHDhpGamgrgXB9q/35atWpFWloavXs7u2NvvfVWFi5cWCs+9yXNr7vuOnr37s2aNWvIy8tj6NChgPN+Gp06deKrr75izJgxtGvXDoCZM2eye/fumueaOnUqSikGDRpE586da27/OmDAAPbv309BQQHbt29n1KhRAFRUVDBixIjq39ZcPcn5nvtlZLLmQ+fS5/YgXlMbusy41voD4AOvbY+7fV8ONJ/RZQP0TurN6gOrKa0spYWthdHhhFTHhI7MSp/VbMZjgq2uFkCg/dvGt23w74Pzntljx45l7NixDBo0iAPTN5oAABfRSURBVNdee40hQ4b4XAb8zJkz+JrlpdHEWHxP/3Jf2ttqtVJVVQVAfaaLuS9pPnHiRF5++WW01tx+++0888wzHvv6uuGSrzjclyd3/VxVVYXVauWaa67hrbfe8vHbitjq37FarNir34M1eFPeZAmP5q53295oNN+d/s7oUEKuX/t+PDb8MdrENWzFT2GMXbt21fT7A2zevJkePXr4XQa8devWpKam8skq530vKi5coKyslOzLR7Lmg6XY7XaKior47LPPGDZsmN/X7du3L99//z379u0D8HNy9lzSfNq0aWzZsoWrr76axYsXc/z4cQBOnTrFgQMHGDZsGP/+978pLi6mqqqKd999t0H/F8OHD2f9+vXs3evsJi4tLa1pkVgtCotXUrAoRecOSUFbolwSRTPn6q+P9gHtpfmFjFjwT9LmLWfU/LXNbvFD1zL6afNWRsz7P3fuHLfffjv9+/cnIyOD7du388QTTwRcBvytN/7Bu6+/zMwJo7ltxkTOnCzitpt/yJCsTAYPHsy4ceN47rnn6NKli9/XjY+PZ+HChUyePJnRo0fTo0cPn/u5L2m+c+dObrvtNvr3789TTz3FhAkTyMjI4JprruHIkSOkpKTws5/9jMsvv5zx48fTv3//Bi1R3rFjR1599VVuuukmMjIyGD58eM39ti0KuraJr7khk1KKlLYJzLp+Bu+9915QBrNlmfFmzu6wM/zN4dzQ5wYeGfaI0eGEhHN59c1YL/k5FadGU1F0LQk2a9TPvnfxvtsdUK/3L8uMB5drifKqqipmzJjBXXfdxYwZMwyJRZYZFw1itVi5J+MehnYZanQoIbNg1S7KqqooP3odVWedg4TNaaXc5nq3O7N54okn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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# second segment\n", "xhat2 = np.linspace(my_pwlf_2.fit_breaks[1], my_pwlf_2.fit_breaks[2], 100)\n", "yhat2 = (my_pwlf_2.beta[0] +\n", " (my_pwlf_2.beta[1])*(xhat2-my_pwlf_2.fit_breaks[0]) +\n", " (my_pwlf_2.beta[2])*(xhat2-my_pwlf_2.fit_breaks[1]) +\n", " (my_pwlf_2.beta[6])*(xhat2-my_pwlf_2.fit_breaks[0])**2 +\n", " (my_pwlf_2.beta[7])*(xhat2-my_pwlf_2.fit_breaks[1])**2)\n", "plt.plot(x, y, 'o')\n", "plt.plot(xhat, yhat, '-.', label='First segment')\n", "plt.plot(xhat2, yhat2, '-.', label='Second segment')\n", "\n", "plt.xlabel('x')\n", "plt.ylabel('y')\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# Third segment\n", "xhat3 = np.linspace(my_pwlf_2.fit_breaks[2], my_pwlf_2.fit_breaks[3], 100)\n", "yhat3 = (my_pwlf_2.beta[0] +\n", " (my_pwlf_2.beta[1])*(xhat3-my_pwlf_2.fit_breaks[0]) +\n", " (my_pwlf_2.beta[2])*(xhat3-my_pwlf_2.fit_breaks[1]) +\n", " (my_pwlf_2.beta[3])*(xhat3-my_pwlf_2.fit_breaks[2]) +\n", " (my_pwlf_2.beta[6])*(xhat3-my_pwlf_2.fit_breaks[0])**2 +\n", " (my_pwlf_2.beta[7])*(xhat3-my_pwlf_2.fit_breaks[1])**2 +\n", " (my_pwlf_2.beta[8])*(xhat3-my_pwlf_2.fit_breaks[2])**2)\n", "plt.plot(x, y, 'o')\n", "plt.plot(xhat, yhat, '-.', label='First segment')\n", "plt.plot(xhat2, yhat2, '-.', label='Second segment')\n", "plt.plot(xhat3, yhat3, '-.', label='Third segment')\n", "\n", "plt.xlabel('x')\n", "plt.ylabel('y')\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# Fourth segment\n", "xhat4 = np.linspace(my_pwlf_2.fit_breaks[3], my_pwlf_2.fit_breaks[4], 100)\n", "yhat4 = (my_pwlf_2.beta[0] +\n", " (my_pwlf_2.beta[1])*(xhat4-my_pwlf_2.fit_breaks[0]) +\n", " (my_pwlf_2.beta[2])*(xhat4-my_pwlf_2.fit_breaks[1]) +\n", " (my_pwlf_2.beta[3])*(xhat4-my_pwlf_2.fit_breaks[2]) +\n", " (my_pwlf_2.beta[4])*(xhat4-my_pwlf_2.fit_breaks[3]) +\n", " (my_pwlf_2.beta[6])*(xhat4-my_pwlf_2.fit_breaks[0])**2 +\n", " (my_pwlf_2.beta[7])*(xhat4-my_pwlf_2.fit_breaks[1])**2 +\n", " (my_pwlf_2.beta[8])*(xhat4-my_pwlf_2.fit_breaks[2])**2 +\n", " (my_pwlf_2.beta[9])*(xhat4-my_pwlf_2.fit_breaks[3])**2)\n", "plt.plot(x, y, 'o')\n", "plt.plot(xhat, yhat, '-.', label='First')\n", "plt.plot(xhat2, yhat2, '-.', label='Second')\n", "plt.plot(xhat3, yhat3, '-.', label='Third')\n", "plt.plot(xhat4, yhat4, '-.', label='Fourth')\n", "\n", "plt.xlabel('x')\n", "plt.ylabel('y')\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# Fifth segment\n", "xhat5 = np.linspace(my_pwlf_2.fit_breaks[4], my_pwlf_2.fit_breaks[5], 100)\n", "yhat5 = (my_pwlf_2.beta[0] +\n", " (my_pwlf_2.beta[1])*(xhat5-my_pwlf_2.fit_breaks[0]) +\n", " (my_pwlf_2.beta[2])*(xhat5-my_pwlf_2.fit_breaks[1]) +\n", " (my_pwlf_2.beta[3])*(xhat5-my_pwlf_2.fit_breaks[2]) +\n", " (my_pwlf_2.beta[4])*(xhat5-my_pwlf_2.fit_breaks[3]) +\n", " (my_pwlf_2.beta[5])*(xhat5-my_pwlf_2.fit_breaks[4]) +\n", " (my_pwlf_2.beta[6])*(xhat5-my_pwlf_2.fit_breaks[0])**2 +\n", " (my_pwlf_2.beta[7])*(xhat5-my_pwlf_2.fit_breaks[1])**2 +\n", " (my_pwlf_2.beta[8])*(xhat5-my_pwlf_2.fit_breaks[2])**2 +\n", " (my_pwlf_2.beta[9])*(xhat5-my_pwlf_2.fit_breaks[3])**2 +\n", " (my_pwlf_2.beta[10])*(xhat5-my_pwlf_2.fit_breaks[4])**2)\n", "plt.plot(x, y, 'o')\n", "plt.plot(xhat, yhat, '-.', label='First')\n", "plt.plot(xhat2, yhat2, '-.', label='Second')\n", "plt.plot(xhat3, yhat3, '-.', label='Third')\n", "plt.plot(xhat4, yhat4, '-.', label='Fourth')\n", "plt.plot(xhat5, yhat5, '-.', label='Fifth')\n", "\n", "plt.xlabel('x')\n", "plt.ylabel('y')\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "from sympy import Symbol\n", "from sympy.utilities import lambdify\n", "x = Symbol('x')\n", "\n", "def get_symbolic_eqn(pwlf_, segment_number):\n", " if pwlf_.degree < 1:\n", " raise ValueError('Degree must be at least 1')\n", " if segment_number < 1 or segment_number > pwlf_.n_segments:\n", " raise ValueError('segment_number not possible')\n", " # assemble degree = 1 first\n", " for line in range(segment_number):\n", " if line == 0:\n", " my_eqn = pwlf_.beta[0] + (pwlf_.beta[1])*(x-pwlf_.fit_breaks[0])\n", " else:\n", " my_eqn += (pwlf_.beta[line+1])*(x-pwlf_.fit_breaks[line])\n", " # assemble all other degrees\n", " if pwlf_.degree > 1:\n", " for k in range(2, pwlf_.degree + 1):\n", " for line in range(segment_number):\n", " beta_index = pwlf_.n_segments*(k-1) + line + 1 \n", " my_eqn += (pwlf_.beta[beta_index])*(x-pwlf_.fit_breaks[line])**k\n", " return my_eqn.simplify()\n", "\n", "eqn1 = get_symbolic_eqn(my_pwlf_2, 1)\n", " " ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Equation number: 1\n", "-0.953964059782599*x**2 + 1.89945177490653*x + 0.00538634182565454\n", "Equation number: 2\n", "0.951561315686298*x**2 - 5.69747505830914*x + 7.5772216545711\n", "Equation number: 3\n", "-0.949735350431857*x**2 + 9.48218236957122*x - 22.720785454735\n", "Equation number: 4\n", "0.926850298824217*x**2 - 12.9824424358344*x + 44.5102742956827\n", "Equation number: 5\n", "-1.03016230425747*x**2 + 18.5306546317065*x - 82.3508513333073\n" ] } ], "source": [ "eqn_list = []\n", "f_list = []\n", "for i in range(my_pwlf_2.n_segments):\n", " eqn_list.append(get_symbolic_eqn(my_pwlf_2, i + 1))\n", " print('Equation number: ', i + 1)\n", " print(eqn_list[-1])\n", " f_list.append(lambdify(x, eqn_list[-1]))" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYoAAAEGCAYAAAB7DNKzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+17YcXAAAgAElEQVR4nOydeXhU1fnHP3cmk2Sy7yGZQBKWhCWEBJBFdhAjghC3uiNobW21uFQqtLW1WisV61ZX+rNaW0VENK4YRfZFTCAsCRC2hJDJvkzWyTIz9/dHmDgTEgJhZu7M5H6eJw/JuWfu+Q53ec/yvu8RRFFERkZGRkamJxRSC5CRkZGRcW5kQyEjIyMjc0FkQyEjIyMjc0FkQyEjIyMjc0FkQyEjIyMjc0E8pBZga8LCwsS4uDipZcjIyMi4FPv27asSRTG8u2NuZyji4uLIzs6WWoaMjIyMSyEIwpmejslTTzIyMjIyF0Q2FDIyMjIyF0Q2FDIyMjIyF0Q2FDIyMjIyF0Q2FDIyMjIyF8TtvJ5kZJyVjBwtqzPzKdHpiQ5SszwtkfRUjdSyZGR6RTYUMjIOICNHy8pPDqNvNwKg1elZ+clhANlYyDg98tSTjIwDWJ2Z32kkzOjbjazOzJdIkYzMxSMbChkZB1Ci019SuYyMMyEbChkZBxAdpL6kchkZZ0I2FE5ERo6WKas2E7/iK6as2kxGjlZqSTI2YnlaImqV0qpMrVKyPC1RIkUyMhePvJgtAd15vwDyYqcbY76GsteTe9FfPNkkNRSCIPwbWABUiKKY1M1xAXgZuBZoBpaIorjfsSptS0/eL94qRY+Lne544/UX+suLpD9yIU82cK9OgdQjineBV4H3ejg+Dxh27mci8Ma5f12WnrxfupaZkRc7XRfZJda96elZfvLzPFoNJre67pKuUYiiuB2ouUCVRcB7Ygc/AEGCIEQ5Rp19uNQXv7zY6brILrHuTU/Psk7f7nbX3dkXszXAWYu/i8+VWSEIwi8EQcgWBCG7srLSYeL6Qk8v/iC1Sl7sdDNkl1j35lI7ca583aWeeuoNoZsy8bwCUVwDrAEYP378ecedieVpiVbTEdDxJXX6doLUKrxVCnTN7W4xr9nfiQ5So+3m5SCPEl0Py7WmQLUKQYDa5nYEOl5IvmWHGVe2nW8OHKaqupLBMYMYm5hIbuw1NAfHwbl6U1Ztdsnn2tkNRTEw0OLvGKBEIi02wdL7RavTd95o0GEs1ColL96S4nI3ksz5dNcpkEeJrkfXtSadvr3zmMlkRLf9PRryvmHw7CuJGjiUlkHjCaGacX5GxlZ/yebSCPKH34SgULjseoUgitJ2wAVBiAO+7MHraT7wIB1eTxOBV0RRnHCh840fP150la1Qp6za3GEsRAMiAggdU0+aIDW7VsyWWJ1MX+mu99l1lCh7Q7kO5ue0KwpDCwN3/ZPtP2zDf0wawTPuRlAHdB73ri1keuGnJAZ5s6feg6zR9yIoOmb7nfEZFwRhnyiK47s7JrV77FpgJhAmCEIx8GdABSCK4pvA13QYiZN0uMculUap7amrq8O47Z98PWwbiUEGROBIjQef1I/go/ZfAbJrpSvStfdZ29RK8omt3N94lJDWRgzb/XhzaDKvqifRKKiADq+Y5esP8pcv8uRpRyeku7UF0WRkRu6/SBrkR3XgEhpTbkKtUlpPKUcMITNwGboDbzE52EBr7nscTl7S4zmdGUkNhSiKt/VyXAQecJAch/HtV59x98/vR1dVRkFUBF/VDkKByASfEp4cnMvPGx7gf68X8UzpWLdysesPWHo6tZadpOW7N/iDjwkfhYIaUSSAckadLiDR+AmvxU0he9xNALSbRGqbO6Y05GvtXHS31pSU9wFJgR583+BDY8pNaM4Z964du9WZ+exN+SWeB15jZjBUn/qOkiFzXW6dytnXKNyO7S8uZUjBxyTGxLLg+bd57Jh1oF1KyXes9nmPW8tWsfvsbL4ecF/nMTkAz3kxj/7ML5SErPVs2vYBCm8/HkpcSO2ouRz/5x206fXcmL6Mpae28/TZH1hbcYL30paDwvpRlK+189B1rSmo9AAzfOo5XNtOXsov8Tm37pSequn2eq385DC7ku4h4tgarvE6ysctKSxPm+vor3FZOLt7rFvx/vvv89jz71FgGsCX327hsTuu5dkbRqMJUiPQMW+55FfLCV95kB1l3vxz0GZmlr9vdQ5XG7K6M+bcXHErvuKRdQc6jUT8jx/zsnYvtwxKIOqeVymfcDPhmo7wH0+1mppZt/HYomdZpwzgttZqfvH13xBNpvPOL19r5yA9VdP5nCqM7VxVtYXGNgO7Eu4gJsSXZ28Y3aNBN382OiKc7yLn4qlUMKdog8t1AOQRhYP453sbeOieJXhpRvFE5HM8XthGejA99kL+Ev06vy38NSe2ZdA+ZzqqsA7nL1cbsrorXdcizC4h+jMH2br1Pf4QP4pDi55A6eVznqeTuYf6zvw/os/8O20l+TTs3UDA5Jut2pCvtfNgfk5ffWIFrX7ehE6awYlHllzSZ2E2zyw7w+4vv2LHjh1MmzbNZdYh5RGFA/h46z4W5NzL76b7E5b+e0qbjKz85PAFs8M+vGgyjwS+wA8VXlRmPItoaJVdK52I7qKug6oKUX/9AqoQDdmL/oTBywdNkPq8Hqe5lxkT4su6a1bwbuQoanf8l5YzhzrryNfa+agoLUGXl0NFq4HFD/22T+d4ZNU/KG418thjj/Hp/mJWfnIYrU6PyE9rU86YNVo2FHbEPDXR8O5tDPI3cTDuTpRqf6D3kP70VA3PL5lF4s9+x1Mp5fyu5pkLDnFlHEvXaSHRZGLZ9td5NzyYqIXLUXiqO10gu7tm6akadq2YTeHfF1C05wvmxsbx0t5/o26q7da4yEjP/z39Z9QqFfN+8SAKRd9enT4+Pjz91FN41JTz+urVLpPqQzYUdsI8NRF6+kuWxpXzasEgjkakWdXpbQ46PVXDkXdWMiE5AVXVUYYoq+0pWeYS6DotNO3HtUxWmHgvbDhCxOBLGhH4+/vz1F+exMtk4LbW7T0aFxnpqKuro+hYHpVGgZnXLrisc921+C5mjUwgTJtNd3Fszrg2JRsKO7E6Mx99Wyt/Vv2Hs/XwetiK8+pc7Bx08srvWbHTm+W/+52tZcr0EcuNiLwba7hfm81Bg8jmaff2aUQwafFdZKZdzV/efpuDBw/aS7ZMH3njjTd4a8serv/t7y/7XF/lVpLhOZoPd+ym5fS+844749qUbCjsRIlOz9Xl/2VcuIG/VU2n1SvY6vil9DjDwsNZtORBqg9/x7wlD8i73zkBlp4w6bveJlSppO6+hyhcnd7nEcGzq1YRExRExgNuFzrk0uibmljz2qukpaUxcfLkyzqXeaahIW4aSv9wWrM2WB131rUp2VDYiRh/JcuDvudwlYKNA+61OnapPc6MHC0/eI1l3c/8+KvvhxTXNvHIugPEyVumSkp6qoZPr9dwS1MphwID+eXjv7qs8wUHB7P6uuv4WVU1e956y0YqZS6Xta+9zC8njub+JYsv+1xmJwhBqSL5ymtYMU5DUGkOcOnvBUciGwobY17AHnX8LYYFmfhH/VxQdKRqUKuUvHRLyiX3OFdn5qMXVbxYPYVx4QamVH7S6Y7pzJ4S/YFtjzyKhyCQ/LdnbHK++S+9RIXJROWrr9rkfDKXz8fffMv+shquu/mWyz6X5fpDXcJcTIgkle9GAKdem5INhQ0xDyuLa5t4IGAbR6oVbIu6A7i83oL55vo64h6K6uHXnl9ZHXdWTwl3p+bMGYacPMmR0FBGzJljk3P6BgVRPX06w9oN/Pif/9jknDJ9Jzc3l41btnFF+s0olcreP9ALlusPRp8QDjcIjPIV0aicbwHbEtlQ2BDzsFKhPciWk828UDMFUfC4oJvkxWC+uYweat6oSGbqgFZGVG+xquOMnhLuzvaVv0etUJC44nxHhcvhqlXPUmsyof2nPKqQmv+99DyJ0ZEsXWqbfKSWThAARyMm4umhZErNdpuc317IhsKGmF/WFVkbeWSrJ98OWGpV3lcsb66M8F9Q1yKyxGi9COaMnhLuTHt7O6t37mB9QAApC6+z6bkDwsI4O2oUwxsbOb51q03PLXPxfLT7OH7VpUwYNYaF/zpok+ldSycIAfAZOZXK5lY8tMcvX7AdkQ2FDYkOUhNef5TxbT/il3I1CpV3Z/nlYHlz6b1C+KAkmus11QS3dNy4zuop4c58/PHH7D57lrF/esIu55/2zF9pF0W2/+lppqzaTLzsuOBQMnK0vPHyq6g9VRwLHGXTtUBzsGXBqvns/v1VhAwbQYiXBwd/2GMD5fZBNhQ2ZHlaIrc2fsD3d6kZMm4aYLuXeGck76r5qK9agadSYFHtf53aU8KdOfv8P7h6yBCuvfZau5w/euRI9vn6M7qsmMryKqdP8eBurM7MZ0hdPnX6VipiO55le60Fpt9zHyaTyMb/vWPzc9sK2VDYkOuSB/DS9yXcuC2WWr+hdnuJL1m8hJwaH5aG5bHzdzNlI+FgjmdlMbO+np9PnNTnVA4Xw1eDpxGgUDBx/6edZbLjgmOoLC1hmL+SIy1e4OHZWW6PtcCEpNFUtxvRF5/B1E0WYWdANhQ2ZNOmTRSXlHPXb/5Mwar5dnV3q028jf/mNLNr+5beK8vYlHc//ZTZBae58qm/2LWd7MEzeb+phaNncq3KZccF+5NQsh0PhYJT4WOtyu21FjgwZTwBXip2fJdpl/NfLrKhsCFtX63g4WlBXHedbRc3u2PikqdZ9aMH//twvd3bkvmJ9vZ23n33XWbNm4dmyBC7tqUJ8eO1wdPZdyaX9trSznLZccH+JLadpaa5lRrNFZ1l9lwLvOkXv+JvX28hc9sOu5z/cpENhY2oKzvD1QGnWDg5ES8vL7u35+vry43pC9FlraOtqc7u7cl0sPPll3nLy5tfLlpk97aWpyUSOuYqhnh6MvLgF4DsuOAIqivKifAUaPINJibEt3NTMXuuBUZGa0iZMImMjAy7nP9ykQ2FjTj80TN4eQhEXPWgw9r89fwUPrzOxKENzzuszf7O2Q/XE+rhwcNHg+zuhZSequH5pbP5U+wQHqk6RnSgt+y44AC+/N9/8FAomJue3umd5Iio6UVpc7ky1I+s7Vvt2k5fkA2FjfAt+IaCegUj59zusDbH3rCM275Q8tJG5/bBdhfWb8tjVL2OzapADCq1Q7yQ0lM1iItvZ3HhKd64Nkw2Eg4gO3sfRbX1XH3DTQ5t95r5Cwj392VL5jcObfdikA2FDVj/7Q6S/XWsr4hl6nNbHea+qPLyJnjirXzy2Rc0NjY6pM3+zMbnXsRHoWB7/KTOMnt4IZnzhZljJ4QrF1EnCKxdu9am7cicT3t7O//b+C31miGoPD17/4ANSRiVxHcVDXy6eZtD270YZENxmWTkaMla+yxKhcA33tc43Nf91hsXsnKSiZz18vSTvRl7KptKo5ETI63zOtnSC8mcL8xye8xnt2i5PXUCgzZscFr3SXdh83ff0lBfR3p6uiTtp6ens3fvDxSfLZKk/Z6QDcVlsjozn7mqHE7UCpwKnQI41tf9ymmz+OV4Lzxy1zmkvf5KY3U1V5r0bPUMQlSqrI7Z0gupu7249e1GvAMGMUvpwcGPP7ZZWzLn88OGD3liwRxmz5olSftzZ07nyYVz+eo/b0vSfk/IhuIyqSs/w8zIJr6qHYQg/PTf6Shfdw9PL44Y40jyKqFd3+CQNvsje998E7VCQdbgSVbltvZC6um+2THsKtpFkdMffGCztmQ6sJzqW78rhyNNJvwDAiTR8cj2NgwmkX279zpVBL5sKC6TtOaNqJQC33la90Ac6evuNeZG/D0h94s3HNZmf6M2M5M6k4m7f/9QZ0I3e7hM9nTfBMfGc9LLk8BjclS2LbGc6mtvqCIvP48txDv8JW3WUVLfyskWJbHesHL9PqcxFrKhuExa6mp5M0ckL+QnQ+FoX/cx6Q/S0CbStO8jh7XZnzC2tRFTVk5haAg3TR5iV5fJrmmo4af7yTT+CjTAqR3OGZTlilhO9QWe2YMmOACPgWMcnibFUkeRTxzeKg98i7KdJl2LbCguA6PRyEffH2Bt+1VoQgIcEpjTlYwcLXNfP8D3ZQHEGk7w6b6zDmm3P7Fv3z5WlGjxvP56u7fVNQ215f2Uct/PATj073/bXUd/wXKqb7KhgLunXIEqPM7haVIs2ysbOBmjyURMTZ7TpGvxkFqAK3Nw+5eEUcP9d97EbbfPdnj75uGqvt3I9+3JpPvv4v5//wtB8UvZ396GfLlxIzv0ejbce2/vlW1Aeqqm2+uX6z0IhdFE484fmLJqM8vTEuXrfJlEB3XEw2BsJ85HIK/RA0GhcHialE4dgME3jOLGVmJpo9JJ0rXII4o+kpGjZe///sLB+31565ggyVyi5XB1Z+A8ACY1bnKa4aq70Prpp9wwYQKhoaGSaTB3Cnb7RJCiEKku1sopx22AeaovpDQHtcqDYt9YBDrckh25/0fXKcczBBLj70V5aYlT7EMiG4o+YH5oV2+p4bbN4RS2BUjy0FoOS2v9BpNTqWS2zymnGa66A2cOHmRxWzsDhWBJNw8ydwqyB43DQxAYfWyLnHLcBpin+uLrjwFQrpmIeO6YI2OiLKccAbSBCSgEgYiSbKfYh0Q2FH1gdWY+DTXlFBScYbNqOiDNPgFdh8frqwaTq20gUi328AmZS+XvH2cy7dRJvhx2laSbB5mN/6mEaWQ2NlFVWWhVLtN30lM1JHo0UNqop80v0uqYI59r8+ZkmiA11dHjaDMY0TQWOFxHd8iGog+U6PSMr97Iz8eq8B88xqrckXQdrr5NOvd/oWduSI1DdbgzH322kXp1EK0DEjrLpOwUiCpvfu89gJ1Fx6zKZfqOvqmJAIVIoaH7/0spFrZFTx8+LajhxxOnJNNhiWwo+kB0kJo7fH7gqVlqhPDBVuWOpKuHzOCk8Xh6e2M6tdOhOtwVY1sbT+oKmD5oOIIgWB2TslPgHT+WoIYKAmsK5ZTjNmDb11+gUiqpC+l+fxEpFrYBjquHUHT6KKbWJkl0WCIbij7w2NyhzAyrZWtVMILQ8fBKtU+A5Ubtjy8YzX9+FsYSPnKKBTBX59CnnzLHR01IeOx5x6TsFAyMTmD70GH8qvYH2evJBhzauR2TKHLbXbf1GMPiSMydAp9BoxgVFU5Acbbk+5DI7rF9IKXtAOE+sF85GoGOl4bUrormBfakphQ2Hc6kaGIBKz9pA5BfJn2k8LPPSBBFTo+0jrqXslOQnqpBFGexbNjr6BurHa7BHak5ewbB0MbyaycREqVldWY+JTq9ZM+1ub3nPjdyXVsqufrj3HzDQ5I+x4IoutfC5/jx48Xs7Gy7trHzuVuZ2ryRMzdlEps0qfcPOIApqzaj1elpqyyk9N8PEjr/EfyS5qAJUrNrheNjPNyBz8ekoGxtpX3tJslfHl255ubFbPpiAzHL1qIJ8XMKTa6IwWAgJDiYxbffxqtvrZFaznlcd9VsKuoa2JuVZfe2BEHYJ4ri+O6OySOKPuBd9iOnRQWDncRIwE9z5qqwQQyO9CeheRdHmSN7xfSR5tpaYltaODp4MLf2EAAnFRk5WvINUcxSKag5k4NWMZ6VnxwG5NHjpZKTk0NDYyPTZs/pvbIEjJs6naeffpq6ujoCAwMl0yHpGoUgCNcIgpAvCMJJQRBWdHN8iSAIlYIgHDj383MpdFpibG8lwauaImWc1FKsMM+ZC4KCVfMC+VfqMUCUvWL6yP4PPsBTEIi4yvleIKsz8/EJH8Irmhimnt4FSO8+6aps27COG8YmMW3qVKmldMvkK8ZzbVICmz+Xdi9tyQyF0LEK/BowDxgJ3CYIwshuqq4TRTHl3M//OVRkN+Rv/YgAL1ANc67pHEuvmD3tCcT4wxD9Udkrpo+UfvcdBlFk3J13Si3lPEp0eppCNOQbjKTUlViVy1waRSfyGRoVQbTGOUdiV0ycyLSEeD786EtJAz6lHFFMAE6KonhaFMU24ENgkYR6eiUjR0vG2g5b9XTRSKfyKrL0isn26egd/TzksDwV0Ue8T53ijIcHgZGRvVd2MOZRYo5XEGNoR2lotSqXuThEUWTdzh8pC3HeZ2T72TZKG1sJM+gkDfiU0lBoAMtUp8XnyrpyoyAIhwRB+FgQhIGOkXY+Zq8iz4azHKxUcMw4QPKw+q6YXWW3vvEnypsgtjFHakkuSXNtLbFt7TTGxUktpVvMo8fciETUCgXxp3+U3H3SFSkoKKCiooJJkydLLaVHVmfmU2z0JsbHA8HYDkgzzSiloRC6KevqgvUFECeKYjKwCfhPtycShF8IgpAtCEJ2ZWWljWV2sDozn+Y2A49/Vc3UHeMA550XViiVHG4OZaDprKTDVVclZ/sOtjU2EjJbmu0we8M8eqwZ27F+MrbskENT27sLm9ev5dGrpzE2ebTUUnqkRKenVB2Np4eSoMojVuWOREpDUQxYjhBigBLLCqIoVoui2Hruz38B47o7kSiKa0RRHC+K4vjw8HC7iC3R6TFUF2PS1+MVk2RV7mxk5GjZ3BjHQH+RiIZjTpFUzJXYlpfLshIt45YskVpKj6SnatjyjyUUiSKTW8plI9EHio/kEuyrZuwVE6SW0iPRQWqqIjoMWXhNvlW5I5HSUGQBwwRBiBcEwRO4FfjcsoIgCFEWfy4EjjpQnxXRQWp+1vYpu+/xIWSgdGk7LobVmflkeU0EYGxTx25ozjr6cUaytm1j1KhRkqYVv1h00VEMbGzE0NYmtRSXw1hXQ4OoQOXpKbWUHlmelggRQ6lraSWqrQKQJuBTMkMhiqIBeBDIpMMAfCSKYp4gCE8JgrDwXLVlgiDkCYJwEFgGLJFGbccFq6uqoLzFg9agDkPhrPPCJTo9J4MmUtciMt7Ctjrj6MfZMLS08ERBIfeGDmDKqs1OP3XnP2EivgoFRzZulFqKS6GrribA0wO/yKjeK0tIeqqGVTeNobRVQKMyOHwHTTOSxlGIovi1KIoJoigOEUXxmXNlfxJF8fNzv68URXGUKIpjRFGcJYriMam0pqdq+PpQDfceGoNCECS7YBdDdJAaUaFib7Wa8X6VVuUyF+bQ/v28VlXFRu94tDq9pJ4mF0NiejoAp77fLLES12LXtxtRCALDUrudzXYq0lM1DBuZSKiPF+tuiZPknSMnBbxItGdOoysr4ol7b6Bg1Xx2rZjtlEYCfvKKeeTkJCavqcPUpnfa0Y+zsSMri3/X1nBkiHUAlrNO3cVPnMAiXS2f6mqlluJSHMn6AYCpaddKrOTiGDVhEhX1jfywY7sk7cuG4iIpynyN+pX+XJVyfiZRZ8PsFSPGTULfbiKg4YzTjn6ciYwcLev/9RHhQZF4BISdd9wZp+4UCgWjJk9m9+7dUktxKWrPnkHX0kZMXLzUUi6KGQsW8fy3O8grOCNJ+7KhuEjaTu+i3QjDr5wntZSLIj1Vw65//JKnZnnxt4FZspHohYwcLSs3HOKvrVWsiBzQbR1nnbqbNyyBR/QtlOblSS3FZfBqb8Oo9pVaxkXj5+fHiBEjyHJAcsDukA3FRRLeWsDxJn88PL2llnLRBIeEkJ7kh6L0gNRSnJ7VmfmoS04SqVRwJDDmvOPOPHU3Onk0cZ6eHNyyRWopLkHB8WP4eakIHRQntZRLIi1lFMnoMZlMDm9bNhQXQVtTHcP89NT4DpVayiXzT306i9dVSHJzuRIlOj0JZzp6a8c11gFYzuy4AJBy441cry1ma3Gx1FJcgkMHD7HzRCEZ1cFO79VmyaChwzhdUUVRQYHD25YNxUVwaucGVEoB76HTpJZyyUy+cgq1tbXk5zvfQqwzER2kJrHyNHqTieK4nzxhzPt5OKuRAFCr1aSmpsrrFBfJui1ZZOTkcSZghNN7tVky8ZoFrMs6RM6hQw5vWzYUF0HNoW8BiJt6k8RKLp0pY0ey5W4fyje9KrUUp2Z5WiKjWmo4YhIweXasRTjzdFNXlg4cxF9KSmltapJaitPzw+4f8AzVoPDy6SxzVq82S8aMGYOHhwf79u51eNuyobgIPMoPUlQvED1sjNRSLpmhSeNIGeCB8uwPUktxaq5NDCZRMHHSLxQB559usiQjR8veJj8GeHhw6/3POX3PWEpEUeTuOB9uSB113jFn9GqzRK1W89A1MxCPO35EIe9wdxHECGUUGMMZJHSXx9C5USg9ONEcQKTybO+V+zHHNm7EUxCYsGAWT/51vtRyLhpzVmPvAWOg4AfCTh6Qd7u7AGVlZXyWk4sh8fwNqZzVq80S78AQ1M31mEwmFArH9fPlEUUv6IqPo/E10Rp2fg/EVaj3H8pg/xZaG+WgrJ44s+l7ABLmu46RgA5vLX27kZrwIVQajSTUFbvENIpU5OTkkFNUQt2AJKtyV5lmHDBkKN4qD3KzHesmKxuKXth/KI97P9ejTrlRail9xnvIFDwUAqd3SbudojPTmnuYapOJwRMnSi3lkjBPlwgKBXmCJyPbG63KZazJ2bkNTVAAz963EE2Q2uWmGUdPmgLA/p3bHNqubCh6Yde+XN45YCBphlNvvndBBk3q0F5zeJPESpyXf1RX8/6ggQ4dztsCy+mSfN9wBisVeDfrXGIaRQqajudx97QJ3D5tOLtWzHb6dDxduWL6DAwmE8X5jk2k7VpPhQS0HPmGtCuGEhAQILWUPhMzYjzaBlCWH5RailNSX1/P3mPHiJ4xQ2opl4zlXuknIoYBMOzsfpeYRpECL2M7oo+f1DL6jI+vH3VtBpqrKhzarmwoLoBoMvHQwFwWjzK5VGBOVwRB4Ex7CJGmUqmlOCWHv/qKpcHBTBgxQmopl4zlXukFsWMBmI/WZXrIjqTwxHH8PFWEDnT+fG0XxMcPtcmAKHbdENR+yIbiAvz7233MfKeRF6smuFRgTnc0ByUS72+gqUY2Fl0p/X4zj4VHkDzaebfEvBDmvdKPrLmfUpOJiDJpEsc5O/t2bAVg2Jix0gq5TEIHxuLjqeLMieMOa1M2FBfg+bWZHK0yURTw043lqh4l3iPm8t+DbeTtd3ywjrPzWWsL12UE+tUAACAASURBVDfUE5Xo+tM1+yMj2Fore7d1R2FeLgBjp02XWMnlMXRMKvCT4XMEsqG4AKn1W7l7jArPiDirclf0KBky83YWZ7Sw67Dj88Q4O/v372fYWNfuZZppSUvjn/n51NfXSy3F6dCVatHpW4mJjZNaSp/JyNHy9yNefHHwKH/5JMthsxuyobgA90Qc5bdTfVF4WnuQuKJHSVRUFBpNNEf2y/mALPlo0wHur6unrkJw2TUoS8aPH0+QUsn+7dJscOPMiE2NlLWKLrveaA6uLDH6sbOkntOFpxw2FS4bip4QRZIDmzjUGGRV7CqBOZZk5GiZsmozz81Rsjxkk8s9IPYiI0fLf1/7H1f5+REQOMCl16DMjE1MZPfQYdR8uE5qKU7F+j0nCfTyoExUu+x6ozm4EiA4ejAxbRUOmwqXDUUP1Jw5QoSPiBg52iUDc8yYeyFanZ7P6xJ4cXczj3/4o0s9IPZidWY+A0s6/NELB6UArrsGZSYiPp5X29vZUl8ntRSn4q0PvkQhCFR7R3SWudq1tpzynhSn4d7UODzamhwyFS4bim7IyNHyx9UvAZDZGM/ytESXC8wxY9kL2ayYxJvZbdSXFrjUA2IvSnR6hjaUUWYw0hA6yKrcVcnI0fKh9wDe3JXjktMr9iK3uJwXvt1BSZD1bIArXWvLKe+j3vG8sWUP+mqtQ6bCZUPRBXMPXNN4GJMokuWR6nJDVEssHwTPyHiGBAtE1x10qQfEXkQHqUlobyRf4YlgkfDRFdeg4Kd71ydQw5RWHRUl5S5979oSz8ZSSnQNmMKGWJW70rW2DK7URyRSUFWLqabIIVPhsqHogrkHPsqjhJM6BS3qcJcbolpi+SAoAyLYfa8fy0J2u9QDYmvMazaV5ZXEKQRO+IR2HnPFNSgz5nt3rJ8fb8TEEF+036XvXVsyw6uKpGEJKCy2Mna1a20ZXKkKjmLUQA1X+1U6ZJZDNhRdMPe0kwPqOVTvf165q2HZCxEEBYd0akb71rrUA2JLLNdsNEUHUAoCBWHxgGuuQVlivkcLY5IBiC/PtyrvzwxVGZg2fLBLrzfCT8GVhX+/jnljRhJYV+YQLy55P4ouRAep0ZWfZXCgyH+rNFblroj5QVidmU+JTk9+WyQ/jyxkzKjQXj7pnliu2cSXnlvIHjimc8tTVyY6SI1Wp0cXNphqo5HB9aWd5f2ZlpYWnv78O36/coXLX2MzGTlatO0qRqgNmEymTi8usM8+JPKIogvL0xKJbzxIu1HkqEdHr9vVhqhdMfdCClbNJ+mK6Xh5CBTlfC+1LEmw7F0PqSuhzmiiOnyoW/S6zaNHQaHgOB4Ma2tw+XvXFhw9ehSjyURSsuvtUNkTqzPzqfQIxsdThbqhBLCvF5dsKLqQnqohKiQY37818KPPdJcdovZERPJVAJQf7J8pxy171836BvaKHS9Wd+h1W85hn/QOYogC/jo/wW3u3b6SlfkVN45LYtTIkVJLsRklOj01fjEABNWcsCq3B7Kh6Aah9iz+QSGcevUel3SJvRCDr5hLi0Gk/ex+qaVIgrnXLYomntWe4YnAwW7V6zaPHiddMwVPQSCp/qTUkiSn4vhRRkZHkjh8uNRSbEZ0kJq60AQAQppKrMrtgWwouuEaw7f8IW2Alcuku+Cl9uVUgxe+jYVSS5EEc687TKxHbNMTHpvgViNGM4OmTQPgzDbH7oTmbGTkaNFVVlGuNzDj+e1u4yq8PC0RVUg0dfpWQg06wL5T5PJidhdMRiNh1OATEyO1FLtR5TGAEcqzUsuQjPRUDf5fF9ISF0/Y0llMdDMjATB89mzyRZH6Q4ekliIZGTlafr/hIIu9lJxu9rL7gq8jMev/NkskQtmGJkjN8rREu30veUTRhTNFRcx4p4GioYullmIXMnK0bK0NJ8JHJO0Pb7tND+tSOVFSyqm2NkZeeaXUUuyCt58fWoWAR3H/vL7QseCr1GlRKRVUe4YArpe240Kkp2oIGxBBuFrF9uXT7Wr8ZEPRhcOHO3ocycnJEiuxPeYYgs/aJzDrP00cPVXUbyN3v68o5xW1N/6h7usm/L+YITxQWOyy2VIvlxKdnqDqjs19dH4xVuXuQnhsHCqlkvyD9t3mWDYUXfA7sIadS30YNdL1tsXsDXMMQWVwMlsLjTSUF7tVD+tSOHHokFt2Bsxk5GjZRBQlukoMzXUumS31cokOUhN8bqG3PizRqtxdGD19Nk9kfMuZcvvuoS0bii741+UTEeCFf0Cg1FJsjrknpfQJJG1UEFd75FiV9xcaKypYKyi4yctLail2Y3VmPoGBUdwTEoLmTMd17m+dguVpiYQZ6qhp0mMKGAC4fkxUV5JTx6Jvayc3N9eu7ciG4hzm/D+hpkpy633dsudl2ZP67ZXePJ5QeF55f+D4po4YkrCkJImV2I8SnR7vEA2PhUcwovyoVXl/IT1VQ5Sngao2EYUguF1MFEBISAjXjkum7GC2XduRDQU/zd3XVFUwOFAktzXSLYfplnmfluWPZ/rbDWA0oNXp+9UcdskPHfuGx8+cKa0QOxIdpKYhJIYrtRWsb2qxKu8vGNoNGNpNhEVFuew2ARdD0sBoTLpqu7YhGwp+mrsf3NARhHZcOdgth+mWkbtVgcNpajXQVlMMuOaOX31Ff+wYTSYTgydNklqK3TB3ClrCYmmvLATcb9qlNwpPFaPyvZXohHlSS7ErbbGJvLV5NyaTyW5tyIaCn4bjQ1uPAHBKPcaq3J0wR+4OHzKIv83xIqVpT+cxdzSO3eFZVkapSoVSqZRait0wdwrSIiJ5VtmCxs/D7aZdeuNEwVGeWX8vo6cN6r2yCzMqKQm9Xk9BQYHd2ujVUAiC8KAgCMF2U+AEmIfjCWIhTW0iJYGjrMrdkUqPSB6f4slUZZ5VuTsaR0tMJhMRLS3ow8OklmJ30lM13D9lJPP8/VkzWdWvjATQucA7evRoiZXYl6EDY7h32hVkb/7Obm1czIhiAJAlCMJHgiBcI9gwr8W58+ULgnBSEIQV3Rz3EgRh3bnjewVBiLNV25aYh+nDvao4qlOBQuX2w/TQ0DBO1SlJVFm71bmzcQQoPXqUIIUCz6HDpJbiEDRXTgagcMd2iZU4nvpjPvxy/l8IDnbrfi4jRo9mRFQERUfzeq/cR3o1FKIo/hEYBrwNLAFOCILwN0EQhlzwg70gCIISeA2YB4wEbhMEoWt6x3uBWlEUhwIvAn+/nDZ7wjxMHxGg52hzoFt6R3RleVoi+c0BDPdr7Cxzd+MIcGrLFgDCx42TWIljGDa7Y/8FXT9M5eHZHkhESJTUMuxOZLSGxtY26spL7dbGRa1RiKIoAmXnfgxAMPCxIAjPXUbbE4CToiieFkWxDfgQWNSlziLgP+d+/xiYY8sRjSXTB6r45kQbwYnT3NY7wpL0VA1iaALxQSKK5up+YRwBqvZ3OCwMnT1LYiWOwTskhA8TbyPP8wqppTiU1tY2ArzC8Q6SWoljaBGUmJoa7Hb+XpMCCoKwDLgbqAL+D1guimK7IAgK4ATwuz62rQEsM9MVAxN7qiOKokEQhDog9JwWS42/AH4BMGhQ3xauVF5qhPTXGeLGnjBdKZp0NVdUlPDUQAV3XuMeO3/1xrdeRWRFtPO3hASppTgMVXgsYrMotQyHkrsvHw+lishB7j3tZEblH4hXow6TyYRCYXsfpYs5YxhwgyiKaaIorhdFsR1AFEUTsOAy2u5uZND1br6YOoiiuEYUxfGiKI4PDw/vkxh/f3+WLl3KiBHul7qjJ8aOmorCU8HeU3ulluIw9sRWkjk/wC1TyPeEX5iKEN8BVFZU9V7ZDcjI0fKnt7YC8H5hQ/9w+fYJxVvlwcgH3rZLTNTFrFH8SRTFMz0cO9pd+UVSDAy0+DsGKOmpjiAIHkAgUHMZbcpYMC5+HKJRJL/a/V1iAYxGI4YgA5HKSKmlOBTNkDBUSk/27zkstRS7Yw6e9a5vwWQycsYryO3jgzJytOyu8wXAX3fSLjFRUsZRZAHDBEGIFwTBE7gV+LxLnc/pmPYCuAnYfG69RMYGeCo9UTWqKDOUSS3FIWTlZ6FQKxgW0j88nsyMHt/xffMPFUorxAGYg2fDBS+qmiowqjzdPj5odWY+VQHxAAQ1dixo2/o7S2YoRFE0AA8CmcBR4CNRFPMEQXhKEISF56q9DYQKgnASeBQ4z4VW5vIIJZRW/1YMBoPUUuzO9iMdLqJXxPevhd0howZiEk1UFNVLLcXumOOAIr1DKWupPa/cHSnR6WkJjKGl3UBwW41Vua2QdIc7URS/Br7uUvYni99bgJsdras/MTRoKGXGMg4dPcTY0WOllmNXyk+VU51XzZyX5kgtxaGoPD1oaq+lrcn912Wig9SUllUT5h9Jbmu5Vbm7Eh2kRqvTc0Knp6nNutxWyCk8+jlXxF2BoBDYenir1FLsjjZHi/dOb6JDoqWW4nAEdRu+HqEYjUappdiV5WmJeOtKOXB6O8WeHYbR3eODzAHDnxli2WnsWH+z9XeWDUU/Z/rI6QDsL9ovsRL7kpGj5esDeynzCOtXmXLNtPgIhPlHMeT+/3Pr75+eqmG6ppl/b3qakqCgfhEfZA4YHnntEkJm3G2X7yzp1JOjaG9vp7i4mJaWlt4r90MejXuUtWfWSi3DbmTkaHn8ox8Z+JgPjXk+nV4hgFu/QMxk5GjZomtlVMmPKD31aENj3Pr7e9RqCQgIoOj1Jf3GDTo9VWPXa9kvDEVxcTH+/v7ExcX1mxvnYhFFkTZFG8ah7jslsTozn4bKs+h3lOA9eCpe0T95hbjji7IrqzPzKQ4MZtvbPydg0s0EJ05w6+8fUj+GBxf8XX7WbUi/mHpqaWkhNDRUvnG6QRAEIgIiiA6KpqbGPUNUSnR62ivOotulQ1AlW5X3B0p0egQPFarQGAwVBVbl7oYoiuw+8g1iQJ3UUtyKfmEoANlIXAAfHx8UHgr2HdwntRS7EB2kRhSP4R3rgypEY1XeHzB/z8XTHuW+0XeeV+5OFBUV8X3OBgaNcb8976Wk3xgKmQvgCapgFZuObpJaiV1YnpZIYHIRA++PRVCqAPf3hLHE7BVzxthIbsFOTC2Nbvv9c7IOEewX4fZ7UDga2VA4CKVSSUpKCklJSdx88800Nzf3+Vxbt25lwYKONFuff/45q1at6rGuTqfj9ddfv+D5/Lz8MDWbKM21X5piKUlP1eAV3o6pzhcB+oUnjCVmrxhtiILvD35EoL7Ubb//yawqnr5jLYlD+0/ONkcgGwoHoVarOXDgALm5uXh6evLmm29aHRdFsU973i5cuJAVK3oOWL8YQ6FUKFGJKo5lH7vk9l2B0yWnUQYrmTpwFAWr5veLNPJdSU/V8N1fF6P29OWXI7zc9vs3Vhqo01cRFhkitRS3ol94PVny8MMPc+DAAZueMyUlhZdeeumi60+bNo1Dhw5RWFjIvHnzmDVrFnv27CEjI4P8/Hz+/Oc/09raypAhQ3jnnXfw8/Pjm2++4eGHHyYsLIyxY3+KoH733XfJzs7m1Vdfpby8nPvvv5/Tp08D8MYbb/DKK69w6tQpUlJSmDt3LqtXr+5Wk4enB6cVpzEajW63l3Tm/kwArojtX6k7uhIRFsnfl2RQfsR+O6FJQUaOltWZ+ZTo9NzVoqZeqO39QzKXhDyicDAGg4GNGzd2zqHm5+ezePFicnJy8PX15a9//SubNm1i//79jB8/nhdeeIGWlhbuu+8+vvjiC3bs2EFZWfdJ/JYtW8aMGTM4ePAg+/fvZ9SoUaxatYohQ4Zw4MCBHo0EgFEpMuC+AcT+6i23C8jKKswCYG7KXImVSIvKy4Om9hraG9ynI2DOFqvV6VG0NBPuP4BTLY1udf86A/1uRHEpPX9botfrSUlJATpGFPfeey8lJSXExsYy6dxmST/88ANHjhxhypQpALS1tTF58mSOHTtGfHw8w4Z1ZAG98847WbNmzXltbN68mffeew/oWBMJDAyktrb33lVtcxvtRgWCh4CxNRetLtatArJO1p/EJJhI0PSfzYp6xLsV37ZQtxk5mrPFAgTVVKDwCaXMS+W2MSJS0e8MhVSY1yi64uvr2/m7KIrMnTuXtWuto6QPHDhgV/fe8roWoMMbSKE4BbhXQFqNRw2+Tb69V+wHBEerMZhCyD9ykpGjXd/ryTIWJKyxCXygOigUvRvGiEiJPPXkREyaNIldu3Zx8uRJAJqbmzl+/DjDhw+noKCAU6c6XuJdDYmZOXPm8MYbbwAdm/TU19fj7+9PQ8OF99JtM5oQUWJqFVH4VHSWu0NAVl1jHYTCQK+BvVfuBwwdHQPAgR/cw3HBMhYk0qSkqbWBJr9At4wRkRLZUDgR4eHhvPvuu9x2220kJyczadIkjh07hre3N2vWrGH+/PlMnTqV2NjYbj//8ssvs2XLFkaPHs24cePIy8sjNDSUKVOmkJSUxPLly7v9nKdSAQi0VarwDPnJbdcdHrZv932L4CEwJmqM1FKcgtTJIwEoyq/opaZrYI4RAYjyCkLbWIaPl4dbxohIiTz15CAaGxvPK4uLiyM3N9eqbPbs2WRlZZ1X95prruHYsfN7gUuWLGHJkiUAREZG8tlnn51X54MPPrigtshAb7QCGBqCUce10pjfiK9foFs8bJXHKjm56iTpmelSS3EKQiICaGippVHfLrUUm2CeGn3ui8NE+WvYX5/vtjEiUiKPKGQI9vEkyEeFD4NRqpUEiu7zsB0+cBivWi9GD5Mjdc20KurwNLpPiov0VA0vpoWwJvMJ5i+IdYv71tmQDYUMAD6eHrx+910AzB2hc5uH7cf2Hxl57Ug515cFfuFKgn0jqa50n3iDAwdyOFqczcTpKVJLcUtkQyHTyeRhkxENInnV7hGQZTAY0KfoCUx1n96zLWgIU/HOpqdJefhfbhMzc3x/CWMTp/W4fidzeciGQqYTTw9Pgr8Kpm6j66dozsjRcsXy9zj68FFO7h/hFi9DW5CRo+X9EoGc09tpKs3v3MTJ1f9//JriWDjxHnnkaCdkQyFjxdiBY8nNye1T3ilnwRyte/b4YTBCk+8wt3gZ2oLVmfm0ewUQG3sFg1o6AtXMMTOuSnt7Oy9lPEpreEHvlWX6hGwoZKyIHROL3zw/fjz6o9RS+ow5WtczYh+Rt2hQhWhc/mVoK8yxMdeOu5uFcXPOK3dF8vLyqG/SMXrccKmluC2yoXAQ5jTj5p/CwkKys7NZtmzZRZ/jYjLBXi7xifGEXhPK5oOb7dqOPTG/9HziqvEbGYygUFqV92fMsTHfNRznn58/irG5zqrcFflh02GuHbeYsSnjpJbitsiGwkGYU3iYf+Li4hg/fjyvvPLKeXUNBkO353CEoZg3fh6nHz5N9b5qu7ZjT6KD1IjGFryioF0XZFXe3zEHqDWEDkDXVEVb2UmX38SoLL+ZWck3MixhqNRS3Jb+GXD3zvze6ySkwZRlP9VPuR1S74CmavhosXXdpV/1ScbWrVt5/vnn+fLLL3nyyScpKSmhsLCQsLAw/vCHP7B06VLa2towmUxs2LCBJ5544qJShl8O3l7ejE0ey48/uu7U0/K0RB79979QeCkwtceh9OxfO9pdCLPb87MZ7YwYfSO0tvGQq8fM6L1pVFahUMr9XnvRPw2FBFhmj42Pj+fTTz89r86+ffvYuXMnarWa3/zmNzz00EPccccdtLW1YTQaWbVqFbm5uTbfT6Mr8XPj2aPfQ0trC95e3nZty5ZY7ksgiB0R74LHGDRBapanJbr2y9CGpKdqSE/V8LfCZvS4dsxMTbWOEJ8oWv1LpJbi1vRPQ3GpIwDL+r6hfRpB9JQ91pKFCxeiVndMj0yePJlnnnmG4uJibrjhhs4U447AEByCT7wPwx59jkEDp7rES9bs6WROOW3yLMLYbOLFO3/GjeMGSazOOTGoGlG3BSOKosu6le7+fj9KhQeDk6KkluLWyGM1J8Iy5fjtt9/O559/jlqtJi0tjc2bHbO4nJGjJftsx4tVNB1yGT97y30JADxDG2kt8+CF705KqMq5CYlRE+gTxomjrudWmpGjZcqqzbyxtiMvWuOAARIrcm9kQ+GknD59msGDB7Ns2TIWLlzIoUOHLipl+OWyOjOfdmUCRr0JpfdZwDX87C09mkzt9XhFKTDUhcqeThdgxLh4AB7++1fEr/jKZaK0LXe1i8aL2qYqVu0udQntropsKJyUdevWkZSUREpKCseOHWPx4sUXlTL8cinR6REEJa2lHniGNViVOzOWHk2mlmwEpYDJMFT2dLoA1SHhGIzt+NW3IoLLjR5FUWSg7wCKmstdojPjyvTPNQoJ6C7N+MyZM5k5cyYATz75pNWxlStXsnLlyvM+01vK8MslOkiNVqenvS6MgNHltFbXofB0/o1glqcldq5RGHVnaDnbgrfPBNnT6QK8vKOIqfVaNB7+nWWusLOhudPiXV9DqF8Me1tKrMplbI88opCxwuxnLxqGIXgIiC1ZLuFamp6q4dkbRqMJUlOfXcrZFxpZfds8p37hSU2JTs/ZNh0DAwaCod2q3Jkxd1rUVVp0TVWU+/lYlcvYHtlQyFhhfuEOCJ0KgLfXcZfZmyI9VcPOx2cR0HCam66d4xKapSQ6SE2plxIvlTfBlSVW5c6MuTNTUJTNH9feQWXoAJfozLgysqGQOY/0VA1Zf7kTU42J0Ihql3rhZh3LIviJYKKmy+6SvbE8LZG6yEj0bU2oa8oB1whMNHdmxPJ8vCKHEBMe4DKdGVdFXqOQ6ZHohmhOnTnlUn72WfuyqNtbx4z7Z0gtxelJT9UgilO5fe5EFKGDSJkyw6ljZiwDKqN8FDw8aTnKATX8ecVsqaW5PfKIQqZHbom+hYJ/F3DixAmppVw0ebvzaPikgXlXzJNaiktw/dgYbp4/B1/dKXY+PsupjYTZJVYEyo8fQVt9CiFMHjk6AtlQyPTI1Kkd6xRbd26VVsglsCtvFxMnTUSpVEotxWW4Mvkq7p3xDLn7j0ktpUe6BlTWFB3k35ue5stWLwlV9R9kQ+FAnnnmGUaNGkVycjIpKSns3bvXoe1v3bqVBQsWXHT9hIQEhj83nA/LP7SjKttRXlOO8W4jwfODpZbiUqSMT6ahuZasvfulltIjXT2xhLICVGGDqGz3lEhR/0Jeo3AQe/bs4csvv2T//v14eXlRVVVFW1ub1LIuiEKhILI8kpJ810i4tn7XegSlwNTBU6WW4lJMmJ7MvOtn0Rp9E/dwh9RyusUc3wMgGo2suPK3ZNccQ+vkHlrugiSGQhCEEGAdEAcUAj8TRbG2m3pG4PC5P4tEUVxoi/aXfrO01zozYmawJGlJZ/1FQxeRPjSd2pZaHt36qFXdd655p9fzlZaWEhYWhpdXx1A5LCwM6MgY++ijj9LY2EhYWBjvvvsuUVFRnDx5kvvvv5/KykqUSiXr169n8ODB/O53v2Pjxo0IgsAf//hHbrnlFrZu3cqTTz5JWFgYubm5jBs3jv/9738IgsA333zDww8/TFhYGGPHjr3E/ylYMGABy/+xnPLyciIjIy/5845k+6ntiIEiN0+5WWopLoVCoeDKK69k05Y9TFm1mRKdnmgny7hrGVAZWF2Kr/cw6n3VTu+h5S5INfW0AvheFMVhwPfn/u4OvSiKKed+bGIkpOLqq6/m7NmzJCQk8Otf/5pt27bR3t7Ob37zGz7++GP27dvHPffcwx/+8AcA7rjjDh544AEOHjzI7t27iYqK4pNPPuHAgQMcPHiQTZs2sXz5ckpLSwHIycnhpZde4siRI5w+fZpdu3bR0tLCfffdxxdffMGOHTsoKyu7ZN1Tp07FM9yTr3b0bc8NR3Ky5SRClUBUiLzAeakkxl7DI7NfpFJb7pTpPCwDKqPr6gG44YaJTmPI3B2ppp4WATPP/f4fYCvwuKMav5gRQE/1g72DL/nzAH5+fuzbt48dO3awZcsWbrnlFv74xz+Sm5vL3LlzATAajURFRdHQ0IBWq+X6668HwNu7Y0+InTt3ctttt6FUKomMjGTGjBlkZWUREBDAhAkTiImJAejcatXPz4/4+PjOFOV33nkna9asuSTdSWOSGPq3oWwo2MA93HPJ39tRNDY30hraSmxdrNRSXJLDJjVDFEoiK0o5Gx8AOF86D/M+Gn9eepJ6fTUrbpoltaR+g1SGIlIUxVIAURRLBUGI6KGetyAI2YABWCWKYkZ3lQRB+AXwC4BBg5x37wGlUtmZ32n06NG89tprjBo1ij179ljVq6+v7/bzoij2eG7zlJa5HfN2qpcb/+Cn9sOz2pOzwtnLOo+9Wb9jPQpPBdPipkktxSU55RdOm66Fga0mLK+0s6XzMJlM+Ihh6D2qXSa2xx2w29STIAibBEHI7eZn0SWcZpAoiuOB24GXBEEY0l0lURTXiKI4XhTF8eHh4TbRb2vy8/Ot4hEOHDjAiBEjqKys7DQU7e3t5OXlERAQQExMDBkZHXaxtbWV5uZmpk+fzrp16zAajVRWVrJ9+3YmTJjQY5vDhw+noKCAU6dOAbB27do+aR/mPQxTuImzFc5rLDKPZAJw67RbJVbimkSF+VNYV0S8t/Xz42zpPH7ccRA/7yA0CUG9V5axGXYzFKIoXiWKYlI3P58B5YIgRAGc+7eih3OUnPv3NB3TU6n20mtvGhsbufvuuxk5ciTJyckcOXKEp556io8//pjHH3+cMWPGkJKSwu7duwH473//yyuvvEJycjJXXnklZWVlXH/99SQnJzNmzBhmz57Nc889x4ALbNji7e3NmjVrmD9/PlOnTiU2tm/TMmkj0xAUAu9vfb9Pn3cEx/XHEaoE4iPjpZbikixPS+QszUQHDURV3+FX4ozpPH7c3LHF7dRrLt0xQ6bvCBeazrBbo4KwGqgWRXGVIAgrgBBRFH/XpU4w0CyKYqsgCGHAHmCRKIpHLnTuikcjiwAAGHJJREFU8ePHi9nZ2VZlR48eZcSIEbb9Em7Ghf6PGvQNTHp/EoHFUQjev3c6r5j6xnomfzCZuIY4vvqt8y+6OysvvJmJ1wEVH1XvxzBurNNcX0ueuPstfAhhxbs3yVNPNkYQhH3nZnDOQyqvp1XAXEEQTgBzz/2NIAjjBUH4v3N1RgDZgiAcBLbQsUZxQSMhYx/81f4oK7yo9CzpTKHgLF4xGTlarnzkLQr+XsDpU6Ml1+PK/OaeObS0NzMr2J9dK2Y7nZFoa20jQDEAo7peNhIORhJDIYpitSiKc0RRHHbu35pz5dmiKP783O+7RVEcLYrimHP/vi2F1v6OeW9iXXkk3holxpbizmNS7ypmzv9TnLcPfUErTV7jnMJ4uSoqTw8aTOV46AOkltItWVlZvPP9MwyZECK1lH6HnMJDpkesErGJHXPCYusOqzpSesWY8/94xxwmcNJQFF4+khsvVyc01ptg30jyDhyXWsp5bN6ymWPF2Vy9aLrUUvodcgoPmR6xTMSmUF+BsTkDpdr6BSKlV0yJTo+pVUfILJHG/Gircpm+MX1BCvfe+hsUo29hVEqC1HKsUovH7zzLhHFXExoaKrWsfoc8opDpEcsXriCoKP3An5L3CjvjOaT2iokOUqMvyOXYsmO06+ZYlcv0jeRxI6kxFbIxU3qnAMsRLS3NXDc8nfhBV8tTixIgGwqZHun6wlX4XoGhupL26rNogtSS7yq2PC2R9jM5oPTBMyIZkN54uTqCIHDdNTfQovVB3yztyMxyRNtUdIA//PcWfvSWpxalQDYUDqC6upqUlBRSUlIYMGAAGo2GlJQUgoKCGDlyZLef+dOf/sSmTZt6PXdhYSFJSUm2lgz8tDexGfXgVCJuiGB8zI9O4RWzcEwU4VefRrMoCYVC6RTGyx2YkjqHRVf8gu++2CmpDssRrf5UNi2CiCE6QZ5alAB5jcIBhIaGcuDAAQCefPJJ/Pz8eOyxxygsLOxxf4innnqq23Kj0eiwTXnML1zzHHHsoDhq48Mprch1SPu98d2+7/Ae6cW1wyfy8tL5UstxG9JumsLwlUnc5nMTC2+ZK5kOc2px0SRyc9QUcv0HUaH0kKcWJaBfGoozdy3utY7fzJmE3ntPZ/3A668n6IbrMdTWol32kFXd2P++12ctRqOR++67j927d6PRaPjss89Qq9UsWbKEBQsWcNNNNxEXF8c999zDt99+y4MPPsiwYcO455578PHx6dyFzl6YE7GZWfH7BTz/4vPU/bGOwMBAu7bdGx/u/RD8YfG03q+nzMUTGBxA8rgRfP3117zwwguS6TCnFvcoLmRc3FRKag7RIE8tSoI89SQxJ06c4IEHHiAvL4+goCA2bNjQbT1vb2927tzJrbfeytKlS3nllVfOSyboCBZcuwCj0UhmZqbD2zZjju3YXpFNW7mJsw09pzGR6RvzZqczbdCt5OZItx5gTi0+vLERAP2QQfLUokT0yxHFpY4ALOt7BAdf1giiK/Hx8aSkpAAwbtw4CgsLu613yy23AFBXV4dOp2PGjBkA3HXXXWzcuNFmenpj0qRJDHl0CG+efJOf8TOHtWvG7AnT1FROwCiR+kNhrPykY28r+QViO2bOmoGqoIzNGVkkpUrXg09P1ZDjFUp1s5Ztb94lmY7+jjyikJie0oN3xdfXF+hINS5l+gIPDw8iwyMpCyjDYOxeqz0xe8KY9N8ieAiIxolykJ0dSJkwktrmMqpOt0iq48wpLWG+A1FHOv5ek/kJ2VC4GEFBQQQGBrJzZ4dHyvvvOz6j68yYmSj9lazd0re05ZeD2eNF5X8EQ4MRhe8Uq3IZ25CRo+VoSwVh6limrNwgWezCV2u3oRAUTLs2RZL2ZTqQDYUL8s477/DAAw8wefJk1GrHe4A8MO8BRIPIupx1DmvTvC4hAiZDI+rBrejPBCIIHbOnsieM7TBP7x0L8EepUOJ9/JRkObRK85uo01cyYcYYh7ct8xOSpBm3J3Ka8b5xqf9HE56bQINHI0EtL1NW32bXtOPmF5c5+MpQ9ynBk/aiy56O0vda1CqlvMhpQ6as2tzplvrz0mbKmir4KiEeTZCaXStmO0yHtqiMDX89hN5Xy+MvLnVYu/0VZ0wzLuPixHqMwyNUyVntFrunHbeM0AVozj/O/7d358FR1dkCx7+nOx2ydCCBhCWJmqAsYuIDBEZBkYiKDC7gMqjD09IZsUQZH4o+F8SZmufIlCigojXCAFo6+NRBUHwIigGVUTQjaEARMQgkQchCIBshJL/3R6ebTki3Ienu20nOp4oi3bnePtem7vnd33J+pR+XY4u+RBfZBYG7G09swvbKfPr36EdEVXnIu/fefS0buy2CUb8+N6Sfq06miUK1yp7yizHHDfaIE6t3gzWo7H2Dqq+toWxTDoe/6IfNFhUWK8Q7Gu9uvB+6xhJhd5C2vyDk3XsbP1vPtoJPGTm23W5s2WFoolCtUlQdR9VuO9FpxRhzYkZKMFqd3jeo2oPr6dLbEHvOxTouESTepVuKe6ZQVH6Ac0xMSBe6FRYW8sZ7S+l+bg02m96mrKbfgGqV5Phoag9l4Ohup658Q6P3A837xtWlzybSHkgn4cxzdYVukLgXuqXER2OzCTnVeWzdsZYh3UMzRXXllgKunfYc3WISWXO0v1aLDQOaKFSrPDBuADFdr6bwlSKOfPE1ELzKre4bV0/HMfL/toOqjX2Zc/152uUURBOHpLDpoUvYPWcCzz/zWz7evopXX3016J+7cksBD/3zG8Z0OYupE+ZQ6kjUXQvDgCYK1SoTh6Tw199cSET5eVR88xm9ownqoPLEISncnnKAuiM1rHzkz5okQigtLY2xWZfx2fvbqa+vD+pnPbX2ew7n72TxmlmsqcoDrN9yV2miCBm73e4pNT548GCfpTpO1fz586mqqvK8djqdATlvS0wcksKbz/w3CRfFcEXal0G9eRtjeHHXi2RMzPCUPFGhM3ncHYw/5/esX70pqJ9TWFZNZe6HlFSVcKhvRqP3lXU6Za0nK0RHR3tKjQdKXV0d8+fPZ8qUKcTExAT03C01auQoel/Vm3d3vMsf+SPQePvKQK2vWPHJCuoz6xnZf6SlJUw6q+t/fzmjR1zGueX9uOzqi4L2OclRcEniMD4d6qQm6kSjRycuWKtTJoq3n/7qF49Jy0xkyOWne44feEEfzh7Zh+qKY7z/t8b7MUy6f2ir4jh69Ch33XUXOTk5RERE8Mwzz5CVlcWyZcvIycnh+eefB+DKK69k5syZjBkzBqfTyX333cfatWuZMGEChYWFZGVlkZiYSHZ2NgCPPvooq1evJjo6mlWrVtGrV69WxdcSdpudm+tvZtacWWydvJWfTFKjxXHu9RXQtqJ9CzctpD6xnseufSwgcatTk9CjG2OvGskLL7zA008/Te/ega3Y625cxORuZ2j6RWzr3YsDDb/TXQutp11PIVJdXe3pdpo0aRIACxcuBCA3N5fly5dz6623cvSo/yJslZWVZGRksHnzZmbPnk1ycjLZ2dmeJFFZWcn555/P119/zejRo1m0aFFwLwyYdts0YmJiWPD8gpMWx0Hb+5j3HdzHge4H6F3Wm9TE1LaGq1ph5ZYCPrEP5dpfTeOue5YFdHDZvfI+v7SK4Y4k9h8p4EAv1/esCyrDQ6d8ojjVJwDv46Odka16gmiu6+nTTz9l+vTpAAwcOJAzzjiDnTt3+j2P3W7nuuuu8/n7yMhIz6555513Hh988MEpx3qqEhISGHffOD7v+TmVeRdi63Jya7Mtfcyz35yNzWlj+rnT2xKmaiVPCRVHDy7sfTY9o3vwyBuup/JA3MDdjYveBXmkxGewqvRrsHUPeckQ5Zs+UVjIV52tiIiIRrNLvJ8yoqKi/G6F6nA4PH34/sqWB9od19xBRNcIuvBms79vbR9zeWU5m49vxvGzg4kXTGxLiKqVvJ8Sv4ysIT6mO8k/7AzYTCR3I2LE0QiOVJfx42npjd5X1tNEYaHRo0d7yoTv3LmTvXv3MmDAANLS0ti6dSv19fXs27ePL774wuc54uLiKC8vD1XIPo0fNp6o/VE4ztpLpGkcT1v6mGctn4U93s7tGbcHIkzVCt437IKUvuSX7WVUZC8Ki8s9VX3TH3qPUXM+alWXVHJ8NAmFexiYNJDPK3ZT53B43lfhQROFhaZNm0ZdXR2ZmZlMnjyZZcuW0aVLF0aNGkV6ejqZmZnMnDmToUN9d3VNnTqV8ePHk5WVFcLImzfjghnYnXbS41aQEh+N0LY+5sqjlXxY/iG2gzamjZ8W+IBVi3jfsMUmbLIdoWfXZAYW5PHwilxXpVlaXxjygXEDGFVpqKwpZ/vpaYAOYIcbLTOugMD9Pxr+1+FUOitZ95t1pCa1beD5nhfuYWPsRm6Pv50Z18xoc2yqdZqWeTf1hpv3FhMdEcXSnpGYyC6Njm/p2IJ7plPttu3c6jyHj0q28NWZA4Nasl75pmXGVcg8etGj2J127lx6Z5vOU1ZWxvLHlxO/MZ57r7o3QNGp1vCu/QRgswkbHFX0cPYkY/cPJx3fkrGFEzOdKhheUcehqhJ29D2LeZMHa0XgMKSJQgWEu696xjsOjn7nZE+PPazLWfeLx/vq235k9iMcKjnEc394TquHhgF37aeU+GgMsD/lDLYXfUdW9wxiykoaHduSsQX3AHnF1jUsXTOLN6t/pFIitFRHmOqU02NVYDXtmqg5djuOmnnMWDeTx+veZkH2nkartAG/i/JeXvcyH531EWPvvIG73y+l8PX3tDsiTHg/LWxMiuPMWkNi7ifsufBqRGwtHlsoLKvGcehnyj9+FXtyf8r6ZiDoTKdwpYlCtVnTRXa2yGTKvx2KyMfc/fAjxI64BTiREKIcNp+L8sakx/LnWX/GlhXB3vhJ1DTcOAK1wlu1TXJ8NAUN30llfA8W5G5g378Wk9ClnrMv/22Lk3lvp4MLfjyOueIJ3uzTzTOlW2c6hSd9pldt1lwr0O68kdIN/Sje8BZVuzZ73q+ureNQVW2z5ykoPcLYq68j798/caTgd9SYuEa/1yqi1vPeGwRAMi7GOWAkSTu/4v6UY80miabdjG9/lU/XLa/w4ZcvsyXyOBHdEgGd6RTO9IlCtZl3K/MEoftl06iv2UfcoH9QU1JJRJzvmTCmvhZ7/V8oyvyZhOg7iEod1Oxx2jVhLXcicBd9TEmI4clFS9j10hd8umInj2/6nor4s3x2M+aXVvHYg0+yff1rXHvb3fzcfzASwOKRKjg0UYSI3W4nMzPT83rlypUUFxfzyiuv8Oyzz1JTU8OECRMoLi7m4YcfZv/+/UydOtVTFdbpdFJRUWFV+H49MG5Ao5sBuFqHUTFxcOV92KKfpfTDRcScWUds5qUkxERSc7zec3z9sYPYu8wn9px6Dm/pR9yQK31+lnZNWG/ikJSTbuiLSw4zb9otHC7aTa9x08kfNOakbkb70Sou2rOP8/pOZOnxGt5c/KxOVGgnNFGESHO1ntLS0hg2zDVtecuWLdTW1nqOSUtLs7R8+Klo2sps3Jqsp+LnR6mvnkPJmgVE9ljPoCFjGZA8nLdydlB29DOcAwqxx9g4nNMXW6zvabXaNRG+lu6owHn9bKpXPsl1JomIvHy2SgUlcV1JqKsj5Ug55zvT6NbzXDYWfU3piImaJNqRTpko/vdPD/3iMX2HjmD4Vdd6jj/n4kvJGHMpVUcO8+68JxsdO/nxOa2KY8OGDcydO5clS5YwZcoUioqKGDx4MLfddpvl5cNPVXOtTLen1n6P/aa/YNu1hqjhG8nttp7cmvWQCfFAVV4Exw5Nwh47wuf5U7RrIqwVllVjj+lGrxv/Ql7eTkbFJHJDTP8TB0RDXmkeq2wlFPUf6FmTodqHTpkorOAuMw6Qnp7O22+/7fldz549Wbx4MXPnzmX16tUAzJs3j+zsbBITXQN97vLhTzzxBA8++CCLFi1i1qxZob+QU9Q4gVxFTW0NqzavIndvLrFdYnlxU1ck8gzsPh6coh12LTPdDrjHqcRm49uzBvJtXR09Sn+ka2UV9SIUJ3Snsm8fQJ8M2yNLEoWI3AD8ETgbGGGMyfFx3BXAAsAOLDbGtK7p3sSpPgF4Hx/TtVurniDausOdFeXDA+3EznexJMdn8cC4AaT+8H0zA+Eu+hTRfpw0TmW3U5KUTEmS66V7T0L9TtsnqzoJtwHXAh/7OkBE7MBCYDwwCLhJRJqfCtMJWFU+PFDci/KaFpDLGpjUaLoluFqc87WUQ7vStMxHU4YTSeKptd+3qdqsCj1LEoUx5jtjzC9NiB8B7DLG5BljjgGvA9cEP7rwEC7lwwPF18532TuKPDeYtlabVdZyl/nwtaO5u3HQ1mqzKvTCeYwiBdjn9Tof+FVzB4rIVGAqwOmnnx78yELAXT68T58+nsHs9szX+ofCsmq/A+Gq/Wl+XQ3YRXyuyNfvP7wF7YlCRD4UkW3N/GnpU0FzDZNma6IbY14yxgwzxgxLSkpqfdBB1NwaiDFjxngGr71/Bpg+fTo7duzwJAnv//76669n2bJlwQ04wHytf9B1ER1P09Xb4OpOrPOxpYEuogx/QUsUxphLjTEZzfxZ1cJT5AOneb1OBQoDH6kKBV83D5390vF4j1d4dyf6Gr/QxkL4C+eupy+BfiKSDhQANwI3WxuSai1fi/K0y6Fj8tWd2NwKfm0shD+rpsdOAp4DkoD3RGSrMWaciCTjmgb7a2PMcRG5B1iLa3rsEmPM9tZ+pjHGM2tINRaqXQ51LKJz08ZC+9UptkLdvXs3cXFx9OjRQ5NFE8YYSkpKKC8vJz093epwlFIW8bcVajh3PQVMamoq+fn5FBUVWR1KWIqKiiI1tW37WyulOq5OkSgcDoe2lpVSqpW0fKNSSim/NFEopZTySxOFUkopvzrcrCcRKQL2tOEUiUBxgMJpLzrbNXe26wW95s6iLdd8hjGm2dIWHS5RtJWI5PiaItZRdbZr7mzXC3rNnUWwrlm7npRSSvmliUIppZRfmihO9pLVAVigs11zZ7te0GvuLIJyzTpGoZRSyi99olBKKeWXJgqllFJ+aaJoICJXiMj3IrJLRB6yOp5gE5HTRCRbRL4Tke0icq/VMYWKiNhFZIuIrP7lo9s/EYkXkbdEZEfD932B1TEFm4jMaPh3vU1ElotIlNUxBZqILBGRgyKyzeu97iLygYj80PB3QiA+SxMFrhsHsBAYDwwCbhKRQdZGFXTHgfuNMWcD5wN3d4JrdrsX+M7qIEJoAfC+MWYg8B908GsXkRTgD8AwY0wGrv1sbrQ2qqBYBlzR5L2HgPXGmH7A+obXbaaJwmUEsMsYk2eMOQa8DrR0b+92yRiz3xjzVcPP5bhuHh1+BxkRSQUmAIutjiUURKQrMBr4O4Ax5pgxpszaqEIiAogWkQgghg64jbIx5mOgtMnb1wAvN/z8MjAxEJ+licIlBdjn9TqfTnDTdBORNGAIsNnaSEJiPvAgUG91ICHSFygCljZ0ty0WkVirgwomY0wBMBfYC+wHDhtj1lkbVcj0MsbsB1djEOgZiJNqonBpbtu7TjFvWEScwD+B/zLGHLE6nmASkSuBg8aYf1sdSwhFAEOBF40xQ4BKAtQdEa4a+uWvAdKBZCBWRKZYG1X7ponCJR84zet1Kh3wUbUpEXHgShKvGWNWWB1PCIwCrhaRn3B1L14iIq9aG1LQ5QP5xhj30+JbuBJHR3YpsNsYU2SMqQVWACMtjilUDohIH4CGvw8G4qSaKFy+BPqJSLqIROIa+HrH4piCSlybh/8d+M4Y84zV8YSCMeZhY0yqMSYN13f8kTGmQ7c0jTE/A/tEZEDDW2OBby0MKRT2AueLSEzDv/OxdPABfC/vALc2/HwrsCoQJ+0UW6H+EmPMcRG5B1iLa4bEEmPMdovDCrZRwH8CuSKyteG9R4wx/2dhTCo4pgOvNTSC8oDbLI4nqIwxm0XkLeArXLP7ttABy3mIyHJgDJAoIvnA48Ac4A0R+R2uhHlDQD5LS3gopZTyR7uelFJK+aWJQimllF+aKJRSSvmliUIppZRfmiiUUkr5pYlCKaWUX5oolFJK+aWJQqkgE5HhIvKNiESJSGzDPgkZVselVEvpgjulQkBE/geIAqJx1V560uKQlGoxTRRKhUBD+YwvgaPASGNMncUhKdVi2vWkVGh0B5xAHK4nC6XaDX2iUCoEROQdXKXN04E+xph7LA5JqRbT6rFKBZmI3AIcN8b8o2F/9n+JyCXGmI+sjk2pltAnCqWUUn7pGIVSSim/NFEopZTySxOFUkopvzRRKKWU8ksThVJKKb80USillPJLE4VSSim//h8h+/wy08sLxQAAAABJRU5ErkJggg==\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "yhat1 = f_list[0](xhat)\n", "yhat2 = f_list[1](xhat2)\n", "yhat3 = f_list[2](xhat3)\n", "yhat4 = f_list[3](xhat4)\n", "yhat5 = f_list[4](xhat5)\n", "xtemp = np.linspace(0,10, 1000)\n", "plt.plot(my_pwlf_2.x_data, my_pwlf_2.y_data, 'o')\n", "plt.plot(xtemp, my_pwlf_2.predict(xtemp), '-k', label='Predict')\n", "plt.plot(xhat, yhat1, '-.', label='First')\n", "plt.plot(xhat2, yhat2, '-.', label='Second')\n", "plt.plot(xhat3, yhat3, '-.', label='Third')\n", "plt.plot(xhat4, yhat4, '-.', label='Fourth')\n", "plt.plot(xhat5, yhat5, '-.', label='Fifth')\n", "\n", "plt.xlabel('x')\n", "plt.ylabel('y')\n", "plt.legend()\n", "plt.show()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.8" } }, "nbformat": 4, "nbformat_minor": 2 } ================================================ FILE: examples/useCustomOptimizationRoutine.py ================================================ # use your own custom optimization routine to find the optimal location # of line segment locations # import our libraries import numpy as np import matplotlib.pyplot as plt import pwlf # import custom optimization library from scipy.optimize import minimize # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # initialize custom optimization number_of_line_segments = 3 my_pwlf.use_custom_opt(number_of_line_segments) # i have number_of_line_segments - 1 number of variables # let's guess the correct location of the two unknown variables # (the program defaults to have end segments at x0= min(x) and xn=max(x) xGuess = np.zeros(number_of_line_segments - 1) xGuess[0] = 0.02 xGuess[1] = 0.10 res = minimize(my_pwlf.fit_with_breaks_opt, xGuess) # set up the break point locations x0 = np.zeros(number_of_line_segments + 1) x0[0] = np.min(x) x0[-1] = np.max(x) x0[1:-1] = res.x # calculate the parameters based on the optimal break point locations my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() ================================================ FILE: examples/weighted_least_squares_ex.py ================================================ from time import time import os os.environ['OMP_NUM_THREADS'] = '1' import numpy as np import matplotlib.pyplot as plt import pwlf t0 = time() np.random.seed(123) n = 100 n_data_sets = 100 n_segments = 6 # generate sine data x = np.linspace(0, 10, n) y = np.zeros((n_data_sets, n)) sigma_change = np.linspace(0.001, 0.05, 100) for i in range(n_data_sets): y[i] = np.sin(x * np.pi / 2) # add noise to the data y[i] = np.random.normal(0, sigma_change, 100) + y[i] X = np.tile(x, n_data_sets) # perform an ordinary pwlf fit to the entire data my_pwlf = pwlf.PiecewiseLinFit(X.flatten(), y.flatten()) my_pwlf.fit(n_segments) # compute the standard deviation in y y_std = np.std(y, axis=0) # set the weights to be one over the standard deviation weights = 1.0 / y_std # perform a weighted least squares to the data my_pwlf_w = pwlf.PiecewiseLinFit(x, y.mean(axis=0), weights=weights) my_pwlf_w.fit(n_segments) # compare the fits xhat = np.linspace(0, 10, 1000) yhat = my_pwlf.predict(xhat) yhat_w = my_pwlf_w.predict(xhat) t1 = time() print('Runtime:', t1-t0) plt.figure() plt.plot(X.flatten(), y.flatten(), '.') plt.plot(xhat, yhat, '-', label='Ordinary LS') plt.plot(xhat, yhat_w, '-', label='Weighted LS') plt.legend() plt.show() ================================================ FILE: examples/weighted_least_squares_ex_stats.py ================================================ from time import time import os os.environ['OMP_NUM_THREADS'] = '1' import numpy as np import matplotlib.pyplot as plt import pwlf t0 = time() np.random.seed(123) n = 100 n_data_sets = 100 n_segments = 6 # generate sine data x = np.linspace(0, 10, n) y = np.zeros((n_data_sets, n)) sigma_change = np.linspace(0.001, 0.05, 100) for i in range(n_data_sets): y[i] = np.sin(x * np.pi / 2) # add noise to the data y[i] = np.random.normal(0, sigma_change, 100) + y[i] X = np.tile(x, n_data_sets) # perform an ordinary pwlf fit to the entire data my_pwlf = pwlf.PiecewiseLinFit(X.flatten(), y.flatten()) my_pwlf.fit(n_segments) se = my_pwlf.standard_errors() pv = my_pwlf.prediction_variance(x) # compute the standard deviation in y y_std = np.std(y, axis=0) # set the weights to be one over the standard deviation weights = 1.0 / y_std # perform a weighted least squares to the data my_pwlf_w = pwlf.PiecewiseLinFit(x, y.mean(axis=0), weights=weights) my_pwlf_w.fit(n_segments) se_w = my_pwlf_w.standard_errors() pv_w = my_pwlf_w.prediction_variance(x) print('Standard errors', se, se_w) print('Prediction varance', pv, pv_w) # compare the fits xhat = np.linspace(0, 10, 1000) yhat = my_pwlf.predict(xhat) yhat_w = my_pwlf_w.predict(xhat) t1 = time() print('Runtime:', t1-t0) plt.figure() plt.plot(X.flatten(), y.flatten(), '.') plt.plot(xhat, yhat, '-', label='Ordinary LS') plt.plot(xhat, yhat_w, '-', label='Weighted LS') plt.legend() plt.show() ================================================ FILE: pwlf/__init__.py ================================================ from .pwlf import PiecewiseLinFit # noqa F401 from .version import __version__ # noqa F401 ================================================ FILE: pwlf/pwlf.py ================================================ # -- coding: utf-8 -- # MIT License # # Copyright (c) 2017-2022 Charles Jekel # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # import libraries import numpy as np from scipy.optimize import differential_evolution from scipy.optimize import fmin_l_bfgs_b from scipy import linalg from scipy import stats # piecewise linear fit library class PiecewiseLinFit(object): def __init__( self, x, y, disp_res=False, lapack_driver="gelsd", degree=1, weights=None, seed=None, ): r""" An object to fit a continuous piecewise linear function to data. Initiate the library with the supplied x and y data. Supply the x and y data of which you'll be fitting a continuous piecewise linear model to where y(x). by default pwlf won't print the optimization results.; Parameters ---------- x : array_like The x or independent data point locations as list or 1 dimensional numpy array. y : array_like The y or dependent data point locations as list or 1 dimensional numpy array. disp_res : bool, optional Whether the optimization results should be printed. Default is False. lapack_driver : str, optional Which LAPACK driver is used to solve the least-squares problem. Default lapack_driver='gelsd'. Options are 'gelsd', 'gelsy', 'gelss'. For more see https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.lstsq.html http://www.netlib.org/lapack/lug/node27.html degree : int, list, optional The degree of polynomial to use. The default is degree=1 for linear models. Use degree=0 for constant models. Use a list for mixed degrees (only supports degrees 1 or 0). List should be read from left to right, degree=[1,0,1] corresponds to a mixed degree model, where the left most segment has degree 1, the middle segment degree 0, and the right most segment degree 1. weights : None, or array_like The individual weights are typically the reciprocal of the standard deviation for each data point, where weights[i] corresponds to one over the standard deviation of the ith data point. Default weights=None. seed : None, or int Pick an integer which will set the numpy.random.seed on init. The fit and fitfast methods rely on stochastic methods and setting this value will make the results reproducible. The default behavior is to not specify a seed. Attributes ---------- beta : ndarray (1-D) The model parameters for the continuous piecewise linear fit. break_0 : float The smallest x value. break_n : float The largest x value. c_n : int The number of constraint points. This is the same as len(x_c). degree: int, list The degree of polynomial to use. The default is degree=1 for linear models. Use degree=0 for constant models. This will be a list if the user provided a list. fit_breaks : ndarray (1-D) breakpoint locations stored as a 1-D numpy array. intercepts : ndarray (1-D) The y-intercept of each line segment as a 1-D numpy array. lapack_driver : str Which LAPACK driver is used to solve the least-squares problem. print : bool Whether the optimization results should be printed. Default is False. n_data : int The number of data points. n_parameters : int The number of model parameters. This is equivalent to the len(beta). n_segments : int The number of line segments. nVar : int The number of variables in the global optimization problem. se : ndarray (1-D) Standard errors associated with each beta parameter. Specifically se[0] correspounds to the standard error for beta[0], and so forth. seed : int Numpy random seed number set on init. slopes : ndarray (1-D) The slope of each ling segment as a 1-D numpy array. This assumes that x[0] <= x[1] <= ... <= x[n]. Thus, slopes[0] is the slope of the first line segment. ssr : float Optimal sum of square error. x_c : ndarray (1-D) The x locations of the data points that the piecewise linear function will be forced to go through. y_c : ndarray (1-D) The y locations of the data points that the piecewise linear function will be forced to go through. x_data : ndarray (1-D) The inputted parameter x from the 1-D data set. y_data : ndarray (1-D) The inputted parameter y from the 1-D data set. y_w : ndarray (1-D) The weighted y data vector. zeta : ndarray (1-D) The model parameters associated with the constraint function. Methods ------- fit(n_segments, x_c=None, y_c=None, **kwargs) Fit a continuous piecewise linear function for a specified number of line segments. fitfast(n_segments, pop=2, **kwargs) Fit a continuous piecewise linear function for a specified number of line segments using a specialized optimization routine that should be faster than fit() for large problems. The tradeoff may be that fitfast() results in a lower quality model. fit_with_breaks(breaks) Fit a continuous piecewise linear function where the breakpoint locations are known. fit_with_breaks_force_points(breaks, x_c, y_c) Fit a continuous piecewise linear function where the breakpoint locations are known, and force the fit to go through points at x_c and y_c. predict(x, beta=None, breaks=None) Evaluate the continuous piecewise linear function at new untested points. fit_with_breaks_opt(var) The objective function to perform a continuous piecewise linear fit for a specified number of breakpoints. This is to be used with a custom optimization routine, and after use_custom_opt has been called. fit_force_points_opt(var)' Same as fit_with_breaks_opt(var), except this allows for points to be forced through x_c and y_c. use_custom_opt(n_segments, x_c=None, y_c=None) Function to initialize the attributes necessary to use a custom optimization routine. Must be used prior to calling fit_with_breaks_opt() or fit_force_points_opt(). calc_slopes() Calculate the slopes of the lines after a piecewise linear function has been fitted. standard_errors() Calculate the standard error of each model parameter in the fitted piecewise linear function. Note, this assumes no uncertainty in breakpoint locations. prediction_variance(x) Calculate the prediction variance at x locations for the fitted piecewise linear function. Note, assumes no uncertainty in break point locations. r_squared() Calculate the coefficient of determination, or 'R-squared' value for a fitted piecewise linear function. Examples -------- Initialize for x, y data >>> import pwlf >>> my_pwlf = pwlf.PiecewiseLinFit(x, y) Initialize for x,y data and print optimization results >>> my_pwlf = pwlf.PiecewiseLinFit(x, y, disp_res=True) """ self.print = disp_res self.lapack_driver = lapack_driver x, y = self._switch_to_np_array(x), self._switch_to_np_array(y) self.x_data, self.y_data = x, y # calculate the number of data points self.n_data = x.size # set the first and last break x values to be the min and max of x self.break_0, self.break_n = np.min(self.x_data), np.max(self.x_data) self.mixed_degree = False if isinstance(degree, list): # make sure the min and max are withing the limit max_degree = max(degree) min_degree = min(degree) if min_degree >= 0 and max_degree <= 1: self.mixed_degree = True self.degree = degree else: not_suported = "Not supported mixed degree. Max mixed degree=1" ", and min mixed degree=0" raise ValueError(not_suported) elif degree < 12 and degree >= 0: # I actually don't know what the upper degree limit should self.degree = int(degree) else: raise ValueError(f"degree = {degree} is not supported.") self.y_w = None self.weights = None # self.weights2 = None # the squared weights vector if weights is not None: self.weights = self._switch_to_np_array(weights) # self.weights2 = weights*weights self.y_w = np.dot(self.y_data, np.eye(self.n_data) * self.weights) if seed is not None: np.random.seed(seed) self.seed = seed # initialize all empty attributes as None self.fit_breaks = None self.n_segments = None self.n_parameters = None self.beta = None self.ssr = None self.x_c = None self.y_c = None self.c_n = None self.zeta = None self.nVar = None self.slopes = None self.intercepts = None self.se = None @staticmethod def _switch_to_np_array(input_): r""" Check the input, if it's not a Numpy array transform it to one. Parameters ---------- input_ : array_like The object that requires a check. Returns ------- input_ : ndarray The input data that's been transformed when required. """ if isinstance(input_, np.ndarray) is False: input_ = np.array(input_) return input_ def _get_breaks(self, input_): r""" Use input to form a ndarray containing breakpoints Parameters ---------- input_ : array_like The object containing some of the breakpoints Returns ------- b_ : ndarray All the line breaks """ v = np.sort(input_) b_ = np.zeros(len(v) + 2) b_[0], b_[1:-1], b_[-1] = self.break_0, v.copy(), self.break_n return b_ def _fit_one_segment(self): r""" Fit for a single line segment """ self.fit_with_breaks([self.break_0, self.break_n]) def _fit_one_segment_force_points(self, x_c, y_c): r""" Fit for a single line segment with force points """ self.fit_with_breaks_force_points([self.break_0, self.break_n], x_c, y_c) def _check_mixed_degree_list(self, n_segments): r""" Fit for a single line segment with force points """ if self.mixed_degree: degree_list_len = len(self.degree) error_message = f""" Error: degree list does not match n_segments! degree list length {degree_list_len} must equal {n_segments}""" if degree_list_len != n_segments: raise ValueError(error_message) def assemble_regression_matrix(self, breaks, x): r""" Assemble the linear regression matrix A Parameters ---------- breaks : array_like The x locations where each line segment terminates. These are referred to as breakpoints for each line segment. This should be structured as a 1-D numpy array. x : ndarray (1-D) The x locations which the linear regression matrix is assembled on. This must be a numpy array! Returns ------- A : ndarray (2-D) The assembled linear regression matrix. Examples -------- Assemble the linear regression matrix on the x data for some set of breakpoints. >>> import pwlf >>> my_pwlf = pwlf.PiecewiseLinFit(x, y) >>> breaks = [0.0, 0.5, 1.0] >>> A = assemble_regression_matrix(breaks, self.x_data) """ breaks = self._switch_to_np_array(breaks) # Sort the breaks, then store them breaks_order = np.argsort(breaks) self.fit_breaks = breaks[breaks_order] # store the number of parameters and line segments self.n_segments = len(breaks) - 1 # Assemble the regression matrix A_list = [np.ones_like(x)] if self.mixed_degree: if len(self.degree) != self.n_segments: raise ValueError( "Number of degrees does not much number of segments.", ) for i in range(self.n_segments): degree = self.degree[i] if i == 0: A_list = [np.ones_like(x)] if degree == 1: A_list.append(x - self.fit_breaks[0]) if i > 0: if degree == 0: # all previous slopes must be written to 0 inds = np.argwhere(x > self.fit_breaks[i]) a_size = len(A_list) for j in range(1, a_size): A_list[j][inds] = 0.0 # add the new zero slopes A_list.append( np.where(x > self.fit_breaks[i], 1.0, 0.0) ) elif degree == 1: A_list.append( np.where( x > self.fit_breaks[i], x - self.fit_breaks[i], 0.0 ) ) elif self.degree >= 1: A_list.append(x - self.fit_breaks[0]) for i in range(self.n_segments - 1): A_list.append( np.where( x > self.fit_breaks[i + 1], x - self.fit_breaks[i + 1], 0.0 ) ) if self.degree >= 2: for k in range(2, self.degree + 1): A_list.append((x - self.fit_breaks[0]) ** k) for i in range(self.n_segments - 1): A_list.append( np.where( x > self.fit_breaks[i + 1], (x - self.fit_breaks[i + 1]) ** k, 0.0, ) ) else: A_list = [np.ones_like(x)] for i in range(self.n_segments - 1): A_list.append(np.where(x > self.fit_breaks[i + 1], 1.0, 0.0)) A = np.vstack(A_list).T self.n_parameters = A.shape[1] return A def fit_with_breaks(self, breaks): r""" A function which fits a continuous piecewise linear function for specified breakpoint locations. The function minimizes the sum of the square of the residuals for the x y data. If you want to understand the math behind this read https://jekel.me/2018/Continous-piecewise-linear-regression/ Other useful resources: http://golovchenko.org/docs/ContinuousPiecewiseLinearFit.pdf https://www.mathworks.com/matlabcentral/fileexchange/40913-piecewise-linear-least-square-fittic http://www.regressionist.com/2018/02/07/continuous-piecewise-linear-fitting/ Parameters ---------- breaks : array_like The x locations where each line segment terminates. These are referred to as breakpoints for each line segment. This should be structured as a 1-D numpy array. Returns ------- ssr : float Returns the sum of squares of the residuals. Raises ------ LinAlgError This typically means your regression problem is ill-conditioned. Examples -------- If your x data exists from 0 <= x <= 1 and you want three piecewise linear lines where the lines terminate at x = 0.0, 0.3, 0.6, and 1.0. This assumes that x is linearly spaced from [0, 1), and y is random. >>> import pwlf >>> x = np.linspace(0.0, 1.0, 10) >>> y = np.random.random(10) >>> my_pwlf = pwlf.PiecewiseLinFit(x, y) >>> breaks = [0.0, 0.3, 0.6, 1.0] >>> ssr = my_pwlf.fit_with_breaks(breaks) """ self._check_mixed_degree_list(len(breaks)-1) breaks = self._switch_to_np_array(breaks) A = self.assemble_regression_matrix(breaks, self.x_data) # try to solve the regression problem try: ssr = self.lstsq(A) except linalg.LinAlgError: # the computation could not converge! # on an error, return ssr = np.print_function # You might have a singular Matrix!!! ssr = np.inf if ssr is None: ssr = np.inf # something went wrong... self.ssr = ssr return ssr def fit_with_breaks_force_points(self, breaks, x_c, y_c): r""" A function which fits a continuous piecewise linear function for specified breakpoint locations, where you force the fit to go through the data points at x_c and y_c. The function minimizes the sum of the square of the residuals for the pair of x, y data points. If you want to understand the math behind this read https://jekel.me/2018/Force-piecwise-linear-fit-through-data/ Parameters ---------- breaks : array_like The x locations where each line segment terminates. These are referred to as breakpoints for each line segment. This should be structured as a 1-D numpy array. x_c : array_like The x locations of the data points that the piecewise linear function will be forced to go through. y_c : array_like The x locations of the data points that the piecewise linear function will be forced to go through. Returns ------- L : float Returns the Lagrangian function value. This is the sum of squares of the residuals plus the constraint penalty. Raises ------ LinAlgError This typically means your regression problem is ill-conditioned. ValueError You can't specify weights with x_c and y_c. Examples -------- If your x data exists from 0 <= x <= 1 and you want three piecewise linear lines where the lines terminate at x = 0.0, 0.3, 0.6, and 1.0. This assumes that x is linearly spaced from [0, 1), and y is random. Additionally you desired that the piecewise linear function go through the point (0.0, 0.0) >>> import pwlf >>> x = np.linspace(0.0, 1.0, 10) >>> y = np.random.random(10) >>> x_c = [0.0] >>> y_c = [0.0] >>> my_pwlf = pwlf.PiecewiseLinFit(x, y) >>> breaks = [0.0, 0.3, 0.6, 1.0] >>> L = my_pwlf.fit_with_breaks_force_points(breaks, x_c, y_c) """ self._check_mixed_degree_list(len(breaks)-1) x_c, y_c = self._switch_to_np_array(x_c), self._switch_to_np_array(y_c) # sort the x_c and y_c data points, then store them x_c_order = np.argsort(x_c) self.x_c, self.y_c = x_c[x_c_order], y_c[x_c_order] # store the number of constraints self.c_n = len(self.x_c) if self.weights is not None: raise ValueError( "Constrained least squares with weights are" " not supported since these have a tendency " "of being numerically instable." ) breaks = self._switch_to_np_array(breaks) A = self.assemble_regression_matrix(breaks, self.x_data) L = self.conlstsq(A) return L def predict(self, x, beta=None, breaks=None): r""" Evaluate the fitted continuous piecewise linear function at untested points. You can manfully specify the breakpoints and calculated values for beta if you want to quickly predict from different models and the same data set. Parameters ---------- x : array_like The x locations where you want to predict the output of the fitted continuous piecewise linear function. beta : none or ndarray (1-D), optional The model parameters for the continuous piecewise linear fit. Default is None. breaks : none or array_like, optional The x locations where each line segment terminates. These are referred to as breakpoints for each line segment. This should be structured as a 1-D numpy array. Default is None. Returns ------- y_hat : ndarray (1-D) The predicted values at x. Examples -------- Fits a simple model, then predict at x_new locations which are linearly spaced. >>> import pwlf >>> x = np.linspace(0.0, 1.0, 10) >>> y = np.random.random(10) >>> my_pwlf = pwlf.PiecewiseLinFit(x, y) >>> breaks = [0.0, 0.3, 0.6, 1.0] >>> ssr = my_pwlf.fit_with_breaks(breaks) >>> x_new = np.linspace(0.0, 1.0, 100) >>> yhat = my_pwlf.predict(x_new) """ if beta is not None and breaks is not None: self.beta = beta # Sort the breaks, then store them breaks_order = np.argsort(breaks) self.fit_breaks = breaks[breaks_order] self.n_parameters = len(self.fit_breaks) self.n_segments = self.n_parameters - 1 x = self._switch_to_np_array(x) A = self.assemble_regression_matrix(self.fit_breaks, x) # solve the regression problem y_hat = np.dot(A, self.beta) return y_hat def fit_with_breaks_opt(self, var): r""" The objective function to perform a continuous piecewise linear fit for a specified number of breakpoints. This is to be used with a custom optimization routine, and after use_custom_opt has been called. This was intended for advanced users only. See the following example https://github.com/cjekel/piecewise_linear_fit_py/blob/master/examples/useCustomOptimizationRoutine.py Parameters ---------- var : array_like The breakpoint locations, or variable, in a custom optimization routine. Returns ------- ssr : float The sum of square of the residuals. Raises ------ LinAlgError This typically means your regression problem is ill-conditioned. Notes ----- You should run use_custom_opt to initialize necessary object attributes first. Unlike fit_with_breaks, fit_with_breaks_opt automatically assumes that the first and last breakpoints occur at the min and max values of x. """ breaks = self._get_breaks(input_=var) A = self.assemble_regression_matrix(breaks, self.x_data) # try to solve the regression problem try: # least squares solver ssr = self.lstsq(A, calc_slopes=False) except linalg.LinAlgError: # the computation could not converge! # on an error, return ssr = np.inf # You might have a singular Matrix!!! ssr = np.inf if ssr is None: ssr = np.inf # something went wrong... return ssr def fit_force_points_opt(self, var): r""" The objective function to perform a continuous piecewise linear fit for a specified number of breakpoints. This is to be used with a custom optimization routine, and after use_custom_opt has been called. Use this function if you intend to be force the model through x_c and y_c, while performing a custom optimization. This was intended for advanced users only. See the following example https://github.com/cjekel/piecewise_linear_fit_py/blob/master/examples/useCustomOptimizationRoutine.py Parameters ---------- var : array_like The breakpoint locations, or variable, in a custom optimization routine. Returns ------- ssr : float The sum of square of the residuals. Raises ------ LinAlgError This typically means your regression problem is ill-conditioned. Notes ----- You should run use_custom_opt to initialize necessary object attributes first. Unlike fit_with_breaks_force_points, fit_force_points_opt automatically assumes that the first and last breakpoints occur at the min and max values of x. """ breaks = self._get_breaks(input_=var) # Sort the breaks, then store them breaks_order = np.argsort(breaks) breaks = breaks[breaks_order] A = self.assemble_regression_matrix(breaks, self.x_data) L = self.conlstsq(A, calc_slopes=False) return L def fit(self, n_segments, x_c=None, y_c=None, bounds=None, **kwargs): r""" Fit a continuous piecewise linear function for a specified number of line segments. Uses differential evolution to finds the optimum location of breakpoints for a given number of line segments by minimizing the sum of the square error. Parameters ---------- n_segments : int The desired number of line segments. x_c : array_like, optional The x locations of the data points that the piecewise linear function will be forced to go through. y_c : array_like, optional The x locations of the data points that the piecewise linear function will be forced to go through. bounds : array_like, optional Bounds for each breakpoint location within the optimization. This should have the shape of (n_segments, 2). **kwargs : optional Directly passed into scipy.optimize.differential_evolution(). This will override any pwlf defaults when provided. See Note for more information. Returns ------- fit_breaks : float breakpoint locations stored as a 1-D numpy array. Raises ------ ValueError You probably provided x_c without y_c (or vice versa). You must provide both x_c and y_c if you plan to force the model through data point(s). ValueError You can't specify weights with x_c and y_c. Notes ----- All **kwargs are passed into sicpy.optimize.differential_evolution. If any **kwargs is used, it will override my differential_evolution, defaults. This allows advanced users to tweak their own optimization. For me information see: https://github.com/cjekel/piecewise_linear_fit_py/issues/15#issuecomment-434717232 Examples -------- This example shows you how to fit three continuous piecewise lines to a dataset. This assumes that x is linearly spaced from [0, 1), and y is random. >>> import pwlf >>> x = np.linspace(0.0, 1.0, 10) >>> y = np.random.random(10) >>> my_pwlf = pwlf.PiecewiseLinFit(x, y) >>> breaks = my_pwlf.fit(3) Additionally you desired that the piecewise linear function go through the point (0.0, 0.0). >>> x_c = [0.0] >>> y_c = [0.0] >>> breaks = my_pwlf.fit(3, x_c=x_c, y_c=y_c) Additionally you desired that the piecewise linear function go through the points (0.0, 0.0) and (1.0, 1.0). >>> x_c = [0.0, 1.0] >>> y_c = [0.0, 1.0] >>> breaks = my_pwlf.fit(3, x_c=x_c, y_c=y_c) """ self._check_mixed_degree_list(n_segments) # check to see if you've provided just x_c or y_c logic1 = x_c is not None and y_c is None logic2 = y_c is not None and x_c is None if logic1 or logic2: raise ValueError("You must provide both x_c and y_c!") # set the function to minimize min_function = self.fit_with_breaks_opt # if you've provided both x_c and y_c if x_c is not None and y_c is not None: x_c = self._switch_to_np_array(x_c) y_c = self._switch_to_np_array(y_c) # sort the x_c and y_c data points, then store them x_c_order = np.argsort(x_c) self.x_c, self.y_c = x_c[x_c_order], y_c[x_c_order] # store the number of constraints self.c_n = len(self.x_c) # Use a different function to minimize min_function = self.fit_force_points_opt if self.weights is not None: raise ValueError( "Constrained least squares with weights are" " not supported since these have a tendency " "of being numerically instable." ) elif self.mixed_degree: raise ValueError( "Constrained least squares with mixed degree" " lists is not supported." ) # store the number of line segments and number of parameters self.n_segments = int(n_segments) self.n_parameters = self.n_segments + 1 # calculate the number of variables I have to solve for self.nVar = self.n_segments - 1 # special fit for one line segment if self.n_segments == 1: if x_c is None and y_c is None: self._fit_one_segment() else: self._fit_one_segment_force_points(self.x_c, self.y_c) return self.fit_breaks # initiate the bounds of the optimization if bounds is None: bounds = np.zeros([self.nVar, 2]) bounds[:, 0] = self.break_0 bounds[:, 1] = self.break_n # run the optimization if len(kwargs) == 0: res = differential_evolution( min_function, bounds, strategy="best1bin", maxiter=1000, popsize=50, tol=1e-3, mutation=(0.5, 1), recombination=0.7, seed=None, callback=None, disp=False, polish=True, init="latinhypercube", atol=1e-4, ) else: res = differential_evolution(min_function, bounds, **kwargs) if self.print is True: print(res) self.ssr = res.fun breaks = self._get_breaks(input_=res.x) # assign values if x_c is None and y_c is None: self.fit_with_breaks(breaks) else: self.fit_with_breaks_force_points(breaks, self.x_c, self.y_c) return self.fit_breaks def fitfast(self, n_segments, pop=2, bounds=None, **kwargs): r""" Uses multi start LBFGSB optimization to find the location of breakpoints for a given number of line segments by minimizing the sum of the square of the errors. The idea is that we generate n random latin hypercube samples and run LBFGSB optimization on each one. This isn't guaranteed to find the global optimum. It's suppose to be a reasonable compromise between speed and quality of fit. Let me know how it works. Since this is based on random sampling, you might want to run it multiple times and save the best version... The best version will have the lowest self.ssr (sum of square of residuals). There is no guarantee that this will be faster than fit(), however you may find it much faster sometimes. Parameters ---------- n_segments : int The desired number of line segments. pop : int, optional The number of latin hypercube samples to generate. Default pop=2. bounds : array_like, optional Bounds for each breakpoint location within the optimization. This should have the shape of (n_segments, 2). **kwargs : optional Directly passed into scipy.optimize.fmin_l_bfgs_b(). This will override any pwlf defaults when provided. See Note for more information. Returns ------- fit_breaks : float breakpoint locations stored as a 1-D numpy array. Notes ----- The default number of multi start optimizations is 2. - Decreasing this number will result in a faster run time. - Increasing this number will improve the likelihood of finding good results - You can specify the number of starts using the following call - Minimum value of pop is 2 All **kwargs are passed into sicpy.optimize.fmin_l_bfgs_b. If any **kwargs is used, it will override my defaults. This allows advanced users to tweak their own optimization. For me information see: https://github.com/cjekel/piecewise_linear_fit_py/issues/15#issuecomment-434717232 Examples -------- This example shows you how to fit three continuous piecewise lines to a dataset. This assumes that x is linearly spaced from [0, 1), and y is random. >>> import pwlf >>> x = np.linspace(0.0, 1.0, 10) >>> y = np.random.random(10) >>> my_pwlf = pwlf.PiecewiseLinFit(x, y) >>> breaks = my_pwlf.fitfast(3) You can change the number of latin hypercube samples (or starting point, locations) to use with pop. The following example will use 50 samples. >>> breaks = my_pwlf.fitfast(3, pop=50) """ self._check_mixed_degree_list(n_segments) pop = int(pop) # ensure that the population is integer self.n_segments = int(n_segments) self.n_parameters = self.n_segments + 1 # calculate the number of variables I have to solve for self.nVar = self.n_segments - 1 # special fit for one line segment if self.n_segments == 1: self._fit_one_segment() return self.fit_breaks # initiate the bounds of the optimization if bounds is None: bounds = np.zeros([self.nVar, 2]) bounds[:, 0] = self.break_0 bounds[:, 1] = self.break_n # perform latin hypercube sampling lhs = stats.qmc.LatinHypercube(self.nVar, seed=self.seed) mypop = lhs.random(n=pop) # scale the sampling to my variable range mypop = mypop * (self.break_n - self.break_0) + self.break_0 x = np.zeros((pop, self.nVar)) f = np.zeros(pop) d = [] for i, x0 in enumerate(mypop): if len(kwargs) == 0: resx, resf, resd = fmin_l_bfgs_b( self.fit_with_breaks_opt, x0, fprime=None, args=(), approx_grad=True, bounds=bounds, m=10, factr=1e2, pgtol=1e-05, epsilon=1e-08, iprint=-1, maxfun=15000, maxiter=15000, disp=None, callback=None, ) else: resx, resf, resd = fmin_l_bfgs_b( self.fit_with_breaks_opt, x0, fprime=None, approx_grad=True, bounds=bounds, **kwargs, ) x[i, :] = resx f[i] = resf d.append(resd) if self.print is True: print(f"{i + 1} of {pop} complete") # find the best result best_ind = np.nanargmin(f) best_val = f[best_ind] best_break = x[best_ind] res = (x[best_ind], f[best_ind], d[best_ind]) if self.print is True: print(res) self.ssr = best_val breaks = self._get_breaks(input_=best_break) # assign parameters self.fit_with_breaks(breaks) return self.fit_breaks def fit_guess(self, guess_breakpoints, bounds=None, **kwargs): r""" Uses L-BFGS-B optimization to find the location of breakpoints from a guess of where breakpoint locations should be. In some cases you may have a good idea where the breakpoint locations occur. It generally won't be necessary to run a full global optimization to search the entire domain for the breakpoints when you have a good idea where the breakpoints occur. Here a local optimization is run from a guess of the breakpoint locations. Parameters ---------- guess_breakpoints : array_like Guess where the breakpoints occur. This should be a list or numpy array containing the locations where it appears breakpoints occur. bounds : array_like, optional Bounds for each breakpoint location within the optimization. This should have the shape of (n_segments, 2). **kwargs : optional Directly passed into scipy.optimize.fmin_l_bfgs_b(). This will override any pwlf defaults when provided. See Note for more information. Returns ------- fit_breaks : float breakpoint locations stored as a 1-D numpy array. Notes ----- All **kwargs are passed into sicpy.optimize.fmin_l_bfgs_b. If any **kwargs is used, it will override my defaults. This allows advanced users to tweak their own optimization. For me information see: https://github.com/cjekel/piecewise_linear_fit_py/issues/15#issuecomment-434717232 You do not need to specify the x.min() or x.max() in geuss_breakpoints! Examples -------- In this example we see two distinct linear regions, and we believe a breakpoint occurs at 6.0. We'll use the fit_guess() function to find the best breakpoint location starting with this guess. >>> import pwlf >>> x = np.array([4., 5., 6., 7., 8.]) >>> y = np.array([11., 13., 16., 28.92, 42.81]) >>> my_pwlf = pwlf.PiecewiseLinFit(x, y) >>> breaks = my_pwlf.fit_guess([6.0]) Note specifying one breakpoint will result in two line segments. If we wanted three line segments, we'll have to specify two breakpoints. >>> breaks = my_pwlf.fit_guess([5.5, 6.0]) """ self._check_mixed_degree_list(guess_breakpoints) # calculate the number of variables I have to solve for self.nVar = len(guess_breakpoints) self.n_segments = self.nVar + 1 self.n_parameters = self.n_segments + 1 # initiate the bounds of the optimization if bounds is None: bounds = np.zeros([self.nVar, 2]) bounds[:, 0] = self.break_0 bounds[:, 1] = self.break_n if len(kwargs) == 0: resx, resf, _ = fmin_l_bfgs_b( self.fit_with_breaks_opt, guess_breakpoints, fprime=None, args=(), approx_grad=True, bounds=bounds, m=10, factr=1e2, pgtol=1e-05, epsilon=1e-08, iprint=-1, maxfun=15000, maxiter=15000, disp=None, callback=None, ) else: resx, resf, _ = fmin_l_bfgs_b( self.fit_with_breaks_opt, guess_breakpoints, fprime=None, approx_grad=True, bounds=bounds, **kwargs, ) self.ssr = resf breaks = self._get_breaks(input_=resx) # assign values self.fit_with_breaks(breaks) return self.fit_breaks def use_custom_opt(self, n_segments, x_c=None, y_c=None): r""" Provide the number of line segments you want to use with your custom optimization routine. Run this function first to initialize necessary attributes!!! This was intended for advanced users only. See the following example https://github.com/cjekel/piecewise_linear_fit_py/blob/master/examples/useCustomOptimizationRoutine.py Parameters ---------- n_segments : int The x locations where each line segment terminates. These are referred to as breakpoints for each line segment. This should be structured as a 1-D numpy array. x_c : none or array_like, optional The x locations of the data points that the piecewise linear function will be forced to go through. y_c : none or array_like, optional The x locations of the data points that the piecewise linear function will be forced to go through. Notes ----- Optimize fit_with_breaks_opt(var) where var is a 1D array containing the x locations of your variables var has length n_segments - 1, because the two breakpoints are always defined (1. the min of x, 2. the max of x). fit_with_breaks_opt(var) will return the sum of the square of the residuals which you'll want to minimize with your optimization routine. Raises ------ ValueError You can't specify weights with x_c and y_c. """ self._check_mixed_degree_list(n_segments) self.n_segments = int(n_segments) self.n_parameters = self.n_segments + 1 # calculate the number of variables I have to solve for self.nVar = self.n_segments - 1 if x_c is not None or y_c is not None: x_c = self._switch_to_np_array(x_c) y_c = self._switch_to_np_array(y_c) # sort the x_c and y_c data points, then store them x_c_order = np.argsort(x_c) self.x_c, self.y_c = x_c[x_c_order], y_c[x_c_order] # store the number of constraints self.c_n = len(self.x_c) if self.weights is not None: raise ValueError( "Constrained least squares with weights are" " not supported since these have a tendency " "of being numerically instable." ) def calc_slopes(self): r""" Calculate the slopes of the lines after a piecewise linear function has been fitted. This will also calculate the y-intercept from each line in the form y = mx + b. The intercepts are stored at self.intercepts. Returns ------- slopes : ndarray(1-D) The slope of each ling segment as a 1-D numpy array. This assumes that x[0] <= x[1] <= ... <= x[n]. Thus, slopes[0] is the slope of the first line segment. Examples -------- Calculate the slopes after performing a simple fit >>> import pwlf >>> x = np.linspace(0.0, 1.0, 10) >>> y = np.random.random(10) >>> my_pwlf = pwlf.PiecewiseLinFit(x, y) >>> breaks = my_pwlf.fit(3) >>> slopes = my_pwlf.calc_slopes() """ y_hat = self.predict(self.fit_breaks) self.slopes = np.divide( (y_hat[1: self.n_segments + 1] - y_hat[: self.n_segments]), ( self.fit_breaks[1: self.n_segments + 1] - self.fit_breaks[: self.n_segments] ), ) self.intercepts = y_hat[0:-1] - self.slopes * self.fit_breaks[0:-1] return self.slopes def standard_errors(self, method="linear", step_size=1e-4): r""" Calculate the standard errors for each beta parameter determined from the piecewise linear fit. Typically +- 1.96*se will yield the center of a 95% confidence region around your parameters. This assumes the parmaters follow a normal distribution. For more information see: https://en.wikipedia.org/wiki/Standard_error This calculation follows the derivation provided in [1]_. Parameters ---------- method : string, optional Calculate the standard errors for a linear or non-linear regression problem. The default is method='linear'. A taylor- series expansion is performed when method='non-linear' (which is commonly referred to as the Delta method). step_size : float, optional The step size to perform forward differences for the taylor- series expansion when method='non-linear'. Default is step_size=1e-4. Returns ------- se : ndarray (1-D) Standard errors associated with each beta parameter. Specifically se[0] correspounds to the standard error for beta[0], and so forth. Raises ------ AttributeError You have probably not performed a fit yet. ValueError You supplied an unsupported method. LinAlgError This typically means your regression problem is ill-conditioned. Notes ----- The linear regression problem is when you know the breakpoint locations (e.g. when using the fit_with_breaks function). The non-linear regression problem is when you don't know the breakpoint locations (e.g. when using the fit, fitfast, and fit_guess functions). References ---------- .. [1] Coppe, A., Haftka, R. T., and Kim, N. H., “Uncertainty Identification of Damage Growth Parameters Using Nonlinear Regression,” AIAA Journal, Vol. 49, No. 12, dec 2011, pp. 2818–2821. Examples -------- Calculate the standard errors after performing a simple fit. >>> import pwlf >>> x = np.linspace(0.0, 1.0, 10) >>> y = np.random.random(10) >>> my_pwlf = pwlf.PiecewiseLinFit(x, y) >>> breaks = my_pwlf.fitfast(3) >>> se = my_pwlf.standard_errors() """ try: nb = self.beta.size except AttributeError: errmsg = ( "You do not have any beta parameters. You must perform" " a fit before using standard_errors()." ) raise AttributeError(errmsg) ny = self.n_data if method == "linear": A = self.assemble_regression_matrix(self.fit_breaks, self.x_data) y_hat = np.dot(A, self.beta) e = y_hat - self.y_data elif method == "non-linear": nb = self.beta.size + self.fit_breaks.size - 2 f0 = self.predict(self.x_data) A = np.zeros((ny, nb)) orig_beta = self.beta.copy() orig_breaks = self.fit_breaks.copy() # calculate differentials due to betas for i in range(self.beta.size): temp_beta = orig_beta.copy() temp_beta[i] += step_size # vary beta and keep breaks constant f = self.predict( self.x_data, beta=temp_beta, breaks=orig_breaks, ) A[:, i] = (f - f0) / step_size # append differentials due to break points for i in range(self.beta.size, nb): # 'ind' ignores first and last entry in self.fit_breaks since # these are simply the min/max of self.x_data. ind = i - self.beta.size + 1 temp_breaks = orig_breaks.copy() temp_breaks[ind] += step_size # vary break and keep betas constant f = self.predict( self.x_data, beta=orig_beta, breaks=temp_breaks, ) A[:, i] = (f - f0) / step_size e = f0 - self.y_data # reset beta and breaks back to original values self.beta = orig_beta self.fit_breaks = orig_breaks else: errmsg = f"Error: method='{method}' is not supported!" raise ValueError(errmsg) # try to solve for the standard errors try: variance = np.dot(e, e) / (ny - nb) if self.weights is None: # solve for the unbiased estimate of variance A2inv = np.abs(linalg.pinv(np.dot(A.T, A)).diagonal()) self.se = np.sqrt(variance * A2inv) else: A = (A.T * self.weights).T A2inv = np.abs(linalg.pinv(np.dot(A.T, A)).diagonal()) self.se = np.sqrt(variance * A2inv) return self.se except linalg.LinAlgError: raise linalg.LinAlgError("Singular matrix") def prediction_variance(self, x): r""" Calculate the prediction variance for each specified x location. The prediction variance is the uncertainty of the model due to the lack of data. This can be used to find a 95% confidence interval of possible piecewise linear models based on the current data. This would be done typically as y_hat +- 1.96*np.sqrt(pre_var). The prediction_variance needs to be calculated at various x locations. For more information see: www2.mae.ufl.edu/haftka/vvuq/lectures/Regression-accuracy.pptx Parameters ---------- x : array_like The x locations where you want the prediction variance from the fitted continuous piecewise linear function. Returns ------- pre_var : ndarray (1-D) Numpy array (floats) of prediction variance at each x location. Raises ------ AttributeError You have probably not performed a fit yet. LinAlgError This typically means your regression problem is ill-conditioned. Notes ----- This assumes that your breakpoint locations are exact! and does not consider the uncertainty with your breakpoint locations. Examples -------- Calculate the prediction variance at x_new after performing a simple fit. >>> import pwlf >>> x = np.linspace(0.0, 1.0, 10) >>> y = np.random.random(10) >>> my_pwlf = pwlf.PiecewiseLinFit(x, y) >>> breaks = my_pwlf.fitfast(3) >>> x_new = np.linspace(0.0, 1.0, 100) >>> pre_var = my_pwlf.prediction_variance(x_new) see also examples/prediction_variance.py """ try: nb = self.beta.size except AttributeError: errmsg = ( "You do not have any beta parameters. You must perform" " a fit before using standard_errors()." ) raise AttributeError(errmsg) ny = self.n_data x = self._switch_to_np_array(x) # Regression matrix on training data Ad = self.assemble_regression_matrix(self.fit_breaks, self.x_data) # try to solve for the unbiased variance estimation try: y_hat = np.dot(Ad, self.beta) e = y_hat - self.y_data # solve for the unbiased estimate of variance variance = np.dot(e, e) / (ny - nb) except linalg.LinAlgError: raise linalg.LinAlgError("Singular matrix") # Regression matrix on prediction data A = self.assemble_regression_matrix(self.fit_breaks, x) # try to solve for the prediction variance at the x locations try: pre_var = variance * np.dot( np.dot(A, linalg.pinv(np.dot(Ad.T, Ad))), A.T, ) return pre_var.diagonal() except linalg.LinAlgError: raise linalg.LinAlgError("Singular matrix") def r_squared(self): r""" Calculate the coefficient of determination ("R squared", R^2) value after a fit has been performed. For more information see: https://en.wikipedia.org/wiki/Coefficient_of_determination Returns ------- rsq : float Coefficient of determination, or 'R squared' value. Raises ------ AttributeError You have probably not performed a fit yet. LinAlgError This typically means your regression problem is ill-conditioned. Examples -------- Calculate the R squared value after performing a simple fit. >>> import pwlf >>> x = np.linspace(0.0, 1.0, 10) >>> y = np.random.random(10) >>> my_pwlf = pwlf.PiecewiseLinFit(x, y) >>> breaks = my_pwlf.fitfast(3) >>> rsq = my_pwlf.r_squared() """ if self.fit_breaks is None: errmsg = ( "You do not have any beta parameters. You must perform" " a fit before using standard_errors()." ) raise AttributeError(errmsg) ssr = self.fit_with_breaks(self.fit_breaks) ybar = np.ones(self.n_data) * np.mean(self.y_data) ydiff = self.y_data - ybar try: sst = np.dot(ydiff, ydiff) rsq = 1.0 - (ssr / sst) return rsq except linalg.LinAlgError: raise linalg.LinAlgError("Singular matrix") def p_values(self, method="linear", step_size=1e-4): r""" Calculate the p-values for each beta parameter. This calculates the p-values for the beta parameters under the assumption that your breakpoint locations are known. Section 2.4.2 of [2]_ defines how to calculate the p-value of individual parameters. This is really a marginal test since each parameter is dependent upon the other parameters. These values are typically compared to some confidence level alpha for significance. A 95% confidence level would have alpha = 0.05. Parameters ---------- method : string, optional Calculate the standard errors for a linear or non-linear regression problem. The default is method='linear'. A taylor- series expansion is performed when method='non-linear' (which is commonly referred to as the Delta method). step_size : float, optional The step size to perform forward differences for the taylor- series expansion when method='non-linear'. Default is step_size=1e-4. Returns ------- p : ndarray (1-D) p-values for each beta parameter where p-value[0] corresponds to beta[0] and so forth Raises ------ AttributeError You have probably not performed a fit yet. ValueError You supplied an unsupported method. Notes ----- The linear regression problem is when you know the breakpoint locations (e.g. when using the fit_with_breaks function). The non-linear regression problem is when you don't know the breakpoint locations (e.g. when using the fit, fitfast, and fit_guess functions). See https://github.com/cjekel/piecewise_linear_fit_py/issues/14 References ---------- .. [2] Myers RH, Montgomery DC, Anderson-Cook CM. Response surface methodology . Hoboken. New Jersey: John Wiley & Sons, Inc. 2009;20:38-44. Examples -------- After performing a fit, one can calculate the p-value for each beta parameter >>> import pwlf >>> x = np.linspace(0.0, 1.0, 10) >>> y = np.random.random(10) >>> my_pwlf = pwlf.PiecewiseLinFit(x, y) >>> breaks = my_pwlf.fitfast(3) >>> x_new = np.linspace(0.0, 1.0, 100) >>> p = my_pwlf.p_values(x_new) see also examples/standard_errrors_and_p-values.py """ n = self.n_data # degrees of freedom for t-distribution if self.beta is None: errmsg = ( "You do not have any beta parameters. You must perform" " a fit before using standard_errors()." ) raise AttributeError(errmsg) k = self.beta.size - 1 if method == "linear": self.standard_errors() # calculate my t-value t = self.beta / self.se elif method == "non-linear": nb = self.beta.size + self.fit_breaks.size - 2 k = nb - 1 self.standard_errors(method=method, step_size=step_size) # the parameters for a non-linear model include interior breaks parameters = np.concatenate((self.beta, self.fit_breaks[1:-1])) # calculate my t-value t = parameters / self.se else: errmsg = f"Error: method='{method}' is not supported!" raise ValueError(errmsg) # calculate the p-values p = 2.0 * stats.t.sf(np.abs(t), df=n - k - 1) return p def lstsq(self, A, calc_slopes=True): r""" Perform the least squares fit for A matrix. Parameters ---------- A : ndarray (2-D) The regression matrix you want to fit in the linear system of equations Ab=y. calc_slopes : boolean, optional Whether to calculate slopes after performing a fit. Default is calc_slopes=True. """ if self.weights is None: beta, ssr, _, _ = linalg.lstsq( A, self.y_data, lapack_driver=self.lapack_driver ) # ssr is only calculated if self.n_data > self.n_parameters # in this case I'll need to calculate ssr manually # where ssr = sum of square of residuals if self.n_data <= self.n_parameters: y_hat = np.dot(A, beta) e = y_hat - self.y_data ssr = np.dot(e, e) else: beta, _, _, _ = linalg.lstsq( (A.T * self.weights).T, self.y_w, lapack_driver=self.lapack_driver, ) # calculate the weighted sum of square of residuals y_hat = np.dot(A, beta) e = y_hat - self.y_data r = e * self.weights ssr = np.dot(r, r) if isinstance(ssr, list): ssr = ssr[0] elif isinstance(ssr, np.ndarray): if ssr.size == 0: y_hat = np.dot(A, beta) e = y_hat - self.y_data ssr = np.dot(e, e) else: ssr = ssr[0] # save the beta parameters self.beta = beta if calc_slopes: # save the slopes self.calc_slopes() return ssr def conlstsq(self, A, calc_slopes=True): r""" Perform a constrained least squares fit for A matrix. Parameters ---------- A : ndarray (2-D) The regression matrix you want to fit in the linear system of equations Ab=y. calc_slopes : boolean, optional Whether to calculate slopes after performing a fit. Default is calc_slopes=True. """ # Assemble the constraint matrix C_list = [np.ones_like(self.x_c)] if self.degree >= 1: C_list.append(self.x_c - self.fit_breaks[0]) for i in range(self.n_segments - 1): C_list.append( np.where( self.x_c > self.fit_breaks[i + 1], self.x_c - self.fit_breaks[i + 1], 0.0, ) ) if self.degree >= 2: for k in range(2, self.degree + 1): C_list.append((self.x_c - self.fit_breaks[0]) ** k) for i in range(self.n_segments - 1): C_list.append( np.where( self.x_c > self.fit_breaks[i + 1], (self.x_c - self.fit_breaks[i + 1]) ** k, 0.0, ) ) else: for i in range(self.n_segments - 1): C_list.append( np.where( self.x_c > self.fit_breaks[i + 1], 1.0, 0.0, ) ) C = np.vstack(C_list).T _, m = A.shape o, _ = C.shape K = np.zeros((m + o, m + o)) K[:m, :m] = 2.0 * np.dot(A.T, A) K[:m, m:] = C.T K[m:, :m] = C # Assemble right hand side vector yt = np.dot(2.0 * A.T, self.y_data) z = np.zeros(self.n_parameters + self.c_n) z[: self.n_parameters] = yt z[self.n_parameters:] = self.y_c # try to solve the regression problem try: # Solve the least squares problem beta_prime = linalg.solve(K, z) # save the beta parameters self.beta = beta_prime[0: self.n_parameters] # save the zeta parameters self.zeta = beta_prime[self.n_parameters:] # save the slopes if calc_slopes: self.calc_slopes() # Calculate ssr # where ssr = sum of square of residuals y_hat = np.dot(A, self.beta) e = y_hat - self.y_data ssr = np.dot(e, e) self.ssr = ssr # Calculate the Lagrangian function # c_x_y = np.dot(C, self.x_c.T) - self.y_c p = np.dot(C.T, self.zeta) L = np.sum(np.abs(p)) + ssr except linalg.LinAlgError: # the computation could not converge! # on an error, return L = np.inf # You might have a singular Matrix!!! L = np.inf if L is None: L = np.inf # something went wrong... return L ================================================ FILE: pwlf/version.py ================================================ __version__ = "2.5.2" ================================================ FILE: setup.py ================================================ import io from distutils.core import setup # load the version from version.py version = {} with open("pwlf/version.py") as fp: exec(fp.read(), version) setup( name="pwlf", version=version["__version__"], author="Charles Jekel", author_email="cjekel@gmail.com", packages=["pwlf"], url="https://github.com/cjekel/piecewise_linear_fit_py", license="MIT License", description="fit piecewise linear functions to data", long_description=io.open("README.rst", encoding="utf-8").read(), # long_description_content_type='text/markdown', platforms=["any"], install_requires=[ "numpy >= 1.14.0", "scipy >= 1.8.0", ], classifiers=[ "Programming Language :: Python :: 3", ], python_requires=">3.5", ) ================================================ FILE: sphinx_build_script.sh ================================================ #!/usr/bin/env bash pip install . # clean docs and doctrees rm -r docs/* rm -r doctrees/* # make the documentation in gitignore folder cp examples/README.rst sphinxdocs/source/examples.rst cd sphinxdocs make clean make html # copy sphinx build into docs and doctrees cp -r build/doctrees/* ../doctrees/ cp -r build/html/* ../docs/ # grep -rl -e "word-break: break-word" --exclude=sphinx_build_script.sh | xargs sed -i 's/word-break: break-word/word-break: break-all/g' #cd .. #sed -i 's/break-word/break-all/g' docs/_static/basic.css ================================================ FILE: sphinxdocs/Makefile ================================================ # Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ================================================ FILE: sphinxdocs/make.bat ================================================ @ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source set BUILDDIR=build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd ================================================ FILE: sphinxdocs/source/about.rst ================================================ About ============ A library for fitting continuous piecewise linear functions to data. Just specify the number of line segments you desire and provide the data. .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/examplePiecewiseFit.png :alt: Example of a continuous piecewise linear fit to data. Example of a continuous piecewise linear fit to data. All changes now stored in `CHANGELOG.md `__ Please cite pwlf in your publications if it helps your research. .. code:: bibtex @Manual{pwlf, author = {Jekel, Charles F. and Venter, Gerhard}, title = {{pwlf:} A Python Library for Fitting 1D Continuous Piecewise Linear Functions}, year = {2019}, url = {https://github.com/cjekel/piecewise_linear_fit_py} } ================================================ FILE: sphinxdocs/source/conf.py ================================================ # -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys import pwlf sys.path.insert(0, os.path.abspath('../../pwlf')) # -- Project information ----------------------------------------------------- project = 'pwlf' copyright = '2024, Charles Jekel' author = 'Charles Jekel' # The short X.Y version version = '' # The full version, including alpha/beta/rc tags release = pwlf.__version__ # -- General configuration --------------------------------------------------- # autoclass_content = 'both' # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.githubpages', 'numpydoc', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # #html_theme = 'sphinx' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {'analytics_id': 'G-QKPGZSZ8CD'} html_favicon = 'favicon.ico' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'pwlfdoc' # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'pwlf.tex', 'pwlf Documentation', 'Charles Jekel', 'manual'), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'pwlf', 'pwlf Documentation', [author], 1) ] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'pwlf', 'pwlf Documentation', author, 'pwlf', 'One line description of project.', 'Miscellaneous'), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # -- Extension configuration ------------------------------------------------- # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} ================================================ FILE: sphinxdocs/source/examples.rst ================================================ Examples ======== All of these examples will use the following data and imports. .. code:: python import numpy as np import matplotlib.pyplot as plt import pwlf # your data y = np.array([0.00000000e+00, 9.69801700e-03, 2.94350340e-02, 4.39052750e-02, 5.45343950e-02, 6.74104940e-02, 8.34831790e-02, 1.02580042e-01, 1.22767939e-01, 1.42172312e-01, 0.00000000e+00, 8.58600000e-06, 8.31543400e-03, 2.34184100e-02, 3.39709150e-02, 4.03581990e-02, 4.53545600e-02, 5.02345260e-02, 5.55253360e-02, 6.14750770e-02, 6.82125120e-02, 7.55892510e-02, 8.38356810e-02, 9.26413070e-02, 1.02039790e-01, 1.11688258e-01, 1.21390666e-01, 1.31196948e-01, 0.00000000e+00, 1.56706510e-02, 3.54628780e-02, 4.63739040e-02, 5.61442590e-02, 6.78542550e-02, 8.16388310e-02, 9.77756110e-02, 1.16531753e-01, 1.37038283e-01, 0.00000000e+00, 1.16951050e-02, 3.12089850e-02, 4.41776550e-02, 5.42877590e-02, 6.63321350e-02, 8.07655920e-02, 9.70363280e-02, 1.15706975e-01, 1.36687642e-01, 0.00000000e+00, 1.50144640e-02, 3.44519970e-02, 4.55907760e-02, 5.59556700e-02, 6.88450940e-02, 8.41374060e-02, 1.01254006e-01, 1.20605073e-01, 1.41881288e-01, 1.62618058e-01]) x = np.array([0.00000000e+00, 8.82678000e-03, 3.25615100e-02, 5.66106800e-02, 7.95549800e-02, 1.00936330e-01, 1.20351520e-01, 1.37442010e-01, 1.51858250e-01, 1.64433570e-01, 0.00000000e+00, -2.12600000e-05, 7.03872000e-03, 1.85494500e-02, 3.00926700e-02, 4.17617000e-02, 5.37279600e-02, 6.54941000e-02, 7.68092100e-02, 8.76596300e-02, 9.80525800e-02, 1.07961810e-01, 1.17305210e-01, 1.26063930e-01, 1.34180360e-01, 1.41725010e-01, 1.48629710e-01, 1.55374770e-01, 0.00000000e+00, 1.65610200e-02, 3.91016100e-02, 6.18679400e-02, 8.30997400e-02, 1.02132890e-01, 1.19011260e-01, 1.34620080e-01, 1.49429370e-01, 1.63539960e-01, -0.00000000e+00, 1.01980300e-02, 3.28642800e-02, 5.59461900e-02, 7.81388400e-02, 9.84458400e-02, 1.16270210e-01, 1.31279040e-01, 1.45437090e-01, 1.59627540e-01, 0.00000000e+00, 1.63404300e-02, 4.00086000e-02, 6.34390200e-02, 8.51085900e-02, 1.04787860e-01, 1.22120350e-01, 1.36931660e-01, 1.50958760e-01, 1.65299640e-01, 1.79942720e-01]) 1. `fit with known breakpoint locations <#fit-with-known-breakpoint-locations>`__ 2. `fit for specified number of line segments <#fit-for-specified-number-of-line-segments>`__ 3. `fitfast for specified number of line segments <#fitfast-for-specified-number-of-line-segments>`__ 4. `force a fit through data points <#force-a-fit-through-data-points>`__ 5. `use custom optimization routine <#use-custom-optimization-routine>`__ 6. `pass differential evolution keywords <#pass-differential-evolution-keywords>`__ 7. `find the best number of line segments <#find-the-best-number-of-line-segments>`__ 8. `model persistence <#model-persistence>`__ 9. `bad fits when you have more unknowns than data <#bad-fits-when-you-have-more-unknowns-than-data>`__ 10. `fit with a breakpoint guess <#fit-with-a-breakpoint-guess>`__ 11. `get the linear regression matrix <#get-the-linear-regression-matrix>`__ 12. `use of TensorFlow <#use-of-tensorflow>`__ 13. `fit constants or polynomials <#fit-constants-or-polynomials>`__ 14. `specify breakpoint bounds <#specify-breakpoint-bounds>`__ 15. `non-linear standard errors and p-values <#non-linear-standard-errors-and-p-values>`__ 16. `obtain the equations of fitted pwlf <#obtain-the-equations-of-fitted-pwlf>`__ 17. `weighted least squares fit <#weighted-least-squares-fit>`__ 18. `reproducible results <#reproducible results>`__ fit with known breakpoint locations ----------------------------------- You can perform a least squares fit if you know the breakpoint locations. .. code:: python # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data with the specified break points # (ie the x locations of where the line segments # will terminate) my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/fit_breaks.png :alt: fit with known breakpoint locations fit with known breakpoint locations fit for specified number of line segments ----------------------------------------- Use a global optimization to find the breakpoint locations that minimize the sum of squares error. This uses `Differential Evolution `__ from scipy. .. code:: python # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments res = my_pwlf.fit(4) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/numberoflines.png :alt: fit for specified number of line segments fit for specified number of line segments fitfast for specified number of line segments --------------------------------------------- This performs a fit for a specified number of line segments with a multi-start gradient based optimization. This should be faster than `Differential Evolution `__ for a small number of starting points. .. code:: python # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments # this performs 3 multi-start optimizations res = my_pwlf.fitfast(4, pop=3) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/fitfast.png :alt: fitfast for specified number of line segments fitfast for specified number of line segments force a fit through data points ------------------------------- Sometimes it’s necessary to force the piecewise continuous model through a particular data point, or a set of data points. The following example finds the best 4 line segments that go through two data points. .. code:: python # initialize piecewise linear fit with your x and y data myPWLF = pwlf.PiecewiseLinFit(x, y) # fit the function with four line segments # force the function to go through the data points # (0.0, 0.0) and (0.19, 0.16) # where the data points are of the form (x, y) x_c = [0.0, 0.19] y_c = [0.0, 0.2] res = myPWLF.fit(4, x_c, y_c) # predict for the determined points xHat = np.linspace(min(x), 0.19, num=10000) yHat = myPWLF.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/force.png :alt: force a fit through data points force a fit through data points use custom optimization routine ------------------------------- You can use your favorite optimization routine to find the breakpoint locations. The following example uses scipy’s `minimize `__ function. .. code:: python from scipy.optimize import minimize # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # initialize custom optimization number_of_line_segments = 3 my_pwlf.use_custom_opt(number_of_line_segments) # i have number_of_line_segments - 1 number of variables # let's guess the correct location of the two unknown variables # (the program defaults to have end segments at x0= min(x) # and xn=max(x) xGuess = np.zeros(number_of_line_segments - 1) xGuess[0] = 0.02 xGuess[1] = 0.10 res = minimize(my_pwlf.fit_with_breaks_opt, xGuess) # set up the break point locations x0 = np.zeros(number_of_line_segments + 1) x0[0] = np.min(x) x0[-1] = np.max(x) x0[1:-1] = res.x # calculate the parameters based on the optimal break point locations my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() pass differential evolution keywords ------------------------------------ You can pass keyword arguments from the ``fit`` function into scipy’s `Differential Evolution `__. .. code:: python # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data for four line segments # this sets DE to have an absolute tolerance of 0.1 res = my_pwlf.fit(4, atol=0.1) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() find the best number of line segments ------------------------------------- This example uses EGO (bayesian optimization) and a penalty function to find the best number of line segments. This will require careful use of the penalty parameter ``l``. Use this template to automatically find the best number of line segments. .. code:: python from GPyOpt.methods import BayesianOptimization # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # define your objective function def my_obj(x): # define some penalty parameter l # you'll have to arbitrarily pick this # it depends upon the noise in your data, # and the value of your sum of square of residuals l = y.mean()*0.001 f = np.zeros(x.shape[0]) for i, j in enumerate(x): my_pwlf.fit(j[0]) f[i] = my_pwlf.ssr + (l*j[0]) return f # define the lower and upper bound for the number of line segments bounds = [{'name': 'var_1', 'type': 'discrete', 'domain': np.arange(2, 40)}] np.random.seed(12121) myBopt = BayesianOptimization(my_obj, domain=bounds, model_type='GP', initial_design_numdata=10, initial_design_type='latin', exact_feval=True, verbosity=True, verbosity_model=False) max_iter = 30 # perform the bayesian optimization to find the optimum number # of line segments myBopt.run_optimization(max_iter=max_iter, verbosity=True) print('\n \n Opt found \n') print('Optimum number of line segments:', myBopt.x_opt) print('Function value:', myBopt.fx_opt) myBopt.plot_acquisition() myBopt.plot_convergence() # perform the fit for the optimum my_pwlf.fit(myBopt.x_opt) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() model persistence ----------------- You can save fitted models with pickle. Alternatively see `joblib `__. .. code:: python # if you use Python 2.x you should import cPickle # import cPickle as pickle # if you use Python 3.x you can just use pickle import pickle # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # fit the data with the specified break points my_pwlf.fit_with_breaks(x0) # save the fitted model with open('my_fit.pkl', 'wb') as f: pickle.dump(my_pwlf, f, pickle.HIGHEST_PROTOCOL) # load the fitted model with open('my_fit.pkl', 'rb') as f: my_pwlf = pickle.load(f) bad fits when you have more unknowns than data ---------------------------------------------- You can get very bad fits with pwlf when you have more unknowns than data points. The following example will fit 99 line segments to the 59 data points. While this will result in an error of zero, the model will have very weird predictions within the data. You should not fit more unknowns than you have data with pwlf! .. code:: python break_locations = np.linspace(min(x), max(x), num=100) # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) my_pwlf.fit_with_breaks(break_locations) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o') plt.plot(xHat, yHat, '-') plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/badfit.png :alt: bad fits when you have more unknowns than data bad fits when you have more unknowns than data fit with a breakpoint guess --------------------------- In this example we see two distinct linear regions, and we believe a breakpoint occurs at 6.0. We’ll use the fit_guess() function to find the best breakpoint location starting with this guess. These fits should be much faster than the ``fit`` or ``fitfast`` function when you have a reasonable idea where the breakpoints occur. .. code:: python import numpy as np import pwlf x = np.array([4., 5., 6., 7., 8.]) y = np.array([11., 13., 16., 28.92, 42.81]) my_pwlf = pwlf.PiecewiseLinFit(x, y) breaks = my_pwlf.fit_guess([6.0]) Note specifying one breakpoint will result in two line segments. If we wanted three line segments, we’ll have to specify two breakpoints. .. code:: python breaks = my_pwlf.fit_guess([5.5, 6.0]) get the linear regression matrix -------------------------------- In some cases it may be desirable to work with the linear regression matrix directly. The following example grabs the linear regression matrix ``A`` for a specific set of breakpoints. In this case we assume that the breakpoints occur at each of the data points. Please see the `paper `__ for details about the regression matrix ``A``. .. code:: python import numpy as np import pwlf # select random seed for reproducibility np.random.seed(123) # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) ytrue = y.copy() # add noise to the data y = np.random.normal(0, 0.05, 100) + ytrue my_pwlf_en = pwlf.PiecewiseLinFit(x, y) # copy the x data to use as break points breaks = my_pwlf_en.x_data.copy() # create the linear regression matrix A A = my_pwlf_en.assemble_regression_matrix(breaks, my_pwlf_en.x_data) We can perform fits that are more complicated than a least squares fit when we have the regression matrix. The following uses the Elastic Net regularizer to perform an interesting fit with the regression matrix. .. code:: python from sklearn.linear_model import ElasticNetCV # set up the elastic net en_model = ElasticNetCV(cv=5, l1_ratio=[.1, .5, .7, .9, .95, .99, 1], fit_intercept=False, max_iter=1000000, n_jobs=-1) # fit the model using the elastic net en_model.fit(A, my_pwlf_en.y_data) # predict from the elastic net parameters xhat = np.linspace(x.min(), x.max(), 1000) yhat_en = my_pwlf_en.predict(xhat, breaks=breaks, beta=en_model.coef_) .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/sin_en_net_fit.png :alt: interesting elastic net fit interesting elastic net fit use of tensorflow ----------------- You need to install `pwlftf `__ which will have the ``PiecewiseLinFitTF`` class. For performance benchmarks (these benchmarks are outdated! and the regular pwlf may be faster in many applications) see this blog `post `__. The use of the TF class is nearly identical to the original class, however note the following exceptions. ``PiecewiseLinFitTF`` does: - not have a ``lapack_driver`` option - have an optional parameter ``dtype``, so you can choose between the float64 and float32 data types - have an optional parameter ``fast`` to switch between Cholesky decomposition (default ``fast=True``), and orthogonal decomposition (``fast=False``) .. code:: python import pwlftf as pwlf # your desired line segment end locations x0 = np.array([min(x), 0.039, 0.10, max(x)]) # initialize TF piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFitTF(x, y, dtype='float32) # fit the data with the specified break points # (ie the x locations of where the line segments # will terminate) my_pwlf.fit_with_breaks(x0) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat = my_pwlf.predict(xHat) fit constants or polynomials ---------------------------- You can use pwlf to fit segmented constant models, or piecewise polynomials. The following example fits a segmented constant model, piecewise linear, and a piecewise quadratic model to a sine wave. .. code:: python # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, 100) + y # initialize piecewise linear fit with your x and y data # pwlf lets you fit continuous model for many degree polynomials # degree=0 constant # degree=1 linear (default) # degree=2 quadratic my_pwlf_0 = pwlf.PiecewiseLinFit(x, y, degree=0) my_pwlf_1 = pwlf.PiecewiseLinFit(x, y, degree=1) # default my_pwlf_2 = pwlf.PiecewiseLinFit(x, y, degree=2) # fit the data for four line segments res0 = my_pwlf_0.fitfast(5, pop=50) res1 = my_pwlf_1.fitfast(5, pop=50) res2 = my_pwlf_2.fitfast(5, pop=50) # predict for the determined points xHat = np.linspace(min(x), max(x), num=10000) yHat0 = my_pwlf_0.predict(xHat) yHat1 = my_pwlf_1.predict(xHat) yHat2 = my_pwlf_2.predict(xHat) # plot the results plt.figure() plt.plot(x, y, 'o', label='Data') plt.plot(xHat, yHat0, '-', label='degree=0') plt.plot(xHat, yHat1, '--', label='degree=1') plt.plot(xHat, yHat2, ':', label='degree=2') plt.legend() plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/multi_degree.png :alt: Example of multiple degree fits to a sine wave. Example of multiple degree fits to a sine wave. specify breakpoint bounds ------------------------- You may want extra control over the search space for feasible breakpoints. One way to do this is to specify the bounds for each breakpoint location. .. code:: python # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, 100) + y # initialize piecewise linear fit with your x and y data my_pwlf = pwlf.PiecewiseLinFit(x, y) # define custom bounds for the interior break points n_segments = 4 bounds = np.zeros((n_segments-1, 2)) # first breakpoint bounds[0, 0] = 0.0 # lower bound bounds[0, 1] = 3.5 # upper bound # second breakpoint bounds[1, 0] = 3.0 # lower bound bounds[1, 1] = 7.0 # upper bound # third breakpoint bounds[2, 0] = 6.0 # lower bound bounds[2, 1] = 10.0 # upper bound res = my_pwlf.fit(n_segments, bounds=bounds) non-linear standard errors and p-values --------------------------------------- You can calculate non-linear standard errors using the Delta method. This will calculate the standard errors of the piecewise linear parameters (intercept + slopes) and the breakpoint locations! First let us generate true piecewise linear data. .. code:: python from __future__ import print_function # generate a true piecewise linear data np.random.seed(5) n_data = 100 x = np.linspace(0, 1, num=n_data) y = np.random.random(n_data) my_pwlf_gen = pwlf.PiecewiseLinFit(x, y) true_beta = np.random.normal(size=5) true_breaks = np.array([0.0, 0.2, 0.5, 0.75, 1.0]) y = my_pwlf_gen.predict(x, beta=true_beta, breaks=true_breaks) plt.figure() plt.title('True piecewise linear data') plt.plot(x, y) plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/figs/true_pwlf.png :alt: True piecewise linear data. True piecewise linear data. Now we can perform a fit, calculate the standard errors, and p-values. The non-linear method uses a first order taylor series expansion to linearize the non-linear regression problem. A positive step_size performs a forward difference, and a negative step_size would perform a backwards difference. .. code:: python my_pwlf = pwlf.PiecewiseLinFit(x, y) res = my_pwlf.fitfast(4, pop=100) p = my_pwlf.p_values(method='non-linear', step_size=1e-4) se = my_pwlf.se # standard errors The standard errors and p-values correspond to each model parameter. First the beta parameters (intercept + slopes) and then the breakpoints. We can assemble the parameters, and print a table of the result with the following code. .. code:: python parameters = np.concatenate((my_pwlf.beta, my_pwlf.fit_breaks[1:-1])) header = ['Parameter type', 'Parameter value', 'Standard error', 't', 'P > np.abs(t) (p-value)'] print(*header, sep=' | ') values = np.zeros((parameters.size, 5), dtype=np.object_) values[:, 1] = np.around(parameters, decimals=3) values[:, 2] = np.around(se, decimals=3) values[:, 3] = np.around(parameters / se, decimals=3) values[:, 4] = np.around(p, decimals=3) for i, row in enumerate(values): if i < my_pwlf.beta.size: row[0] = 'Beta' print(*row, sep=' | ') else: row[0] = 'Breakpoint' print(*row, sep=' | ') ============== =============== ============== ============== ======================= Parameter type Parameter value Standard error t P > np.abs(t) (p-value) ============== =============== ============== ============== ======================= Beta 1.821 0.0 1763191476.046 0.0 Beta -0.427 0.0 -46404554.493 0.0 Beta -1.165 0.0 -111181494.162 0.0 Beta -1.397 0.0 -168954500.421 0.0 Beta 0.873 0.0 93753841.242 0.0 Breakpoint 0.2 0.0 166901856.885 0.0 Breakpoint 0.5 0.0 537785803.646 0.0 Breakpoint 0.75 0.0 482311769.159 0.0 ============== =============== ============== ============== ======================= obtain the equations of fitted pwlf ----------------------------------- Sometimes you may want the mathematical equations that represent your fitted model. This is easy to perform if you don’t mind using sympy. The following code will fit 5 line segments of degree=2 to a sin wave. .. code:: python import numpy as np import pwlf # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.05, 100) + y my_pwlf_2 = pwlf.PiecewiseLinFit(x, y, degree=2) res2 = my_pwlf_2.fitfast(5, pop=50) Given this fit, the following code will print the mathematical equation for each line segment. .. code:: python from sympy import Symbol from sympy.utilities import lambdify x = Symbol('x') def get_symbolic_eqn(pwlf_, segment_number): if pwlf_.degree < 1: raise ValueError('Degree must be at least 1') if segment_number < 1 or segment_number > pwlf_.n_segments: raise ValueError('segment_number not possible') # assemble degree = 1 first for line in range(segment_number): if line == 0: my_eqn = pwlf_.beta[0] + (pwlf_.beta[1])*(x-pwlf_.fit_breaks[0]) else: my_eqn += (pwlf_.beta[line+1])*(x-pwlf_.fit_breaks[line]) # assemble all other degrees if pwlf_.degree > 1: for k in range(2, pwlf_.degree + 1): for line in range(segment_number): beta_index = pwlf_.n_segments*(k-1) + line + 1 my_eqn += (pwlf_.beta[beta_index])*(x-pwlf_.fit_breaks[line])**k return my_eqn.simplify() eqn_list = [] f_list = [] for i in range(my_pwlf_2.n_segments): eqn_list.append(get_symbolic_eqn(my_pwlf_2, i + 1)) print('Equation number: ', i + 1) print(eqn_list[-1]) f_list.append(lambdify(x, eqn_list[-1])) which should print out something like the following: .. code:: python Equation number: 1 -0.953964059782599*x**2 + 1.89945177490653*x + 0.00538634182565454 Equation number: 2 0.951561315686298*x**2 - 5.69747505830914*x + 7.5772216545711 Equation number: 3 -0.949735350431857*x**2 + 9.48218236957122*x - 22.720785454735 Equation number: 4 0.926850298824217*x**2 - 12.9824424358344*x + 44.5102742956827 Equation number: 5 -1.03016230425747*x**2 + 18.5306546317065*x - 82.3508513333073 For more information on how this works, see `this `__ jupyter notebook. weighted least squares fit -------------------------- Sometimes your data will not have a constant variance (heteroscedasticity), and you need to perform a weighted least squares fit. The following example will perform a standard and weighted fit so you can compare the differences. First we need to generate a data set which will be a good candidate to use for weighted least squares fits. .. code:: python # generate data with heteroscedasticity n = 100 n_data_sets = 100 n_segments = 6 # generate sine data x = np.linspace(0, 10, n) y = np.zeros((n_data_sets, n)) sigma_change = np.linspace(0.001, 0.05, 100) for i in range(n_data_sets): y[i] = np.sin(x * np.pi / 2) # add noise to the data y[i] = np.random.normal(0, sigma_change, 100) + y[i] X = np.tile(x, n_data_sets) The individual weights in pwlf are the reciprocal of the standard deviation for each data point. Here weights[i] corresponds to one over the standard deviation of the ith data point. The result of this is that data points with higher variance are less important to the overall pwlf than data point with small variance. Let’s perform a standard pwlf fit and a weighted fit. .. code:: python # perform an ordinary pwlf fit to the entire data my_pwlf = pwlf.PiecewiseLinFit(X.flatten(), y.flatten()) my_pwlf.fit(n_segments) # compute the standard deviation in y y_std = np.std(y, axis=0) # set the weights to be one over the standard deviation weights = 1.0 / y_std # perform a weighted least squares to the data my_pwlf_w = pwlf.PiecewiseLinFit(x, y.mean(axis=0), weights=weights) my_pwlf_w.fit(n_segments) # compare the fits xhat = np.linspace(0, 10, 1000) yhat = my_pwlf.predict(xhat) yhat_w = my_pwlf_w.predict(xhat) plt.figure() plt.plot(X.flatten(), y.flatten(), '.') plt.plot(xhat, yhat, '-', label='Ordinary LS') plt.plot(xhat, yhat_w, '-', label='Weighted LS') plt.legend() plt.show() .. figure:: https://raw.githubusercontent.com/cjekel/piecewise_linear_fit_py/master/examples/weighted_least_squares_example.png :alt: Weighted pwlf fit. Weighted pwlf fit. We can see that the weighted pwlf fit tries fit data with low variance better than data with high variance, however the ordinary pwlf fits the data assuming a uniform variance. reproducible results -------------------- The `fit` and `fitfast` methods are stochastic and may not give the same result every time the program is run. To have reproducible results you can manually specify a numpy.random.seed on init. Now everytime the following program is run, the results of the fit(2) should be the same. .. code:: python # initialize piecewise linear fit with a random seed my_pwlf = pwlf.PiecewiseLinFit(x, y, seed=123) # Now the fit() method will be reproducible my_pwlf.fit(2) ================================================ FILE: sphinxdocs/source/how_it_works.rst ================================================ How it works ============ This `paper `__ explains how this library works in detail. This is based on a formulation of a piecewise linear least squares fit, where the user must specify the location of break points. See `this post `__ which goes through the derivation of a least squares regression problem if the break point locations are known. Alternatively check out `Golovchenko (2004) `__. Global optimization is used to find the best location for the user defined number of line segments. I specifically use the `differential evolution `__ algorithm in SciPy. I default the differential evolution algorithm to be aggressive, and it is probably overkill for your problem. So feel free to pass your own differential evolution keywords to the library. See `this example `__. ================================================ FILE: sphinxdocs/source/index.rst ================================================ pwlf: piecewise linear fitting ================================ Fit piecewise linear functions to data! .. toctree:: :maxdepth: 2 installation how_it_works examples pwlf about requirements license Indices and tables ================== * :ref:`genindex` * :ref:`search` ================================================ FILE: sphinxdocs/source/installation.rst ================================================ Installation ============ Python Package Index (PyPI) --------------------------- You can now install with pip. :: python -m pip install pwlf Conda ----- If you have conda, you can also install from conda-forge. :: conda install -c conda-forge pwlf From source ----------- Or clone the repo :: git clone https://github.com/cjekel/piecewise_linear_fit_py.git then install with pip :: python -m pip install ./piecewise_linear_fit_py ================================================ FILE: sphinxdocs/source/license.rst ================================================ License ======= MIT License Copyright (c) 2017-2020 Charles Jekel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: sphinxdocs/source/modules.rst ================================================ pwlf ==== .. toctree:: :maxdepth: 4 pwlf ================================================ FILE: sphinxdocs/source/pwlf.rst ================================================ pwlf package contents ============ .. autosummary:: :toctree: stubs pwlf.PiecewiseLinFit .. autoclass:: pwlf.PiecewiseLinFit :members: :undoc-members: :show-inheritance: ================================================ FILE: sphinxdocs/source/requirements.rst ================================================ Requirements ============ `NumPy `__ (>= 1.14.0) `SciPy `__ (>= 1.2.0) `pyDOE `__ ( >= 0.3.8) ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/tests.py ================================================ import numpy as np import unittest import pwlf class TestEverything(unittest.TestCase): x_small = np.array((0.0, 1.0, 1.5, 2.0)) y_small = np.array((0.0, 1.0, 1.1, 1.5)) x_sin = np.linspace(0, 10, num=100) y_sin = np.sin(x_sin * np.pi / 2) def test_break_point_spot_on(self): # check that I can fit when break points spot on a my_fit1 = pwlf.PiecewiseLinFit(self.x_small, self.y_small) x0 = self.x_small.copy() ssr = my_fit1.fit_with_breaks(x0) self.assertTrue(np.isclose(ssr, 0.0)) def test_break_point_spot_on_list(self): # check that I can fit when break points spot on a my_fit1 = pwlf.PiecewiseLinFit(list(self.x_small), list(self.y_small)) x0 = self.x_small.copy() ssr = my_fit1.fit_with_breaks(x0) self.assertTrue(np.isclose(ssr, 0.0)) def test_degree_not_supported(self): try: _ = pwlf.PiecewiseLinFit(self.x_small, self.y_small, degree=100) self.assertTrue(False) except ValueError: self.assertTrue(True) def test_x_c_not_supplied(self): try: mf = pwlf.PiecewiseLinFit(self.x_small, self.y_small, degree=1) mf.fit(2, x_c=[1.0]) self.assertTrue(False) except ValueError: self.assertTrue(True) def test_y_c_not_supplied(self): try: mf = pwlf.PiecewiseLinFit(self.x_small, self.y_small, degree=1) mf.fit(2, y_c=[1.0]) self.assertTrue(False) except ValueError: self.assertTrue(True) def test_lapack_driver(self): # check that I can fit when break points spot on a my_fit1 = pwlf.PiecewiseLinFit(self.x_small, self.y_small, lapack_driver='gelsy') x0 = self.x_small.copy() ssr = my_fit1.fit_with_breaks(x0) self.assertTrue(np.isclose(ssr, 0.0)) def test_assembly(self): # check that I can fit when break points spot on a my_fit = pwlf.PiecewiseLinFit(self.x_small, self.y_small) x0 = self.x_small.copy() A = my_fit.assemble_regression_matrix(x0, my_fit.x_data) Asb = np.array([[1., 0., 0., 0.], [1., x0[1]-x0[0], 0., 0.], [1., x0[2]-x0[0], x0[2]-x0[1], 0.], [1., x0[3]-x0[0], x0[3]-x0[1], x0[3]-x0[2]]]) self.assertTrue(np.allclose(A, Asb)) def test_assembly_list(self): # check that I can fit when break points spot on a my_fit = pwlf.PiecewiseLinFit(self.x_small, self.y_small) x0 = self.x_small.copy() A = my_fit.assemble_regression_matrix(x0, list(my_fit.x_data)) Asb = np.array([[1., 0., 0., 0.], [1., x0[1]-x0[0], 0., 0.], [1., x0[2]-x0[0], x0[2]-x0[1], 0.], [1., x0[3]-x0[0], x0[3]-x0[1], x0[3]-x0[2]]]) self.assertTrue(np.allclose(A, Asb)) def test_break_point_spot_on_r2(self): # test r squared value with known solution my_fit1 = pwlf.PiecewiseLinFit(self.x_small, self.y_small) x0 = self.x_small.copy() my_fit1.fit_with_breaks(x0) rsq = my_fit1.r_squared() self.assertTrue(np.isclose(rsq, 1.0)) def test_break_point_diff_x0_0(self): # check diff loc my_fit2 = pwlf.PiecewiseLinFit(self.x_small, self.y_small) x0 = self.x_small.copy() x0[1] = 1.00001 x0[2] = 1.50001 ssr2 = my_fit2.fit_with_breaks(x0) self.assertTrue(np.isclose(ssr2, 0.0)) def test_break_point_diff_x0_1(self): # check if my duplicate is in a different location x0 = self.x_small.copy() my_fit3 = pwlf.PiecewiseLinFit(self.x_small, self.y_small) x0[1] = 0.9 ssr3 = my_fit3.fit_with_breaks(x0) self.assertTrue(np.isclose(ssr3, 0.0)) def test_break_point_diff_x0_2(self): # check if my duplicate is in a different location x0 = self.x_small.copy() my_fit4 = pwlf.PiecewiseLinFit(self.x_small, self.y_small) x0[1] = 1.1 ssr4 = my_fit4.fit_with_breaks(x0) self.assertTrue(np.isclose(ssr4, 0.0)) def test_break_point_diff_x0_3(self): # check if my duplicate is in a different location x0 = self.x_small.copy() my_fit5 = pwlf.PiecewiseLinFit(self.x_small, self.y_small) x0[2] = 1.6 ssr5 = my_fit5.fit_with_breaks(x0) self.assertTrue(np.isclose(ssr5, 0.0)) def test_break_point_diff_x0_4(self): # check if my duplicate is in a different location x0 = self.x_small.copy() my_fit6 = pwlf.PiecewiseLinFit(self.x_small, self.y_small) x0[2] = 1.4 ssr6 = my_fit6.fit_with_breaks(x0) self.assertTrue(np.isclose(ssr6, 0.0)) def test_diff_evo(self): my_pwlf = pwlf.PiecewiseLinFit(self.x_small, self.y_small) my_pwlf.fit(4, disp=False) self.assertTrue(np.isclose(my_pwlf.ssr, 0.0)) def test_custom_bounds1(self): my_pwlf = pwlf.PiecewiseLinFit(self.x_small, self.y_small) n_segments = 3 bounds = np.zeros((n_segments-1, 2)) bounds[0, 0] = self.x_small[0] bounds[0, 1] = self.x_small[2] bounds[1, 0] = self.x_small[1] bounds[1, 1] = self.x_small[-1] my_pwlf.fit(n_segments, bounds=bounds) self.assertTrue(np.isclose(my_pwlf.ssr, 0.0)) def test_custom_bounds2(self): my_pwlf = pwlf.PiecewiseLinFit(self.x_small, self.y_small) n_segments = 3 bounds = np.zeros((n_segments-1, 2)) bounds[0, 0] = self.x_small[0] bounds[0, 1] = self.x_small[2] bounds[1, 0] = self.x_small[1] bounds[1, 1] = self.x_small[-1] my_pwlf.fitfast(n_segments, pop=20, bounds=bounds) self.assertTrue(np.isclose(my_pwlf.ssr, 0.0)) def test_predict(self): my_pwlf = pwlf.PiecewiseLinFit(self.x_small, self.y_small) xmax = np.max(self.x_small) xmin = np.min(self.x_small) my_pwlf.fit_with_breaks((xmin, xmax)) x = np.linspace(xmin, xmax, 10) yHat = my_pwlf.predict(x) self.assertTrue(np.isclose(np.sum(yHat), 8.085714285714287)) def test_custom_opt(self): my_pwlf = pwlf.PiecewiseLinFit(self.x_small, self.y_small) my_pwlf.use_custom_opt(3) x_guess = np.array((0.9, 1.1)) from scipy.optimize import minimize res = minimize(my_pwlf.fit_with_breaks_opt, x_guess) self.assertTrue(np.isclose(res['fun'], 0.0)) def test_custom_opt_with_con(self): my_pwlf = pwlf.PiecewiseLinFit(self.x_small, self.y_small) my_pwlf.use_custom_opt(3, x_c=[0.], y_c=[0.]) x_guess = np.array((0.9, 1.1)) from scipy.optimize import minimize _ = minimize(my_pwlf.fit_with_breaks_opt, x_guess) self.assertTrue(True) def test_single_force_break_point1(self): my_fit = pwlf.PiecewiseLinFit(self.x_small, self.y_small) x_c = [-0.5] y_c = [-0.5] my_fit.fit_with_breaks_force_points([0.2, 0.7], x_c, y_c) yhat = my_fit.predict(x_c) self.assertTrue(np.isclose(y_c, yhat)) def test_single_force_break_point2(self): my_fit = pwlf.PiecewiseLinFit(self.x_small, self.y_small) x_c = [2.0] y_c = [1.5] my_fit.fit_with_breaks_force_points([0.2, 0.7], x_c, y_c) yhat = my_fit.predict(x_c) self.assertTrue(np.isclose(y_c, yhat)) def test_single_force_break_point_degree(self): my_fit = pwlf.PiecewiseLinFit(self.x_small, self.y_small, degree=2) x_c = [2.0] y_c = [1.5] my_fit.fit_with_breaks_force_points([0.2, 0.7], x_c, y_c) yhat = my_fit.predict(x_c) self.assertTrue(np.isclose(y_c, yhat)) def test_single_force_break_point_degree_zero(self): my_fit = pwlf.PiecewiseLinFit(self.x_small, self.y_small, degree=0) x_c = [2.0] y_c = [1.5] my_fit.fit_with_breaks_force_points([0.2, 0.7], x_c, y_c) yhat = my_fit.predict(x_c) self.assertTrue(np.isclose(y_c, yhat)) def test_opt_fit_with_force_points(self): # I need more data points to test this function because of # ill conditioning in the least squares problem... x = np.linspace(0.0, 1.0, num=100) y = np.sin(6.0*x) my_fit = pwlf.PiecewiseLinFit(x, y, disp_res=True) x_c = [0.0] y_c = [0.0] my_fit.fit(3, x_c, y_c) yhat = my_fit.predict(x_c) self.assertTrue(np.isclose(y_c, yhat)) def test_opt_fit_single_segment(self): # Test fit for a single segment without force points x = np.linspace(0.0, 1.0, num=100) y = x + 1 my_fit = pwlf.PiecewiseLinFit(x, y, disp_res=True) my_fit.fit(1) xhat = 0 yhat = my_fit.predict(xhat) self.assertTrue(np.isclose(xhat + 1, yhat)) def test_opt_fit_with_force_points_single_segment(self): # Test fit for a single segment (same as above) # but with a force point x = np.linspace(0.0, 1.0, num=100) y = x + 1 my_fit = pwlf.PiecewiseLinFit(x, y, disp_res=True) x_c = [0.0] y_c = [0.0] my_fit.fit(1, x_c, y_c) yhat = my_fit.predict(x_c) self.assertTrue(np.isclose(y_c, yhat)) def test_se(self): # check to see if it will let me calculate standard errors my_pwlf = pwlf.PiecewiseLinFit(np.random.random(20), np.random.random(20)) my_pwlf.fitfast(2) my_pwlf.standard_errors() def test_p(self): # check to see if it will let me calculate p-values my_pwlf = pwlf.PiecewiseLinFit(np.random.random(20), np.random.random(20)) my_pwlf.fitfast(2) my_pwlf.p_values() def test_nonlinear_p_and_se(self): # generate a true piecewise linear data np.random.seed(1) n_data = 20 x = np.linspace(0, 1, num=n_data) y = np.random.random(n_data) my_pwlf = pwlf.PiecewiseLinFit(x, y) true_beta = np.array((1.0, 0.2, 0.2)) true_breaks = np.array((0.0, 0.5, 1.0)) y = my_pwlf.predict(x, beta=true_beta, breaks=true_breaks) my_pwlf = pwlf.PiecewiseLinFit(x, y) my_pwlf.fitfast(2) # calculate p-values p = my_pwlf.p_values(method='non-linear', step_size=1e-4) self.assertTrue(p.max() <= 0.05) def test_pv(self): # check to see if it will let me calculate prediction variance for # random data my_pwlf = pwlf.PiecewiseLinFit(np.random.random(20), np.random.random(20)) my_pwlf.fitfast(2) my_pwlf.prediction_variance(np.random.random(20)) def test_predict_with_custom_param(self): # check to see if predict runs with custom parameters x = np.random.random(20) my_pwlf = pwlf.PiecewiseLinFit(x, np.random.random(20)) my_pwlf.predict(x, beta=np.array((1e-4, 1e-2, 1e-3)), breaks=np.array((0.0, 0.5, 1.0))) def test_fit_guess(self): x = np.array([4., 5., 6., 7., 8.]) y = np.array([11., 13., 16., 28.92, 42.81]) my_pwlf = pwlf.PiecewiseLinFit(x, y) breaks = my_pwlf.fit_guess([6.0]) self.assertTrue(np.isclose(breaks[1], 6.0705297)) def test_fit_guess_kwrds(self): x = np.array([4., 5., 6., 7., 8.]) y = np.array([11., 13., 16., 28.92, 42.81]) my_pwlf = pwlf.PiecewiseLinFit(x, y) breaks = my_pwlf.fit_guess([6.0], m=10, factr=1e2, pgtol=1e-05, epsilon=1e-6, iprint=-1, maxfun=1500000, maxiter=150000, disp=None) self.assertTrue(np.isclose(breaks[1], 6.0705297)) def test_multi_start_fitfast(self): my_pwlf = pwlf.PiecewiseLinFit(self.x_small, self.y_small) my_pwlf.fitfast(4, 50) self.assertTrue(np.isclose(my_pwlf.ssr, 0.0)) # ================================================= # Start of degree tests def test_n_parameters_correct(self): my_pwlf_0 = pwlf.PiecewiseLinFit(self.x_sin, self.y_sin, degree=0) my_pwlf_1 = pwlf.PiecewiseLinFit(self.x_sin, self.y_sin, degree=1) my_pwlf_2 = pwlf.PiecewiseLinFit(self.x_sin, self.y_sin, degree=2) breaks = np.array([0., 1.03913513, 3.04676334, 4.18647526, 10.]) A0 = my_pwlf_0.assemble_regression_matrix(breaks, self.x_sin) A1 = my_pwlf_1.assemble_regression_matrix(breaks, self.x_sin) A2 = my_pwlf_2.assemble_regression_matrix(breaks, self.x_sin) self.assertTrue(A0.shape[1] == my_pwlf_0.n_parameters) self.assertTrue(A1.shape[1] == my_pwlf_1.n_parameters) self.assertTrue(A2.shape[1] == my_pwlf_2.n_parameters) # Also check n_segments correct self.assertTrue(4 == my_pwlf_0.n_segments) self.assertTrue(4 == my_pwlf_1.n_segments) self.assertTrue(4 == my_pwlf_2.n_segments) def test_force_fits_through_points_other_degrees(self): # generate sin wave data x = np.linspace(0, 10, num=100) y = np.sin(x * np.pi / 2) # add noise to the data y = np.random.normal(0, 0.15, 100) + y # linear fit my_pwlf_1 = pwlf.PiecewiseLinFit(x, y, degree=1) my_pwlf_1.fit(n_segments=6, x_c=[0], y_c=[0]) y_predict_1 = my_pwlf_1.predict(x) # quadratic fit my_pwlf_2 = pwlf.PiecewiseLinFit(x, y, degree=2) my_pwlf_2.fit(n_segments=5, x_c=[0], y_c=[0]) y_predict_2 = my_pwlf_2.predict(x) self.assertTrue(np.isclose(0, y_predict_1[0])) self.assertTrue(np.isclose(0, y_predict_2[0])) def test_fitfast(self): my_pwlf_0 = pwlf.PiecewiseLinFit( self.x_sin, self.y_sin, degree=0, seed=123 ) my_pwlf_1 = pwlf.PiecewiseLinFit( self.x_sin, self.y_sin, degree=1, seed=123, ) my_pwlf_2 = pwlf.PiecewiseLinFit( self.x_sin, self.y_sin, degree=2, seed=123, ) # fit the data for four line segments my_pwlf_0.fitfast(4, pop=10) my_pwlf_1.fitfast(4, pop=10) my_pwlf_2.fitfast(4, pop=10) self.assertTrue(my_pwlf_0.ssr <= 35.) self.assertTrue(my_pwlf_1.ssr <= 15.) self.assertTrue(my_pwlf_2.ssr <= 2.0) def test_fit(self): my_pwlf_0 = pwlf.PiecewiseLinFit(self.x_sin, self.y_sin, degree=0) my_pwlf_1 = pwlf.PiecewiseLinFit(self.x_sin, self.y_sin, degree=1) my_pwlf_2 = pwlf.PiecewiseLinFit(self.x_sin, self.y_sin, degree=2) # fit the data for four line segments np.random.seed(123123) my_pwlf_0.fit(5) my_pwlf_1.fit(5) my_pwlf_2.fit(5) self.assertTrue(my_pwlf_0.ssr <= 10.) self.assertTrue(my_pwlf_1.ssr <= 7.0) self.assertTrue(my_pwlf_2.ssr <= 0.5) def test_se_no_fit(self): my_pwlf_0 = pwlf.PiecewiseLinFit(self.x_sin, self.y_sin) try: my_pwlf_0.standard_errors() self.assertTrue(False) except AttributeError: self.assertTrue(True) def test_se_no_method(self): my_fit1 = pwlf.PiecewiseLinFit(self.x_small, self.y_small) x0 = self.x_small.copy() _ = my_fit1.fit_with_breaks(x0) try: my_fit1.standard_errors(method='blah') self.assertTrue(False) except ValueError: self.assertTrue(True) def test_pv_list(self): my_fit1 = pwlf.PiecewiseLinFit(self.x_small, self.y_small) x0 = self.x_small.copy() _ = my_fit1.fit_with_breaks(x0) my_fit1.prediction_variance(list(self.x_small)) def test_pv_no_fit(self): my_pwlf_0 = pwlf.PiecewiseLinFit(self.x_sin, self.y_sin) try: my_pwlf_0.prediction_variance(self.x_sin) self.assertTrue(False) except AttributeError: self.assertTrue(True) def test_r2_no_fit(self): my_pwlf_0 = pwlf.PiecewiseLinFit(self.x_sin, self.y_sin) try: my_pwlf_0.r_squared() self.assertTrue(False) except AttributeError: self.assertTrue(True) def test_pvalue_no_fit(self): my_pwlf_0 = pwlf.PiecewiseLinFit(self.x_sin, self.y_sin) try: my_pwlf_0.p_values() self.assertTrue(False) except AttributeError: self.assertTrue(True) def test_pvalues_wrong_method(self): my_fit1 = pwlf.PiecewiseLinFit(self.x_small, self.y_small) x0 = self.x_small.copy() _ = my_fit1.fit_with_breaks(x0) try: my_fit1.p_values(method='blah') self.assertTrue(False) except ValueError: self.assertTrue(True) def test_all_stats(self): np.random.seed(121) my_pwlf_0 = pwlf.PiecewiseLinFit(self.x_sin, self.y_sin, degree=0) my_pwlf_0.fitfast(3) my_pwlf_0.standard_errors() my_pwlf_0.prediction_variance(np.random.random(20)) my_pwlf_0.p_values() my_pwlf_0.r_squared() my_pwlf_0.calc_slopes() my_pwlf_2 = pwlf.PiecewiseLinFit(self.x_sin, self.y_sin, degree=2) my_pwlf_2.fitfast(3) my_pwlf_2.standard_errors() my_pwlf_2.prediction_variance(np.random.random(20)) my_pwlf_2.p_values() my_pwlf_2.r_squared() my_pwlf_2.calc_slopes() my_pwlf_3 = pwlf.PiecewiseLinFit(self.x_sin, self.y_sin, degree=3) my_pwlf_3.fitfast(3) my_pwlf_3.standard_errors() my_pwlf_3.prediction_variance(np.random.random(20)) my_pwlf_3.p_values() my_pwlf_3.r_squared() my_pwlf_3.calc_slopes() # End of degree tests # ================================================= # ================================================= # Start weighted least squares tests def test_weighted_same_as_ols(self): # test that weighted least squares is same as OLS # when the weight is equal to 1.0 n_segments = 2 my = pwlf.PiecewiseLinFit(self.x_small, self.y_small) x = np.random.random() breaks = my.fit_guess([x]) my_w = pwlf.PiecewiseLinFit(self.x_small, self.y_small, weights=np.ones_like(self.x_small)) breaks_w = my_w.fit_guess([x]) self.assertTrue(np.isclose(my.ssr, my_w.ssr)) for i in range(n_segments+1): self.assertTrue(np.isclose(breaks[i], breaks_w[i])) def test_heteroscedastic_data(self): n_segments = 3 weights = self.y_small.copy() weights[0] = 0.01 weights = 1.0 / weights my_w = pwlf.PiecewiseLinFit(self.x_small, self.y_small, weights=weights) _ = my_w.fit(n_segments) _ = my_w.standard_errors() def test_not_supported_fit(self): x = np.linspace(0.0, 1.0, num=100) y = np.sin(6.0*x) w = np.random.random(size=100) my_fit = pwlf.PiecewiseLinFit(x, y, disp_res=True, weights=w) x_c = [0.0] y_c = [0.0] try: my_fit.fit(3, x_c, y_c) self.assertTrue(False) except ValueError: self.assertTrue(True) def test_not_supported_fit_with_breaks_force_points(self): x = np.linspace(0.0, 1.0, num=100) y = np.sin(6.0*x) w = list(np.random.random(size=100)) my_fit = pwlf.PiecewiseLinFit(x, y, disp_res=True, weights=w) x_c = [0.0] y_c = [0.0] try: my_fit.fit_with_breaks_force_points([0.1, 0.2, 0.3], x_c, y_c) self.assertTrue(False) except ValueError: self.assertTrue(True) def test_custom_opt_not_supported(self): my_pwlf = pwlf.PiecewiseLinFit(self.x_small, self.y_small, weights=self.y_small) try: my_pwlf.use_custom_opt(3, x_c=[0], y_c=[0]) self.assertTrue(False) except ValueError: self.assertTrue(True) def test_random_seed_fit(self): np.random.seed(1) my_pwlf = pwlf.PiecewiseLinFit(self.x_small, self.y_small, seed=123) fit1 = my_pwlf.fit(2) np.random.seed(2) my_pwlf = pwlf.PiecewiseLinFit(self.x_small, self.y_small, seed=123) fit2 = my_pwlf.fit(2) same_breaks = np.isclose(fit1, fit2) self.assertTrue(same_breaks.sum() == same_breaks.size) def test_random_seed_fitfast(self): # specifically test for seed = 0 np.random.seed(1) my_pwlf = pwlf.PiecewiseLinFit(self.x_small, self.y_small, seed=0) fit1 = my_pwlf.fitfast(2) np.random.seed(2) my_pwlf = pwlf.PiecewiseLinFit(self.x_small, self.y_small, seed=0) fit2 = my_pwlf.fitfast(2) same_breaks = np.isclose(fit1, fit2) self.assertTrue(same_breaks.sum() == same_breaks.size) def test_one_segment_fits(self): my_pwlf = pwlf.PiecewiseLinFit(self.x_small, self.y_small) fit1 = my_pwlf.fitfast(1) my_pwlf = pwlf.PiecewiseLinFit(self.x_small, self.y_small) fit2 = my_pwlf.fit(1) same_breaks = np.isclose(fit1, fit2) self.assertTrue(same_breaks[0]) self.assertTrue(same_breaks[1]) def test_float32(self): my_pwlf = pwlf.PiecewiseLinFit( np.linspace(0, 10, 3, dtype=np.float32), np.random.random(3).astype(np.float32), ) _ = my_pwlf.fitfast(2) self.assertTrue(True) def test_lfloat128(self): try: x = np.linspace(0, 10, 3, dtype=np.float128) y = np.random.random(3).astype(np.float128) my_pwlf = pwlf.PiecewiseLinFit(x, y) _ = my_pwlf.fitfast(2) self.assertTrue(True) except AttributeError: # this means that float128 is not supported self.assertTrue(True) def test_mixed_degree1(self): x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] y = [1, 2, 3, 4, 4.25, 3.75, 4, 5, 6, 7] degree_list = [1, 0, 1] my_pwlf = pwlf.PiecewiseLinFit(x, y, degree=degree_list) _ = my_pwlf.fit(3) # generate predictions x_hat = np.linspace(min(x), max(x), 1000) _ = my_pwlf.predict(x_hat) def test_mixed_degree2(self): x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] y = [1, 2, 3, 4, 4.25, 3.75, 4, 5, 6, 7] degree_list = [1, 1, 1] my_pwlf = pwlf.PiecewiseLinFit(x, y, degree=degree_list) _ = my_pwlf.fit(3) # generate predictions x_hat = np.linspace(min(x), max(x), 1000) _ = my_pwlf.predict(x_hat) def test_mixed_degree3(self): x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] y = [1, 2, 3, 4, 4.25, 3.75, 4, 5, 6, 7] degree_list = [1, 3] try: my_pwlf = pwlf.PiecewiseLinFit(x, y, degree=degree_list) _ = my_pwlf.fit(3) self.assertTrue(False) except ValueError: self.assertTrue(True) def test_mixed_degree_wrong_list(self): x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] y = [1, 2, 3, 4, 4.25, 3.75, 4, 5, 6, 7] degree_list = [1, 1] my_pwlf = pwlf.PiecewiseLinFit(x, y, degree=degree_list) try: _ = my_pwlf.fit(3) self.assertTrue(False) except ValueError: self.assertTrue(True) try: _ = my_pwlf.fitfast(3) self.assertTrue(False) except ValueError: self.assertTrue(True) try: _ = my_pwlf.fit_with_breaks([0, 3, 4, 5]) self.assertTrue(False) except ValueError: self.assertTrue(True) def test_mixed_degree_force_point(self): x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] y = [1, 2, 3, 4, 4.25, 3.75, 4, 5, 6, 7] degree_list = [1, 1] my_pwlf = pwlf.PiecewiseLinFit(x, y, degree=degree_list) try: _ = my_pwlf.fit(2, x_c=[0,], y_c=[0,]) self.assertTrue(False) except ValueError: self.assertTrue(True) if __name__ == '__main__': unittest.main()