├── 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 | [](https://travis-ci.com/cjekel/tolerance_interval_py) [](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 |
43 |
45 |
47 |
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 |
45 |
47 |
49 |
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 |
61 |
63 |
65 |
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 |
74 |
76 |
77 |
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 |
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 |
16 |
17 |
18 |
43 |
44 |
45 |
46 |
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 |
--------------------------------------------------------------------------------