├── tests ├── __init__.py ├── test_hk_errors.py ├── test_checks.py ├── test_twoside_normal.py ├── test_twoside_lognormal.py ├── test_oneside_nonparmetric.py ├── test_oneside_lognormal.py ├── test_oneside_normal.py ├── test_oneside_hansonkoopmans_cmh.py ├── test_oneside_hansonkoopmans.py ├── test_twoside_normal_factor_mhe.py └── test_twoside_normal_factor_iso.py ├── toleranceinterval ├── VERSION ├── twoside │ ├── __init__.py │ ├── _normal_approx.py │ ├── twoside.py │ └── _normal_exact.py ├── oneside │ ├── __init__.py │ └── oneside.py ├── __init__.py ├── checks.py └── hk.py ├── MANIFEST.in ├── environment.yml ├── setup.py ├── .github └── workflows │ ├── cron.yml │ └── ci.yml ├── LICENSE ├── CHANGELOG.md ├── .gitignore ├── README.md └── docs ├── twoside └── index.html ├── oneside └── index.html ├── index.html ├── checks.html └── twoside.html /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /toleranceinterval/VERSION: -------------------------------------------------------------------------------- 1 | 1.0.3 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | -------------------------------------------------------------------------------- /toleranceinterval/twoside/__init__.py: -------------------------------------------------------------------------------- 1 | from .twoside import normal_factor # noqa F401 2 | from .twoside import normal # noqa F401 3 | from .twoside import lognormal # noqa F401 4 | -------------------------------------------------------------------------------- /toleranceinterval/oneside/__init__.py: -------------------------------------------------------------------------------- 1 | from .oneside import normal # noqa F401 2 | from .oneside import lognormal # noqa F401 3 | from .oneside import non_parametric # noqa F401 4 | from .oneside import hanson_koopmans # noqa F401 5 | from .oneside import hanson_koopmans_cmh # noqa F401 6 | -------------------------------------------------------------------------------- /toleranceinterval/__init__.py: -------------------------------------------------------------------------------- 1 | from . import oneside # noqa F401 2 | from . import twoside # noqa F401 3 | from . import hk # noqa F401 4 | from . import checks # noqa F401 5 | import os as _os # noqa F401 6 | 7 | # add rudimentary version tracking 8 | __VERSION_FILE__ = _os.path.join(_os.path.dirname(__file__), 'VERSION') 9 | __version__ = open(__VERSION_FILE__).read().strip() 10 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | # Simple environment for developing tolerance_interval_py 2 | # To use: 3 | # $ conda env create -f environment.yml # `mamba` works too for this command 4 | # $ conda activate tolerance-interval-py 5 | # 6 | name: tolerance-interval-py 7 | channels: 8 | - conda-forge 9 | dependencies: 10 | - numpy 11 | - scipy 12 | - sympy 13 | - setuptools 14 | # Avoid pulling in large MKL libraries. 15 | - nomkl 16 | -------------------------------------------------------------------------------- /tests/test_hk_errors.py: -------------------------------------------------------------------------------- 1 | # -- coding: utf-8 -- 2 | from toleranceinterval.hk import HansonKoopmans 3 | import unittest 4 | 5 | 6 | class TestEverything(unittest.TestCase): 7 | 8 | def test_p_value_error(self): 9 | with self.assertRaises(ValueError): 10 | _ = HansonKoopmans(-.1, 0.9, 10, 8) 11 | 12 | def test_g_value_error(self): 13 | with self.assertRaises(ValueError): 14 | _ = HansonKoopmans(.1, -0.9, 10, 8) 15 | 16 | def test_j_value_error(self): 17 | with self.assertRaises(ValueError): 18 | _ = HansonKoopmans(.1, 0.9, 10, -200) 19 | 20 | 21 | if __name__ == '__main__': 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | packages = find_packages() 3 | setup( 4 | name='toleranceinterval', 5 | version=open('toleranceinterval/VERSION').read().strip(), 6 | author='Charles Jekel', 7 | author_email='cjekel@gmail.com', 8 | packages=packages, 9 | package_data={'toleranceinterval': ['VERSION']}, 10 | py_modules=['toleranceinterval.__init__'], 11 | url='https://github.com/cjekel/tolerance_interval_py', 12 | license='MIT License', 13 | description='A small Python library for one-sided tolerance bounds and two-sided tolerance intervals.', # noqa E501 14 | long_description=open('README.md').read(), 15 | long_description_content_type='text/markdown', 16 | platforms=['any'], 17 | install_requires=[ 18 | "numpy >= 1.14.0", 19 | "scipy >= 0.19.0", 20 | "sympy >= 1.4", 21 | "setuptools >= 38.6.0", 22 | ], 23 | python_requires=">3.5", 24 | ) -------------------------------------------------------------------------------- /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: toleranceinterval cron 2 | 3 | on: 4 | schedule: 5 | # Run tests every saturday 6 | - cron: '* * * * */6' 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: [3.7, 3.8, 3.9, '3.10'] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install flake8 coverage pytest pytest-cov 25 | - name: Install toleranceinterval 26 | run: | 27 | python -m pip install . --no-cache-dir 28 | - name: Lint with flake8 29 | run: | 30 | flake8 toleranceinterval 31 | - name: Test with pytest 32 | run: | 33 | pytest --cov=toleranceinterval --cov-report=xml -p no:warnings 34 | -------------------------------------------------------------------------------- /tests/test_checks.py: -------------------------------------------------------------------------------- 1 | # -- coding: utf-8 -- 2 | import numpy as np 3 | from toleranceinterval import checks 4 | import unittest 5 | 6 | 7 | class TestEverything(unittest.TestCase): 8 | 9 | def test_assert_2d_sort(self): 10 | for i in range(10): 11 | x = np.random.random(5) 12 | x = checks.numpy_array(x) 13 | x_sort = x.copy() 14 | x_sort.sort() 15 | x = checks.assert_2d_sort(x) 16 | for idx, x_new in enumerate(x[0]): 17 | # print(x_new, x_sort[idx]) 18 | self.assertTrue(np.isclose(x_new, x_sort[idx])) 19 | 20 | def test_x_unmodified(self): 21 | for i in range(10): 22 | x = np.random.random(5) 23 | x = checks.numpy_array(x) 24 | x.sort() 25 | x[0] = 12919.1 26 | xnew = checks.assert_2d_sort(x) 27 | # print(xnew[0, -1], 12919.1) 28 | self.assertTrue(np.isclose(xnew[0, -1], 12919.1)) 29 | self.assertTrue(np.isclose(x[0], 12919.1)) 30 | 31 | 32 | if __name__ == '__main__': 33 | unittest.main() 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Charles Jekel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: toleranceinterval ci 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build: 8 | 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: [3.7, 3.8, 3.9, '3.10'] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install flake8 coverage pytest pytest-cov 23 | - name: Install toleranceinterval 24 | run: | 25 | python -m pip install . --no-cache-dir 26 | - name: Lint with flake8 27 | run: | 28 | flake8 toleranceinterval 29 | - name: Test with pytest 30 | run: | 31 | pytest --cov=toleranceinterval --cov-report=xml -p no:warnings 32 | - name: Upload coverage to Codecov 33 | uses: codecov/codecov-action@v1 34 | with: 35 | token: ${{ secrets.CODECOV_TOKEN }} 36 | file: ./coverage.xml 37 | directory: ./coverage/reports/ 38 | flags: unittests 39 | env_vars: OS,PYTHON 40 | name: codecov-umbrella 41 | fail_ci_if_error: true 42 | verbose: false 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.0.3] - 2023-03-26 8 | ### Changed 9 | - Fixed a bug introduced in `1.0.2` where `checks.assert_2d_sort` was not sorting 10 | 11 | ## [1.0.2] - 2023-03-25 12 | ### Changed 13 | - Fixed a bug where a sort function was modifying the input data array. This could have unintended consequence for a user without their knowledge. See [PR](https://github.com/cjekel/tolerance_interval_py/pull/7). Thanks to Jed Ludlow](https://github.com/jedludlow) 14 | 15 | ## [1.0.1] - 2022-02-24 16 | ### Added 17 | - Fix docstring for oneside.non_parametric thanks to [Jed Ludlow](https://github.com/jedludlow) 18 | 19 | ## [1.0.0] - 2022-01-03 20 | ### Added 21 | - exact two-sided normal method thanks to [Jed Ludlow](https://github.com/jedludlow) 22 | - normal_factor method thanks to [Jed Ludlow](https://github.com/jedludlow) 23 | ### Changed 24 | - Fixed references listed in documentation 25 | ### Removed 26 | - Python 2.X is no longer supported. Python 3.6 is the minimum supported version. 27 | 28 | ## [0.0.3] - 2020-05-22 29 | ### Changed 30 | - Docstrings, documentation, and readme had the wrong percentile values for many examples. I've corrected these examples. Sorry for the confusion this may have caused. 31 | 32 | ## [0.0.2] - 2019-11-13 33 | ### Added 34 | - setuptools is now listed in the requirements 35 | 36 | ## [0.0.1] - 2019-11-03 37 | ### Added 38 | - Everything you've seen so far! 39 | -------------------------------------------------------------------------------- /toleranceinterval/checks.py: -------------------------------------------------------------------------------- 1 | # -- coding: utf-8 -- 2 | # MIT License 3 | # 4 | # Copyright (c) 2019 Charles Jekel 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | import numpy as np 25 | 26 | 27 | def numpy_array(x): 28 | if isinstance(x, np.ndarray) is False: 29 | x = np.array(x) 30 | return x 31 | 32 | 33 | def assert_2d_sort(x): 34 | if x.ndim == 1: 35 | x = x.reshape(1, -1) 36 | elif x.ndim > 2: 37 | raise ValueError('x can not be more than 2 dimensions') 38 | # Prevent modifications to input data by copying x before sorting. 39 | x = x.copy() 40 | x.sort() 41 | return x 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode folder 2 | .vscode* 3 | 4 | # vim temp file 5 | *~ 6 | *.swp 7 | *.un~ 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | files.txt 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # celery beat schedule file 88 | celerybeat-schedule 89 | 90 | # SageMath parsed files 91 | *.sage.py 92 | 93 | # Environments 94 | .env 95 | .venv 96 | env/ 97 | venv/ 98 | ENV/ 99 | env.bak/ 100 | venv.bak/ 101 | 102 | # Spyder project settings 103 | .spyderproject 104 | .spyproject 105 | 106 | # Sublime project settings 107 | *.sublime-project 108 | *.sublime-workspace 109 | 110 | # Rope project settings 111 | .ropeproject 112 | 113 | # mkdocs documentation 114 | /site 115 | 116 | # mypy 117 | .mypy_cache/ 118 | 119 | # documentation staging 120 | .docs_test 121 | .docs_test/* -------------------------------------------------------------------------------- /tests/test_twoside_normal.py: -------------------------------------------------------------------------------- 1 | # -- coding: utf-8 -- 2 | import numpy as np 3 | from toleranceinterval.twoside import normal 4 | # from scipy.stats import chi2 5 | import unittest 6 | 7 | 8 | class TestEverything(unittest.TestCase): 9 | 10 | def _run_test_cases(self, N, P, G, K, method): 11 | for i, k in enumerate(K): 12 | n = N[i] 13 | x = np.random.random(n) * 10 14 | xmu = x.mean() 15 | xstd = x.std(ddof=1) 16 | p = P[i] 17 | g = G[i] 18 | bound = normal(x, p, g, method=method) 19 | k_hat_l = (xmu - bound[0, 0]) / xstd 20 | k_hat_u = (bound[0, 1] - xmu) / xstd 21 | self.assertTrue(np.isclose(k, k_hat_l, rtol=1e-4, atol=1e-5)) 22 | self.assertTrue(np.isclose(k, k_hat_u, rtol=1e-4, atol=1e-5)) 23 | 24 | def test_exact(self): 25 | # We test only one case here mostly as a sanity check to make sure 26 | # the functions are hooked up correctly. See two-sided tolerance 27 | # factor unit tests for exhaustive checking of exact tolerance factors. 28 | # Test case is drawn from ISO Table F1. 29 | G = [0.90] 30 | N = [35] 31 | P = [0.90] 32 | K = [1.9906] 33 | self._run_test_cases(N, P, G, K, 'exact') 34 | 35 | def test_guenther_approx(self): 36 | # values from: 37 | # https://ncss-wpengine.netdna-ssl.com/wp-content/themes/ncss/pdf/Procedures/PASS/Tolerance_Intervals_for_Normal_Data.pdf 38 | G = [0.9, 0.95, 0.95, 0.95, 0.95, 0.95, 0.95, 0.95, 0.95, 0.95, 0.95, 39 | 0.95] 40 | N = [26, 200, 400, 600, 800, 1000, 1200, 1400, 1600, 5910, 866, 179] 41 | P = [0.8, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9] 42 | K = [1.6124, 1.7984, 1.7493, 1.7287, 1.7168, 1.7088, 1.7029, 1.6984, 43 | 1.6948, 1.6703, 1.7138, 1.8084] 44 | self._run_test_cases(N, P, G, K, 'guenther') 45 | 46 | def test_howe_approx(self): 47 | # values from: 48 | # https://www.itl.nist.gov/div898/handbook/prc/section2/prc263.htm 49 | # Howe's method is implicitly tested to some extent by Guenther's 50 | # method. 51 | G = [0.99] 52 | N = [43] 53 | P = [0.9] 54 | K = [2.2173] 55 | self._run_test_cases(N, P, G, K, 'howe') 56 | 57 | def test_random_shapes(self): 58 | M = [3, 10, 20] 59 | N = [5, 10, 20] 60 | for m in M: 61 | for n in N: 62 | x = np.random.random((m, n)) 63 | bounds = normal(x, 0.95, 0.95) 64 | _m, _ = bounds.shape 65 | self.assertTrue(_m == m) 66 | 67 | def test_value_error(self): 68 | with self.assertRaises(ValueError): 69 | x = np.random.random((1, 2, 4, 3)) 70 | normal(x, 0.9, 0.9) 71 | 72 | 73 | if __name__ == '__main__': 74 | np.random.seed(121) 75 | unittest.main() 76 | -------------------------------------------------------------------------------- /tests/test_twoside_lognormal.py: -------------------------------------------------------------------------------- 1 | # -- coding: utf-8 -- 2 | import numpy as np 3 | from toleranceinterval.twoside import lognormal 4 | # from scipy.stats import chi2 5 | import unittest 6 | 7 | 8 | class TestEverything(unittest.TestCase): 9 | 10 | def _run_test_cases(self, N, P, G, K, method): 11 | for i, k in enumerate(K): 12 | n = N[i] 13 | x = np.random.random(n) * 10 14 | xmu = np.mean(np.log(x)) 15 | xstd = np.std(np.log(x), ddof=1) 16 | p = P[i] 17 | g = G[i] 18 | bound = lognormal(x, p, g, method=method) 19 | k_hat_l = (xmu - np.log(bound[0, 0])) / xstd 20 | k_hat_u = (np.log(bound[0, 1]) - xmu) / xstd 21 | self.assertTrue(np.isclose(k, k_hat_l, rtol=1e-4, atol=1e-5)) 22 | self.assertTrue(np.isclose(k, k_hat_u, rtol=1e-4, atol=1e-5)) 23 | 24 | def test_exact(self): 25 | # We test only one case here mostly as a sanity check to make sure 26 | # the functions are hooked up correctly. See two-sided tolerance 27 | # factor unit tests for exhaustive checking of exact tolerance factors. 28 | # Test case is drawn from ISO Table F1. 29 | G = [0.90] 30 | N = [35] 31 | P = [0.90] 32 | K = [1.9906] 33 | self._run_test_cases(N, P, G, K, 'exact') 34 | 35 | def test_guenther_approx(self): 36 | # values from: 37 | # https://ncss-wpengine.netdna-ssl.com/wp-content/themes/ncss/pdf/Procedures/PASS/Tolerance_Intervals_for_Normal_Data.pdf 38 | G = [0.9, 0.95, 0.95, 0.95, 0.95, 0.95, 0.95, 0.95, 0.95, 0.95, 0.95, 39 | 0.95] 40 | N = [26, 200, 400, 600, 800, 1000, 1200, 1400, 1600, 5910, 866, 179] 41 | P = [0.8, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9] 42 | K = [1.6124, 1.7984, 1.7493, 1.7287, 1.7168, 1.7088, 1.7029, 1.6984, 43 | 1.6948, 1.6703, 1.7138, 1.8084] 44 | self._run_test_cases(N, P, G, K, 'guenther') 45 | 46 | def test_howe_approx(self): 47 | # values from: 48 | # https://www.itl.nist.gov/div898/handbook/prc/section2/prc263.htm 49 | # Howe's method is implicitly tested to some extent by Guenther's 50 | # method. 51 | G = [0.99] 52 | N = [43] 53 | P = [0.9] 54 | K = [2.2173] 55 | self._run_test_cases(N, P, G, K, 'howe') 56 | 57 | def test_random_shapes(self): 58 | M = [3, 10, 20] 59 | N = [5, 10, 20] 60 | for m in M: 61 | for n in N: 62 | x = np.random.random((m, n)) 63 | bounds = lognormal(x, 0.95, 0.95) 64 | _m, _ = bounds.shape 65 | self.assertTrue(_m == m) 66 | 67 | def test_value_error(self): 68 | with self.assertRaises(ValueError): 69 | x = np.random.random((1, 2, 4, 3)) 70 | lognormal(x, 0.9, 0.9) 71 | 72 | 73 | if __name__ == '__main__': 74 | np.random.seed(121) 75 | unittest.main() 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | ### toleranceinterval 4 | 5 | A small Python library for one-sided tolerance bounds and two-sided tolerance intervals. 6 | 7 | [![Build Status](https://travis-ci.com/cjekel/tolerance_interval_py.svg?branch=master)](https://travis-ci.com/cjekel/tolerance_interval_py) [![codecov](https://codecov.io/gh/cjekel/tolerance_interval_py/branch/master/graph/badge.svg?token=K7JGW0PXHU)](https://codecov.io/gh/cjekel/tolerance_interval_py) 8 | 9 | # Methods 10 | 11 | Checkout the [documentation](https://jekel.me/tolerance_interval_py/index.html). This is what has been implemented so far: 12 | 13 | ## twoside 14 | 15 | - normal 16 | - normal_factor 17 | - lognormal 18 | 19 | ## oneside 20 | 21 | - normal 22 | - lognormal 23 | - non_parametric 24 | - hanson_koopmans 25 | - hanson_koopmans_cmh 26 | 27 | # Requirements 28 | 29 | ```Python 30 | "numpy >= 1.14.0" 31 | "scipy >= 0.19.0" 32 | "sympy >= 1.4" 33 | "setuptools >= 38.6.0" 34 | ``` 35 | # Installation 36 | 37 | ``` 38 | python -m pip install toleranceinterval 39 | ``` 40 | 41 | or clone and install from source 42 | 43 | ``` 44 | git clone https://github.com/cjekel/tolerance_interval_py 45 | python -m pip install ./tolerance_interval_py 46 | ``` 47 | 48 | # Examples 49 | 50 | The syntax follows ```(x, p, g)```, where ```x``` is the random sample, ```p``` is the percentile, and ```g``` is the confidence level. Here ```x``` can be a single set of random samples, or sets of random samples of the same size. 51 | 52 | Estimate the 10th percentile to 95% confidence, of a random sample ```x``` using the Hanson and Koopmans 1964 method. 53 | 54 | ```python 55 | import numpy as np 56 | import toleranceinterval as ti 57 | x = np.random.random(100) 58 | bound = ti.oneside.hanson_koopmans(x, 0.1, 0.95) 59 | print(bound) 60 | ``` 61 | 62 | Estimate the central 90th percentile to 95% confidence, of a random sample ```x``` assuming ```x``` follows a Normal distribution. 63 | 64 | ```python 65 | import numpy as np 66 | import toleranceinterval as ti 67 | x = np.random.random(100) 68 | bound = ti.twoside.normal(x, 0.9, 0.95) 69 | print('Lower bound:', bound[:, 0]) 70 | print('Upper bound:', bound[:, 1]) 71 | ``` 72 | 73 | All methods will allow you to specify sets of samples as 2-D numpy arrays. The caveat here is that each set must be the same size. This example estimates the 95th percentile to 90% confidence using the non-parametric method. Here ```x``` will be 7 random sample sets, where each set is of 500 random samples. 74 | 75 | ```python 76 | import numpy as np 77 | import toleranceinterval as ti 78 | x = np.random.random((7, 500)) 79 | bound = ti.oneside.non_parametric(x, 0.95, 0.9) 80 | # here bound will print for each set of n=500 samples 81 | print('Bounds:', bound) 82 | ``` 83 | 84 | # Changelog 85 | 86 | Changes will be stored in [CHANGELOG.md](https://github.com/cjekel/tolerance_interval_py/blob/master/CHANGELOG.md). 87 | 88 | # Contributing 89 | 90 | All contributions are welcome! Please let me know if you have any questions, or run into any issues. 91 | 92 | # License 93 | 94 | MIT License 95 | 96 | -------------------------------------------------------------------------------- /tests/test_oneside_nonparmetric.py: -------------------------------------------------------------------------------- 1 | # -- coding: utf-8 -- 2 | import numpy as np 3 | from toleranceinterval.oneside import non_parametric 4 | import unittest 5 | 6 | 7 | class TestEverything(unittest.TestCase): 8 | 9 | # Tabuluar values from Table J11 from: 10 | # Meeker, W.Q., Hahn, G.J. and Escobar, L.A., 2017. Statistical intervals: 11 | # A guide for practitioners and researchers (Vol. 541). John Wiley & Sons. 12 | 13 | sample_sizes = np.array([10, 15, 20, 25, 30, 35, 40, 50, 60, 80, 100, 200, 14 | 300, 400, 500, 600, 800, 1000]) 15 | P = np.array([0.75, 0.75, 0.75, 0.90, 0.90, 0.90, 0.95, 0.95, 0.95, 0.99, 16 | 0.99, 0.99]) 17 | G = np.array([0.90, 0.95, 0.99, 0.90, 0.95, 0.99, 0.90, 0.95, 0.99, 0.90, 18 | 0.95, 0.99]) 19 | K = np.array([1, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, 20 | np.nan, np.nan, np.nan, np.nan, 2, 1, np.nan, np.nan, np.nan, 21 | np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, 3, 2, 22 | 1, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, 23 | np.nan, np.nan, 4, 3, 2, 1, np.nan, np.nan, np.nan, np.nan, 24 | np.nan, np.nan, np.nan, np.nan, 5, 4, 2, 1, 1, np.nan, 25 | np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, 6, 5, 3, 1, 26 | 1, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, 7, 27 | 6, 4, 2, 1, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, 28 | np.nan, 9, 8, 6, 2, 2, 1, 1, np.nan, np.nan, np.nan, np.nan, 29 | np.nan, 11, 10, 8, 3, 2, 1, 1, 1, np.nan, np.nan, np.nan, 30 | np.nan, 15, 14, 11, 5, 4, 2, 2, 1, np.nan, np.nan, np.nan, 31 | np.nan, 20, 18, 15, 6, 5, 4, 2, 2, 1, np.nan, np.nan, np.nan, 32 | 42, 40, 36, 15, 13, 11, 6, 5, 4, np.nan, np.nan, np.nan, 65, 33 | 63, 58, 23, 22, 19, 10, 9, 7, 1, 1, np.nan, 89, 86, 80, 32, 34 | 30, 27, 15, 13, 11, 2, 1, np.nan, 113, 109, 103, 41, 39, 35, 35 | 19, 17, 14, 2, 2, 1, 136, 133, 126, 51, 48, 44, 23, 21, 18, 36 | 3, 2, 1, 184, 180, 172, 69, 66, 61, 32, 30, 26, 5, 4, 2, 233, 37 | 228, 219, 88, 85, 79, 41, 39, 35, 6, 5, 3]) - 1. 38 | K = K.reshape(sample_sizes.size, P.size) 39 | 40 | def test_upper_table_bounds(self): 41 | for i, row in enumerate(self.K): 42 | n = self.sample_sizes[i] 43 | x = np.arange(n) 44 | for j, k in enumerate(row): 45 | k = n - k - 1 46 | p = self.P[j] 47 | g = self.G[j] 48 | bound = non_parametric(x, p, g)[0] 49 | if np.isnan(k) and np.isnan(bound): 50 | self.assertTrue(True) 51 | else: 52 | self.assertEqual(k, bound) 53 | 54 | def test_lower_table_bounds(self): 55 | for i, row in enumerate(self.K): 56 | n = self.sample_sizes[i] 57 | x = np.arange(n) 58 | for j, k in enumerate(row): 59 | p = 1.0 - self.P[j] 60 | g = self.G[j] 61 | bound = non_parametric(x, p, g)[0] 62 | if np.isnan(k) and np.isnan(bound): 63 | self.assertTrue(True) 64 | else: 65 | self.assertEqual(k, bound) 66 | 67 | def test_random_shapes(self): 68 | M = [3, 10, 20] 69 | N = [5, 10, 20] 70 | for m in M: 71 | for n in N: 72 | x = np.random.random((m, n)) 73 | bounds = non_parametric(x, 0.1, 0.95) 74 | _m = bounds.size 75 | self.assertTrue(_m == m) 76 | 77 | def test_value_error(self): 78 | with self.assertRaises(ValueError): 79 | x = np.random.random((1, 2, 4, 3)) 80 | non_parametric(x, 0.1, 0.9) 81 | 82 | 83 | if __name__ == '__main__': 84 | np.random.seed(121) 85 | unittest.main() 86 | -------------------------------------------------------------------------------- /toleranceinterval/twoside/_normal_approx.py: -------------------------------------------------------------------------------- 1 | # -- coding: utf-8 -- 2 | # MIT License 3 | # 4 | # Copyright (c) 2019 Charles Jekel 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | r""" 25 | Algorithms for computing approximate two-sided statistical tolerance interval 26 | factors under the assumption of a normal distribution. 27 | 28 | """ 29 | 30 | import numpy as np 31 | from scipy.stats import norm, chi2 32 | 33 | 34 | def tolerance_factor_howe(n, p, g, m=None, nu=None): 35 | r""" 36 | Compute two-side central tolerance interval factor using Howe's method. 37 | 38 | Computes the two-sided tolerance interval (TI) factor under a normal 39 | distribution assumption using Howe's method. This follows the derivation 40 | in [1]. This is an approximation, and does not represent the exact TI. 41 | 42 | Parameters 43 | ---------- 44 | n : scalar 45 | Sample size. 46 | p : float 47 | Percentile for central TI to estimate. 48 | g : float 49 | Confidence level where g > 0. and g < 1. 50 | m : scalar 51 | Number of independent random samples (of size n). If None, 52 | default value is m = 1. 53 | nu : scalar 54 | Degrees of freedom for distribution of the (pooled) sample 55 | variance. If None, default value is nu = m*(n-1). 56 | 57 | Returns 58 | ------- 59 | float 60 | The calculated tolerance factor for the tolerance interval. 61 | 62 | References 63 | ---------- 64 | [1] Howe, W. G. (1969). "Two-sided Tolerance Limits for Normal 65 | Populations - Some Improvements", Journal of the American Statistical 66 | Association, 64 , pages 610-620. 67 | 68 | """ 69 | # Handle defaults for keyword inputs. 70 | if m is None: 71 | m = 1 72 | if nu is None: 73 | nu = m * (n - 1) 74 | 75 | alpha = 1.0 - g 76 | zp = norm.ppf((1.0 + p) / 2.0) 77 | u = zp * np.sqrt(1.0 + (1.0 / n)) 78 | chi2_nu = chi2.ppf(alpha, df=nu) 79 | v = np.sqrt(nu / chi2_nu) 80 | k = u * v 81 | return k 82 | 83 | 84 | def tolerance_factor_guenther(n, p, g, m=None, nu=None): 85 | r""" 86 | Compute two-side central tolerance interval factor using Guenther's method. 87 | 88 | Computes the two-sided tolerance interval (TI) factor under a normal 89 | distribution assumption using Guenthers's method. This follows the 90 | derivation in [1]. This is an approximation, and does not represent the 91 | exact TI. 92 | 93 | Parameters 94 | ---------- 95 | n : scalar 96 | Sample size. 97 | p : float 98 | Percentile for central TI to estimate. 99 | g : float 100 | Confidence level where g > 0. and g < 1. 101 | m : scalar 102 | Number of independent random samples (of size n). If None, 103 | default value is m = 1. 104 | nu : scalar 105 | Degrees of freedom for distribution of the (pooled) sample 106 | variance. If None, default value is nu = m*(n-1). 107 | 108 | Returns 109 | ------- 110 | float 111 | The calculated tolerance factor for the tolerance interval. 112 | 113 | References 114 | ---------- 115 | [1] Guenther, W. C. (1977). "Sampling Inspection in Statistical Quality 116 | Control", Griffin's Statistical Monographs, Number 37, London. 117 | 118 | """ 119 | # Handle defaults for keyword inputs. 120 | if m is None: 121 | m = 1 122 | if nu is None: 123 | nu = m * (n - 1) 124 | 125 | k = tolerance_factor_howe(n, p, g, m, nu) 126 | alpha = 1.0 - g 127 | chi2_nu = chi2.ppf(alpha, df=nu) 128 | w = np.sqrt(1.0 + ((n - 3.0 - chi2_nu) / (2.0 * (n + 1.0) ** 2))) 129 | k *= w 130 | return k 131 | -------------------------------------------------------------------------------- /tests/test_oneside_lognormal.py: -------------------------------------------------------------------------------- 1 | # -- coding: utf-8 -- 2 | import numpy as np 3 | from toleranceinterval.oneside import lognormal 4 | import unittest 5 | 6 | np.random.seed(1212) 7 | 8 | 9 | class TestEverything(unittest.TestCase): 10 | 11 | # Tabuluar values from Table XII in from: 12 | # Montgomery, D. C., & Runger, G. C. (2018). Chapter 8. Statistical 13 | # Intervals for a Single Sample. In Applied Statistics and Probability 14 | # for Engineers, 7th Edition. 15 | 16 | # Note there was a mistake in n=20, p=0.95, g=0.9.... 17 | sample_sizes = np.array([2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 18 | 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 30, 40, 50, 19 | 60, 70, 80, 90, 100]) 20 | P = np.array([0.90, 0.95, 0.99, 0.90, 0.95, 0.99, 0.90, 0.95, 0.99]) 21 | G = np.array([0.90, 0.90, 0.90, 0.95, 0.95, 0.95, 0.99, 0.99, 0.99]) 22 | K = np.array([10.253, 13.090, 18.500, 20.581, 26.260, 37.094, 103.029, 23 | 131.426, 185.617, 4.258, 5.311, 7.340, 6.155, 7.656, 10.553, 24 | 13.995, 17.370, 23.896, 3.188, 3.957, 5.438, 4.162, 5.144, 25 | 7.042, 7.380, 9.083, 12.387, 2.742, 3.400, 4.666, 3.407, 26 | 4.203, 5.741, 5.362, 6.578, 8.939, 2.494, 3.092, 4.243, 27 | 3.006, 3.708, 5.062, 4.411, 5.406, 7.335, 2.333, 2.894, 28 | 3.972, 2.755, 3.399, 4.642, 3.859, 4.728, 6.412, 2.219, 29 | 2.754, 3.783, 2.582, 3.187, 4.354, 3.497, 4.285, 5.812, 30 | 2.133, 2.650, 3.641, 2.454, 3.031, 4.143, 3.240, 3.972, 31 | 5.389, 2.066, 2.568, 3.532, 2.355, 2.911, 3.981, 3.048, 32 | 3.738, 5.074, 2.011, 2.503, 3.443, 2.275, 2.815, 3.852, 33 | 2.898, 3.556, 4.829, 1.966, 2.448, 3.371, 2.210, 2.736, 34 | 3.747, 2.777, 3.410, 4.633, 1.928, 2.402, 3.309, 2.155, 35 | 2.671, 3.659, 2.677, 3.290, 4.472, 1.895, 2.363, 3.257, 36 | 2.109, 2.614, 3.585, 2.593, 3.189, 4.337, 1.867, 2.329, 37 | 3.212, 2.068, 2.566, 3.520, 2.521, 3.102, 4.222, 1.842, 38 | 2.299, 3.172, 2.033, 2.524, 3.464, 2.459, 3.028, 4.123, 39 | 1.819, 2.272, 3.137, 2.002, 2.486, 3.414, 2.405, 2.963, 40 | 4.037, 1.800, 2.249, 3.105, 1.974, 2.453, 3.370, 2.357, 41 | 2.905, 3.960, 1.782, 2.227, 3.077, 1.949, 2.423, 3.331, 42 | 2.314, 2.854, 3.892, 1.765, 2.208, 3.052, 1.926, 2.396, 43 | 3.295, 2.276, 2.808, 3.832, 1.750, 2.190, 3.028, 1.905, 44 | 2.371, 3.263, 2.241, 2.766, 3.777, 1.737, 2.174, 3.007, 45 | 1.886, 2.349, 3.233, 2.209, 2.729, 3.727, 1.724, 2.159, 46 | 2.987, 1.869, 2.328, 3.206, 2.180, 2.694, 3.681, 1.712, 47 | 2.145, 2.969, 1.853, 2.309, 3.181, 2.154, 2.662, 3.640, 48 | 1.702, 2.132, 2.952, 1.838, 2.292, 3.158, 2.129, 2.633, 49 | 3.601, 1.657, 2.080, 2.884, 1.777, 2.220, 3.064, 2.030, 50 | 2.515, 3.447, 1.598, 2.010, 2.793, 1.697, 2.125, 2.941, 51 | 1.902, 2.364, 3.249, 1.559, 1.965, 2.735, 1.646, 2.065, 52 | 2.862, 1.821, 2.269, 3.125, 1.532, 1.933, 2.694, 1.609, 53 | 2.022, 2.807, 1.764, 2.202, 3.038, 1.511, 1.909, 2.662, 54 | 1.581, 1.990, 2.765, 1.722, 2.153, 2.974, 1.495, 1.890, 55 | 2.638, 1.559, 1.964, 2.733, 1.688, 2.114, 2.924, 1.481, 56 | 1.874, 2.618, 1.542, 1.944, 2.706, 1.661, 2.082, 2.883, 57 | 1.470, 1.861, 2.601, 1.527, 1.927, 2.684, 1.639, 2.056, 58 | 2.850]) 59 | 60 | K = K.reshape(sample_sizes.size, P.size) 61 | 62 | def test_upper_montgomery_bounds(self): 63 | for i, row in enumerate(self.K): 64 | n = self.sample_sizes[i] 65 | x = np.random.random(n) 66 | xmu = np.mean(np.log(x)) 67 | xstd = np.std(np.log(x), ddof=1) 68 | for j, k in enumerate(row): 69 | p = self.P[j] 70 | g = self.G[j] 71 | bound = lognormal(x, p, g) 72 | k_hat = (np.log(bound) - xmu)/xstd 73 | self.assertTrue(np.isclose(k, k_hat[0], rtol=1e-3, atol=1e-4)) 74 | 75 | def test_lower_montgomery_bounds(self): 76 | for i, row in enumerate(self.K): 77 | n = self.sample_sizes[i] 78 | x = np.random.random(n) 79 | xmu = np.mean(np.log(x)) 80 | xstd = np.std(np.log(x), ddof=1) 81 | for j, k in enumerate(row): 82 | p = 1.0 - self.P[j] 83 | g = self.G[j] 84 | bound = lognormal(x, p, g) 85 | k_hat = (xmu - np.log(bound))/xstd 86 | self.assertTrue(np.isclose(k, k_hat[0], rtol=1e-2, atol=1e-3)) 87 | 88 | def test_random_shapes(self): 89 | M = [3, 10, 20] 90 | N = [5, 10, 20] 91 | for m in M: 92 | for n in N: 93 | x = np.random.random((m, n)) 94 | bounds = lognormal(x, 0.1, 0.95) 95 | _m = bounds.size 96 | self.assertTrue(_m == m) 97 | 98 | def test_value_error(self): 99 | with self.assertRaises(ValueError): 100 | x = np.random.random((1, 2, 4, 3)) 101 | lognormal(x, 0.1, 0.9) 102 | 103 | 104 | if __name__ == '__main__': 105 | np.random.seed(121) 106 | unittest.main() 107 | -------------------------------------------------------------------------------- /tests/test_oneside_normal.py: -------------------------------------------------------------------------------- 1 | # -- coding: utf-8 -- 2 | import numpy as np 3 | from toleranceinterval.oneside import normal 4 | import unittest 5 | 6 | 7 | class TestEverything(unittest.TestCase): 8 | 9 | # Tabuluar values from Table XII in from: 10 | # Montgomery, D. C., & Runger, G. C. (2018). Chapter 8. Statistical 11 | # Intervals for a Single Sample. In Applied Statistics and Probability 12 | # for Engineers, 7th Edition. 13 | 14 | # Note there was a mistake in n=20, p=0.95, g=0.9.... 15 | sample_sizes = np.array([2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 | 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 30, 40, 50, 17 | 60, 70, 80, 90, 100]) 18 | P = np.array([0.90, 0.95, 0.99, 0.90, 0.95, 0.99, 0.90, 0.95, 0.99]) 19 | G = np.array([0.90, 0.90, 0.90, 0.95, 0.95, 0.95, 0.99, 0.99, 0.99]) 20 | K = np.array([10.253, 13.090, 18.500, 20.581, 26.260, 37.094, 103.029, 21 | 131.426, 185.617, 4.258, 5.311, 7.340, 6.155, 7.656, 10.553, 22 | 13.995, 17.370, 23.896, 3.188, 3.957, 5.438, 4.162, 5.144, 23 | 7.042, 7.380, 9.083, 12.387, 2.742, 3.400, 4.666, 3.407, 24 | 4.203, 5.741, 5.362, 6.578, 8.939, 2.494, 3.092, 4.243, 25 | 3.006, 3.708, 5.062, 4.411, 5.406, 7.335, 2.333, 2.894, 26 | 3.972, 2.755, 3.399, 4.642, 3.859, 4.728, 6.412, 2.219, 27 | 2.754, 3.783, 2.582, 3.187, 4.354, 3.497, 4.285, 5.812, 28 | 2.133, 2.650, 3.641, 2.454, 3.031, 4.143, 3.240, 3.972, 29 | 5.389, 2.066, 2.568, 3.532, 2.355, 2.911, 3.981, 3.048, 30 | 3.738, 5.074, 2.011, 2.503, 3.443, 2.275, 2.815, 3.852, 31 | 2.898, 3.556, 4.829, 1.966, 2.448, 3.371, 2.210, 2.736, 32 | 3.747, 2.777, 3.410, 4.633, 1.928, 2.402, 3.309, 2.155, 33 | 2.671, 3.659, 2.677, 3.290, 4.472, 1.895, 2.363, 3.257, 34 | 2.109, 2.614, 3.585, 2.593, 3.189, 4.337, 1.867, 2.329, 35 | 3.212, 2.068, 2.566, 3.520, 2.521, 3.102, 4.222, 1.842, 36 | 2.299, 3.172, 2.033, 2.524, 3.464, 2.459, 3.028, 4.123, 37 | 1.819, 2.272, 3.137, 2.002, 2.486, 3.414, 2.405, 2.963, 38 | 4.037, 1.800, 2.249, 3.105, 1.974, 2.453, 3.370, 2.357, 39 | 2.905, 3.960, 1.782, 2.227, 3.077, 1.949, 2.423, 3.331, 40 | 2.314, 2.854, 3.892, 1.765, 2.208, 3.052, 1.926, 2.396, 41 | 3.295, 2.276, 2.808, 3.832, 1.750, 2.190, 3.028, 1.905, 42 | 2.371, 3.263, 2.241, 2.766, 3.777, 1.737, 2.174, 3.007, 43 | 1.886, 2.349, 3.233, 2.209, 2.729, 3.727, 1.724, 2.159, 44 | 2.987, 1.869, 2.328, 3.206, 2.180, 2.694, 3.681, 1.712, 45 | 2.145, 2.969, 1.853, 2.309, 3.181, 2.154, 2.662, 3.640, 46 | 1.702, 2.132, 2.952, 1.838, 2.292, 3.158, 2.129, 2.633, 47 | 3.601, 1.657, 2.080, 2.884, 1.777, 2.220, 3.064, 2.030, 48 | 2.515, 3.447, 1.598, 2.010, 2.793, 1.697, 2.125, 2.941, 49 | 1.902, 2.364, 3.249, 1.559, 1.965, 2.735, 1.646, 2.065, 50 | 2.862, 1.821, 2.269, 3.125, 1.532, 1.933, 2.694, 1.609, 51 | 2.022, 2.807, 1.764, 2.202, 3.038, 1.511, 1.909, 2.662, 52 | 1.581, 1.990, 2.765, 1.722, 2.153, 2.974, 1.495, 1.890, 53 | 2.638, 1.559, 1.964, 2.733, 1.688, 2.114, 2.924, 1.481, 54 | 1.874, 2.618, 1.542, 1.944, 2.706, 1.661, 2.082, 2.883, 55 | 1.470, 1.861, 2.601, 1.527, 1.927, 2.684, 1.639, 2.056, 56 | 2.850]) 57 | 58 | K = K.reshape(sample_sizes.size, P.size) 59 | 60 | def test_upper_montgomery_bounds(self): 61 | for i, row in enumerate(self.K): 62 | n = self.sample_sizes[i] 63 | x = np.random.random(n) 64 | xmu = x.mean() 65 | xstd = x.std(ddof=1) 66 | for j, k in enumerate(row): 67 | p = self.P[j] 68 | g = self.G[j] 69 | bound = normal(x, p, g) 70 | k_hat = (bound - xmu)/xstd 71 | self.assertTrue(np.isclose(k, k_hat[0], rtol=1e-3, atol=1e-4)) 72 | 73 | def test_lower_montgomery_bounds(self): 74 | for i, row in enumerate(self.K): 75 | n = self.sample_sizes[i] 76 | x = np.random.random(n) 77 | xmu = x.mean() 78 | xstd = x.std(ddof=1) 79 | for j, k in enumerate(row): 80 | p = 1.0 - self.P[j] 81 | g = self.G[j] 82 | bound = normal(x, p, g) 83 | k_hat = (xmu - bound)/xstd 84 | self.assertTrue(np.isclose(k, k_hat[0], rtol=1e-3, atol=1e-4)) 85 | 86 | def test_random_shapes(self): 87 | M = [3, 10, 20] 88 | N = [5, 10, 20] 89 | for m in M: 90 | for n in N: 91 | x = np.random.random((m, n)) 92 | bounds = normal(x, 0.1, 0.95) 93 | _m = bounds.size 94 | self.assertTrue(_m == m) 95 | 96 | def test_value_error(self): 97 | with self.assertRaises(ValueError): 98 | x = np.random.random((1, 2, 4, 3)) 99 | normal(x, 0.1, 0.9) 100 | 101 | def test_lists(self): 102 | M = [3, 5] 103 | N = [5, 7] 104 | for m in M: 105 | for n in N: 106 | x = np.random.random((m, n)) 107 | bounds = normal(list(x), 0.1, 0.95) 108 | _m = bounds.size 109 | self.assertTrue(_m == m) 110 | 111 | 112 | if __name__ == '__main__': 113 | np.random.seed(121) 114 | unittest.main() 115 | -------------------------------------------------------------------------------- /tests/test_oneside_hansonkoopmans_cmh.py: -------------------------------------------------------------------------------- 1 | # -- coding: utf-8 -- 2 | import numpy as np 3 | from toleranceinterval.oneside import hanson_koopmans_cmh 4 | import unittest 5 | 6 | 7 | class TestEverything(unittest.TestCase): 8 | 9 | # B and A basis values from: 10 | # Volume 1: Guidelines for Characterization of Structural Materials. 11 | # (2017). In Composite Materials Handbook. SAE International. 12 | 13 | b_range = [35.177, 7.859, 4.505, 4.101, 3.064, 2.858, 2.382, 2.253, 2.137, 14 | 1.897, 1.814, 1.738, 1.599, 1.540, 1.485, 1.434, 1.354, 1.311, 15 | 1.253, 1.218, 1.184, 1.143, 1.114, 1.087, 1.060, 1.035, 1.010] 16 | n_range_b = list(range(2, 29)) 17 | j_range = [2, 3, 4, 4, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 8, 9, 9, 10, 10, 18 | 10, 11, 11, 11, 11, 11, 12] 19 | a_range = [80.00380, 16.91220, 9.49579, 6.89049, 5.57681, 4.78352, 4.25011, 20 | 3.86502, 3.57267, 3.34227, 3.15540, 3.00033, 2.86924, 2.75672, 21 | 2.65889, 2.57290, 2.49660, 2.42833, 2.36683, 2.31106, 2.26020, 22 | 2.21359, 2.17067, 2.13100, 2.09419, 2.05991, 2.02790, 1.99791, 23 | 1.96975, 1.94324, 1.91822, 1.89457, 1.87215, 1.85088, 1.83065, 24 | 1.81139, 1.79301, 1.77546, 1.75868, 1.74260, 1.72718, 1.71239, 25 | 1.69817, 1.68449, 1.67132, 1.65862, 1.64638, 1.63456, 1.62313, 26 | 1.60139, 1.58101, 1.56184, 1.54377, 1.52670, 1.51053, 1.49520, 27 | 1.48063, 1.46675, 1.45352, 1.44089, 1.42881, 1.41724, 1.40614, 28 | 1.39549, 1.38525, 1.37541, 1.36592, 1.35678, 1.34796, 1.33944, 29 | 1.33120, 1.32324, 1.31553, 1.30806, 1.29036, 1.27392, 1.25859, 30 | 1.24425, 1.23080, 1.21814, 1.20620, 1.19491, 1.18421, 1.17406, 31 | 1.16440, 1.15519, 1.14640, 1.13801, 1.12997, 1.12226, 1.11486, 32 | 1.10776, 1.10092, 1.09434, 1.08799, 1.08187, 1.07595, 1.07024, 33 | 1.06471, 1.05935, 1.05417, 1.04914, 1.04426, 1.03952, 1.01773] 34 | n_range0 = np.arange(2, 51) 35 | n_range1 = np.arange(52, 102, 2) 36 | n_range2 = np.arange(105, 255, 5) 37 | n_range3 = [275] 38 | n_range = np.concatenate((n_range0, n_range1, n_range2, n_range3)) 39 | 40 | def test_b_basis(self): 41 | p = 0.1 42 | g = 0.95 43 | for i, b in enumerate(self.b_range): 44 | j = self.j_range[i]-1 45 | n = self.n_range_b[i] 46 | x = np.random.random(n) 47 | x.sort() 48 | bound = hanson_koopmans_cmh(x, p, g, j=j)[0] 49 | b_ = np.log(bound / x[j]) / np.log(x[0]/x[j]) 50 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 51 | 52 | def test_a_basis(self): 53 | p = 0.01 54 | g = 0.95 55 | for i, b in enumerate(self.a_range): 56 | n = self.n_range[i] 57 | j = n-1 58 | x = np.random.random(n) 59 | x.sort() 60 | bound = hanson_koopmans_cmh(x, p, g)[0] 61 | b_ = np.log(bound / x[j]) / np.log(x[0]/x[j]) 62 | self.assertTrue(np.isclose(b, b_, rtol=1e-4, atol=1e-5)) 63 | 64 | def test_random_shapes(self): 65 | M = [3, 10, 20] 66 | N = [5, 10, 20] 67 | J = [1, 2] 68 | for m in M: 69 | for n in N: 70 | for j in J: 71 | x = np.random.random((m, n)) 72 | bounds = hanson_koopmans_cmh(x, 0.1, 0.95, j=j) 73 | _m = bounds.size 74 | self.assertTrue(_m == m) 75 | 76 | def test_value_error_shape(self): 77 | with self.assertRaises(ValueError): 78 | x = np.random.random((1, 2, 4, 3)) 79 | hanson_koopmans_cmh(x, 0.1, 0.9) 80 | 81 | def test_value_error_upper(self): 82 | with self.assertRaises(ValueError): 83 | x = np.random.random((10, 10)) 84 | hanson_koopmans_cmh(x, 0.9, 0.95) 85 | 86 | def test_step_size(self): 87 | p = 0.1 88 | g = 0.95 89 | i = 0 90 | b = self.b_range[i] 91 | j = self.j_range[i]-1 92 | n = self.n_range_b[i] 93 | x = np.random.random(n) 94 | x.sort() 95 | bound = hanson_koopmans_cmh(x, p, g, j=j, step_size=1e-5)[0] 96 | b_ = np.log(bound / x[j]) / np.log(x[0]/x[j]) 97 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 98 | 99 | def test_new_raphson(self): 100 | p = 0.1 101 | g = 0.95 102 | i = 0 103 | b = self.b_range[i] 104 | j = self.j_range[i]-1 105 | n = self.n_range_b[i] 106 | x = np.random.random(n) 107 | x.sort() 108 | bound = hanson_koopmans_cmh(x, p, g, j=j, method='newton-raphson')[0] 109 | b_ = np.log(bound / x[j]) / np.log(x[0]/x[j]) 110 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 111 | bound = hanson_koopmans_cmh(x, p, g, j=j, method='newton-raphson', 112 | max_iter=50)[0] 113 | b_ = np.log(bound / x[j]) / np.log(x[0]/x[j]) 114 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 115 | bound = hanson_koopmans_cmh(x, p, g, j=j, method='newton-raphson', 116 | tol=1e-6)[0] 117 | b_ = np.log(bound / x[j]) / np.log(x[0]/x[j]) 118 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 119 | 120 | def test_halley(self): 121 | p = 0.1 122 | g = 0.95 123 | i = 0 124 | b = self.b_range[i] 125 | j = self.j_range[i]-1 126 | n = self.n_range_b[i] 127 | x = np.random.random(n) 128 | x.sort() 129 | bound = hanson_koopmans_cmh(x, p, g, j=j, method='halley')[0] 130 | b_ = np.log(bound / x[j]) / np.log(x[0]/x[j]) 131 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 132 | bound = hanson_koopmans_cmh(x, p, g, j=j, method='halley', 133 | max_iter=50)[0] 134 | b_ = np.log(bound / x[j]) / np.log(x[0]/x[j]) 135 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 136 | bound = hanson_koopmans_cmh(x, p, g, j=j, method='halley', 137 | tol=1e-6)[0] 138 | b_ = np.log(bound / x[j]) / np.log(x[0]/x[j]) 139 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 140 | 141 | def test_fall_back(self): 142 | p = 0.01 143 | g = 0.95 144 | n = 300 145 | x = np.random.random(n) 146 | x.sort() 147 | bound = hanson_koopmans_cmh(x, p, g)[0] 148 | self.assertTrue(np.isclose(bound, x[0])) 149 | 150 | 151 | if __name__ == '__main__': 152 | np.random.seed(121) 153 | unittest.main() 154 | -------------------------------------------------------------------------------- /docs/twoside/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | toleranceinterval.twoside API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module toleranceinterval.twoside

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
from .twoside import normal_factor  # noqa F401
30 | from .twoside import normal  # noqa F401
31 | from .twoside import lognormal  # noqa F401
32 |
33 |
34 |
35 |

