Update PEP 485 from Chris Barker's edits (w/ minor formatting)

This commit is contained in:
Chris Angelico 2015-02-11 23:13:44 +11:00
parent b6fa3e2e91
commit 2ca23b9a32
1 changed files with 101 additions and 89 deletions

View File

@ -16,16 +16,14 @@ Abstract
This PEP proposes the addition of a function to the standard library This PEP proposes the addition of a function to the standard library
that determines whether one value is approximately equal or "close" to that determines whether one value is approximately equal or "close" to
another value. It is also proposed that an assertion be added to the another value.
``unittest.TestCase`` class to provide easy access for those using
unittest for testing.
Rationale Rationale
========= =========
Floating point values contain limited precision, which results in Floating point values contain limited precision, which results in
their being unable to exactly represent some values, and for error to their being unable to exactly represent some values, and for errors to
accumulate with repeated computation. As a result, it is common accumulate with repeated computation. As a result, it is common
advice to only use an equality comparison in very specific situations. advice to only use an equality comparison in very specific situations.
Often a inequality comparison fits the bill, but there are times Often a inequality comparison fits the bill, but there are times
@ -49,8 +47,8 @@ method, but it:
* Is an absolute difference test. Often the measure of difference * Is an absolute difference test. Often the measure of difference
requires, particularly for floating point numbers, a relative error, requires, particularly for floating point numbers, a relative error,
i.e "Are these two values within x% of each-other?", rather than an i.e. "Are these two values within x% of each-other?", rather than an
absolute error. Particularly when the magnatude of the values is absolute error. Particularly when the magnitude of the values is
unknown a priori. unknown a priori.
The numpy package has the ``allclose()`` and ``isclose()`` functions, The numpy package has the ``allclose()`` and ``isclose()`` functions,
@ -63,7 +61,7 @@ One can also find discussion and sample implementations on Stack
Overflow and other help sites. Overflow and other help sites.
Many other non-python systems provide such a test, including the Boost C++ Many other non-python systems provide such a test, including the Boost C++
library and the APL language (reference?). library and the APL language [4]_.
These existing implementations indicate that this is a common need and These existing implementations indicate that this is a common need and
not trivial to write oneself, making it a candidate for the standard not trivial to write oneself, making it a candidate for the standard
@ -73,28 +71,31 @@ library.
Proposed Implementation Proposed Implementation
======================= =======================
NOTE: this PEP is the result of an extended discussion on the NOTE: this PEP is the result of extended discussions on the
python-ideas list [1]_. python-ideas list [1]_.
The new function will have the following signature:: The new function will go into the math module, and have the following
signature::
is_close(a, b, rel_tolerance=1e-9, abs_tolerance=0.0) isclose(a, b, rel_tol=1e-9, abs_tol=0.0)
``a`` and ``b``: are the two values to be tested to relative closeness ``a`` and ``b``: are the two values to be tested to relative closeness
``rel_tolerance``: is the relative tolerance -- it is the amount of ``rel_tol``: is the relative tolerance -- it is the amount of error
error allowed, relative to the magnitude a and b. For example, to set allowed, relative to the larger absolute value of a or b. For example,
a tolerance of 5%, pass tol=0.05. The default tolerance is 1e-8, which to set a tolerance of 5%, pass tol=0.05. The default tolerance is 1e-9,
assures that the two values are the same within about 8 decimal which assures that the two values are the same within about 9 decimal
digits. digits. ``rel_tol`` must be greater than 0.0
``abs_tolerance``: is an minimum absolute tolerance level -- useful for ``abs_tol``: is an minimum absolute tolerance level -- useful for
comparisons near zero. comparisons near zero.
Modulo error checking, etc, the function will return the result of:: Modulo error checking, etc, the function will return the result of::
abs(a-b) <= max( rel_tolerance * min(abs(a), abs(b), abs_tolerance ) abs(a-b) <= max( rel_tol * max(abs(a), abs(b)), abs_tol )
The name, ``isclose``, is selected for consistency with the existing
``isnan`` and ``isinf``.
Handling of non-finite numbers Handling of non-finite numbers
------------------------------ ------------------------------
@ -111,17 +112,18 @@ Non-float types
The primary use-case is expected to be floating point numbers. The primary use-case is expected to be floating point numbers.
However, users may want to compare other numeric types similarly. In However, users may want to compare other numeric types similarly. In
theory, it should work for any type that supports ``abs()``, theory, it should work for any type that supports ``abs()``,
comparisons, and subtraction. The code will be written and tested to multiplication, comparisons, and subtraction. The code will be written
accommodate these types: and tested to accommodate these types:
* ``Decimal``: for Decimal, the tolerance must be set to a Decimal type. * ``Decimal``
* ``int`` * ``int``
* ``Fraction`` * ``Fraction``
* ``complex``: for complex, ``abs(z)`` will be used for scaling and * ``complex``: for complex, the absolute value of the complex values
comparison. will be used for scaling and comparison. If a complex tolerance is
passed in, the absolute value will be used as the tolerance.
Behavior near zero Behavior near zero
@ -130,9 +132,9 @@ Behavior near zero
Relative comparison is problematic if either value is zero. By Relative comparison is problematic if either value is zero. By
definition, no value is small relative to zero. And computationally, definition, no value is small relative to zero. And computationally,
if either value is zero, the difference is the absolute value of the if either value is zero, the difference is the absolute value of the
other value, and the computed absolute tolerance will be rel_tolerance other value, and the computed absolute tolerance will be ``rel_tol``
times that value. rel-tolerance is always less than one, so the times that value. When ``rel_tol`` is less than one, the difference will
difference will never be less than the tolerance. never be less than the tolerance.
However, while mathematically correct, there are many use cases where However, while mathematically correct, there are many use cases where
a user will need to know if a computed value is "close" to zero. This a user will need to know if a computed value is "close" to zero. This
@ -146,50 +148,51 @@ There is a similar issue if the two values to be compared straddle zero:
if a is approximately equal to -b, then a and b will never be computed if a is approximately equal to -b, then a and b will never be computed
as "close". as "close".
To handle this case, an optional parameter, ``abs_tolerance`` can be To handle this case, an optional parameter, ``abs_tol`` can be
used to set a minimum tolerance used in the case of very small or zero used to set a minimum tolerance used in the case of very small or zero
computed absolute tolerance. That is, the values will be always be computed relative tolerance. That is, the values will be always be
considered close if the difference between them is less than the considered close if the difference between them is less than
abs_tolerance ``abs_tol``
The default absolute tolerance value is set to zero because there is The default absolute tolerance value is set to zero because there is
no value that is appropriate for the general case. It is impossible to no value that is appropriate for the general case. It is impossible to
know an appropriate value without knowing the likely values expected know an appropriate value without knowing the likely values expected
for a given use case. If all the values tested are on order of one, for a given use case. If all the values tested are on order of one,
then a value of about 1e-8 might be appropriate, but that would be far then a value of about 1e-9 might be appropriate, but that would be far
too large if expected values are on order of 1e-12 or smaller. too large if expected values are on order of 1e-9 or smaller.
Any non-zero default might result in user's tests passing totally Any non-zero default might result in user's tests passing totally
inappropriately. If, on the other hand a test against zero fails the inappropriately. If, on the other hand, a test against zero fails the
first time with defaults, a user will be prompted to select an first time with defaults, a user will be prompted to select an
appropriate value for the problem at hand in order to get the test to appropriate value for the problem at hand in order to get the test to
pass. pass.
NOTE: that the author of this PEP has resolved to go back over many of NOTE: that the author of this PEP has resolved to go back over many of
his tests that use the numpy ``all_close()`` function, which provides his tests that use the numpy ``allclose()`` function, which provides
a default abs_tolerance, and make sure that the default value is a default absolute tolerance, and make sure that the default value is
appropriate. appropriate.
If the user sets the rel_tolerance parameter to 0.0, then only the If the user sets the rel_tol parameter to 0.0, then only the
absolute tolerance will effect the result. While not the goal of the absolute tolerance will effect the result. While not the goal of the
function, it does allow it to be used as a purely absolute tolerance function, it does allow it to be used as a purely absolute tolerance
check as well. check as well.
unittest assertion
-------------------
[need text here] Implementation
implementation
-------------- --------------
A sample implementation is available (as of Jan 22, 2015) on gitHub: A sample implementation is available (as of Jan 22, 2015) on gitHub:
https://github.com/PythonCHB/close_pep/blob/master https://github.com/PythonCHB/close_pep/blob/master/is_close.py
This implementation has a flag that lets the user select which This implementation has a flag that lets the user select which
relative tolerance test to apply -- this PEP does not suggest that relative tolerance test to apply -- this PEP does not suggest that
that be retained, but rather than the strong test be selected. that be retained, but rather than the weak test be selected.
There are also drafts of this PEP and test code, etc. there:
https://github.com/PythonCHB/close_pep
Relative Difference Relative Difference
=================== ===================
@ -215,7 +218,7 @@ consideration, for instance:
4) The absolute value of the arithmetic mean of the two 4) The absolute value of the arithmetic mean of the two
These lead to the following possibilities for determining if two These leads to the following possibilities for determining if two
values, a and b, are close to each other. values, a and b, are close to each other.
1) ``abs(a-b) <= tol*abs(a)`` 1) ``abs(a-b) <= tol*abs(a)``
@ -228,9 +231,9 @@ values, a and b, are close to each other.
NOTE: (2) and (3) can also be written as: NOTE: (2) and (3) can also be written as:
2) ``(abs(a-b) <= tol*abs(a)) or (abs(a-b) <= tol*abs(a))`` 2) ``(abs(a-b) <= abs(tol*a)) or (abs(a-b) <= abs(tol*b))``
3) ``(abs(a-b) <= tol*abs(a)) and (abs(a-b) <= tol*abs(a))`` 3) ``(abs(a-b) <= abs(tol*a)) and (abs(a-b) <= abs(tol*b))``
(Boost refers to these as the "weak" and "strong" formulations [3]_) (Boost refers to these as the "weak" and "strong" formulations [3]_)
These can be a tiny bit more computationally efficient, and thus are These can be a tiny bit more computationally efficient, and thus are
@ -292,7 +295,7 @@ maximum tolerance ``tol * a == 0.1 * 10 == 1.0``
minimum tolerance ``tol * b == 0.1 * 9.0 == 0.9`` minimum tolerance ``tol * b == 0.1 * 9.0 == 0.9``
delta = ``(1.0 - 0.9) * 0.1 = 0.1`` or ``tol**2 * a = 0.1**2 * 10 = .01`` delta = ``(1.0 - 0.9) = 0.1`` or ``tol**2 * a = 0.1**2 * 10 = .1``
The absolute difference between the maximum and minimum tolerance The absolute difference between the maximum and minimum tolerance
tests in this case could be substantial. However, the primary use tests in this case could be substantial. However, the primary use
@ -310,7 +313,7 @@ precision of floating point. That is, each of the four methods will
yield exactly the same results for all values of a and b. yield exactly the same results for all values of a and b.
In addition, in common use, tolerances are defined to 1 significant In addition, in common use, tolerances are defined to 1 significant
figure -- that is, 1e-8 is specifying about 8 decimal digits of figure -- that is, 1e-9 is specifying about 9 decimal digits of
accuracy. So the difference between the various possible tests is well accuracy. So the difference between the various possible tests is well
below the precision to which the tolerance is specified. below the precision to which the tolerance is specified.
@ -321,11 +324,11 @@ Symmetry
A relative comparison can be either symmetric or non-symmetric. For a A relative comparison can be either symmetric or non-symmetric. For a
symmetric algorithm: symmetric algorithm:
``is_close_to(a,b)`` is always the same as ``is_close_to(b,a)`` ``isclose(a,b)`` is always the same as ``isclose(b,a)``
If a relative closeness test uses only one of the values (such as (1) If a relative closeness test uses only one of the values (such as (1)
above), then the result is asymmetric, i.e. is_close_to(a,b) is not above), then the result is asymmetric, i.e. isclose(a,b) is not
necessarily the same as is_close_to(b,a). necessarily the same as isclose(b,a).
Which approach is most appropriate depends on what question is being Which approach is most appropriate depends on what question is being
asked. If the question is: "are these two numbers close to each asked. If the question is: "are these two numbers close to each
@ -369,18 +372,31 @@ The case that uses the arithmetic mean of the two values requires that
the value be either added together before dividing by 2, which could the value be either added together before dividing by 2, which could
result in extra overflow to inf for very large numbers, or require result in extra overflow to inf for very large numbers, or require
each value to be divided by two before being added together, which each value to be divided by two before being added together, which
could result in underflow to -inf for very small numbers. This effect could result in underflow to zero for very small numbers. This effect
would only occur at the very limit of float values, but it was decided would only occur at the very limit of float values, but it was decided
there as no benefit to the method worth reducing the range of there was no benefit to the method worth reducing the range of
functionality. functionality or adding the complexity of checking values to determine
the order of computation.
This leaves the boost "weak" test (2)-- or using the smaller value to This leaves the boost "weak" test (2)-- or using the smaller value to
scale the tolerance, or the Boost "strong" (3) test, which uses the scale the tolerance, or the Boost "strong" (3) test, which uses the
smaller of the values to scale the tolerance. For small tolerance, smaller of the values to scale the tolerance. For small tolerance,
they yield the same result, but this proposal uses the boost "strong" they yield the same result, but this proposal uses the boost "weak"
test case: it is symmetric and provides a slightly stricter criteria test case: it is symmetric and provides a more useful result for very
for tolerance. large tolerances.
Large tolerances
----------------
The most common use case is expected to be small tolerances -- on order of the
default 1e-9. However there may be use cases where a user wants to know if two
fairly disparate values are within a particular range of each other: "is a
within 200% (rel_tol = 2.0) of b? In this case, the string test would never
indicate that two values are within that range of each other if one of them is
zero. The strong case, however would use the larger (non-zero) value for the
test, and thus return true if one value is zero. For example: is 0 within 200%
of 10? 200% of ten is 20, so the range within 200% of ten is -10 to +30. Zero
falls within that range, so it will return True.
Defaults Defaults
======== ========
@ -392,16 +408,15 @@ Relative Tolerance Default
The relative tolerance required for two values to be considered The relative tolerance required for two values to be considered
"close" is entirely use-case dependent. Nevertheless, the relative "close" is entirely use-case dependent. Nevertheless, the relative
tolerance needs to be less than 1.0, and greater than 1e-16 tolerance needs to be greater than 1e-16 (approximate precision of a
(approximate precision of a python float). The value of 1e-9 was python float). The value of 1e-9 was selected because it is the
selected because it is the largest relative tolerance for which the largest relative tolerance for which the various possible methods will
various possible methods will yield the same result, and it is also yield the same result, and it is also about half of the precision
about half of the precision available to a python float. In the available to a python float. In the general case, a good numerical
general case, a good numerical algorithm is not expected to lose more algorithm is not expected to lose more than about half of available
than about half of available digits of accuracy, and if a much larger digits of accuracy, and if a much larger tolerance is acceptable, the
tolerance is acceptable, the user should be considering the proper user should be considering the proper value in that case. Thus 1-e9 is
value in that case. Thus 1-e9 is expected to "just work" for many expected to "just work" for many cases.
cases.
Absolute tolerance default Absolute tolerance default
-------------------------- --------------------------
@ -437,19 +452,16 @@ The primary expected use case is various forms of testing -- "are the
results computed near what I expect as a result?" This sort of test results computed near what I expect as a result?" This sort of test
may or may not be part of a formal unit testing suite. Such testing may or may not be part of a formal unit testing suite. Such testing
could be used one-off at the command line, in an iPython notebook, could be used one-off at the command line, in an iPython notebook,
part of doctests, or simple assets in an ``if __name__ == "__main__"`` part of doctests, or simple asserts in an ``if __name__ == "__main__"``
block. block.
The proposed unitest.TestCase assertion would have course be used in
unit testing.
It would also be an appropriate function to use for the termination It would also be an appropriate function to use for the termination
criteria for a simple iterative solution to an implicit function:: criteria for a simple iterative solution to an implicit function::
guess = something guess = something
while True: while True:
new_guess = implicit_function(guess, *args) new_guess = implicit_function(guess, *args)
if is_close(new_guess, guess): if isclose(new_guess, guess):
break break
guess = new_guess guess = new_guess
@ -481,13 +493,13 @@ places (default 7), and comparing to zero.
This method is purely an absolute tolerance test, and does not address This method is purely an absolute tolerance test, and does not address
the need for a relative tolerance test. the need for a relative tolerance test.
numpy ``is_close()`` numpy ``isclose()``
-------------------- -------------------
http://docs.scipy.org/doc/numpy-dev/reference/generated/numpy.isclose.html http://docs.scipy.org/doc/numpy-dev/reference/generated/numpy.isclose.html
The numpy package provides the vectorized functions is_close() and The numpy package provides the vectorized functions isclose() and
all_close, for similar use cases as this proposal: allclose(), for similar use cases as this proposal:
``isclose(a, b, rtol=1e-05, atol=1e-08, equal_nan=False)`` ``isclose(a, b, rtol=1e-05, atol=1e-08, equal_nan=False)``
@ -522,9 +534,9 @@ Boost floating-point comparison
------------------------------- -------------------------------
The Boost project ( [3]_ ) provides a floating point comparison The Boost project ( [3]_ ) provides a floating point comparison
function. Is is a symmetric approach, with both "weak" (larger of the function. It is a symmetric approach, with both "weak" (larger of the
two relative errors) and "strong" (smaller of the two relative errors) two relative errors) and "strong" (smaller of the two relative errors)
options. This proposal uses the Boost "strong" approach. There is no options. This proposal uses the Boost "weak" approach. There is no
need to complicate the API by providing the option to select different need to complicate the API by providing the option to select different
methods when the results will be similar in most cases, and the user methods when the results will be similar in most cases, and the user
is unlikely to know which to select in any case. is unlikely to know which to select in any case.
@ -545,10 +557,6 @@ most appropriate. However, that would require anyone needing such a
test to, at the very least, copy the function into their code base, test to, at the very least, copy the function into their code base,
and select the comparison method to use. and select the comparison method to use.
In addition, adding the function to the standard library allows it to
be used in the ``unittest.TestCase.assertIsClose()`` method, providing
a substantial convenience to those using unittest.
``zero_tol`` ``zero_tol``
'''''''''''' ''''''''''''
@ -568,16 +576,15 @@ No absolute tolerance
''''''''''''''''''''' '''''''''''''''''''''
Given the issues with comparing to zero, another possibility would Given the issues with comparing to zero, another possibility would
have been to only provide a relative tolerance, and let every have been to only provide a relative tolerance, and let comparison to
comparison to zero fail. In this case, the user would need to do a zero fail. In this case, the user would need to do a simple absolute
simple absolute test: `abs(val) < zero_tol` in the case where the test: `abs(val) < zero_tol` in the case where the comparison involved
comparison involved zero. zero.
However, this would not allow the same call to be used for a sequence However, this would not allow the same call to be used for a sequence
of values, such as in a loop or comprehension, or in the of values, such as in a loop or comprehension. Making the function far
``TestCase.assertClose()`` method. Making the function far less less useful. It is noted that the default abs_tol=0.0 achieves the
useful. It is noted that the default abs_tolerance=0.0 achieves the same effect if the default is not overridden.
same effect if the default is not overidden.
Other tests Other tests
'''''''''''' ''''''''''''
@ -605,6 +612,11 @@ References
http://www.boost.org/doc/libs/1_35_0/libs/test/doc/components/test_tools/floating_point_comparison.html http://www.boost.org/doc/libs/1_35_0/libs/test/doc/components/test_tools/floating_point_comparison.html
.. [4] 1976. R. H. Lathwell. APL comparison tolerance. Proceedings of
the eighth international conference on APL Pages 255 - 258
http://dl.acm.org/citation.cfm?doid=800114.803685
.. Bruce Dawson's discussion of floating point. .. Bruce Dawson's discussion of floating point.
https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/