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.
@@ -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
+ [![Run tests](https://github.com/edbennett/format_multiple_errors/actions/workflows/pytest.yaml/badge.svg)](https://github.com/edbennett/format_multiple_errors/actions/workflows/pytest.yaml)
18
+ [![Code quality](https://github.com/edbennett/format_multiple_errors/actions/workflows/codequality.yaml/badge.svg)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ format_multiple_errors = format_multiple_errors.__main__:cli
@@ -0,0 +1 @@
1
+ format_multiple_errors