Sub-modules

36 |
37 |
toleranceinterval.twoside.twoside
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 68 |
69 | 72 | 73 | -------------------------------------------------------------------------------- /docs/oneside/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | toleranceinterval.oneside API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module toleranceinterval.oneside

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
from .oneside import normal  # noqa F401
30 | from .oneside import lognormal  # noqa F401
31 | from .oneside import non_parametric  # noqa F401
32 | from .oneside import hanson_koopmans  # noqa F401
33 | from .oneside import hanson_koopmans_cmh  # noqa F401
34 |
35 |
36 |
37 |

Sub-modules

38 |
39 |
toleranceinterval.oneside.oneside
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | 70 |
71 | 74 | 75 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | toleranceinterval API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Package toleranceinterval

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
from . import oneside  # noqa F401
30 | from . import twoside  # noqa F401
31 | from . import hk  # noqa F401
32 | from . import checks  # noqa F401
33 | import os as _os  # noqa F401
34 | 
35 | # add rudimentary version tracking
36 | __VERSION_FILE__ = _os.path.join(_os.path.dirname(__file__), 'VERSION')
37 | __version__ = open(__VERSION_FILE__).read().strip()
38 |
39 |
40 |
41 |

Sub-modules

42 |
43 |
toleranceinterval.checks
44 |
45 |
46 |
47 |
toleranceinterval.hk
48 |
49 |
50 |
51 |
toleranceinterval.oneside
52 |
53 |
54 |
55 |
toleranceinterval.twoside
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | 84 |
85 | 88 | 89 | -------------------------------------------------------------------------------- /tests/test_oneside_hansonkoopmans.py: -------------------------------------------------------------------------------- 1 | # -- coding: utf-8 -- 2 | import numpy as np 3 | from toleranceinterval.oneside import hanson_koopmans 4 | import unittest 5 | 6 | 7 | class TestEverything(unittest.TestCase): 8 | 9 | # Values from: 10 | # Hanson, D. L., & Koopmans, L. H. (1964). Tolerance Limits for 11 | # the Class of Distributions with Increasing Hazard Rates. Ann. Math. 12 | # Statist., 35(4), 1561-1570. https://doi.org/10.1214/aoms/1177700380 13 | # 14 | # data[:, [n, p, g, b]] 15 | 16 | data = np.array([[2, 0.25, 0.9, 8.618], 17 | [2, 0.25, 0.95, 17.80], 18 | [2, 0.25, 0.99, 91.21], 19 | [3, 0.25, 0.90, 5.898], 20 | [3, 0.25, 0.95, 12.27], 21 | [3, 0.25, 0.99, 63.17], 22 | [4, 0.25, 0.90, 4.116], 23 | [4, 0.25, 0.95, 8.638], 24 | [4, 0.25, 0.99, 44.78], 25 | [5, 0.25, 0.90, 2.898], 26 | [5, 0.25, 0.95, 6.154], 27 | [5, 0.25, 0.99, 32.17], 28 | [6, 0.25, 0.90, 2.044], 29 | [6, 0.25, 0.95, 4.411], 30 | [6, 0.25, 0.99, 23.31], 31 | [7, 0.25, 0.90, 1.437], 32 | [7, 0.25, 0.95, 3.169], 33 | [7, 0.25, 0.99, 16.98], 34 | [8, 0.25, 0.90, 1.001], 35 | [8, 0.25, 0.95, 2.275], 36 | [8, 0.25, 0.99, 12.42], 37 | [9, 0.25, 0.95, 1.627], 38 | [9, 0.25, 0.99, 9.100], 39 | [2, 0.10, 0.90, 17.09], 40 | [2, 0.10, 0.95, 35.18], 41 | [2, 0.10, 0.99, 179.8], 42 | [3, 0.10, 0.90, 13.98], 43 | [3, 0.10, 0.95, 28.82], 44 | [3, 0.10, 0.99, 147.5], 45 | [4, 0.10, 0.90, 11.70], 46 | [4, 0.10, 0.95, 24.17], 47 | [4, 0.10, 0.99, 123.9], 48 | [5, 0.10, 0.90, 9.931], 49 | [5, 0.10, 0.95, 20.57], 50 | [5, 0.10, 0.99, 105.6], 51 | [6, 0.10, 0.90, 8.512], 52 | [6, 0.10, 0.95, 17.67], 53 | [6, 0.10, 0.99, 90.90], 54 | [7, 0.10, 0.90, 7.344], 55 | [7, 0.10, 0.95, 15.29], 56 | [7, 0.10, 0.99, 78.80], 57 | [8, 0.10, 0.90, 6.368], 58 | [8, 0.10, 0.95, 13.30], 59 | [8, 0.10, 0.99, 68.68], 60 | [9, 0.10, 0.90, 5.541], 61 | [9, 0.10, 0.95, 11.61], 62 | [9, 0.10, 0.99, 60.10], 63 | [2, 0.05, 0.90, 23.65], 64 | [2, 0.05, 0.95, 48.63], 65 | [2, 0.05, 0.99, 248.4], 66 | [3, 0.05, 0.90, 20.48], 67 | [3, 0.05, 0.95, 42.15], 68 | [3, 0.05, 0.99, 215.4], 69 | [4, 0.05, 0.90, 18.12], 70 | [4, 0.05, 0.95, 37.32], 71 | [4, 0.05, 0.99, 190.9], 72 | [5, 0.05, 0.90, 16.24], 73 | [5, 0.05, 0.95, 33.49], 74 | [5, 0.05, 0.99, 171.4], 75 | [6, 0.05, 0.90, 14.70], 76 | [6, 0.05, 0.95, 30.33], 77 | [6, 0.05, 0.99, 155.4], 78 | [7, 0.05, 0.90, 13.39], 79 | [7, 0.05, 0.95, 27.66], 80 | [7, 0.05, 0.99, 141.8], 81 | [8, 0.05, 0.90, 12.26], 82 | [8, 0.05, 0.95, 25.35], 83 | [8, 0.05, 0.99, 130.0], 84 | [9, 0.05, 0.90, 11.27], 85 | [9, 0.05, 0.95, 23.33], 86 | [9, 0.05, 0.99, 119.8], 87 | [20, 0.05, 0.90, 5.077], 88 | [20, 0.05, 0.95, 10.68], 89 | [20, 0.05, 0.99, 55.47]]) 90 | 91 | def test_upper_table_bounds(self): 92 | j = 1 93 | for i, row in enumerate(self.data): 94 | n = int(row[0]) 95 | p = 1.0-row[1] 96 | g = row[2] 97 | b = row[3] 98 | x = np.random.random(n) + 1000. 99 | x.sort() 100 | bound = hanson_koopmans(x, p, g, j=1)[0] 101 | b_ = (bound - x[n-j-1]) / (x[-1] - x[n-j-1]) 102 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 103 | 104 | def test_lower_table_bounds(self): 105 | j = 1 106 | for i, row in enumerate(self.data): 107 | n = int(row[0]) 108 | p = row[1] 109 | g = row[2] 110 | b = row[3] 111 | x = np.random.random(n) + 1000. 112 | x.sort() 113 | bound = hanson_koopmans(x, p, g, j=1)[0] 114 | b_ = (x[j] - bound) / (x[j] - x[0]) 115 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 116 | 117 | def test_random_shapes(self): 118 | M = [3, 10, 20] 119 | N = [5, 10, 20] 120 | J = [1, 2] 121 | for m in M: 122 | for n in N: 123 | for j in J: 124 | x = np.random.random((m, n)) 125 | bounds = hanson_koopmans(x, 0.1, 0.95, j=j) 126 | _m = bounds.size 127 | self.assertTrue(_m == m) 128 | 129 | def test_value_error(self): 130 | with self.assertRaises(ValueError): 131 | x = np.random.random((1, 2, 4, 3)) 132 | hanson_koopmans(x, 0.1, 0.9) 133 | 134 | def test_step_size(self): 135 | i = 0 136 | row = self.data[i] 137 | n = int(row[0]) 138 | j = n-1 139 | p = row[1] 140 | g = row[2] 141 | b = row[3] 142 | x = np.random.random(n) 143 | x.sort() 144 | bound = hanson_koopmans(x, p, g, step_size=1e-6)[0] 145 | b_ = (x[j] - bound) / (x[j] - x[0]) 146 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 147 | 148 | def test_new_raphson(self): 149 | i = 0 150 | row = self.data[i] 151 | n = int(row[0]) 152 | j = n-1 153 | p = row[1] 154 | g = row[2] 155 | b = row[3] 156 | x = np.random.random(n) 157 | x.sort() 158 | bound = hanson_koopmans(x, p, g, method='newton-raphson')[0] 159 | b_ = (x[j] - bound) / (x[j] - x[0]) 160 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 161 | bound = hanson_koopmans(x, p, g, method='newton-raphson', 162 | max_iter=50)[0] 163 | b_ = (x[j] - bound) / (x[j] - x[0]) 164 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 165 | bound = hanson_koopmans(x, p, g, method='newton-raphson', 166 | tol=1e-6)[0] 167 | b_ = (x[j] - bound) / (x[j] - x[0]) 168 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 169 | 170 | def test_halley(self): 171 | i = 0 172 | row = self.data[i] 173 | n = int(row[0]) 174 | j = n-1 175 | p = row[1] 176 | g = row[2] 177 | b = row[3] 178 | x = np.random.random(n) 179 | x.sort() 180 | bound = hanson_koopmans(x, p, g, method='halley')[0] 181 | b_ = (x[j] - bound) / (x[j] - x[0]) 182 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 183 | bound = hanson_koopmans(x, p, g, method='halley', max_iter=50)[0] 184 | b_ = (x[j] - bound) / (x[j] - x[0]) 185 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 186 | bound = hanson_koopmans(x, p, g, method='halley', tol=1e-6)[0] 187 | b_ = (x[j] - bound) / (x[j] - x[0]) 188 | self.assertTrue(np.isclose(b, b_, rtol=1e-3, atol=1e-4)) 189 | 190 | def test_fall_back(self): 191 | p = 0.01 192 | g = 0.95 193 | n = 300 194 | x = np.random.random(n) 195 | x.sort() 196 | bound = hanson_koopmans(x, p, g)[0] 197 | self.assertTrue(np.isclose(bound, x[0])) 198 | 199 | 200 | if __name__ == '__main__': 201 | np.random.seed(121) 202 | unittest.main() 203 | -------------------------------------------------------------------------------- /docs/checks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | toleranceinterval.checks API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module toleranceinterval.checks

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
# -- coding: utf-8 --
 30 | # MIT License
 31 | #
 32 | # Copyright (c) 2019 Charles Jekel
 33 | #
 34 | # Permission is hereby granted, free of charge, to any person obtaining a copy
 35 | # of this software and associated documentation files (the "Software"), to deal
 36 | # in the Software without restriction, including without limitation the rights
 37 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 38 | # copies of the Software, and to permit persons to whom the Software is
 39 | # furnished to do so, subject to the following conditions:
 40 | #
 41 | # The above copyright notice and this permission notice shall be included in
 42 | # all copies or substantial portions of the Software.
 43 | #
 44 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 45 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 46 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 47 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 48 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 49 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 50 | # SOFTWARE.
 51 | 
 52 | import numpy as np
 53 | 
 54 | 
 55 | def numpy_array(x):
 56 |     if isinstance(x, np.ndarray) is False:
 57 |         x = np.array(x)
 58 |     return x
 59 | 
 60 | 
 61 | def assert_2d_sort(x):
 62 |     if x.ndim == 1:
 63 |         x = x.reshape(1, -1)
 64 |     elif x.ndim > 2:
 65 |         raise ValueError('x can not be more than 2 dimensions')
 66 |     # Prevent modifications to input data by copying x before sorting.
 67 |     x = x.copy()
 68 |     x.sort()
 69 |     return x
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |

