# Printing (number, uncertainty) pair in a formal way

I am trying to do an auxilliary function to print pairs of value-uncertainty in a formal way, given their name, value and uncertainty.

The "technical" agreement is that "uncertainty rules", meaning that the significant digits of a pair to be printed are given by the decimal size of the uncertainty value. For example: real values (123.456789, 0.012345) should be converted to a representation of (123.456, 0.012), and real values (123456.789, 123.45) should be converted to a representation of (123450, 120).

• If an uncertainty is greater than 10^3 or smaller than 10^-3, then exponent notation will be used with 1 decimal digit for the value (ideally, the pair should share the exponent factor which would be given by the value exponent factor, but I don't know how to do that yet - I marked it as to-do in the code). For example, (6.31e+03, 0.31e+03) would be the ideal, now I am getting (6.310000e+03, 3.1000000e+02).
• Don't print decimal point if the uncertainty value is greater than 10.

Some options I try to give:

Whitespace formatting:

'{:{ws}{nfmt}}'.format(ws = ' ' if ws in [True, ' '] else '', nfmt = ...)
# nfmt stands for number formatting, e.g. .1f, etc. see code.


Representation formatting: useful for latex, unicode printing, simple/ascii or shorthand notation (I don't know how to implement exponent notation in short notation representation, that's why I refer to making a custom repr method in the code).

• Simple/ascii: 0.210+/-0.011
• Short: 0.210(11) # uncertainty takes integer form. I don't know how to do this one
• Short-composite: 0.210(0.011)
• Fancy/utf-8: 0.210 ± 0.011
• Latex: $0.210 \pm 0.011$

Note that the exponent factor should be printed outside: if it was (0.000210, 0.000011) (to comply with condition of u <= 10^-3), it should print (2.10 ± 0.11)e-04 and variants or 210(11)e-06 for short notation. I don't know how to simply do neither of them either :(.

Edit: the short notation is more complicated than I thought. I added what it should look like on short+exponent notation. Also added exponent condition for small numbers.

Edit #2: I'm seeing a lot of bad style here. Technically, exponent notation should be also printed outside for all the representations. That's another reason to do a specific formal repr function for pairs. Now I'm realizing that this probably isn't a good post. If somebody tells me to, I'll delete the post and redo it after I've given it a better try. Only issue is I should probably be studying for finals :p

Here is the full code:

def _print_fres(names, vals, uncs, sigds = 2, rfmt = 'pm', ws = False):
# sigds are the significance digits
# inputs are lists of names, values and uncertainties respectively
try:
if all([str(u).lower() not in 'inf' for u in uncs]):
sigs = [
(re.search('[1-9]', str(u)).start()-2 \
if re.match('0\.', str(u)) \
else -re.search('\.', str(float(u))).start())+sigds \
for u in uncs
]
# significant digits rule in uncertainty
else:
print('Warning: infinity in uncertainty values')
sigs = [sigds] * len(uncs)
except TypeError: #NaN or None
raise TypeError('Error: odd uncertainty values')

rfmt = rfmt.lower()
# this can be done better/prettier I think
if rfmt in ['fancy', 'pms']: # pms stands for pmsign
res_str = '{{0}} = {{1:{ws}{nfmt}}} ± {{2:{ws}{nfmt}}}'
elif rfmt in ['basic', 'pm', 'ascii']:
res_str = '{{0}} = {{1:{ws}{nfmt}}}+/-{{2:{ws}{nfmt}}}'
elif rfmt in ['tex', 'latex']:
res_str = '${{0}} = {{1:{ws}{nfmt}}} \\pm {{2:{ws}{nfmt}}}$'
elif rfmt in ['s1', 'short1']:
res_str = '{{0}} = {{1:{ws}{nfmt}}} ± {{2:{ws}{nfmt}}}'
# not yet supported. to do: shorthand notation
elif rfmt in ['s2', 'short2']:
res_str = '{{0}} = {{1:{ws}{nfmt}}}({{2:{ws}{nfmt}}})'
else:
raise KeyError('rfmt value is invalid')

for i in range(len(vals)):
try:
print((res_str.format(
nfmt = '1e' if uncs[i] >= 1000 or uncs[i] <= 0.001 \
# 1 decimal exponent notation for big/small numbers
else (
'd' if sigs[i] <= 0 \
# integer if uncertainty >= 10
else '.{}f'.format(sigs[i])),
ws = ' ' if ws in [True, ' '] else ''
)
).format(
names[i],
round(vals[i], sigs[i]),
round(uncs[i], sigs[i])
# round to allow non-decimal significances
)
)

except (TypeError, ValueError, OverflowError) as e:
print('{} value is invalid'.format(uncs[i]))
print(e)
continue
# to do: a repr method to get numbers well represented
# instead of this whole mess

• What does "(123.456789, 0.012345) should be a representation of (123.456, 0.012)" mean? Is either tuple a decimal with its uncertainty? If so, which one, and what is the other tuple?
– l0b0
Jan 3, 2019 at 8:22
• @l0b0 sorry, I meant "converted to representation of"; edited. Either way, both of them are pairs of numbers, not necessarily tuples. The program takes a list of measurements a list of their uncertainties, that's why the inputs are lists instead of single elements. Also, the function is was originally intended to be private since it's a minor part of a much bigger function/algorithm. I'm working on a solution via class, however. I'll probably post it in another post since it's completely different? I don't know, I'm new to this site and haven't really taken the time to read the whole faq. Jan 3, 2019 at 17:05

Some suggestions:

• Rather than explaining parameters in comments, simply name the variables so that the comments become unnecessary. A signature of def format_uncertain_values(names, values, uncertainties, significant_digits=2, format='pm', word_separated=False) should do it.
• There are three distinct sections to your method, which should probably be separate methods.
• This code is a great candidate for type hints. Declare types using for example names: typing.List[str] and validate them using the mypy command line tool (be sure to look into the MyPy options, because the default configuration is very lax). This will highlight at least one implementation issue: ws is treated as either a boolean or a string, but
• boolean arguments are a code smell (1, 2) and
• making it a string (possibly defaulting it to the empty string) would remove the need for the ws = ' ' if … line.
• The code would be much simpler if it simply took a single name, value and uncertainty. Then the user
• avoids having to wrap everything in lists to format a single variable and
• can still trivially loop over their values to print all of them.
• Send the code through at least one linter such as flake8 or pycodestyle until it passes all the tests. Then your code will be much more idiomatic, and therefore easier to read and maintain for people familiar with Python.
• Default arguments are IMO a code smell. They should only really be used if it is completely obvious what the default is, even for someone who only reads the body of the method.
• The format names are magic strings, and as such should be either constants or enums.
• The format synonyms such as ['fancy', 'pms'] feel like over-engineering. If you're the only user for now, why make your code more complicated than it needs to be?
• The method should return rather than print() the result (see above about handling only a single variable at a time) so that it can be reused by other code. It would be trivial to add a wrapper like def main() to print() it.
• Why are boolean arguments a code smell? I see them used frequently in libraries such as scipy, pandas, etc. Jan 3, 2019 at 17:14