format-multiple-errors 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- format_multiple_errors/__init__.py +17 -0
- format_multiple_errors/__main__.py +186 -0
- format_multiple_errors/formatter.py +342 -0
- format_multiple_errors/pandas.py +227 -0
- format_multiple_errors/typing.py +16 -0
- format_multiple_errors-0.0.2.dist-info/LICENSE +21 -0
- format_multiple_errors-0.0.2.dist-info/METADATA +188 -0
- format_multiple_errors-0.0.2.dist-info/RECORD +11 -0
- format_multiple_errors-0.0.2.dist-info/WHEEL +5 -0
- format_multiple_errors-0.0.2.dist-info/entry_points.txt +2 -0
- format_multiple_errors-0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""A package for formatting numbers with zero or more errors or pairs of errors"""
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
from .formatter import format_multiple_errors as format_multiple_errors
|
|
8
|
+
|
|
9
|
+
with warnings.catch_warnings():
|
|
10
|
+
warnings.simplefilter("ignore")
|
|
11
|
+
from .pandas import (
|
|
12
|
+
format_column_errors as format_column_errors,
|
|
13
|
+
format_dataframe_errors as format_dataframe_errors,
|
|
14
|
+
ColumnSpec as ColumnSpec,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
del warnings
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""Command-line interface for format_multiple_errors"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from argparse import ArgumentParser, FileType, Namespace
|
|
8
|
+
import logging
|
|
9
|
+
from sys import exit, stderr
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import pandas as pd
|
|
13
|
+
except ImportError:
|
|
14
|
+
pd = None
|
|
15
|
+
|
|
16
|
+
from .formatter import format_multiple_errors
|
|
17
|
+
from .pandas import format_dataframe_errors, ColumnSpec
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _format_numbers(args: Namespace) -> None:
|
|
21
|
+
"""Format a single number."""
|
|
22
|
+
print(
|
|
23
|
+
format_multiple_errors(
|
|
24
|
+
args.value,
|
|
25
|
+
*args.errors,
|
|
26
|
+
length_control=args.length_control,
|
|
27
|
+
significant_figures=args.significant_figures,
|
|
28
|
+
abbreviate=args.abbreviate,
|
|
29
|
+
exponential=args.exponential,
|
|
30
|
+
latex=args.latex,
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _check_pandas() -> None:
|
|
36
|
+
"""Check if Pandas is available; complain and exit if not."""
|
|
37
|
+
if pd is None:
|
|
38
|
+
print(
|
|
39
|
+
"Pandas is not installed, but is required to process a table.",
|
|
40
|
+
file=stderr,
|
|
41
|
+
)
|
|
42
|
+
exit()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _format_table(args: Namespace) -> None:
|
|
46
|
+
"""Format a table from a CSV."""
|
|
47
|
+
_check_pandas()
|
|
48
|
+
if not args.latex:
|
|
49
|
+
logging.warning(
|
|
50
|
+
"--latex not specified; for LaTeX table output this will be forced on.",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if not args.headings:
|
|
54
|
+
args.headings = None
|
|
55
|
+
else:
|
|
56
|
+
assert len(args.headings) == len(args.column_specs)
|
|
57
|
+
|
|
58
|
+
df = pd.read_csv(args.input_file)
|
|
59
|
+
formatted_df = format_dataframe_errors(
|
|
60
|
+
df=df,
|
|
61
|
+
columns=args.column_specs,
|
|
62
|
+
length_control=args.length_control,
|
|
63
|
+
significant_figures=args.significant_figures,
|
|
64
|
+
abbreviate=args.abbreviate,
|
|
65
|
+
exponential=args.exponential,
|
|
66
|
+
latex=True,
|
|
67
|
+
)
|
|
68
|
+
formatted_df.to_latex(args.output_file, index=False, header=args.headings)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _float_or_pair(arg: str) -> float | tuple[float, float]:
|
|
72
|
+
"""Takes a string containing either a float, or two floats separated by commas,
|
|
73
|
+
and returns them as either a float or a tuple of floats."""
|
|
74
|
+
|
|
75
|
+
split_arg = arg.split(",")
|
|
76
|
+
if len(split_arg) == 1:
|
|
77
|
+
return float(arg)
|
|
78
|
+
elif len(split_arg) == 2:
|
|
79
|
+
return tuple(map(float, split_arg))
|
|
80
|
+
else:
|
|
81
|
+
message = f"Can't parse {arg} as a number or pair of numbers."
|
|
82
|
+
raise ValueError(message)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _parse_columnspec(arg: str) -> str | ColumnSpec:
|
|
86
|
+
"""Take a string containing a specification for columns,
|
|
87
|
+
and return a column name (if a single column is specified)
|
|
88
|
+
or a ColumnSpec (if multiple columns are specified)."""
|
|
89
|
+
|
|
90
|
+
split_arg = arg.split(",")
|
|
91
|
+
if len(split_arg) == 1:
|
|
92
|
+
return arg
|
|
93
|
+
|
|
94
|
+
if not isinstance(split_arg[0], str):
|
|
95
|
+
raise ValueError("First column has to be a single central value.")
|
|
96
|
+
|
|
97
|
+
error_columns = []
|
|
98
|
+
for error_spec in split_arg[1:]:
|
|
99
|
+
split_error = error_spec.split("-")
|
|
100
|
+
if len(split_error) == 1:
|
|
101
|
+
error_columns.append(error_spec)
|
|
102
|
+
else:
|
|
103
|
+
error_columns.append(tuple(split_error))
|
|
104
|
+
|
|
105
|
+
return ColumnSpec(split_arg[0], *error_columns)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_args(override_args: list | None = None) -> Namespace:
|
|
109
|
+
"""Parse command line."""
|
|
110
|
+
parser = ArgumentParser(prog="format_multiple_errors")
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"--abbreviate",
|
|
113
|
+
action="store_true",
|
|
114
|
+
help="Abbreviate the uncertainty - e.g. 1.23(4) instead of 1.23 ± 0.04",
|
|
115
|
+
)
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--exponential", action="store_true", help="Use exponential notation"
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"--latex",
|
|
121
|
+
action="store_true",
|
|
122
|
+
help="Use LaTeX rather than plain text format - e.g. 1.23 \\pm 0.04 instead of 1.23 ± 0.04",
|
|
123
|
+
)
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"--significant_figures",
|
|
126
|
+
type=int,
|
|
127
|
+
default=2,
|
|
128
|
+
help="Number of significant figures to display (used in conjunction with --length_control)",
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"--length_control",
|
|
132
|
+
default="smallest",
|
|
133
|
+
choices=["smallest", "central"],
|
|
134
|
+
help="Value to control the significant figures of (`smallest` uncertainty or `central` value)",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
subparsers = parser.add_subparsers(title="commands")
|
|
138
|
+
|
|
139
|
+
format_parser = subparsers.add_parser("format", help="Format a single number")
|
|
140
|
+
format_parser.add_argument("value", type=float, help="The central value")
|
|
141
|
+
format_parser.add_argument(
|
|
142
|
+
"errors",
|
|
143
|
+
type=_float_or_pair,
|
|
144
|
+
metavar="error",
|
|
145
|
+
nargs="+",
|
|
146
|
+
help="Uncertainties in the value. Asymmetric uncertainties are specified as upper,lower.",
|
|
147
|
+
)
|
|
148
|
+
format_parser.set_defaults(func=_format_numbers)
|
|
149
|
+
|
|
150
|
+
table_parser = subparsers.add_parser(
|
|
151
|
+
"table", help="Format a CSV file into a LaTeX table"
|
|
152
|
+
)
|
|
153
|
+
table_parser.add_argument(
|
|
154
|
+
"input_file", type=FileType("r"), default="-", help="The CSV file to read in"
|
|
155
|
+
)
|
|
156
|
+
table_parser.add_argument(
|
|
157
|
+
"--output_file",
|
|
158
|
+
type=FileType("w"),
|
|
159
|
+
default="-",
|
|
160
|
+
help="Where to place the output LaTeX",
|
|
161
|
+
)
|
|
162
|
+
table_parser.add_argument(
|
|
163
|
+
"column_specs",
|
|
164
|
+
type=_parse_columnspec,
|
|
165
|
+
metavar="column_spec",
|
|
166
|
+
nargs="+",
|
|
167
|
+
help="Specifications of columns to include in the table, in the form value_column[,error_column[-lower_error_column]][,error_column[-lower_error_column]][...]",
|
|
168
|
+
)
|
|
169
|
+
table_parser.add_argument(
|
|
170
|
+
"--headings",
|
|
171
|
+
nargs="+",
|
|
172
|
+
metavar="heading",
|
|
173
|
+
help="Column headings to use. By default, these are taken from the CSV. If supplied, the count must match the number of specified columns",
|
|
174
|
+
)
|
|
175
|
+
table_parser.set_defaults(func=_format_table)
|
|
176
|
+
return parser.parse_args(override_args)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def cli(override_args: list | None = None) -> None:
|
|
180
|
+
"""Run the CLI."""
|
|
181
|
+
args = get_args(override_args)
|
|
182
|
+
args.func(args)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
cli()
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""Implementation of multiple error formatting."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Sequence, Set
|
|
8
|
+
|
|
9
|
+
# It would be nice to vectorise this with numpy, but that needs more clever thinking.
|
|
10
|
+
import math
|
|
11
|
+
|
|
12
|
+
from typing import Callable, TypeVar
|
|
13
|
+
|
|
14
|
+
from .typing import Value, Error, Errors
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def format_multiple_errors(
|
|
18
|
+
value: Value,
|
|
19
|
+
*errors: Error,
|
|
20
|
+
length_control: str = "smallest",
|
|
21
|
+
significant_figures: int = 2,
|
|
22
|
+
abbreviate: bool = False,
|
|
23
|
+
exponential: bool = False,
|
|
24
|
+
latex: bool = False,
|
|
25
|
+
) -> str: # pylint: disable=r0913
|
|
26
|
+
"""Formats the value and errors consistently.
|
|
27
|
+
|
|
28
|
+
Parameters:
|
|
29
|
+
|
|
30
|
+
value:
|
|
31
|
+
The central value to format.
|
|
32
|
+
If a number with error (pyerrors.Obs or uncertainties.UFloat),
|
|
33
|
+
then the nominal value is taken and the uncertainty is prepended to `errors`.
|
|
34
|
+
|
|
35
|
+
*errors:
|
|
36
|
+
The uncertainties to format.
|
|
37
|
+
Each should be a single number (symmetric), or a tuple of two numbers (upper, lower).
|
|
38
|
+
|
|
39
|
+
length_control (default: "smallest"):
|
|
40
|
+
The variable to use for controlling the length of the printed number.
|
|
41
|
+
Options:
|
|
42
|
+
"smallest": The smallest uncertainty is printed
|
|
43
|
+
with `significant_figures` significant figures.
|
|
44
|
+
"central": The central `value` is printed
|
|
45
|
+
with `significant_figures` significant figures.
|
|
46
|
+
|
|
47
|
+
significant_figures (default: 2):
|
|
48
|
+
The number of significant figures to format.
|
|
49
|
+
Other values printed may have more significant figures, but will not have fewer.
|
|
50
|
+
|
|
51
|
+
abbreviate (default: False):
|
|
52
|
+
Abbreviate the uncertainties with bracketed notation.
|
|
53
|
+
(I.e. 1.234(56) instead of 1.234 +/- 0.056.)
|
|
54
|
+
|
|
55
|
+
exponential (default: False):
|
|
56
|
+
Use standard form (e.g. 1.2 × 10^{-3}) rather than leading/trailing zeroes (0.0012).
|
|
57
|
+
|
|
58
|
+
latex (default: False):
|
|
59
|
+
Format the numbers in LaTeX form rather than plain text.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
normalised_value, normalised_errors = _normalize_integrated_errors(
|
|
63
|
+
value, list(errors)
|
|
64
|
+
)
|
|
65
|
+
exponent = 0
|
|
66
|
+
if exponential:
|
|
67
|
+
normalised_value, normalised_errors, exponent = _normalize(
|
|
68
|
+
normalised_value, normalised_errors
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
length_value = _get_length_value(
|
|
72
|
+
normalised_value, normalised_errors, length_control
|
|
73
|
+
)
|
|
74
|
+
first_digit_index, decimal_places = _get_rounding_indices(
|
|
75
|
+
length_value, significant_figures
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if _decimals_required(first_digit_index, significant_figures, exponential):
|
|
79
|
+
formatted_numbers = [f"{normalised_value:.0{decimal_places}f}"] + list(
|
|
80
|
+
_format_errors_only(normalised_errors, decimal_places, abbreviate)
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
formatted_numbers = _map_recursive(
|
|
84
|
+
lambda v: str(int(round(v, decimal_places))),
|
|
85
|
+
[normalised_value] + list(normalised_errors),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return _join_numbers(
|
|
89
|
+
formatted_numbers, exponent=exponent, abbreviate=abbreviate, latex=latex
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _normalize_integrated_errors(
|
|
94
|
+
value: Value, errors: list[Error]
|
|
95
|
+
) -> tuple[float, Errors]:
|
|
96
|
+
"""Take any errors already included in `value` and prepends them to `errors`.
|
|
97
|
+
|
|
98
|
+
Parameters:
|
|
99
|
+
|
|
100
|
+
value:
|
|
101
|
+
A central value that may have an associated uncertainty.
|
|
102
|
+
|
|
103
|
+
errors:
|
|
104
|
+
A list of other uncertainties.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
A tuple containing the central value, and a combined list of errors.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
if hasattr(value, "nominal_value") and hasattr(value, "std_dev"):
|
|
111
|
+
errors = [value.std_dev] + errors
|
|
112
|
+
value = value.nominal_value
|
|
113
|
+
elif hasattr(value, "value") and hasattr(value, "dvalue"):
|
|
114
|
+
if value.dvalue == 0.0:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
"pyerrors will not give an error before calling .gamma_method()"
|
|
117
|
+
)
|
|
118
|
+
errors = [value.dvalue] + errors
|
|
119
|
+
value = value.value
|
|
120
|
+
|
|
121
|
+
return value, errors
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _normalize(value: Value, errors: Errors) -> tuple[float, Errors, int]:
|
|
125
|
+
"""
|
|
126
|
+
Divides `value` and all elements of `errors` by a common power of 10
|
|
127
|
+
such that `value` is in [1, 10).
|
|
128
|
+
|
|
129
|
+
Arguments:
|
|
130
|
+
|
|
131
|
+
value: a central value
|
|
132
|
+
|
|
133
|
+
errors: a list of errors, either numbers or tuples of two numbers
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
|
|
137
|
+
value: the normalized central value
|
|
138
|
+
|
|
139
|
+
errors: the normalized errors
|
|
140
|
+
|
|
141
|
+
exponent: the power of 10 by which the values have been multiplied
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
if value == 0:
|
|
145
|
+
exponent = _first_digit(max(_flatten_errors(errors)))
|
|
146
|
+
else:
|
|
147
|
+
exponent = _first_digit(value)
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
value / 10**exponent,
|
|
151
|
+
_map_recursive(lambda error: error / 10**exponent, errors),
|
|
152
|
+
exponent,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
RecursiveMappable = TypeVar("RecursiveMappable", list, tuple)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _map_recursive(func: Callable, data: RecursiveMappable) -> RecursiveMappable:
|
|
160
|
+
"""
|
|
161
|
+
Map a function `f` across elements in some non-uniform nested structure of iterables.
|
|
162
|
+
"""
|
|
163
|
+
to_return = []
|
|
164
|
+
for datum in data:
|
|
165
|
+
# Ideally this would be done with isinstance(datum, InstantiableSequence)
|
|
166
|
+
# where InstantiableSequence is like Sequence, but can be instantiated from
|
|
167
|
+
# elements as is done below. I haven't got that to work with mypy however.
|
|
168
|
+
if isinstance(datum, list):
|
|
169
|
+
to_return.append(_map_recursive(func, datum))
|
|
170
|
+
elif isinstance(datum, tuple):
|
|
171
|
+
to_return.append(list(_map_recursive(func, datum)))
|
|
172
|
+
else:
|
|
173
|
+
to_return.append(func(datum))
|
|
174
|
+
|
|
175
|
+
return type(data)(to_return)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _first_digit(value: float) -> int:
|
|
179
|
+
"""
|
|
180
|
+
Return the first digit position of the given value, as an integer.
|
|
181
|
+
0 is the digit just before the decimal point. Digits to the right
|
|
182
|
+
of the decimal point have a negative position.
|
|
183
|
+
Return 0 for a null value.
|
|
184
|
+
"""
|
|
185
|
+
if value == 0:
|
|
186
|
+
return 0
|
|
187
|
+
|
|
188
|
+
return int(math.floor(math.log10(abs(value))))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _get_length_value(value: float, errors: Errors, length_control: str) -> float:
|
|
192
|
+
"""
|
|
193
|
+
Get the value that will be controlling the length,
|
|
194
|
+
from either the central `value` or the list of `errors`.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
if length_control == "central":
|
|
198
|
+
return value
|
|
199
|
+
if length_control == "smallest":
|
|
200
|
+
length = _get_smallest(errors)
|
|
201
|
+
if length is None:
|
|
202
|
+
return value
|
|
203
|
+
|
|
204
|
+
return length
|
|
205
|
+
|
|
206
|
+
raise ValueError(
|
|
207
|
+
f"{length_control} is not a value option for length_control."
|
|
208
|
+
'(Available options are "smallest", "central".)'
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _get_smallest(errors: Errors) -> float | None:
|
|
213
|
+
"""Given a list of errors (number or tuples of two numbers),
|
|
214
|
+
find the smallest number."""
|
|
215
|
+
flat_errors = _flatten_errors(errors, exclude=[0, 0.0])
|
|
216
|
+
if not flat_errors:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
return min(flat_errors)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _flatten_errors(errors: Errors, exclude: Set | Sequence = frozenset()) -> list:
|
|
223
|
+
"""Given a list of errors (number or tuples of two numbers),
|
|
224
|
+
flatten it to a list of numbers."""
|
|
225
|
+
|
|
226
|
+
flat_errors = []
|
|
227
|
+
for error in errors:
|
|
228
|
+
try:
|
|
229
|
+
for value in error: # type: ignore[union-attr]
|
|
230
|
+
if value not in exclude:
|
|
231
|
+
flat_errors.append(value)
|
|
232
|
+
except (TypeError, ValueError):
|
|
233
|
+
if error not in exclude:
|
|
234
|
+
flat_errors.append(error)
|
|
235
|
+
|
|
236
|
+
return flat_errors
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _get_rounding_indices(
|
|
240
|
+
length_value: float, significant_figures: int
|
|
241
|
+
) -> tuple[int, int]:
|
|
242
|
+
"""
|
|
243
|
+
Get the index of the first digit of length_value,
|
|
244
|
+
and from it the number of decimal places corresponding to the
|
|
245
|
+
given value of significant_figures.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
# Repeat, as the first pass will break
|
|
249
|
+
# if the rounding changes the number of significant figures
|
|
250
|
+
for _ in range(2):
|
|
251
|
+
first_digit_index = _first_digit(length_value)
|
|
252
|
+
decimal_places = significant_figures - first_digit_index - 1
|
|
253
|
+
length_value = round(length_value, decimal_places)
|
|
254
|
+
|
|
255
|
+
return first_digit_index, decimal_places
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _decimals_required(
|
|
259
|
+
first_digit_index: int, significant_figures: int, exponential: bool
|
|
260
|
+
) -> bool:
|
|
261
|
+
"""
|
|
262
|
+
Determine whether a decimal point is needed to represent a number to a particular precision.
|
|
263
|
+
"""
|
|
264
|
+
return first_digit_index + 1 < significant_figures or exponential
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _format_errors_only(
|
|
268
|
+
errors: Errors, decimal_places: int, abbreviate: bool
|
|
269
|
+
) -> list[str]:
|
|
270
|
+
"""
|
|
271
|
+
Return a list of `errors` formatted to the specified number of `decimal_places`.
|
|
272
|
+
If `abbreviate` is specified, format only the portion of the number needed
|
|
273
|
+
to express the error.
|
|
274
|
+
"""
|
|
275
|
+
formatters = {True: _abbreviated_single_error, False: _unabbreviated_single_error}
|
|
276
|
+
|
|
277
|
+
return _map_recursive(
|
|
278
|
+
lambda error: formatters[abbreviate](error, decimal_places), errors
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _abbreviated_single_error(error: float, decimal_places: int) -> str:
|
|
283
|
+
"""
|
|
284
|
+
Take a single `error` and return its correct abbreviation
|
|
285
|
+
to the given number of `decimal_places`.
|
|
286
|
+
"""
|
|
287
|
+
if error >= 1:
|
|
288
|
+
return f"{error:.0{decimal_places}f}"
|
|
289
|
+
|
|
290
|
+
return str(int(round(error * 10**decimal_places)))
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _unabbreviated_single_error(error: float, decimal_places: int) -> str:
|
|
294
|
+
"""
|
|
295
|
+
Take a single `error` and return its correct unabbreviated format
|
|
296
|
+
to the given number of `decimal_places`.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
rounded_error = round(error, decimal_places)
|
|
300
|
+
if rounded_error == 0:
|
|
301
|
+
if decimal_places > 0:
|
|
302
|
+
return "0.0"
|
|
303
|
+
|
|
304
|
+
return "0"
|
|
305
|
+
|
|
306
|
+
return f"{rounded_error:.0{decimal_places}f}"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _join_numbers(
|
|
310
|
+
formatted_numbers: list[str], abbreviate: bool, latex: bool, exponent: int = 0
|
|
311
|
+
) -> str:
|
|
312
|
+
"""Take a list of `formatted_numbers`, and join them to form a full formatted string."""
|
|
313
|
+
|
|
314
|
+
# Dict indexed by the tuple (abbreviate, latex, [is single-component])
|
|
315
|
+
formatters = {
|
|
316
|
+
(True, True, True): "({error})",
|
|
317
|
+
(True, True, False): "({{}}^{{{error[0]}}}_{{{error[1]}}})",
|
|
318
|
+
(True, False, True): "({error})",
|
|
319
|
+
(True, False, False): "(+{error[0]}/-{error[1]})",
|
|
320
|
+
(False, True, True): " \\pm {error}",
|
|
321
|
+
(False, True, False): " {{}}^{{+{error[0]}}}_{{-{error[1]}}}",
|
|
322
|
+
(False, False, True): " ± {error}",
|
|
323
|
+
(False, False, False): " (+{error[0]} / -{error[1]})",
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
elements = list(formatted_numbers[:1])
|
|
327
|
+
for error in formatted_numbers[1:]:
|
|
328
|
+
elements.append(
|
|
329
|
+
formatters[abbreviate, latex, isinstance(error, str)].format(error=error)
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if exponent:
|
|
333
|
+
if not abbreviate:
|
|
334
|
+
# Errors and central value must be grouped together
|
|
335
|
+
# so the exponent applies to all
|
|
336
|
+
elements = ["("] + elements + [")"]
|
|
337
|
+
if latex:
|
|
338
|
+
elements.append(f" \\times 10^{{{exponent}}}")
|
|
339
|
+
else:
|
|
340
|
+
elements.append(f"e{exponent}")
|
|
341
|
+
|
|
342
|
+
return "".join(elements)
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""Functions to assist with formatting errors in Pandas DataFrames."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import cast
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import pandas as pd
|
|
13
|
+
except ImportError:
|
|
14
|
+
warnings.warn("Unable to import Pandas.", RuntimeWarning)
|
|
15
|
+
|
|
16
|
+
from .formatter import format_multiple_errors
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _format_column(value: pd.Series, *errors: pd.Series, **fme_kwargs) -> pd.Series:
|
|
20
|
+
"""Formats the values and errors in a list of columns consistently.
|
|
21
|
+
|
|
22
|
+
Parameters:
|
|
23
|
+
|
|
24
|
+
value:
|
|
25
|
+
A Pandas Series containing the central value to format.
|
|
26
|
+
If a number with error (pyerrors.Obs or uncertainties.UFloat),
|
|
27
|
+
then the nominal value is taken and the uncertainty is prepended to `errors`.
|
|
28
|
+
|
|
29
|
+
*errors:
|
|
30
|
+
A Pandas Series containing the uncertainties to format.
|
|
31
|
+
Columns should contain a single number (symmetric),
|
|
32
|
+
or a tuple of two numbers (upper, lower);
|
|
33
|
+
a tuple of two columns may also be used to specify (upper, lower).
|
|
34
|
+
|
|
35
|
+
**fme_kwargs:
|
|
36
|
+
Optional keyword arguments passed to `format_multiple_errors`
|
|
37
|
+
and documented therein.
|
|
38
|
+
"""
|
|
39
|
+
errors_flattened = []
|
|
40
|
+
for error in errors:
|
|
41
|
+
if isinstance(error, tuple):
|
|
42
|
+
errors_flattened.append(_tuplify(error))
|
|
43
|
+
else:
|
|
44
|
+
errors_flattened.append(error)
|
|
45
|
+
|
|
46
|
+
df = pd.concat([value] + errors_flattened, axis=1)
|
|
47
|
+
index = []
|
|
48
|
+
formatted_errors = []
|
|
49
|
+
for single_index, single_value, *single_errors in df.itertuples():
|
|
50
|
+
index.append(single_index)
|
|
51
|
+
number = format_multiple_errors(single_value, *single_errors, **fme_kwargs)
|
|
52
|
+
if fme_kwargs.get("latex"):
|
|
53
|
+
formatted_errors.append(f"${number}$")
|
|
54
|
+
else:
|
|
55
|
+
formatted_errors.append(number)
|
|
56
|
+
|
|
57
|
+
return pd.Series(data=formatted_errors, index=index)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _tuplify(columns: list[pd.Series]) -> pd.Series:
|
|
61
|
+
"""
|
|
62
|
+
Turn columns into one column of tuples.
|
|
63
|
+
Given a list of Pandas Series, returns a single Series
|
|
64
|
+
containing the elements of each input series as a tuple.
|
|
65
|
+
|
|
66
|
+
Parameters:
|
|
67
|
+
|
|
68
|
+
columns:
|
|
69
|
+
A list of Pandas Series objects.
|
|
70
|
+
"""
|
|
71
|
+
df = pd.concat(columns, axis=1)
|
|
72
|
+
index = []
|
|
73
|
+
values = []
|
|
74
|
+
for row in df.itertuples():
|
|
75
|
+
index.append(row[0])
|
|
76
|
+
values.append(tuple(row[1:]))
|
|
77
|
+
return pd.Series(data=values, index=index)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def format_column_errors(
|
|
81
|
+
value: str | pd.Series,
|
|
82
|
+
*errors: str | pd.Series,
|
|
83
|
+
df: pd.DataFrame | None = None,
|
|
84
|
+
**fme_kwargs,
|
|
85
|
+
):
|
|
86
|
+
"""Formats the values and errors in a DataFrame consistently.
|
|
87
|
+
|
|
88
|
+
Parameters:
|
|
89
|
+
|
|
90
|
+
value:
|
|
91
|
+
The column containing the central value to format.
|
|
92
|
+
This may be a Pandas Series object, or if `df` is specified,
|
|
93
|
+
then it may be the name of a column.
|
|
94
|
+
If a number with error (pyerrors.Obs or uncertainties.UFloat),
|
|
95
|
+
then the nominal value is taken and the uncertainty is prepended to `errors`.
|
|
96
|
+
|
|
97
|
+
*errors:
|
|
98
|
+
The columns containing the uncertainties to format.
|
|
99
|
+
This may be a Pandas Series object, or if `df` is specified,
|
|
100
|
+
then they may be the names of a columns,
|
|
101
|
+
consistently with the argument to `values`.
|
|
102
|
+
Columns should contain a single number (symmetric),
|
|
103
|
+
or a tuple of two numbers (upper, lower);
|
|
104
|
+
a tuple of two columns may also be used to specify (upper, lower).
|
|
105
|
+
|
|
106
|
+
**fme_kwargs:
|
|
107
|
+
Optional keyword arguments passed to `format_multiple_errors`
|
|
108
|
+
and documented therein.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
if df is None:
|
|
112
|
+
value = cast(pd.Series, value)
|
|
113
|
+
errors = cast(tuple[pd.Series], errors)
|
|
114
|
+
return _format_column(value, *errors, **fme_kwargs)
|
|
115
|
+
|
|
116
|
+
if not isinstance(value, str):
|
|
117
|
+
message = (
|
|
118
|
+
"If `df` is specified, then `value` must be a column name, "
|
|
119
|
+
f"not {type(value)}."
|
|
120
|
+
)
|
|
121
|
+
raise TypeError(message)
|
|
122
|
+
error_series = []
|
|
123
|
+
for error_idx, error in enumerate(errors):
|
|
124
|
+
if isinstance(error, str):
|
|
125
|
+
error_series.append(df[error])
|
|
126
|
+
elif isinstance(error, tuple):
|
|
127
|
+
error_series.append(_tuplify([df[element] for element in error]))
|
|
128
|
+
else:
|
|
129
|
+
message = (
|
|
130
|
+
f"Invalid error at index {error_idx}. "
|
|
131
|
+
"If `df` is specified, then each element of `errors` must be "
|
|
132
|
+
f"a column name or tuple of column names, not {type(error)}."
|
|
133
|
+
)
|
|
134
|
+
raise TypeError(message)
|
|
135
|
+
|
|
136
|
+
return _format_column(df[value], *error_series, **fme_kwargs)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class ColumnSpec:
|
|
141
|
+
"""
|
|
142
|
+
Specification of columns to include in calls to `format_dataframe_errors()`
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(self, value: str, *errors: str, name: str | None = None, **fme_kwargs):
|
|
146
|
+
"""
|
|
147
|
+
Specify a set of columns to turn into a single column containing formatted
|
|
148
|
+
values and uncertainties.
|
|
149
|
+
|
|
150
|
+
Arguments:
|
|
151
|
+
|
|
152
|
+
value:
|
|
153
|
+
The name of the column to use as the central value.
|
|
154
|
+
If the column contains numbers with errors
|
|
155
|
+
(pyerrors.Obs or uncertainties.UFloat), then the
|
|
156
|
+
nominal value is taken and the uncertainty is prepended to `errors`.
|
|
157
|
+
|
|
158
|
+
errors:
|
|
159
|
+
Names of columns to use as symmetric uncertainties,
|
|
160
|
+
and/or tuples of pairs of columns to use as upper and lower uncertainties.
|
|
161
|
+
|
|
162
|
+
name:
|
|
163
|
+
The name to give the resulting series.
|
|
164
|
+
If not specified, the `value` argument will be used.
|
|
165
|
+
|
|
166
|
+
**fme_kwargs:
|
|
167
|
+
Arguments for formatting the uncertainties,
|
|
168
|
+
to be passed to `format_multiple_errors()`.
|
|
169
|
+
"""
|
|
170
|
+
self.value = value
|
|
171
|
+
self.errors = errors
|
|
172
|
+
self.fme_kwargs = fme_kwargs
|
|
173
|
+
if name is not None:
|
|
174
|
+
self.name = name
|
|
175
|
+
else:
|
|
176
|
+
self.name = value
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def format_dataframe_errors(
|
|
180
|
+
df: pd.DataFrame, columns: list[str], **fme_kwargs
|
|
181
|
+
) -> pd.DataFrame:
|
|
182
|
+
"""
|
|
183
|
+
Formats groups of columns in a DataFrame into strings with errors.
|
|
184
|
+
|
|
185
|
+
Parameters:
|
|
186
|
+
|
|
187
|
+
df:
|
|
188
|
+
The Pandas DataFrame to take data from.
|
|
189
|
+
|
|
190
|
+
columns:
|
|
191
|
+
A list of column specifications, either `str`, `tuple`, `list`, or `ColumnSpec`.
|
|
192
|
+
Strings will be interpreted as columns to take with no modification.
|
|
193
|
+
|
|
194
|
+
For tuples and lists,
|
|
195
|
+
the first element is the name of the column containing the central value,
|
|
196
|
+
and the subsequent elements the names of the columns containing the uncertainties
|
|
197
|
+
(or tuples of column names for upper and lower uncertainties).
|
|
198
|
+
The name of the value column will be used as the name of the formatted column.
|
|
199
|
+
|
|
200
|
+
**fme_kwargs:
|
|
201
|
+
Default arguments for formatting the errors.
|
|
202
|
+
If columns are specified using ColumnSpec instances,
|
|
203
|
+
then any arguments set therein will take priority over those specified here.
|
|
204
|
+
"""
|
|
205
|
+
content = []
|
|
206
|
+
for column_index, column in enumerate(columns):
|
|
207
|
+
if isinstance(column, str):
|
|
208
|
+
content.append(df[column])
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
if isinstance(column, (tuple, list)):
|
|
212
|
+
column = ColumnSpec(column[0], *column[1:])
|
|
213
|
+
|
|
214
|
+
if not isinstance(column, ColumnSpec):
|
|
215
|
+
message = (
|
|
216
|
+
"Each column must be a str, tuple, list, or ColumnSpec, "
|
|
217
|
+
f"not {type(column)} as found at index {column_index}."
|
|
218
|
+
)
|
|
219
|
+
raise TypeError(message)
|
|
220
|
+
|
|
221
|
+
new_column = format_column_errors(
|
|
222
|
+
column.value, *column.errors, df=df, **{**fme_kwargs, **column.fme_kwargs}
|
|
223
|
+
)
|
|
224
|
+
new_column.name = column.name
|
|
225
|
+
content.append(new_column)
|
|
226
|
+
|
|
227
|
+
return pd.concat(content, axis=1)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from typing import Union, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from pyerrors import Obs # type: ignore[import-untyped]
|
|
8
|
+
from uncertainties import UFloat # type: ignore[import-untyped]
|
|
9
|
+
|
|
10
|
+
Value = Union[float, Obs, UFloat]
|
|
11
|
+
else:
|
|
12
|
+
Value = float
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Error = Union[float, tuple[float, float]]
|
|
16
|
+
Errors = list[Error]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Ed Bennett/Swansea University
|
|
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.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: format_multiple_errors
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: A small widget to be able to format multiple, asymmetric errors easily.
|
|
5
|
+
Author-email: Ed Bennett <e.j.bennett@swansea.ac.uk>
|
|
6
|
+
Project-URL: Homepage, https://github.com/edbennett/format_multiple_errors
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/edbennett/format_multiple_errors/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
|
|
15
|
+
# format_multiple_errors
|
|
16
|
+
|
|
17
|
+
[](https://github.com/edbennett/format_multiple_errors/actions/workflows/pytest.yaml)
|
|
18
|
+
[](https://github.com/edbennett/format_multiple_errors/actions/workflows/codequality.yaml)
|
|
19
|
+
|
|
20
|
+
A small library intended to make it easy to format numbers like
|
|
21
|
+
|
|
22
|
+
$$1.623(11)({}^{3}_{4})\times 10^{-7}$$
|
|
23
|
+
|
|
24
|
+
or
|
|
25
|
+
|
|
26
|
+
$$(6.829 \pm 0.013 {}^{+0.104}_{-0.096})\times10^{5}$$
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
To install, open a terminal and run:
|
|
32
|
+
|
|
33
|
+
pip install https://github.com/edbennett/format_multiple_errors
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Usage as a library
|
|
37
|
+
|
|
38
|
+
### Formatting numbers
|
|
39
|
+
|
|
40
|
+
The `format_multiple_errors` package provides a function to format numbers,
|
|
41
|
+
also named `format_multiple_errors`.
|
|
42
|
+
|
|
43
|
+
from format_multiple_errors import format_multiple_errors
|
|
44
|
+
|
|
45
|
+
This function takes a central value,
|
|
46
|
+
and zero or more uncertainties.
|
|
47
|
+
It returns a string containing all numbers formatted to the same absolute precision.
|
|
48
|
+
|
|
49
|
+
Uncertainties may be single numbers:
|
|
50
|
+
|
|
51
|
+
>>> format_multiple_errors(1, 0.1, 0.2)
|
|
52
|
+
'1.00 ± 0.10 ± 0.20'
|
|
53
|
+
|
|
54
|
+
or they may also be tuples of two numbers,
|
|
55
|
+
representing the upper and lower error respectively:
|
|
56
|
+
|
|
57
|
+
>>> format_multiple_errors(1, 0.1, (0.2, 0.3))
|
|
58
|
+
'1.00 ± 0.10 (+0.20 / -0.30)'
|
|
59
|
+
|
|
60
|
+
A number of keyword arguments control the formatting.
|
|
61
|
+
The `abbreviate` option uses the compact form of expressing errors:
|
|
62
|
+
|
|
63
|
+
>>> format_multiple_errors(1.001, 0.010, (0.020, 0.034), abbreviate=True)
|
|
64
|
+
'1.001(10)(+20 / -34)'
|
|
65
|
+
|
|
66
|
+
The `latex` option uses LaTeX macros rather than Unicode characters,
|
|
67
|
+
and generally formats the result for inclusion in a LaTeX document:
|
|
68
|
+
|
|
69
|
+
>>> format_multiple_errors(1.001, 0.010, (0.020, 0.034), latex=True)
|
|
70
|
+
'1.001 \\pm 0.010 {}^{+0.020}_{-0.034}'
|
|
71
|
+
|
|
72
|
+
Numbers are formatted by default with leading or trailing zeroes.
|
|
73
|
+
To use exponential notation instead,
|
|
74
|
+
use the `exponential` parameter:
|
|
75
|
+
|
|
76
|
+
>>> format_multiple_errors(0.00123, 0.00045, (0.00067, 0.00089), exponential=True)
|
|
77
|
+
'(1.23 ± 0.45 (+0.67 / -0.89))e-3'
|
|
78
|
+
|
|
79
|
+
By default the precision is controlled
|
|
80
|
+
by setting the number of significant digits presented for the smallest uncertainty.
|
|
81
|
+
Setting `length_control="central"` instead controls the significant digits of the central value:
|
|
82
|
+
|
|
83
|
+
>>> format_multiple_errors(1.0, 0.1, (0.2, 0.3), length_control="central")
|
|
84
|
+
'1.0 ± 0.1 (+0.2 / -0.3)'
|
|
85
|
+
|
|
86
|
+
The number of significant figures presented can be controlled with the `significant_figures` parameter:
|
|
87
|
+
|
|
88
|
+
>>> format_multiple_errors(1.001, 0.001, (0.002, 0.0034), significant_figures=1)
|
|
89
|
+
'1.001 ± 0.001 (+0.002 / -0.003)'
|
|
90
|
+
|
|
91
|
+
These options may be combined:
|
|
92
|
+
|
|
93
|
+
>>> format_multiple_errors(123.45, 3.14, (2.82, 12.91), length_control="central", significant_figures=5, latex=True, abbreviate=True, exponential=True)
|
|
94
|
+
'1.2345(314)({}^{282}_{1291}) \\times 10^{2}'
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
### Formatting DataFrames
|
|
98
|
+
|
|
99
|
+
The library provides two functions for working with Pandas DataFrames.
|
|
100
|
+
|
|
101
|
+
The `format_column_errors` function accepts and returns columns.
|
|
102
|
+
For example,
|
|
103
|
+
it can take `pd.Series` objects:
|
|
104
|
+
|
|
105
|
+
>>> import pandas as pd
|
|
106
|
+
>>> from format_multiple_errors import format_column_errors
|
|
107
|
+
>>> df = pd.DataFrame([{"a": 3.14, "b": 0.59, "c": 0.26}, {"a": 2.17, "b": 0.82, "c": 0.81}])
|
|
108
|
+
>>> format_column_errors(df["a"], (df["b"], df["c"]), abbreviate=True)
|
|
109
|
+
0 3.14(+59/-26)
|
|
110
|
+
1 2.17(+82/-81)
|
|
111
|
+
dtype: object
|
|
112
|
+
|
|
113
|
+
It can also take a `pd.DataFrame` and specifications of the column names to use:
|
|
114
|
+
|
|
115
|
+
>>> format_column_errors("a", ("b", "c"), df=df, abbreviate=True)
|
|
116
|
+
0 3.14(+59/-26)
|
|
117
|
+
1 2.17(+82/-81)
|
|
118
|
+
dtype: object
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
### Interaction with `pyerrors` and `uncertainties`
|
|
122
|
+
|
|
123
|
+
If the central value passed to `format_multiple_errors` already has an uncertainty,
|
|
124
|
+
due to being an instance of `pyerrors.Obs` or `uncertainties.UFloat`,
|
|
125
|
+
then this is prepended to the list of errors.
|
|
126
|
+
For example,
|
|
127
|
+
|
|
128
|
+
>>> from uncertainties import ufloat
|
|
129
|
+
>>> result = ufloat(1.01, 0.1)
|
|
130
|
+
>>> systematic = (0.2, 0.34)
|
|
131
|
+
>>> format_multiple_errors(result, systematic)
|
|
132
|
+
'1.01 ± 0.10 (+0.20 / -0.34)'
|
|
133
|
+
|
|
134
|
+
Instances of `pyerrors.Obs` must already have an uncertainty computed
|
|
135
|
+
(must have had the `.gamma_method()` method called on them)
|
|
136
|
+
before being passed to `format_multiple_errors`,
|
|
137
|
+
otherwise an error is raised.
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
## Command-line interface
|
|
141
|
+
|
|
142
|
+
A command-line interface is also provided.
|
|
143
|
+
To format a single number,
|
|
144
|
+
use the `format_multiple_errors format` command:
|
|
145
|
+
|
|
146
|
+
$ format_multiple_errors --abbreviate format 3.141 0.059 0.026,0.053
|
|
147
|
+
3.141(59)(+26/-53)
|
|
148
|
+
|
|
149
|
+
To format a CSV as a LaTeX table,
|
|
150
|
+
use the `format_multiple_errors table` command:
|
|
151
|
+
|
|
152
|
+
$ format_multiple_errors --latex --abbreviate table input.csv \
|
|
153
|
+
> a b c_value,c_error d_value,d_upper-d_lower,d_systematic \
|
|
154
|
+
> --headings '$a$' '$b$' '$c$' '$d$' --output_file output.tex
|
|
155
|
+
$ cat output.tex
|
|
156
|
+
\begin{tabular}{rrll}
|
|
157
|
+
\toprule
|
|
158
|
+
$a$ & $b$ & $c$ & $d$ \\
|
|
159
|
+
\midrule
|
|
160
|
+
3 & 1 & $4.16(26)$ & $3.59({}^{79}_{24})(46)$ \\
|
|
161
|
+
2 & 7 & $1.83(18)$ & $8.459({}^{45}_{235})(360)$ \\
|
|
162
|
+
\bottomrule
|
|
163
|
+
\end{tabular}
|
|
164
|
+
|
|
165
|
+
Formatting options are specified before the subcommand (`format` or `table`).
|
|
166
|
+
Options specific to the command are specified afterwards.
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
## Development
|
|
170
|
+
|
|
171
|
+
To be able to run the test suite,
|
|
172
|
+
create a virtual environment using the tooling of your choice,
|
|
173
|
+
and then install the developer dependencies:
|
|
174
|
+
|
|
175
|
+
pip install -r requirements_dev.txt
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
The test suite can then be run by calling
|
|
179
|
+
|
|
180
|
+
pytest
|
|
181
|
+
|
|
182
|
+
Before committing,
|
|
183
|
+
you should ensure that the repository's pre-commit hooks are installed:
|
|
184
|
+
|
|
185
|
+
pre-commit install
|
|
186
|
+
|
|
187
|
+
Then some basic code quality checks will be run by Git
|
|
188
|
+
before it accepts your commit.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
format_multiple_errors/__init__.py,sha256=ZLtmZ1Pe3zyxnic-b4nhtuajNj8llWykspVPpFaN7Rc,460
|
|
2
|
+
format_multiple_errors/__main__.py,sha256=tV8VPGPwq2d3CWJp9hFSDDMyHHL3uY2jvipxeoH_dGY,5825
|
|
3
|
+
format_multiple_errors/formatter.py,sha256=GnfJjP4HbYXxTq-Yx1E8DhbviPqRj0t3_icPTYgQ2P0,10744
|
|
4
|
+
format_multiple_errors/pandas.py,sha256=qoK_UvXIErJJdWKfJRyOvtMNeR6JHaUv_QUlTZjnCHg,7605
|
|
5
|
+
format_multiple_errors/typing.py,sha256=k0p3GDaHGXHd3zmBtNg0-cvcMzEupzN4RHaLdNvxX6U,342
|
|
6
|
+
format_multiple_errors-0.0.2.dist-info/LICENSE,sha256=G3SZheaYJW8JV0Njnnyfi7QwUJWh9evcPRQ-nAXi8Ro,1086
|
|
7
|
+
format_multiple_errors-0.0.2.dist-info/METADATA,sha256=1vJY4LlcZhCuxYANbRIGpUxs8coFFgecUKof8zuk6t4,6334
|
|
8
|
+
format_multiple_errors-0.0.2.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
9
|
+
format_multiple_errors-0.0.2.dist-info/entry_points.txt,sha256=PeHYlzbvQciSJX3DWuM-Y5Xwi9Is0jO-LlUgdSkUTp4,79
|
|
10
|
+
format_multiple_errors-0.0.2.dist-info/top_level.txt,sha256=I_xD5oYbrw0qjNhbzAlj_sqNhLrkoJsTE_ozCqnlaZc,23
|
|
11
|
+
format_multiple_errors-0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
format_multiple_errors
|