Functions

78 |
79 |
80 | def assert_2d_sort(x) 81 |
82 |
83 |
84 |
85 | 86 | Expand source code 87 | 88 |
def assert_2d_sort(x):
 89 |     if x.ndim == 1:
 90 |         x = x.reshape(1, -1)
 91 |     elif x.ndim > 2:
 92 |         raise ValueError('x can not be more than 2 dimensions')
 93 |     # Prevent modifications to input data by copying x before sorting.
 94 |     x = x.copy()
 95 |     x.sort()
 96 |     return x
97 |
98 |
99 |
100 | def numpy_array(x) 101 |
102 |
103 |
104 |
105 | 106 | Expand source code 107 | 108 |
def numpy_array(x):
109 |     if isinstance(x, np.ndarray) is False:
110 |         x = np.array(x)
111 |     return x
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | 138 |
139 | 142 | 143 | -------------------------------------------------------------------------------- /toleranceinterval/hk.py: -------------------------------------------------------------------------------- 1 | # -- coding: utf-8 -- 2 | # MIT License 3 | # 4 | # Copyright (c) 2019 Charles Jekel 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | from sympy import Symbol, Integral, factorial 25 | from sympy import gamma, hyper, exp_polar, I, pi, log 26 | from scipy.special import betainc, betaincinv 27 | import numpy as np 28 | from warnings import warn 29 | 30 | 31 | class HansonKoopmans(object): 32 | 33 | def __init__(self, p, g, n, j, method='secant', max_iter=200, 34 | tol=1e-5, step_size=1e-4): 35 | r""" 36 | An object to solve for the Hanson-Koopmans bound. 37 | 38 | Solve the Hanson-Koopmans [1] bound for any percentile, confidence 39 | level, and number of samples. This assumes the lowest value is the 40 | first order statistic, but you can specify the index of the second 41 | order statistic as j. 42 | 43 | Parameters 44 | ---------- 45 | p : float 46 | Percentile where p < 0.5 and p > 0. 47 | g : float 48 | Confidence level where g > 0. and g < 1. 49 | n : int 50 | Number of samples. 51 | j : int 52 | Index of the second value to use for the second order statistic. 53 | method : string, optional 54 | Which rootfinding method to use to solve for the Hanson-Koopmans 55 | bound. Default is method='secant' which appears to converge 56 | quickly. Other choices include 'newton-raphson' and 'halley'. 57 | max_iter : int, optional 58 | Maximum number of iterations for the root finding method. 59 | tol : float, optional 60 | Tolerance for the root finding method to converge. 61 | step_size : float, optional 62 | Step size for the secant solver. Default step_size = 1e-4. 63 | 64 | Attributes 65 | ---------- 66 | b : float_like 67 | Hanson-Koopmans bound. 68 | un_conv : bool 69 | Unconvergence status. If un_conv, then the method did not converge. 70 | count : int 71 | Number of iterations used in the root finding method. 72 | fall_back : bool 73 | Whether to fall back to the traditional non-parametric method. 74 | 75 | Raises 76 | ------ 77 | ValueError 78 | Incorrect input, or unable to comptue the Hanson-Koopmans bound. 79 | 80 | References 81 | ---------- 82 | [1] Hanson, D. L., & Koopmans, L. H. (1964). Tolerance Limits for 83 | the Class of Distributions with Increasing Hazard Rates. Ann. Math. 84 | Statist., 35(4), 1561–1570. https://doi.org/10.1214/aoms/1177700380 85 | 86 | [2] Vangel, M. G. (1994). One-sided nonparametric tolerance limits. 87 | Communications in Statistics - Simulation and Computation, 23(4), 88 | 1137–1154. https://doi.org/10.1080/03610919408813222 89 | 90 | """ 91 | self.max_iter = max_iter 92 | self.tol = tol 93 | self.step_size = step_size 94 | # create a dummy variable v 95 | self.v = Symbol('v', nonzero=True, rational=True, positive=True) 96 | # check that p, g, n, j are valid 97 | if not (p < 0.5 and p > 0.): 98 | self.invalid_value(p, 'p') 99 | else: 100 | self.p = p 101 | if not (g > 0. and g < 1.): 102 | self.invalid_value(g, 'g') 103 | else: 104 | self.g = g 105 | self.n = int(n) 106 | if not (j < n and j > -1): 107 | self.invalid_value(j, 'j') 108 | else: 109 | self.j = int(j) 110 | # compute the stuff that doesn't depend on b 111 | self.constant_vales() 112 | # compute b = 1 113 | pi_B_1 = self.piB(0) # remember that b - 1 = B; b = B + 1 114 | if pi_B_1 >= self.g: 115 | self.fall_back = True 116 | # raise ValueError('b = 1, defer to traditional methods...') 117 | # raise RunTimeWarning? 118 | else: 119 | self.fall_back = False 120 | b_guess = self.vangel_approx(p=float(self.p)) 121 | # print(float(b_guess)) 122 | if np.isnan(b_guess): 123 | raise RuntimeError('Bad Vangel Approximation is np.nan') 124 | elif b_guess <= 0: 125 | b_guess = 1e-2 126 | # print(b_guess) 127 | self.b_guess = b_guess 128 | if method == 'secant': 129 | B, status, count = self.secant_solver(b_guess - 1.) 130 | elif method == 'newton-raphson': 131 | B, status, count = self.nr_solver(b_guess - 1.) 132 | elif method == 'halley': 133 | B, status, count = self.halley_solver(b_guess - 1.) 134 | else: 135 | raise ValueError(str(method) + ' is not a valid method!') 136 | 137 | self.b = B + 1. 138 | self.un_conv = status 139 | self.count = count 140 | if self.un_conv: 141 | war = 'HansonKoopmans root finding method failed to converge!' 142 | warn(war, RuntimeWarning) 143 | # This should raise RuntimeError if not converged! 144 | 145 | def invalid_value(self, value, variable): 146 | err = str(value) + ' was not a valid value for ' + variable 147 | raise ValueError(err) 148 | 149 | def constant_vales(self): 150 | self.nj = self.n-self.j-1 151 | self.A = factorial(self.n) / (factorial(self.nj) * 152 | factorial(self.j-1)) 153 | # compute the left integral 154 | int_left = (self.p*self.p**self.j*gamma(self.j + 1) * 155 | hyper((-self.nj, self.j + 1), 156 | (self.j + 2,), 157 | self.p*exp_polar(2*I*pi)) / 158 | (self.j*gamma(self.j + 2))) 159 | self.int_left = int_left.evalf() # evaluates to double precision 160 | 161 | def piB(self, B): 162 | int_right_exp = (self.v**self.j*(1 - self.v)**self.nj/self.j 163 | - (1 - self.v)**self.nj*(-self.p**(1/(B + 1)) * 164 | self.v**(B/(B + 1)) + self.v)**self.j/self.j) 165 | int_right = Integral(int_right_exp, (self.v, self.p, 1)).evalf() 166 | return (self.int_left + int_right)*self.A 167 | 168 | def dpiB(self, B): 169 | d_int_right_exp_B = (self.v**self.j*(1 - self.v)**self.nj/self.j - 170 | (1 - self.v)**self.nj*(-self.p**(1/(B + 1)) * 171 | self.v**(B/(B + 1))*(-B/(B + 1)**2 + 1/(B + 1)) * 172 | log(self.v) + self.p**(1/(B + 1))*self.v ** 173 | (B / (B + 1))*log(self.p)/(B + 1)**2 + self.v) ** 174 | self.j / self.j) 175 | d_int_right = Integral(d_int_right_exp_B, (self.v, self.p, 1)).evalf() 176 | return d_int_right*self.A 177 | 178 | def d2piB2(self, B): 179 | d2_int_r_B = (-(1 - self.v)**self.nj*(-self.p**(1/(B + 1))*self.v ** 180 | (B/(B + 1))*(-B/(B + 1)**2 + 1/(B + 1))*log(self.v) + 181 | self.p**(1/(B + 1))*self.v**(B/(B + 1))*log(self.p) / 182 | (B + 1)**2 + self.v)**self.j*(-self.p**(1/(B + 1)) * 183 | self.v**(B/(B + 1))*(2*B/(B + 1)**3 - 2/(B + 1)**2) * 184 | log(self.v) - self.p**(1/(B + 1))*self.v**(B/(B + 1)) * 185 | (-B/(B + 1)**2 + 1/(B + 1))**2*log(self.v)**2 + 2 * 186 | self.p**(1/(B + 1))*self.v**(B/(B + 1)) * 187 | (-B/(B + 1)**2 + 1/(B + 1)) 188 | * log(self.p)*log(self.v)/(B + 1)**2 - 2 * 189 | self.p**(1/(B + 1))*self.v**(B/(B + 1))*log(self.p) / 190 | (B + 1)**3 - self.p**(1/(B + 1))*self.v**(B/(B + 1)) * 191 | log(self.p)**2/(B + 1)**4)/(-self.p**(1/(B + 1)) * 192 | self.v**(B/(B + 1))*(-B/(B + 1)**2 + 1/(B + 1)) * 193 | log(self.v) + self.p**(1/(B + 1))*self.v ** 194 | (B/(B + 1)) * 195 | log(self.p)/(B + 1)**2 + self.v)) 196 | d2_int_right = Integral(d2_int_r_B, (self.v, self.p, 1)).evalf() 197 | return d2_int_right*self.A 198 | 199 | def vangel_approx(self, n=None, i=None, j=None, p=None, g=None): 200 | if n is None: 201 | n = self.n 202 | if i is None: 203 | i = 1 204 | if j is None: 205 | j = self.j+1 206 | if p is None: 207 | p = self.p 208 | if g is None: 209 | g = self.g 210 | betatmp = betainc(j, n-j+1, p) 211 | a = g - betatmp 212 | b = 1.0 - betatmp 213 | q = betaincinv(i, j-i, a/b) 214 | return np.log(((p)*(n+1))/j) / np.log(q) 215 | 216 | def secant_solver(self, B_guess, max_iter=None, tol=None, step_size=None): 217 | if max_iter is None: 218 | max_iter = self.max_iter 219 | if tol is None: 220 | tol = self.tol 221 | if step_size is None: 222 | step_size = self.step_size 223 | count = 0 224 | f = self.piB(B_guess) - self.g 225 | f1 = self.piB(B_guess + step_size) - self.g 226 | dfdx = (f1 - f) / step_size 227 | B_next = B_guess - (f/dfdx) 228 | un_conv = np.abs(B_next - B_guess) > tol 229 | while un_conv and count < max_iter: 230 | B_guess = B_next 231 | f = self.piB(B_guess) - self.g 232 | f1 = self.piB(B_guess + step_size) - self.g 233 | dfdx = (f1 - f) / step_size 234 | B_next = B_guess - (f/dfdx) 235 | un_conv = np.abs(B_next - B_guess) > tol 236 | count += 1 237 | return B_next, un_conv, count 238 | 239 | def nr_solver(self, B_guess, max_iter=None, tol=None): 240 | if max_iter is None: 241 | max_iter = self.max_iter 242 | if tol is None: 243 | tol = self.tol 244 | count = 0 245 | f = self.piB(B_guess) - self.g 246 | dfdx = self.dpiB(B_guess) 247 | B_next = B_guess - (f/dfdx) 248 | un_conv = np.abs(B_next - B_guess) > tol 249 | while un_conv and count < max_iter: 250 | B_guess = B_next 251 | f = self.piB(B_guess) - self.g 252 | dfdx = self.dpiB(B_guess) 253 | B_next = B_guess - (f/dfdx) 254 | un_conv = np.abs(B_next - B_guess) > tol 255 | count += 1 256 | return B_next, un_conv, count 257 | 258 | def halley_solver(self, B_guess, max_iter=None, tol=None): 259 | if max_iter is None: 260 | max_iter = self.max_iter 261 | if tol is None: 262 | tol = self.tol 263 | count = 0 264 | f = self.piB(B_guess) - self.g 265 | dfdx = self.dpiB(B_guess) 266 | d2fdx2 = self.d2piB2(B_guess) 267 | B_next = B_guess - ((2*f*dfdx) / (2*(dfdx**2) - (f*d2fdx2))) 268 | un_conv = np.abs(B_next - B_guess) > tol 269 | while un_conv and count < max_iter: 270 | B_guess = B_next 271 | f = self.piB(B_guess) - self.g 272 | dfdx = self.dpiB(B_guess) 273 | d2fdx2 = self.d2piB2(B_guess) 274 | B_next = B_guess - ((2*f*dfdx) / (2*(dfdx**2) - (f*d2fdx2))) 275 | un_conv = np.abs(B_next - B_guess) > tol 276 | count += 1 277 | return B_next, un_conv, count 278 | -------------------------------------------------------------------------------- /toleranceinterval/twoside/twoside.py: -------------------------------------------------------------------------------- 1 | # -- coding: utf-8 -- 2 | # MIT License 3 | # 4 | # Copyright (c) 2019 Charles Jekel 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | import numpy as np 25 | from ..checks import numpy_array, assert_2d_sort 26 | from . import _normal_exact as exact 27 | from . import _normal_approx as approx 28 | 29 | 30 | def normal_factor(n, p, g, method=None, m=None, nu=None, d2=None, 31 | simultaneous=False, tailprob=False): 32 | r""" 33 | Compute two-sided central tolerance factor using the normal distribution. 34 | 35 | Computes the tolerance factor k for the two-sided central tolerance 36 | interval (TI) to cover a proportion p of the population with confidence g: 37 | 38 | TI = [Xmean - k * S, Xmean + k * S] 39 | 40 | where Xmean = mean(X), S = std(X), X = [X_1,...,X_n] is a random sample 41 | of size n from the distribution N(mu,sig2) with unknown mean mu and 42 | variance sig2. 43 | 44 | The tolerance factor k is determined such that the tolerance intervals 45 | with confidence g cover at least the coverage fraction 46 | of the distribution N(mu,sigma^2), i.e. 47 | Prob[ Prob( Xmean - k * S < X < Xmean + k * S ) >= p ] = g, 48 | for X ~ N(mu,sig2) which is independent of Xmean and S. 49 | 50 | By default, this function uses an 'exact' method for computing the factor 51 | by Gauss-Kronod quadrature as described in the references [1,2,4]. There 52 | are also two approximate methods implemented: the 'howe' method as 53 | described in [5], and the 'guenther' method as described in [6]. A brief 54 | overview of both approximate methods can be found at NIST's website: 55 | https://www.itl.nist.gov/div898/handbook/prc/section2/prc263.htm 56 | 57 | Additional optional parameters are available to consider pooled variance 58 | studies when m random samples of size n are available. Furthermore, 59 | for the 'exact' method, optional parameters are available to 60 | consider simultaneous tolerance intervals as described in [7,8]. 61 | If S is a pooled estimator of sig, based on m random samples of size n, 62 | normal_factor computes the tolerance factor k for the two-sided p-content 63 | and g-confidence tolerance intervals 64 | 65 | TI = [Xmean_i - k * S, Xmean_i + k * S], for i = 1,...,m 66 | 67 | where Xmean_i = mean(X_i), X_i = [X_i1,...,X_in] is a random sample of 68 | size n from the distribution N(mu_i,sig2) with unknown mean mu_i and 69 | variance sig2, and S = sqrt(S2), S2 is the pooled estimator of sig2, 70 | 71 | S2 = (1/nu) * sum_i=1:m ( sum_j=1:n (X_ij - Xmean_i)^2 ) 72 | 73 | with nu degrees of freedom, nu = m * (n-1). For the 'exact' method, both 74 | the simultaneous and non-simultaneous cases can be considered. 75 | 76 | Parameters 77 | ---------- 78 | n : scalar 79 | Sample size 80 | p : scalar in the interval [0.0, 1.0] 81 | Coverage (or content) probability, 82 | Prob( Xmean - k * S < X < Xmean + k * S ) >= p 83 | g : scalar in the interval [0.0, 1.0] 84 | Confidence probability, 85 | Prob[ Prob( Xmean-k*S < X < Xmean+k*S ) >= p ] = g. 86 | method : str 87 | Method to use for computing the factor. Available methods are 'exact', 88 | 'howe', and 'guenther'. If None, the default method is 'exact'. 89 | m : scalar 90 | Number of independent random samples (of size n). If None, 91 | default value is m = 1. 92 | nu : scalar 93 | Degrees of freedom for distribution of the (pooled) sample 94 | variance S2. If None, default value is nu = m*(n-1). 95 | d2 : scalar 96 | Normalizing constant. For computing the factors of the 97 | non-simultaneous tolerance limits (xx'*betaHat +/- k * S) 98 | for the linear regression y = XX*beta +epsilon, set d2 = 99 | xx'*inv(XX'*XX)*xx. 100 | Typically, in simple linear regression the estimator S2 has 101 | nu = n-2 degrees of freedom. If None, default value is d2 = 1/n. 102 | simultaneous : boolean 103 | Logical flag for calculating the factor for 104 | simultaneous tolerance intervals. If False, normal_factor will 105 | calculate the factor for the non-simultaneous tolerance interval. 106 | Default value is False. 107 | tailprob : boolean 108 | Logical flag for representing the input probabilities 109 | 'p' and 'g'. If True, the input parameters are 110 | represented as the tail coverage (i.e. 1 - p) and tail confidence 111 | (i.e. 1 - g). This option is useful if the interest is to 112 | calculate the tolerance factor for extremely large values 113 | of coverage and/or confidence, close to 1, as 114 | e.g. coverage = 1 - 1e-18. Default value is False. 115 | 116 | Returns 117 | ------- 118 | float 119 | The calculated tolerance factor for the tolerance interval. 120 | 121 | References 122 | ---------- 123 | [1] Krishnamoorthy K, Mathew T. (2009). Statistical Tolerance Regions: 124 | Theory, Applications, and Computation. John Wiley & Sons, Inc., 125 | Hoboken, New Jersey. ISBN: 978-0-470-38026-0, 512 pages. 126 | 127 | [2] Witkovsky V. On the exact two-sided tolerance intervals for 128 | univariate normal distribution and linear regression. Austrian 129 | Journal of Statistics 43(4), 2014, 279-92. 130 | http://ajs.data-analysis.at/index.php/ajs/article/viewFile/vol43-4-6/35 131 | 132 | [3] ISO 16269-6:2014: Statistical interpretation of data - Part 6: 133 | Determination of statistical tolerance intervals. 134 | 135 | [4] Janiga I., Garaj I.: Two-sided tolerance limits of normal 136 | distributions with unknown means and unknown common variability. 137 | MEASUREMENT SCIENCE REVIEW, Volume 3, Section 1, 2003, 75-78. 138 | 139 | [5] Howe, W. G. “Two-Sided Tolerance Limits for Normal Populations, 140 | Some Improvements.” Journal of the American Statistical Association, 141 | vol. 64, no. 326, [American Statistical Association, Taylor & Francis, 142 | Ltd.], 1969, pp. 610–20, https://doi.org/10.2307/2283644. 143 | 144 | [6] Guenther, W. C. (1977). "Sampling Inspection in Statistical Quality 145 | Control",, Griffin's Statistical Monographs, Number 37, London. 146 | 147 | [7] Robert W. Mee (1990) Simultaneous Tolerance Intervals for Normal 148 | Populations With Common Variance, Technometrics, 32:1, 83-92, 149 | DOI: 10.1080/00401706.1990.10484595 150 | 151 | [8] K. Krishnamoorthy & Saptarshi Chakraberty (2022). Construction of 152 | simultaneous tolerance intervals for several normal distributions, 153 | Journal of Statistical Computation and Simulation, 92:1, 101-114, 154 | DOI: 10.1080/00949655.2021.1932885 155 | 156 | """ 157 | # Handle default method: 158 | if method is None: 159 | method = 'exact' 160 | 161 | if method == 'exact': 162 | k = exact.tolerance_factor(n, p, g, m, nu, d2, simultaneous, tailprob) 163 | elif method == 'howe': 164 | k = approx.tolerance_factor_howe(n, p, g, m, nu) 165 | elif method == 'guenther': 166 | k = approx.tolerance_factor_guenther(n, p, g, m, nu) 167 | else: 168 | raise ValueError( 169 | "Invalid method requested. Valid methods are 'exact', 'howe', or " 170 | "'guenther'." 171 | ) 172 | return k 173 | 174 | 175 | def normal(x, p, g, method=None, pool_variance=False): 176 | r""" 177 | Compute two-sided central tolerance interval using the normal distribution. 178 | 179 | Computes the two-sided tolerance interval (TI) to cover a proportion p of 180 | the population with confidence g using the normal distribution. This 181 | follows the standard approach to calculate the interval as a factor of 182 | sample standard deviations away from the sample mean. 183 | 184 | TI = [Xmean - k * S, Xmean + k * S] 185 | 186 | where Xmean = mean(X), S = std(X), X = [X_1,...,X_n] is a random sample 187 | of size n from the distribution N(mu,sig2) with unknown mean mu and 188 | variance sig2. 189 | 190 | By default, this function uses an 'exact' method for computing the TI 191 | by Gauss-Kronod quadrature. There are also two approximate methods 192 | implemented: the 'howe' method, and the 'guenther' method. See the 193 | documentation for normal_factor for more details on the methods. 194 | 195 | Parameters 196 | ---------- 197 | x : ndarray (1-D, or 2-D) 198 | Numpy array of samples to compute the tolerance interval. Assumed data 199 | type is np.float. Shape of (m, n) is assumed for 2-D arrays with m 200 | number of sets of sample size n. 201 | p : float 202 | Percentile for central TI to cover. 203 | g : float 204 | Confidence level where g > 0. and g < 1. 205 | method : str 206 | Method to use for computing the TI. Available methods are 'exact', 207 | 'howe', and 'guenther'. If None, the default method is 'exact'. 208 | pool_variance : boolean 209 | Consider the m random samples to share the same variance such that 210 | the degrees of freedom are nu = m*(n-1). Default is False. 211 | 212 | Returns 213 | ------- 214 | ndarray (2-D) 215 | The normal distribution toleranace interval bound. Shape (m, 2) from m 216 | sets of samples, where [:, 0] is the lower bound and [:, 1] is the 217 | upper bound. 218 | 219 | References 220 | ---------- 221 | See the documentation for normal_factor for a complete list of references. 222 | 223 | Examples 224 | -------- 225 | Estimate the 90th percentile central TI with 95% confidence of the 226 | following 100 random samples from a normal distribution. 227 | 228 | >>> import numpy as np 229 | >>> import toleranceinterval as ti 230 | >>> x = np.random.nomral(100) 231 | >>> bound = ti.twoside.normal(x, 0.9, 0.95) 232 | >>> print('Lower bound:', bound[:, 0]) 233 | >>> print('Upper bound:', bound[:, 1]) 234 | 235 | Estimate the 95th percentile central TI with 95% confidence of the 236 | following 100 random samples from a normal distribution. 237 | 238 | >>> bound = ti.twoside.normal(x, 0.95, 0.95) 239 | 240 | """ 241 | x = numpy_array(x) # check if numpy array, if not make numpy array 242 | x = assert_2d_sort(x) 243 | m, n = x.shape 244 | 245 | # Handle pooled variance case 246 | if pool_variance: 247 | _m = m 248 | else: 249 | _m = 1 250 | 251 | k = normal_factor(n, p, g, method, _m) 252 | bound = np.zeros((m, 2)) 253 | xmu = x.mean(axis=1) 254 | kstd = k * x.std(axis=1, ddof=1) 255 | bound[:, 0] = xmu - kstd 256 | bound[:, 1] = xmu + kstd 257 | return bound 258 | 259 | 260 | def lognormal(x, p, g, method=None, pool_variance=False): 261 | r""" 262 | Two-sided central tolerance interval using the lognormal distribution. 263 | 264 | Computes the two-sided tolerance interval using the lognormal distribution. 265 | This just performs a ln and exp transformations of the normal distribution. 266 | 267 | Parameters 268 | ---------- 269 | x : ndarray (1-D, or 2-D) 270 | Numpy array of samples to compute the tolerance interval. Assumed data 271 | type is np.float. Shape of (m, n) is assumed for 2-D arrays with m 272 | number of sets of sample size n. 273 | p : float 274 | Percentile for central TI to estimate. 275 | g : float 276 | Confidence level where g > 0. and g < 1. 277 | method : str 278 | Method to use for computing the TI. Available methods are 'exact', 279 | 'howe', and 'guenther'. If None, the default method is 'exact'. 280 | pool_variance : boolean 281 | Consider the m random samples to share the same variance such that 282 | the degrees of freedom are nu = m*(n-1). Default is False. 283 | 284 | Returns 285 | ------- 286 | ndarray (2-D) 287 | The lognormal distribution toleranace interval bound. Shape (m, 2) 288 | from m sets of samples, where [:, 0] is the lower bound and [:, 1] is 289 | the upper bound. 290 | 291 | Examples 292 | -------- 293 | Estimate the 90th percentile central TI with 95% confidence of the 294 | following 100 random samples from a lognormal distribution. 295 | 296 | >>> import numpy as np 297 | >>> import toleranceinterval as ti 298 | >>> x = np.random.random(100) 299 | >>> bound = ti.twoside.lognormal(x, 0.9, 0.95) 300 | >>> print('Lower bound:', bound[:, 0]) 301 | >>> print('Upper bound:', bound[:, 1]) 302 | 303 | Estimate the 95th percentile central TI with 95% confidence of the 304 | following 100 random samples from a normal distribution. 305 | 306 | >>> bound = ti.twoside.lognormal(x, 0.95, 0.95) 307 | 308 | """ 309 | x = numpy_array(x) # check if numpy array, if not make numpy array 310 | x = assert_2d_sort(x) 311 | return np.exp(normal(np.log(x), p, g, method, pool_variance)) 312 | -------------------------------------------------------------------------------- /tests/test_twoside_normal_factor_mhe.py: -------------------------------------------------------------------------------- 1 | # Author: Copyright (c) 2021 Jed Ludlow 2 | # License: MIT License 3 | 4 | """ 5 | Test normal_factor against tables standard values from Meeker, Hahn, and Escobar. 6 | 7 | Meeker, William Q.; Hahn, Gerald J.; Escobar, Luis A.. Statistical 8 | Intervals: A Guide for Practitioners and Researchers (Wiley Series in 9 | Probability and Statistics). Wiley. Kindle Edition. 10 | 11 | Tables J.5a and J.5b provide tolerance factors for various combinations 12 | of sample size, coverage, and confidence. 13 | 14 | """ 15 | 16 | import numpy as np 17 | import toleranceinterval.twoside as ts 18 | import unittest 19 | 20 | 21 | class BaseTestMHE: 22 | 23 | class TestMeekerHahnEscobarJ5(unittest.TestCase): 24 | 25 | def test_tolerance_factor(self): 26 | for row_idx, row in enumerate(self.factor_g): 27 | for col_idx, g in enumerate(row): 28 | k = ts.normal_factor( 29 | self.sample_size[row_idx], 30 | self.coverage[col_idx], 31 | self.confidence[col_idx], 32 | method='exact') 33 | self.assertAlmostEqual(k, g, places=3) 34 | 35 | 36 | class TestMeekerHahnEscobarJ5a(BaseTestMHE.TestMeekerHahnEscobarJ5): 37 | 38 | # Table J.5a from Meeker, William Q.; Hahn, Gerald J.; Escobar, Luis A.. 39 | # Statistical Intervals: A Guide for Practitioners and Researchers 40 | # (Wiley Series in Probability and Statistics) (p. 535). Wiley. 41 | 42 | coverage = np.array([ 43 | 0.5, 0.5, 0.5, 0.5, 0.5, 44 | 0.7, 0.7, 0.7, 0.7, 0.7, 45 | 0.8, 0.8, 0.8, 0.8, 0.8, 46 | ]) 47 | 48 | confidence = np.array([ 49 | 0.5, 0.8, 0.9, 0.95, 0.99, 50 | 0.5, 0.8, 0.9, 0.95, 0.99, 51 | 0.5, 0.8, 0.9, 0.95, 0.99, 52 | ]) 53 | 54 | sample_size = np.array([ 55 | 2, 3, 4, 5, 6, 7, 8, 9, 10, 56 | 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 57 | 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 58 | 35, 40, 50, 60, 120, 240, 480, np.inf, 59 | ]) 60 | 61 | factor_g = np.array([ 62 | # n = 2 63 | 1.243, 3.369, 6.808, 13.652, 68.316, 1.865, 5.023, 10.142, 64 | 20.331, 101.732, 2.275, 6.110, 12.333, 24.722, 123.699, 65 | # n = 3 66 | 0.942, 1.700, 2.492, 3.585, 8.122, 1.430, 2.562, 3.747, 67 | 5.382, 12.181, 1.755, 3.134, 4.577, 6.572, 14.867, 68 | # n = 4 69 | 0.852, 1.335, 1.766, 2.288, 4.028, 1.300, 2.026, 2.673, 70 | 3.456, 6.073, 1.600, 2.486, 3.276, 4.233, 7.431, 71 | # n = 5 72 | 0.808, 1.173, 1.473, 1.812, 2.824, 1.236, 1.788, 2.239, 73 | 2.750, 4.274, 1.523, 2.198, 2.750, 3.375, 5.240, 74 | # n = 6 75 | 0.782, 1.081, 1.314, 1.566, 2.270, 1.198, 1.651, 2.003, 76 | 2.384, 3.446, 1.477, 2.034, 2.464, 2.930, 4.231, 77 | # n = 7 78 | 0.764, 1.021, 1.213, 1.415, 1.954, 1.172, 1.562, 1.853, 79 | 2.159, 2.973, 1.446, 1.925, 2.282, 2.657, 3.656, 80 | # n = 8 81 | 0.752, 0.979, 1.143, 1.313, 1.750, 1.153, 1.499, 1.749, 82 | 2.006, 2.668, 1.423, 1.849, 2.156, 2.472, 3.284, 83 | # n = 9 84 | 0.742, 0.947, 1.092, 1.239, 1.608, 1.139, 1.451, 1.672, 85 | 1.896, 2.455, 1.407, 1.791, 2.062, 2.337, 3.024, 86 | # n = 10 87 | 0.735, 0.923, 1.053, 1.183, 1.503, 1.128, 1.414, 1.613, 88 | 1.811, 2.297, 1.393, 1.746, 1.990, 2.234, 2.831, 89 | # n = 11 90 | 0.729, 0.903, 1.021, 1.139, 1.422, 1.119, 1.385, 1.566, 91 | 1.744, 2.175, 1.382, 1.710, 1.932, 2.152, 2.682, 92 | # n = 12 93 | 0.724, 0.886, 0.996, 1.103, 1.357, 1.112, 1.360, 1.527, 94 | 1.690, 2.078, 1.374, 1.680, 1.885, 2.086, 2.563, 95 | # n = 13 96 | 0.720, 0.873, 0.974, 1.073, 1.305, 1.106, 1.339, 1.495, 97 | 1.645, 1.999, 1.366, 1.654, 1.846, 2.031, 2.466, 98 | # n = 14 99 | 0.717, 0.861, 0.956, 1.048, 1.261, 1.100, 1.322, 1.467, 100 | 1.608, 1.933, 1.360, 1.633, 1.812, 1.985, 2.386, 101 | # n = 15 102 | 0.714, 0.851, 0.941, 1.027, 1.224, 1.096, 1.307, 1.444, 103 | 1.575, 1.877, 1.355, 1.614, 1.783, 1.945, 2.317, 104 | # n = 16 105 | 0.711, 0.842, 0.927, 1.008, 1.193, 1.092, 1.293, 1.423, 106 | 1.547, 1.829, 1.350, 1.598, 1.758, 1.911, 2.259, 107 | # n = 17 108 | 0.709, 0.835, 0.915, 0.992, 1.165, 1.089, 1.282, 1.405, 109 | 1.522, 1.788, 1.346, 1.584, 1.736, 1.881, 2.207, 110 | # n = 18 111 | 0.707, 0.828, 0.905, 0.978, 1.141, 1.086, 1.271, 1.389, 112 | 1.501, 1.751, 1.342, 1.571, 1.717, 1.854, 2.163, 113 | # n = 19 114 | 0.705, 0.822, 0.895, 0.965, 1.120, 1.083, 1.262, 1.375, 115 | 1.481, 1.719, 1.339, 1.559, 1.699, 1.830, 2.123, 116 | # n = 20 117 | 0.704, 0.816, 0.887, 0.953, 1.101, 1.081, 1.253, 1.362, 118 | 1.464, 1.690, 1.336, 1.549, 1.683, 1.809, 2.087, 119 | # n = 21 120 | 0.702, 0.811, 0.879, 0.943, 1.084, 1.079, 1.246, 1.350, 121 | 1.448, 1.664, 1.333, 1.540, 1.669, 1.789, 2.055, 122 | # n = 22 123 | 0.701, 0.806, 0.872, 0.934, 1.068, 1.077, 1.239, 1.340, 124 | 1.434, 1.640, 1.331, 1.531, 1.656, 1.772, 2.026, 125 | # n = 23 126 | 0.700, 0.802, 0.866, 0.925, 1.054, 1.075, 1.232, 1.330, 127 | 1.420, 1.619, 1.329, 1.523, 1.644, 1.755, 2.000, 128 | # n = 24 129 | 0.699, 0.798, 0.860, 0.917, 1.042, 1.073, 1.226, 1.321, 130 | 1.408, 1.599, 1.327, 1.516, 1.633, 1.741, 1.976, 131 | # n = 25 132 | 0.698, 0.795, 0.855, 0.910, 1.030, 1.072, 1.221, 1.313, 133 | 1.397, 1.581, 1.325, 1.509, 1.623, 1.727, 1.954, 134 | # n = 26 135 | 0.697, 0.791, 0.850, 0.903, 1.019, 1.070, 1.216, 1.305, 136 | 1.387, 1.565, 1.323, 1.503, 1.613, 1.714, 1.934, 137 | # n = 27 138 | 0.696, 0.788, 0.845, 0.897, 1.009, 1.069, 1.211, 1.298, 139 | 1.378, 1.550, 1.322, 1.497, 1.604, 1.703, 1.915, 140 | # n = 28 141 | 0.695, 0.785, 0.841, 0.891, 1.000, 1.068, 1.207, 1.291, 142 | 1.369, 1.535, 1.320, 1.492, 1.596, 1.692, 1.898, 143 | # n = 29 144 | 0.694, 0.783, 0.837, 0.886, 0.991, 1.067, 1.202, 1.285, 145 | 1.360, 1.522, 1.319, 1.487, 1.589, 1.682, 1.882, 146 | # n = 30 147 | 0.694, 0.780, 0.833, 0.881, 0.983, 1.066, 1.199, 1.279, 148 | 1.353, 1.510, 1.318, 1.482, 1.581, 1.672, 1.866, 149 | # n = 35 150 | 0.691, 0.770, 0.817, 0.859, 0.950, 1.061, 1.182, 1.255, 151 | 1.320, 1.459, 1.312, 1.462, 1.551, 1.632, 1.803, 152 | # n = 40 153 | 0.689, 0.761, 0.805, 0.843, 0.925, 1.058, 1.170, 1.236, 154 | 1.296, 1.420, 1.308, 1.446, 1.528, 1.602, 1.756, 155 | # n = 50 156 | 0.686, 0.750, 0.787, 0.820, 0.889, 1.054, 1.152, 1.209, 157 | 1.260, 1.365, 1.303, 1.424, 1.495, 1.558, 1.688, 158 | # n = 60 159 | 0.684, 0.741, 0.775, 0.804, 0.864, 1.051, 1.139, 1.190, 160 | 1.235, 1.327, 1.299, 1.408, 1.471, 1.527, 1.641, 161 | # n = 120 162 | 0.679, 0.718, 0.740, 0.759, 0.797, 1.044, 1.104, 1.137, 163 | 1.166, 1.225, 1.290, 1.365, 1.406, 1.442, 1.514, 164 | # n = 240 165 | 0.677, 0.704, 0.719, 0.731, 0.756, 1.040, 1.082, 1.104, 166 | 1.124, 1.162, 1.286, 1.337, 1.365, 1.390, 1.437, 167 | # n = 480 168 | 0.676, 0.694, 0.705, 0.713, 0.730, 1.038, 1.067, 1.083, 169 | 1.096, 1.122, 1.284, 1.320, 1.339, 1.355, 1.387, 170 | # n = infinity 171 | 0.674, 0.674, 0.674, 0.674, 0.674, 1.036, 1.036, 1.036, 172 | 1.036, 1.036, 1.282, 1.282, 1.282, 1.282, 1.282, 173 | ]) 174 | 175 | factor_g = factor_g.reshape(sample_size.size, coverage.size) 176 | 177 | 178 | class TestMeekerHahnEscobarJ5b(BaseTestMHE.TestMeekerHahnEscobarJ5): 179 | 180 | # Table J.5b from Meeker, William Q.; Hahn, Gerald J.; Escobar, Luis A.. 181 | # Statistical Intervals: A Guide for Practitioners and Researchers 182 | # (Wiley Series in Probability and Statistics) (p. 535). Wiley. 183 | 184 | coverage = np.array([ 185 | 0.90, 0.90, 0.90, 0.90, 0.90, 186 | 0.95, 0.95, 0.95, 0.95, 0.95, 187 | 0.99, 0.99, 0.99, 0.99, 0.99, 188 | ]) 189 | 190 | confidence = np.array([ 191 | 0.5, 0.8, 0.9, 0.95, 0.99, 192 | 0.5, 0.8, 0.9, 0.95, 0.99, 193 | 0.5, 0.8, 0.9, 0.95, 0.99, 194 | ]) 195 | 196 | sample_size = np.array([ 197 | 2, 3, 4, 5, 6, 7, 8, 9, 10, 198 | 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 199 | 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 200 | 35, 40, 50, 60, 120, 240, 480, np.inf, 201 | ]) 202 | 203 | factor_g = np.array([ 204 | # n = 2 205 | 2.869, 7.688, 15.512, 31.092, 155.569, 3.376, 9.032, 18.221, 206 | 36.519, 182.720, 4.348, 11.613, 23.423, 46.944, 234.877, 207 | # n = 3 208 | 2.229, 3.967, 5.788, 8.306, 18.782, 2.634, 4.679, 6.823, 9.789, 209 | 22.131, 3.415, 6.051, 8.819, 12.647, 28.586, 210 | # n = 4 211 | 2.039, 3.159, 4.157, 5.368, 9.416, 2.416, 3.736, 4.913, 6.341, 212 | 11.118, 3.144, 4.850, 6.372, 8.221, 14.405, 213 | # n = 5 214 | 1.945, 2.801, 3.499, 4.291, 6.655, 2.308, 3.318, 4.142, 5.077, 215 | 7.870, 3.010, 4.318, 5.387, 6.598, 10.220, 216 | # n = 6 217 | 1.888, 2.595, 3.141, 3.733, 5.383, 2.243, 3.078, 3.723, 4.422, 218 | 6.373, 2.930, 4.013, 4.850, 5.758, 8.292, 219 | # n = 7 220 | 1.850, 2.460, 2.913, 3.390, 4.658, 2.199, 2.920, 3.456, 4.020, 221 | 5.520, 2.876, 3.813, 4.508, 5.241, 7.191, 222 | # n = 8 223 | 1.823, 2.364, 2.754, 3.156, 4.189, 2.167, 2.808, 3.270, 3.746, 224 | 4.968, 2.836, 3.670, 4.271, 4.889, 6.479, 225 | # n = 9 226 | 1.802, 2.292, 2.637, 2.986, 3.860, 2.143, 2.723, 3.132, 3.546, 227 | 4.581, 2.806, 3.562, 4.094, 4.633, 5.980, 228 | # n = 10 229 | 1.785, 2.235, 2.546, 2.856, 3.617, 2.124, 2.657, 3.026, 3.393, 230 | 4.294, 2.783, 3.478, 3.958, 4.437, 5.610, 231 | # n = 11 232 | 1.772, 2.189, 2.473, 2.754, 3.429, 2.109, 2.604, 2.941, 3.273, 233 | 4.073, 2.764, 3.410, 3.849, 4.282, 5.324, 234 | # n = 12 235 | 1.761, 2.152, 2.414, 2.670, 3.279, 2.096, 2.560, 2.871, 3.175, 236 | 3.896, 2.748, 3.353, 3.759, 4.156, 5.096, 237 | # n = 13 238 | 1.752, 2.120, 2.364, 2.601, 3.156, 2.085, 2.522, 2.812, 3.093, 239 | 3.751, 2.735, 3.306, 3.684, 4.051, 4.909, 240 | # n = 14 241 | 1.744, 2.093, 2.322, 2.542, 3.054, 2.076, 2.490, 2.762, 3.024, 242 | 3.631, 2.723, 3.265, 3.620, 3.962, 4.753, 243 | # n = 15 244 | 1.737, 2.069, 2.285, 2.492, 2.967, 2.068, 2.463, 2.720, 2.965, 245 | 3.529, 2.714, 3.229, 3.565, 3.885, 4.621, 246 | # n = 16 247 | 1.731, 2.049, 2.254, 2.449, 2.893, 2.061, 2.439, 2.682, 2.913, 248 | 3.441, 2.705, 3.198, 3.517, 3.819, 4.507, 249 | # n = 17 250 | 1.726, 2.030, 2.226, 2.410, 2.828, 2.055, 2.417, 2.649, 2.868, 251 | 3.364, 2.698, 3.171, 3.474, 3.761, 4.408, 252 | # n = 18 253 | 1.721, 2.014, 2.201, 2.376, 2.771, 2.050, 2.398, 2.620, 2.828, 254 | 3.297, 2.691, 3.146, 3.436, 3.709, 4.321, 255 | # n = 19 256 | 1.717, 2.000, 2.178, 2.346, 2.720, 2.045, 2.381, 2.593, 2.793, 257 | 3.237, 2.685, 3.124, 3.402, 3.663, 4.244, 258 | # n = 20 259 | 1.714, 1.987, 2.158, 2.319, 2.675, 2.041, 2.365, 2.570, 2.760, 260 | 3.184, 2.680, 3.104, 3.372, 3.621, 4.175, 261 | # n = 21 262 | 1.710, 1.975, 2.140, 2.294, 2.635, 2.037, 2.351, 2.548, 2.731, 263 | 3.136, 2.675, 3.086, 3.344, 3.583, 4.113, 264 | # n = 22 265 | 1.707, 1.964, 2.123, 2.272, 2.598, 2.034, 2.338, 2.528, 2.705, 266 | 3.092, 2.670, 3.070, 3.318, 3.549, 4.056, 267 | # n = 23 268 | 1.705, 1.954, 2.108, 2.251, 2.564, 2.030, 2.327, 2.510, 2.681, 269 | 3.053, 2.666, 3.054, 3.295, 3.518, 4.005, 270 | # n = 24 271 | 1.702, 1.944, 2.094, 2.232, 2.534, 2.027, 2.316, 2.494, 2.658, 272 | 3.017, 2.662, 3.040, 3.274, 3.489, 3.958, 273 | # n = 25 274 | 1.700, 1.936, 2.081, 2.215, 2.506, 2.025, 2.306, 2.479, 2.638, 275 | 2.984, 2.659, 3.027, 3.254, 3.462, 3.915, 276 | # n = 26 277 | 1.698, 1.928, 2.069, 2.199, 2.480, 2.022, 2.296, 2.464, 2.619, 278 | 2.953, 2.656, 3.015, 3.235, 3.437, 3.875, 279 | # n = 27 280 | 1.696, 1.921, 2.058, 2.184, 2.456, 2.020, 2.288, 2.451, 2.601, 281 | 2.925, 2.653, 3.004, 3.218, 3.415, 3.838, 282 | # n = 28 283 | 1.694, 1.914, 2.048, 2.170, 2.434, 2.018, 2.279, 2.439, 2.585, 284 | 2.898, 2.650, 2.993, 3.202, 3.393, 3.804, 285 | # n = 29 286 | 1.692, 1.907, 2.038, 2.157, 2.413, 2.016, 2.272, 2.427, 2.569, 287 | 2.874, 2.648, 2.983, 3.187, 3.373, 3.772, 288 | # n = 30 289 | 1.691, 1.901, 2.029, 2.145, 2.394, 2.014, 2.265, 2.417, 2.555, 290 | 2.851, 2.645, 2.974, 3.173, 3.355, 3.742, 291 | # n = 35 292 | 1.684, 1.876, 1.991, 2.094, 2.314, 2.006, 2.234, 2.371, 2.495, 293 | 2.756, 2.636, 2.935, 3.114, 3.276, 3.618, 294 | # n = 40 295 | 1.679, 1.856, 1.961, 2.055, 2.253, 2.001, 2.211, 2.336, 2.448, 296 | 2.684, 2.628, 2.905, 3.069, 3.216, 3.524, 297 | # n = 50 298 | 1.672, 1.827, 1.918, 1.999, 2.166, 1.992, 2.177, 2.285, 2.382, 299 | 2.580, 2.618, 2.861, 3.003, 3.129, 3.390, 300 | # n = 60 301 | 1.668, 1.807, 1.888, 1.960, 2.106, 1.987, 2.154, 2.250, 2.335, 302 | 2.509, 2.611, 2.830, 2.956, 3.068, 3.297, 303 | # n = 120 304 | 1.656, 1.752, 1.805, 1.851, 1.943, 1.974, 2.087, 2.151, 2.206, 305 | 2.315, 2.594, 2.743, 2.826, 2.899, 3.043, 306 | # n = 240 307 | 1.651, 1.716, 1.753, 1.783, 1.844, 1.967, 2.045, 2.088, 2.125, 308 | 2.197, 2.585, 2.688, 2.744, 2.793, 2.887, 309 | # n = 480 310 | 1.648, 1.694, 1.718, 1.739, 1.780, 1.963, 2.018, 2.048, 2.073, 311 | 2.121, 2.580, 2.652, 2.691, 2.724, 2.787, 312 | # n = infinity 313 | 1.645, 1.645, 1.645, 1.645, 1.645, 1.960, 1.960, 1.960, 1.960, 314 | 1.960, 2.576, 2.576, 2.576, 2.576, 2.576, 315 | ]) 316 | 317 | factor_g = factor_g.reshape(sample_size.size, coverage.size) 318 | -------------------------------------------------------------------------------- /toleranceinterval/twoside/_normal_exact.py: -------------------------------------------------------------------------------- 1 | # Author: Copyright (c) 2021 Jed Ludlow 2 | # License: MIT License 3 | 4 | r""" 5 | Algorithm for computing the exact two-sided statistical tolerance interval 6 | factor under the assumption of a normal distribution. 7 | 8 | This module is a Python port of an algorithm written in MATLAB by 9 | Viktor Witkovsky and posted to the MATLAB Central File Exchange, retrieved 10 | 2021-03-12 at this URL: 11 | 12 | https://www.mathworks.com/matlabcentral/fileexchange/24135-tolerancefactor 13 | 14 | Here is Witkovsky's original copyright and disclaimer: 15 | 16 | ------------------------------------------------------------------------------- 17 | Copyright (c) 2020, Viktor Witkovsky 18 | Copyright (c) 2013, Viktor Witkovsky 19 | All rights reserved. 20 | 21 | Redistribution and use in source and binary forms, with or without 22 | modification, are permitted provided that the following conditions are met: 23 | 24 | * Redistributions of source code must retain the above copyright notice, this 25 | list of conditions and the following disclaimer. 26 | * Redistributions in binary form must reproduce the above copyright notice, 27 | this list of conditions and the following disclaimer in the documentation 28 | and/or other materials provided with the distribution 29 | * Neither the name of Institute of Measurement Science, Slovak Academy of 30 | Sciences, Bratislava, Slovakia nor the names of its 31 | contributors may be used to endorse or promote products derived from this 32 | software without specific prior written permission. 33 | 34 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 35 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 36 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 37 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 38 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 39 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 40 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 41 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 42 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 43 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 44 | ------------------------------------------------------------------------------- 45 | """ 46 | 47 | import numpy as np 48 | import scipy.stats as stats 49 | import scipy.special as spec 50 | import scipy.integrate as integ 51 | import scipy.optimize as optim 52 | 53 | SQRT_2 = 1.4142135623730950488 54 | SQRT_2PI = 2.5066282746310005024 55 | 56 | 57 | def tolerance_factor(n, coverage, confidence, m=None, nu=None, d2=None, 58 | simultaneous=False, tailprob=False): 59 | r""" 60 | Compute the exact tolerance factor k for the two-sided statistical 61 | tolerance interval to contain a proportion of a population with given 62 | confidence under normal distribution assumptions. 63 | 64 | Computes (by Gauss-Kronod quadrature) the exact tolerance factor k for the 65 | two-sided coverage-content and (1-alpha)-confidence tolerance interval 66 | 67 | TI = [Xmean - k * S, Xmean + k * S] 68 | 69 | where Xmean = mean(X), S = std(X), X = [X_1,...,X_n] is a random sample 70 | of size n from the distribution N(mu,sig2) with unknown mean mu and 71 | variance sig2. 72 | 73 | The tolerance factor k is determined such that the tolerance intervals 74 | with confidence (1-alpha) cover at least the coverage fraction 75 | of the distribution N(mu,sigma^2), i.e. 76 | Prob[ Prob( Xmean - k * S < X < Xmean + k * S ) >= p ]= 1-alpha, 77 | for X ~ N(mu,sig2) which is independent with Xmean and S. For more 78 | details see e.g. Krishnamoorthy and Mathew (2009). 79 | 80 | REMARK: 81 | If S is a pooled estimator of sig, based on m random samples of size n, 82 | tolerance_factor computes the simultaneous (or non-simultaneous) exact 83 | tolerance factor k for the two-sided coverage-content and 84 | (1-alpha)-confidence tolerance intervals 85 | 86 | TI = [Xmean_i - k * S, Xmean_i + k * S], for i = 1,...,m 87 | 88 | where Xmean_i = mean(X_i), X_i = [X_i1,...,X_in] is a random sample of 89 | size n from the distribution N(mu,sig2) with unknown mean mu and 90 | variance sig2, and S = sqrt(S2), S2 is the pooled estimator of sig2, 91 | 92 | S2 = (1/nu) * sum_i=1:m ( sum_j=1:n (X_ij - Xmean_i)^2 ) 93 | 94 | with nu degrees of freedom, nu = m * (n-1). 95 | 96 | Parameters 97 | ---------- 98 | n : scalar 99 | Sample size 100 | coverage : scalar in the interval [0.0, 1.0] 101 | Coverage (or content) probability, 102 | Prob( Xmean - k * S < X < Xmean + k * S ) >= coverage 103 | confidence : scalar in the interval [0.0, 1.0] 104 | Confidence probability, 105 | Prob[ Prob( Xmean-k*S < X < Xmean+k*S ) >= coverage ] = confidence. 106 | m : scalar 107 | Number of independent random samples (of size n). If None, 108 | default value is m = 1. 109 | nu : scalar 110 | Degrees of freedom for distribution of the (pooled) sample 111 | variance S2. If None, default value is nu = m*(n-1). 112 | d2 : scalar 113 | Normalizing constant. For computing the factors of the 114 | non-simultaneous tolerance limits (xx'*betaHat +/- k * S) 115 | for the linear regression y = XX*beta +epsilon, set d2 = 116 | xx'*inv(XX'*XX)*xx. 117 | Typically, in simple linear regression the estimator S2 has 118 | nu = n-2 degrees of freedom. If None, default value is d2 = 1/n. 119 | simultaneous : boolean 120 | Logical flag for calculating the factor for 121 | simultaneous tolerance intervals. If False, tolerance_factor will 122 | calculate the factor for the non-simultaneous tolerance interval. 123 | Default value is False. 124 | tailprob : boolean 125 | Logical flag for representing the input probabilities 126 | 'coverage' and 'confidence'. If True, the input parameters are 127 | represented as the tail coverage (i.e. 1 - coverage) and tail 128 | confidence (i.e. 1 - confidence). This option is useful if the 129 | interest is to calculate the tolerance factor for extremely large 130 | values of coverage and/or confidence, close to 1, as e.g. 131 | coverage = 1 - 1e-18. Default value is False. 132 | 133 | Returns 134 | ------- 135 | float 136 | The calculated tolerance factor for the tolerance interval. 137 | 138 | References 139 | ---------- 140 | [1] Krishnamoorthy K, Mathew T. (2009). Statistical Tolerance Regions: 141 | Theory, Applications, and Computation. John Wiley & Sons, Inc., 142 | Hoboken, New Jersey. ISBN: 978-0-470-38026-0, 512 pages. 143 | 144 | [2] Witkovsky V. On the exact two-sided tolerance intervals for 145 | univariate normal distribution and linear regression. Austrian 146 | Journal of Statistics 43(4), 2014, 279-92. 147 | http://ajs.data-analysis.at/index.php/ajs/article/viewFile/vol43-4-6/35 148 | 149 | [3] ISO 16269-6:2014: Statistical interpretation of data - Part 6: 150 | Determination of statistical tolerance intervals. 151 | 152 | [4] Janiga I., Garaj I.: Two-sided tolerance limits of normal 153 | distributions with unknown means and unknown common variability. 154 | MEASUREMENT SCIENCE REVIEW, Volume 3, Section 1, 2003, 75-78. 155 | 156 | [5] Robert W. Mee (1990) Simultaneous Tolerance Intervals for Normal 157 | Populations With Common Variance, Technometrics, 32:1, 83-92, 158 | DOI: 10.1080/00401706.1990.10484595 159 | 160 | """ 161 | # Coverage and confidence must be within [0.0, 1.0]. 162 | if (coverage < 0.0) or (coverage > 1.0): 163 | raise ValueError('coverage must be within the interval [0.0, 1.0].') 164 | if (confidence < 0.0) or (confidence > 1.0): 165 | raise ValueError('confidence must be within the interval [0.0, 1.0].') 166 | 167 | # Handle defaults for keyword inputs. 168 | if m is None: 169 | m = 1 170 | if nu is None: 171 | nu = m * (n - 1) 172 | if d2 is None: 173 | d2 = 1.0 / n 174 | 175 | # Set values for limiting cases of coverage and confidence. 176 | if confidence == 0: 177 | k = np.nan 178 | return k 179 | if confidence == 1: 180 | k = np.inf 181 | return k 182 | if coverage == 1: 183 | k = np.inf 184 | return k 185 | if coverage == 0: 186 | k = 0.0 187 | return k 188 | 189 | # Set the constants. 190 | if tailprob: 191 | tailconfidence = confidence 192 | tailcoverage = coverage 193 | else: 194 | tailconfidence = round(1.0e+16 * (1.0 - confidence)) / 1.0e+16 195 | tailcoverage = round(1.0e+16 * (1.0 - coverage)) / 1.0e+16 196 | 197 | # Return the result for nu = Inf. 198 | if nu == np.inf: 199 | k = SQRT_2 * spec.erfcinv(tailcoverage) 200 | return k 201 | 202 | # Handle the case of nu not being positive. 203 | if nu <= 0: 204 | raise (ValueError, 'Degrees of freedom should be positive.') 205 | 206 | # Compute the two-sided tolerance factor 207 | tol_high_precision = np.spacing(tailconfidence) 208 | 209 | # Integration limits 210 | A = 0.0 211 | B = 10.0 212 | 213 | if m > 1: 214 | k0, _ = _approx_tol_factor_witkovsky( 215 | tailcoverage, tailconfidence, d2, m, nu, A, B) 216 | else: 217 | k0, _ = _approx_tol_factor_wald_wolfowitz( 218 | tailcoverage, tailconfidence, d2, nu) 219 | 220 | # Compute the tolerance factor. 221 | sol = optim.newton(lambda k: (_integral_gauss_kronod( 222 | k, nu, m, d2, tailcoverage, simultaneous, A, B, tol_high_precision) 223 | - tailconfidence), k0) 224 | k = sol 225 | 226 | return k 227 | 228 | 229 | def _integral_gauss_kronod(k, nu, m, c, tailcoverage, simultaneous, A, B, tol): 230 | r""" 231 | Evaluates the integral defined by eqs. (1.2.4) and (2.5.8) in 232 | Krishnamoorthy and Mathew: Statistical Tolerance Regions, Wiley, 2009, 233 | (pp.7 and 52), by adaptive Gauss-Kronod quadrature. 234 | 235 | (The tol argument is currently ignored but may be used at some point. It 236 | was used in Witkovsky's integration, but we ignore it here and use 237 | SciPy default tolerances, which seem to be adequate.) 238 | 239 | """ 240 | val, _ = integ.quad(lambda z: _integrand( 241 | z, k, nu, m, c, tailcoverage, simultaneous), A, B) 242 | val *= 2 243 | return val 244 | 245 | 246 | def _integrand(z, k, nu, m, c, tailcoverage, simultaneous): 247 | r""" 248 | Integrand for Gauss-Kronod quadrature. 249 | 250 | """ 251 | root = _find_root(np.sqrt(c) * z, tailcoverage) 252 | ncx2pts = nu * root**2 253 | factor = np.exp(-0.5 * z**2) / SQRT_2PI 254 | 255 | if simultaneous: 256 | factor = factor * (m * (1 - (spec.erfc(z / SQRT_2))) ** (m - 1)) 257 | 258 | x = ncx2pts / k ** 2 259 | fun = spec.gammainc(nu / 2.0, x / 2.0) * factor 260 | return fun 261 | 262 | 263 | def _find_root(x, tailcoverage): 264 | r""" 265 | Numerically finds the solution (root), of the equation 266 | 267 | normcdf(x+root) - normcdf(x-root) = coverage = 1 - tailcoverage 268 | 269 | by Halley's method for finding the root of the function 270 | 271 | fun(r) = fun(r|x,tailcoverage) 272 | 273 | based on two derivatives, funD1(r|x,tailcoverage) 274 | and funD2(r|x,tailcoverage) of the fun(r|x,tailcoverage), (for given x 275 | and tailcoverage). 276 | 277 | Note that r = sqrt(ncx2inv(1-tailcoverage,1,x^2)), where ncx2inv is the 278 | inverse of the noncentral chi-square cdf. 279 | 280 | """ 281 | # Set the constants 282 | max_iterations = 100 283 | iteration = 0 284 | 285 | # Set the appropriate tolerance 286 | if np.spacing(tailcoverage) < np.spacing(1): 287 | tol = min(10 * np.spacing(tailcoverage), np.spacing(1)) 288 | 289 | # Set the starting value of the root r: r0 = x + norminv(coverage) 290 | r = x + SQRT_2 * spec.erfcinv(2 * tailcoverage) 291 | 292 | # Main loop (Halley's method) 293 | while True: 294 | iteration += 1 295 | fun, fun_d1, fun_d2 = _complementary_content(r, x, tailcoverage) 296 | # Halley's method 297 | r = r - 2 * fun * fun_d1 / (2 * fun_d1 ** 2 - fun * fun_d2) 298 | if iteration > max_iterations: 299 | break 300 | if np.all(np.abs(fun) < tol): 301 | break 302 | return r 303 | 304 | 305 | def _complementary_content(r, x, tailcoverage): 306 | r""" 307 | Complementary content function. 308 | 309 | Calculates difference between the complementary content and given 310 | the tailcoverage 311 | 312 | fun(r|x,tailcoverage) = 1 - (normcdf(x+r) - normcdf(x-r)) - tailcoverage 313 | 314 | and the first (fun_d1) and the second (fun_d2) derivative of the function 315 | 316 | fun(r|x,tailcoverage) 317 | 318 | """ 319 | fun = 0.5 * ( 320 | spec.erfc((x + r) / SQRT_2) 321 | + spec.erfc(-(x - r) / SQRT_2) 322 | ) - tailcoverage 323 | aux1 = np.exp(-0.5 * (x + r)**2) 324 | aux2 = np.exp(-0.5 * (x - r)**2) 325 | fun_d1 = -(aux1 + aux2) / SQRT_2PI 326 | fun_d2 = -((x - r) * aux2 - (x + r) * aux1) / SQRT_2PI 327 | return (fun, fun_d1, fun_d2) 328 | 329 | 330 | def _approx_tol_factor_wald_wolfowitz(tailcoverage, tailconfidence, c, nu): 331 | r""" 332 | Compute the approximate tolerance factor (Wald-Wolfowitz). 333 | 334 | """ 335 | r = _find_root(np.sqrt(c), tailcoverage) 336 | k = r * np.sqrt(nu / stats.chi2.ppf(tailconfidence, nu)) 337 | return (k, r) 338 | 339 | 340 | def _approx_tol_factor_witkovsky(tailcoverage, tailconfidence, c, m, nu, A, B): 341 | r""" 342 | Compute the approximate tolerance factor (Witkovsky). 343 | 344 | """ 345 | val, _ = integ.quad(lambda z: _expect_fun(z, c, tailcoverage, m), A, B) 346 | r = np.sqrt(2 * val) 347 | k = r * np.sqrt(nu / stats.chi2.ppf(tailconfidence, nu)) 348 | return (k, r) 349 | 350 | 351 | def _expect_fun(z, c, tailcoverage, m): 352 | r""" 353 | Expectation function. 354 | 355 | """ 356 | r = _find_root(np.sqrt(c) * z, tailcoverage) 357 | f = r ** 2 * stats.norm.pdf(z) 358 | if m > 1: 359 | f = f * (m * (1 - (spec.erfc(z / SQRT_2))) ** (m - 1)) 360 | return f 361 | -------------------------------------------------------------------------------- /tests/test_twoside_normal_factor_iso.py: -------------------------------------------------------------------------------- 1 | # Author: Copyright (c) 2021 Jed Ludlow 2 | # License: MIT License 3 | 4 | """ 5 | Test normla_factor against standard tables of tolerance factors 6 | as published in ISO 16269-6:2014 Annex F. 7 | 8 | A sampling of values from the tables is included here for brevity. 9 | 10 | """ 11 | 12 | import numpy as np 13 | import toleranceinterval.twoside as ts 14 | import unittest 15 | 16 | 17 | def decimal_ceil(x, places): 18 | """ 19 | Apply ceiling function at a decimal place. 20 | 21 | The tables of tolerance factors in ISO 16269-6:2014 provide 22 | the tolerance factors to a certain number of decimal places. The values 23 | at that final decimal place reflect the application of the ceiling function 24 | at that decimal place. 25 | 26 | """ 27 | x *= 10 ** places 28 | x = np.ceil(x) 29 | x /= 10 ** places 30 | return x 31 | 32 | 33 | class BaseTestIso: 34 | 35 | class TestIsoTableF(unittest.TestCase): 36 | 37 | def test_tolerance_factor(self): 38 | for row_idx, row in enumerate(self.factor_k5): 39 | for col_idx, k5 in enumerate(row): 40 | k = ts.normal_factor( 41 | self.sample_size[row_idx], 42 | self.coverage, 43 | self.confidence, 44 | method='exact', 45 | m=self.number_of_samples[col_idx]) 46 | k = decimal_ceil(k, places=4) 47 | self.assertAlmostEqual(k, k5, places=4) 48 | 49 | 50 | class TestIsoF1(BaseTestIso.TestIsoTableF): 51 | 52 | coverage = 0.90 53 | 54 | confidence = 0.90 55 | 56 | # This is n from the table. 57 | sample_size = np.array([ 58 | 2, 8, 16, 35, 100, 300, 1000, np.inf, 59 | ]) 60 | 61 | # This is m from the table. 62 | number_of_samples = np.arange(1, 11) 63 | 64 | factor_k5 = np.array([ 65 | # n = 2 66 | 15.5124, 6.0755, 4.5088, 3.8875, 3.5544, 67 | 3.3461, 3.2032, 3.0989, 3.0193, 2.9565, 68 | # n = 8 69 | 2.7542, 2.3600, 2.2244, 2.1530, 2.1081, 70 | 2.0769, 2.0539, 2.0361, 2.0220, 2.0104, 71 | # n = 16 72 | 2.2537, 2.0574, 1.9833, 1.9426, 1.9163, 73 | 1.8977, 1.8837, 1.8727, 1.8638, 1.8564, 74 | # n = 35 75 | 1.9906, 1.8843, 1.8417, 1.8176, 1.8017, 76 | 1.7902, 1.7815, 1.7747, 1.7690, 1.7643, 77 | # n = 100 78 | 1.8232, 1.7697, 1.7473, 1.7343, 1.7256, 79 | 1.7193, 1.7144, 1.7105, 1.7073, 1.7047, 80 | # n = 300 81 | 1.7401, 1.7118, 1.6997, 1.6925, 1.6877, 82 | 1.6842, 1.6815, 1.6793, 1.6775, 1.6760, 83 | # n = 1000 84 | 1.6947, 1.6800, 1.6736, 1.6698, 1.6672, 85 | 1.6653, 1.6639, 1.6627, 1.6617, 1.6609, 86 | # n = infinity 87 | 1.6449, 1.6449, 1.6449, 1.6449, 1.6449, 88 | 1.6449, 1.6449, 1.6449, 1.6449, 1.6449, 89 | ]) 90 | 91 | factor_k5 = factor_k5.reshape(sample_size.size, number_of_samples.size) 92 | 93 | 94 | class TestIsoF2(BaseTestIso.TestIsoTableF): 95 | 96 | coverage = 0.95 97 | 98 | confidence = 0.90 99 | 100 | # This is n from the table. 101 | sample_size = np.array([ 102 | 3, 9, 15, 30, 90, 150, 400, 1000, np.inf, 103 | ]) 104 | 105 | # This is m from the table. 106 | number_of_samples = np.arange(1, 11) 107 | 108 | factor_k5 = np.array([ 109 | # n = 3 110 | 6.8233, 4.3320, 3.7087, 3.4207, 3.2528, 111 | 3.1420, 3.0630, 3.0038, 2.9575, 2.9205, 112 | # n = 9 113 | 3.1323, 2.7216, 2.5773, 2.5006, 2.4521, 114 | 2.4182, 2.3931, 2.3737, 2.3581, 2.3454, 115 | # n = 15 116 | 2.7196, 2.4718, 2.3789, 2.3280, 2.2951, 117 | 2.2719, 2.2545, 2.2408, 2.2298, 2.2206, 118 | # n = 30 119 | 2.4166, 2.2749, 2.2187, 2.1870, 2.1662, 120 | 2.1513, 2.1399, 2.1309, 2.1236, 2.1175, 121 | # n = 90 122 | 2.1862, 2.1182, 2.0898, 2.0733, 2.0624, 123 | 2.0544, 2.0482, 2.0433, 2.0393, 2.0360, 124 | # n = 150 125 | 2.1276, 2.0775, 2.0563, 2.0439, 2.0356, 126 | 2.0296, 2.0249, 2.0212, 2.0181, 2.0155, 127 | # n = 400 128 | 2.0569, 2.0282, 2.0158, 2.0085, 2.0035, 129 | 1.9999, 1.9971, 1.9949, 1.9930, 1.9915, 130 | # n = 1000 131 | 2.0193, 2.0018, 1.9942, 1.9897, 1.9866, 132 | 1.9844, 1.9826, 1.9812, 1.9800, 1.9791, 133 | # n = infinity 134 | 1.9600, 1.9600, 1.9600, 1.9600, 1.9600, 135 | 1.9600, 1.9600, 1.9600, 1.9600, 1.9600, 136 | ]) 137 | 138 | factor_k5 = factor_k5.reshape(sample_size.size, number_of_samples.size) 139 | 140 | 141 | class TestIsoF3(BaseTestIso.TestIsoTableF): 142 | 143 | coverage = 0.99 144 | 145 | confidence = 0.90 146 | 147 | # This is n from the table. 148 | sample_size = np.array([ 149 | 4, 8, 17, 28, 100, 300, 1000, np.inf, 150 | ]) 151 | 152 | # This is m from the table. 153 | number_of_samples = np.arange(1, 11) 154 | 155 | factor_k5 = np.array([ 156 | # n = 4 157 | 6.3722, 4.6643, 4.1701, 3.9277, 3.7814, 158 | 3.6825, 3.6108, 3.5562, 3.5131, 3.4782, 159 | # n = 8 160 | 4.2707, 3.6541, 3.4408, 3.3281, 3.2572, 161 | 3.2078, 3.1712, 3.1428, 3.1202, 3.1016, 162 | # n = 17 163 | 3.4741, 3.1819, 3.0708, 3.0095, 2.9698, 164 | 2.9416, 2.9204, 2.9037, 2.8902, 2.8791, 165 | # n = 28 166 | 3.2023, 3.0062, 2.9286, 2.8850, 2.8564, 167 | 2.8358, 2.8203, 2.8080, 2.7980, 2.7896, 168 | # n = 100 169 | 2.8548, 2.7710, 2.7358, 2.7155, 2.7018, 170 | 2.6919, 2.6843, 2.6782, 2.6732, 2.6690, 171 | # n = 300 172 | 2.7249, 2.6806, 2.6616, 2.6504, 2.6429, 173 | 2.6374, 2.6331, 2.6297, 2.6269, 2.6245, 174 | # n = 1000 175 | 2.6538, 2.6308, 2.6208, 2.6148, 2.6108, 176 | 2.6079, 2.6056, 2.6037, 2.6022, 2.6009, 177 | # n = infinity 178 | 2.5759, 2.5759, 2.5759, 2.5759, 2.5759, 179 | 2.5759, 2.5759, 2.5759, 2.5759, 2.5759, 180 | ]) 181 | 182 | factor_k5 = factor_k5.reshape(sample_size.size, number_of_samples.size) 183 | 184 | 185 | class TestIsoF4(BaseTestIso.TestIsoTableF): 186 | 187 | coverage = 0.90 188 | 189 | confidence = 0.95 190 | 191 | # This is n from the table. 192 | sample_size = np.array([ 193 | 2, 8, 16, 35, 150, 500, 1000, np.inf, 194 | ]) 195 | 196 | # This is m from the table. 197 | number_of_samples = np.arange(1, 11) 198 | 199 | factor_k5 = np.array([ 200 | # n = 2 201 | 31.0923, 8.7252, 5.8380, 4.7912, 4.2571, 202 | 3.9341, 3.7179, 3.5630, 3.4468, 3.3565, 203 | # n = 8 204 | 3.1561, 2.5818, 2.3937, 2.2974, 2.2381, 205 | 2.1978, 2.1685, 2.1463, 2.1289, 2.1149, 206 | # n = 16 207 | 2.4486, 2.1771, 2.0777, 2.0241, 1.9899, 208 | 1.9661, 1.9483, 1.9346, 1.9237, 1.9147, 209 | # n = 35 210 | 2.0943, 1.9515, 1.8953, 1.8638, 1.8432, 211 | 1.8285, 1.8174, 1.8087, 1.8016, 1.7957, 212 | # n = 150 213 | 1.8260, 1.7710, 1.7478, 1.7344, 1.7254, 214 | 1.7188, 1.7137, 1.7097, 1.7064, 1.7036, 215 | # n = 500 216 | 1.7374, 1.7098, 1.6979, 1.6908, 1.6861, 217 | 1.6826, 1.6799, 1.6777, 1.6760, 1.6744, 218 | # n = 1000 219 | 1.7088, 1.6898, 1.6816, 1.6767, 1.6734, 220 | 1.6709, 1.6690, 1.6675, 1.6663, 1.6652, 221 | # n = infinity 222 | 1.6449, 1.6449, 1.6449, 1.6449, 1.6449, 223 | 1.6449, 1.6449, 1.6449, 1.6449, 1.6449, 224 | ]) 225 | 226 | factor_k5 = factor_k5.reshape(sample_size.size, number_of_samples.size) 227 | 228 | 229 | class TestIsoF5(BaseTestIso.TestIsoTableF): 230 | 231 | coverage = 0.95 232 | 233 | confidence = 0.95 234 | 235 | # This is n from the table. 236 | sample_size = np.array([ 237 | 5, 10, 26, 90, 200, 1000, np.inf, 238 | ]) 239 | 240 | # This is m from the table. 241 | number_of_samples = np.arange(1, 11) 242 | 243 | factor_k5 = np.array([ 244 | # n = 5 245 | 5.0769, 3.6939, 3.2936, 3.0986, 2.9820, 246 | 2.9041, 2.8482, 2.8062, 2.7734, 2.7472, 247 | # n = 10 248 | 3.3935, 2.8700, 2.6904, 2.5964, 2.5377, 249 | 2.4973, 2.4677, 2.4450, 2.4271, 2.4125, 250 | # n = 26 251 | 2.6188, 2.4051, 2.3227, 2.2771, 2.2476, 252 | 2.2266, 2.2108, 2.1985, 2.1886, 2.1803, 253 | # n = 90 254 | 2.2519, 2.1622, 2.1251, 2.1037, 2.0895, 255 | 2.0792, 2.0713, 2.0650, 2.0598, 2.0555, 256 | # n = 200 257 | 2.1430, 2.0877, 2.0642, 2.0505, 2.0413, 258 | 2.0346, 2.0294, 2.0253, 2.0219, 2.0190, 259 | # n = 1000 260 | 2.0362, 2.0135, 2.0037, 1.9979, 1.9939, 261 | 1.9910, 1.9888, 1.9870, 1.9855, 1.9842, 262 | # n = infinity 263 | 1.9600, 1.9600, 1.9600, 1.9600, 1.9600, 264 | 1.9600, 1.9600, 1.9600, 1.9600, 1.9600, 265 | ]) 266 | 267 | factor_k5 = factor_k5.reshape(sample_size.size, number_of_samples.size) 268 | 269 | 270 | class TestIsoF6(BaseTestIso.TestIsoTableF): 271 | 272 | coverage = 0.99 273 | 274 | confidence = 0.95 275 | 276 | # This is n from the table. 277 | sample_size = np.array([ 278 | 3, 9, 17, 35, 100, 500, np.inf, 279 | ]) 280 | 281 | # This is m from the table. 282 | number_of_samples = np.arange(1, 11) 283 | 284 | factor_k5 = np.array([ 285 | # n = 3 286 | 12.6472, 6.8474, 5.5623, 4.9943, 4.6711, 287 | 4.4612, 4.3133, 4.2032, 4.1180, 4.0500, 288 | # n = 9 289 | 4.6329, 3.8544, 3.5909, 3.4534, 3.3677, 290 | 3.3085, 3.2651, 3.2317, 3.2052, 3.1837, 291 | # n = 17 292 | 3.7606, 3.3572, 3.2077, 3.1264, 3.0743, 293 | 3.0377, 3.0104, 2.9892, 2.9722, 2.9582, 294 | # n = 35 295 | 3.2762, 3.0522, 2.9638, 2.9143, 2.8818, 296 | 2.8586, 2.8411, 2.8273, 2.8161, 2.8068, 297 | # n = 100 298 | 2.9356, 2.8253, 2.7794, 2.7529, 2.7352, 299 | 2.7224, 2.7126, 2.7048, 2.6984, 2.6930, 300 | # n = 500 301 | 2.7208, 2.6775, 2.6588, 2.6478, 2.6403, 302 | 2.6349, 2.6307, 2.6273, 2.6245, 2.6221, 303 | # n = infinity 304 | 2.5759, 2.5759, 2.5759, 2.5759, 2.5759, 305 | 2.5759, 2.5759, 2.5759, 2.5759, 2.5759, 306 | ]) 307 | 308 | factor_k5 = factor_k5.reshape(sample_size.size, number_of_samples.size) 309 | 310 | 311 | class TestIsoF7(BaseTestIso.TestIsoTableF): 312 | 313 | coverage = 0.90 314 | 315 | confidence = 0.99 316 | 317 | # This is n from the table. 318 | sample_size = np.array([ 319 | 4, 10, 22, 80, 200, 1000, np.inf, 320 | ]) 321 | 322 | # This is m from the table. 323 | number_of_samples = np.arange(1, 11) 324 | 325 | factor_k5 = np.array([ 326 | # n = 4 327 | 9.4162, 4.9212, 3.9582, 3.5449, 3.3166, 328 | 3.1727, 3.0742, 3.0028, 2.9489, 2.9068, 329 | # n = 10 330 | 3.6167, 2.8193, 2.5709, 2.4481, 2.3748, 331 | 2.3265, 2.2923, 2.2671, 2.2477, 2.2324, 332 | # n = 22 333 | 2.5979, 2.2631, 2.1429, 2.0791, 2.0393, 334 | 2.0120, 1.9921, 1.9770, 1.9652, 1.9558, 335 | # n = 80 336 | 2.0282, 1.9056, 1.8562, 1.8281, 1.8097, 337 | 1.7964, 1.7864, 1.7785, 1.7721, 1.7668, 338 | # n = 200 339 | 1.8657, 1.7973, 1.7686, 1.7520, 1.7409, 340 | 1.7328, 1.7266, 1.7216, 1.7176, 1.7142, 341 | # n = 1000 342 | 1.7359, 1.7086, 1.6967, 1.6897, 1.6850, 343 | 1.6815, 1.6788, 1.6767, 1.6749, 1.6734, 344 | # n = infinity 345 | 1.6449, 1.6449, 1.6449, 1.6449, 1.6449, 346 | 1.6449, 1.6449, 1.6449, 1.6449, 1.6449, 347 | ]) 348 | 349 | factor_k5 = factor_k5.reshape(sample_size.size, number_of_samples.size) 350 | 351 | 352 | class TestIsoF8(BaseTestIso.TestIsoTableF): 353 | 354 | coverage = 0.95 355 | 356 | confidence = 0.99 357 | 358 | # This is n from the table. 359 | sample_size = np.array([ 360 | 2, 9, 17, 40, 150, 500, np.inf, 361 | ]) 362 | 363 | # This is m from the table. 364 | number_of_samples = np.arange(1, 11) 365 | 366 | factor_k5 = np.array([ 367 | # n = 2 368 | 182.7201, 23.1159, 11.9855, 8.7010, 7.1975, 369 | 6.3481, 5.8059, 5.4311, 5.1573, 4.9489, 370 | # n = 9 371 | 4.5810, 3.4807, 3.1443, 2.9784, 2.8793, 372 | 2.8136, 2.7670, 2.7324, 2.7057, 2.6846, 373 | # n = 17 374 | 3.3641, 2.8501, 2.6716, 2.5784, 2.5207, 375 | 2.4814, 2.4529, 2.4314, 2.4147, 2.4013, 376 | # n = 40 377 | 2.6836, 2.4425, 2.3498, 2.2987, 2.2658, 378 | 2.2427, 2.2254, 2.2120, 2.2013, 2.1926, 379 | # n = 150 380 | 2.2712, 2.1740, 2.1336, 2.1103, 2.0948, 381 | 2.0835, 2.0749, 2.0681, 2.0625, 2.0578, 382 | # n = 500 383 | 2.1175, 2.0697, 2.0492, 2.0372, 2.0291, 384 | 2.0231, 2.0185, 2.0149, 2.0118, 2.0093, 385 | # n = infinity 386 | 1.9600, 1.9600, 1.9600, 1.9600, 1.9600, 387 | 1.9600, 1.9600, 1.9600, 1.9600, 1.9600, 388 | ]) 389 | 390 | factor_k5 = factor_k5.reshape(sample_size.size, number_of_samples.size) 391 | 392 | 393 | class TestIsoF9(BaseTestIso.TestIsoTableF): 394 | 395 | coverage = 0.99 396 | 397 | confidence = 0.99 398 | 399 | # This is n from the table. 400 | sample_size = np.array([ 401 | 3, 7, 15, 28, 70, 200, 1000, np.inf, 402 | ]) 403 | 404 | # This is m from the table. 405 | number_of_samples = np.arange(1, 11) 406 | 407 | factor_k5 = np.array([ 408 | # n = 3 409 | 28.5857, 10.6204, 7.6599, 6.4888, 5.8628, 410 | 5.4728, 5.2065, 5.0131, 4.8663, 4.7512, 411 | # n = 7 412 | 7.1908, 5.0656, 4.4559, 4.1605, 3.9847, 413 | 3.8678, 3.7844, 3.7220, 3.6736, 3.6350, 414 | # n = 15 415 | 4.6212, 3.8478, 3.5825, 3.4441, 3.3581, 416 | 3.2992, 3.2564, 3.2238, 3.1983, 3.1777, 417 | # n = 28 418 | 3.8042, 3.3792, 3.2209, 3.1350, 3.0801, 419 | 3.0418, 3.0135, 2.9916, 2.9742, 2.9600, 420 | # n = 70 421 | 3.2284, 3.0179, 2.9334, 2.8857, 2.8544, 422 | 2.8319, 2.8150, 2.8016, 2.7908, 2.7818, 423 | # n = 200 424 | 2.9215, 2.8144, 2.7695, 2.7434, 2.7260, 425 | 2.7133, 2.7036, 2.6958, 2.6894, 2.6841, 426 | # n = 1000 427 | 2.7184, 2.6756, 2.6570, 2.6461, 2.6387, 428 | 2.6332, 2.6290, 2.6257, 2.6229, 2.6205, 429 | # n = infinity 430 | 2.5759, 2.5759, 2.5759, 2.5759, 2.5759, 431 | 2.5759, 2.5759, 2.5759, 2.5759, 2.5759, 432 | ]) 433 | 434 | factor_k5 = factor_k5.reshape(sample_size.size, number_of_samples.size) 435 | -------------------------------------------------------------------------------- /toleranceinterval/oneside/oneside.py: -------------------------------------------------------------------------------- 1 | # -- coding: utf-8 -- 2 | # MIT License 3 | # 4 | # Copyright (c) 2019 Charles Jekel 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | import numpy as np 25 | from scipy.stats import binom, norm, nct 26 | from ..hk import HansonKoopmans 27 | from ..checks import numpy_array, assert_2d_sort 28 | 29 | 30 | def normal(x, p, g): 31 | r""" 32 | Compute one-side tolerance bound using the normal distribution. 33 | 34 | Computes the one-sided tolerance interval using the normal distribution. 35 | This follows the derivation in [1] to calculate the interval as a factor 36 | of sample standard deviations away from the sample mean. See also [2]. 37 | 38 | Parameters 39 | ---------- 40 | x : ndarray (1-D, or 2-D) 41 | Numpy array of samples to compute the tolerance bound. Assumed data 42 | type is np.float. Shape of (m, n) is assumed for 2-D arrays with m 43 | number of sets of sample size n. 44 | p : float 45 | Percentile for the TI to estimate. 46 | g : float 47 | Confidence level where g > 0. and g < 1. 48 | 49 | Returns 50 | ------- 51 | ndarray (1-D) 52 | The normal distribution toleranace bound. 53 | 54 | References 55 | ---------- 56 | [1] Young, D. S. (2010). tolerance: An R Package for Estimating 57 | Tolerance Intervals. Journal of Statistical Software; Vol 1, Issue 5 58 | (2010). Retrieved from http://dx.doi.org/10.18637/jss.v036.i05 59 | 60 | [2] Montgomery, D. C., & Runger, G. C. (2018). Chapter 8. Statistical 61 | Intervals for a Single Sample. In Applied Statistics and Probability 62 | for Engineers, 7th Edition. 63 | 64 | Examples 65 | -------- 66 | Estimate the 10th percentile lower bound with 95% confidence of the 67 | following 100 random samples from a normal distribution. 68 | 69 | >>> import numpy as np 70 | >>> import toleranceinterval as ti 71 | >>> x = np.random.nomral(100) 72 | >>> lb = ti.oneside.normal(x, 0.1, 0.95) 73 | 74 | Estimate the 90th percentile upper bound with 95% confidence of the 75 | following 100 random samples from a normal distribution. 76 | 77 | >>> ub = ti.oneside.normal(x, 0.9, 0.95) 78 | 79 | """ 80 | x = numpy_array(x) # check if numpy array, if not make numpy array 81 | x = assert_2d_sort(x) 82 | m, n = x.shape 83 | if p < 0.5: 84 | p = 1.0 - p 85 | minus = True 86 | else: 87 | minus = False 88 | zp = norm.ppf(p) 89 | t = nct.ppf(g, df=n-1., nc=np.sqrt(n)*zp) 90 | k = t / np.sqrt(n) 91 | if minus: 92 | return x.mean(axis=1) - (k*x.std(axis=1, ddof=1)) 93 | else: 94 | return x.mean(axis=1) + (k*x.std(axis=1, ddof=1)) 95 | 96 | 97 | def lognormal(x, p, g): 98 | r""" 99 | Compute one-side tolerance bound using the lognormal distribution. 100 | 101 | Computes the one-sided tolerance interval using the lognormal distribution. 102 | This just performs a ln and exp transformations of the normal distribution. 103 | 104 | Parameters 105 | ---------- 106 | x : ndarray (1-D, or 2-D) 107 | Numpy array of samples to compute the tolerance bound. Assumed data 108 | type is np.float. Shape of (m, n) is assumed for 2-D arrays with m 109 | number of sets of sample size n. 110 | p : float 111 | Percentile for the TI to estimate. 112 | g : float 113 | Confidence level where g > 0. and g < 1. 114 | 115 | Returns 116 | ------- 117 | ndarray (1-D) 118 | The normal distribution toleranace bound. 119 | 120 | Examples 121 | -------- 122 | Estimate the 10th percentile lower bound with 95% confidence of the 123 | following 100 random samples from a lognormal distribution. 124 | 125 | >>> import numpy as np 126 | >>> import toleranceinterval as ti 127 | >>> x = np.random.random(100) 128 | >>> lb = ti.oneside.lognormal(x, 0.1, 0.95) 129 | 130 | Estimate the 90th percentile upper bound with 95% confidence of the 131 | following 100 random samples from a lognormal distribution. 132 | 133 | >>> ub = ti.oneside.lognormal(x, 0.9, 0.95) 134 | 135 | """ 136 | x = numpy_array(x) # check if numpy array, if not make numpy array 137 | x = assert_2d_sort(x) 138 | return np.exp(normal(np.log(x), p, g)) 139 | 140 | 141 | def non_parametric(x, p, g): 142 | r""" 143 | Compute one-side tolerance bound using traditional non-parametric method. 144 | 145 | Computes a tolerance interval for any percentile, confidence level, and 146 | number of samples using the traditional non-parametric method [1] [2]. 147 | This assumes that the true distribution is continuous. 148 | 149 | Parameters 150 | ---------- 151 | x : ndarray (1-D, or 2-D) 152 | Numpy array of samples to compute the tolerance bound. Assumed data 153 | type is np.float. Shape of (m, n) is assumed for 2-D arrays with m 154 | number of sets of sample size n. 155 | p : float 156 | Percentile for the TI to estimate. 157 | g : float 158 | Confidence level where g > 0. and g < 1. 159 | 160 | Returns 161 | ------- 162 | ndarray (1-D) 163 | The non-parametric toleranace interval bound. Returns np.nan if a 164 | non-parametric tolerance interval does not exist for the combination 165 | of percentile, confidence level, and number of samples. 166 | 167 | Notes 168 | ----- 169 | The non-parametric tolerance inteval only exists for certain combinations 170 | of percentile, confidence level, and number of samples. 171 | 172 | References 173 | ---------- 174 | [1] Hong, L. J., Huang, Z., & Lam, H. (2017). Learning-based robust 175 | optimization: Procedures and statistical guarantees. ArXiv Preprint 176 | ArXiv:1704.04342. 177 | 178 | [2] 9.5.5.3 Nonparametric Procedure. (2017). In MMPDS-12 : Metallic 179 | materials properties development and standardization. Battelle 180 | Memorial Institute. 181 | 182 | Examples 183 | -------- 184 | Estimate the 10th percentile bound with 95% confidence of the 185 | following 300 random samples from a normal distribution. 186 | 187 | >>> import numpy as np 188 | >>> import toleranceinterval as ti 189 | >>> x = np.random.random(300) 190 | >>> bound = ti.oneside.non_parametric(x, 0.1, 0.95) 191 | 192 | Estimate the 90th percentile bound with 95% confidence of the 193 | following 300 random samples from a normal distribution. 194 | 195 | >>> bound = ti.oneside.non_parametric(x, 0.9, 0.95) 196 | 197 | """ 198 | x = numpy_array(x) # check if numpy array, if not make numpy array 199 | x = assert_2d_sort(x) 200 | m, n = x.shape 201 | r = np.arange(0, n) 202 | if p < 0.5: 203 | left_tail = True 204 | confidence_index = binom.sf(r, n, p) 205 | else: 206 | left_tail = False 207 | confidence_index = binom.cdf(r, n, p) 208 | boolean_index = confidence_index >= g 209 | if boolean_index.sum() > 0: 210 | if left_tail: 211 | return x[:, np.max(np.where(boolean_index))] 212 | else: 213 | return x[:, np.min(np.where(boolean_index))] 214 | else: 215 | return np.nan*np.ones(m) 216 | 217 | 218 | def hanson_koopmans(x, p, g, j=-1, method='secant', max_iter=200, tol=1e-5, 219 | step_size=1e-4): 220 | r""" 221 | Compute left tail probabilities using the HansonKoopmans method [1]. 222 | 223 | Runs the HansonKoopmans solver object to find the left tail bound for any 224 | percentile, confidence level, and number of samples. This assumes the 225 | lowest value is the first order statistic, but you can specify the index 226 | of the second order statistic as j. 227 | 228 | Parameters 229 | ---------- 230 | x : ndarray (1-D, or 2-D) 231 | Numpy array of samples to compute the tolerance bound. Assumed data 232 | type is np.float. Shape of (m, n) is assumed for 2-D arrays with m 233 | number of sets of sample size n. 234 | p : float 235 | Percentile for lower limits when p < 0.5 and upper limits when 236 | p >= 0.5. 237 | g : float 238 | Confidence level where g > 0. and g < 1. 239 | j : int, optional 240 | Index of the second value to use for the second order statistic. 241 | Default is the last value j = -1 = n-1 if p < 0.5. If p >= 0.5, 242 | the second index is defined as index=n-j-1, with default j = n-1. 243 | method : string, optional 244 | Which rootfinding method to use to solve for the Hanson-Koopmans 245 | bound. Default is method='secant' which appears to converge 246 | quickly. Other choices include 'newton-raphson' and 'halley'. 247 | max_iter : int, optional 248 | Maximum number of iterations for the root finding method. 249 | tol : float, optional 250 | Tolerance for the root finding method to converge. 251 | step_size : float, optional 252 | Step size for the secant solver. Default step_size = 1e-4. 253 | 254 | Returns 255 | ------- 256 | ndarray (1-D) 257 | The Hanson-Koopmans toleranace interval bound as np.float with shape m. 258 | Returns np.nan if the rootfinding method did not converge. 259 | 260 | Notes 261 | ----- 262 | The Hanson-Koopmans bound assumes the true distribution belongs to the 263 | log-concave CDF class of distributions [1]. 264 | 265 | This implemnation will always extrapolate beyond the lowest sample. If 266 | interpolation is needed within the sample set, this method falls back to 267 | the traditional non-parametric method using non_parametric(x, p, g). 268 | 269 | j uses Python style index notation. 270 | 271 | 272 | References 273 | ---------- 274 | [1] Hanson, D. L., & Koopmans, L. H. (1964). Tolerance Limits for 275 | the Class of Distributions with Increasing Hazard Rates. Ann. Math. 276 | Statist., 35(4), 1561-1570. https://doi.org/10.1214/aoms/1177700380 277 | 278 | Examples 279 | -------- 280 | Estimate the 10th percentile with 95% confidence of the following 10 281 | random samples. 282 | 283 | >>> import numpy as np 284 | >>> import toleranceinterval as ti 285 | >>> x = np.random.random(10) 286 | >>> bound = ti.oneside.hanson_koopmans(x, 0.1, 0.95) 287 | 288 | Estimate the 90th percentile with 95% confidence. 289 | 290 | >>> bound = ti.oneside.hanson_koopmans(x, 0.9, 0.95) 291 | 292 | """ 293 | x = numpy_array(x) # check if numpy array, if not make numpy array 294 | x = assert_2d_sort(x) 295 | m, n = x.shape 296 | if j == -1: 297 | # Need to use n for the HansonKoopmans solver 298 | j = n - 1 299 | assert j < n 300 | if p < 0.5: 301 | lower = True 302 | myhk = HansonKoopmans(p, g, n, j, method=method, max_iter=max_iter, 303 | tol=tol, step_size=step_size) 304 | else: 305 | lower = False 306 | myhk = HansonKoopmans(1.0-p, g, n, j, method=method, max_iter=max_iter, 307 | tol=tol, step_size=step_size) 308 | if myhk.fall_back: 309 | return non_parametric(x, p, g) 310 | if myhk.un_conv: 311 | return np.nan 312 | else: 313 | b = float(myhk.b) 314 | if lower: 315 | bound = x[:, j] - b*(x[:, j]-x[:, 0]) 316 | else: 317 | bound = b*(x[:, n-1] - x[:, n-j-1]) + x[:, n-j-1] 318 | return bound 319 | 320 | 321 | def hanson_koopmans_cmh(x, p, g, j=-1, method='secant', max_iter=200, tol=1e-5, 322 | step_size=1e-4): 323 | r""" 324 | Compute CMH style tail probabilities using the HansonKoopmans method [1]. 325 | 326 | Runs the HansonKoopmans solver object to find the left tail bound for any 327 | percentile, confidence level, and number of samples. This assumes the 328 | lowest value is the first order statistic, but you can specify the index 329 | of the second order statistic as j. CMH variant is the Composite Materials 330 | Handbook which calculates the same b, but uses a different order statistic 331 | calculation [2]. 332 | 333 | Parameters 334 | ---------- 335 | x : ndarray (1-D, or 2-D) 336 | Numpy array of samples to compute the tolerance bound. Assumed data 337 | type is np.float. Shape of (m, n) is assumed for 2-D arrays with m 338 | number of sets of sample size n. 339 | p : float 340 | Percentile for lower limits when p < 0.5. 341 | g : float 342 | Confidence level where g > 0. and g < 1. 343 | j : int, optional 344 | Index of the second value to use for the second order statistic. 345 | Default is the last value j = -1 = n-1 if p < 0.5. If p >= 0.5, 346 | the second index is defined as index=n-j-1, with default j = n-1. 347 | method : string, optional 348 | Which rootfinding method to use to solve for the Hanson-Koopmans 349 | bound. Default is method='secant' which appears to converge 350 | quickly. Other choices include 'newton-raphson' and 'halley'. 351 | max_iter : int, optional 352 | Maximum number of iterations for the root finding method. 353 | tol : float, optional 354 | Tolerance for the root finding method to converge. 355 | step_size : float, optional 356 | Step size for the secant solver. Default step_size = 1e-4. 357 | 358 | Returns 359 | ------- 360 | ndarray (1-D) 361 | The Hanson-Koopmans toleranace interval bound as np.float with shape m. 362 | Returns np.nan if the rootfinding method did not converge. 363 | 364 | Notes 365 | ----- 366 | The Hanson-Koopmans bound assumes the true distribution belongs to the 367 | log-concave CDF class of distributions [1]. 368 | 369 | This implemnation will always extrapolate beyond the lowest sample. If 370 | interpolation is needed within the sample set, this method falls back to 371 | the traditional non-parametric method using non_parametric(x, p, g). 372 | 373 | j uses Python style index notation. 374 | 375 | CMH variant estimates lower tails only! 376 | 377 | 378 | References 379 | ---------- 380 | [1] Hanson, D. L., & Koopmans, L. H. (1964). Tolerance Limits for 381 | the Class of Distributions with Increasing Hazard Rates. Ann. Math. 382 | Statist., 35(4), 1561-1570. https://doi.org/10.1214/aoms/1177700380 383 | 384 | [2] Volume 1: Guidelines for Characterization of Structural Materials. 385 | (2017). In Composite Materials Handbook. SAE International. 386 | 387 | Examples 388 | -------- 389 | Estimate the 10th percentile with 95% confidence of the following 10 390 | random samples. 391 | 392 | >>> import numpy as np 393 | >>> import toleranceinterval as ti 394 | >>> x = np.random.random(10) 395 | >>> bound = ti.oneside.hanson_koopmans_cmh(x, 0.1, 0.95) 396 | 397 | Estimate the 1st percentile with 95% confidence. 398 | 399 | >>> bound = ti.oneside.hanson_koopmans_cmh(x, 0.01, 0.95) 400 | 401 | """ 402 | x = numpy_array(x) # check if numpy array, if not make numpy array 403 | x = assert_2d_sort(x) 404 | m, n = x.shape 405 | if j == -1: 406 | # Need to use n for the HansonKoopmans solver 407 | j = n - 1 408 | assert j < n 409 | if p >= 0.5: 410 | raise ValueError('p must be < 0.5!') 411 | myhk = HansonKoopmans(p, g, n, j, method=method, max_iter=max_iter, 412 | tol=tol, step_size=step_size) 413 | if myhk.fall_back: 414 | return non_parametric(x, p, g) 415 | if myhk.un_conv: 416 | return np.nan 417 | else: 418 | b = float(myhk.b) 419 | bound = x[:, j] * (x[:, 0]/x[:, j])**b 420 | return bound 421 | -------------------------------------------------------------------------------- /docs/twoside.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | toleranceinterval.twoside API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 44 |
45 |
46 |

47 | toleranceinterval.twoside

48 | 49 | 50 |
51 | View Source 52 |
from .twoside import normal_factor  # noqa F401
 53 | from .twoside import normal  # noqa F401
 54 | from .twoside import lognormal  # noqa F401
 55 | 
56 | 57 |
58 | 59 |
60 |
61 | 229 | --------------------------------------------------------------------------------