24

I'm looking for a python library which comes with support to convert numbers between various SI prefixes, for example, kilo to pico, nano to giga and so on.What would you recommend?

Mikko Ohtamaa
  • 82,057
  • 50
  • 264
  • 435
Zed
  • 5,683
  • 11
  • 49
  • 81
  • 1
    possible duplicate of [Unit Conversion in Python](http://stackoverflow.com/questions/2125076/unit-conversion-in-python) – GWW Jun 10 '12 at 15:07
  • 2
    @GWW: Not really, that question wants to convert units, this is about prefixes. – Junuxx Jun 10 '12 at 15:28
  • 1
    @Zed: It isn't really clear what you want to do. What is the format of the input, for example? Strings? Numbers and strings? Numbers and a prefix index? It might help if you gave an example of what you want to do exactly. – Junuxx Jun 10 '12 at 15:30
  • 3
    But since the number of prefixes is fairly limited, you might be better of with a simple dictionary (e.g. `{'giga':1e9, 'kilo':1e3, 'milli':1e-3, ...}`) – Junuxx Jun 10 '12 at 15:32

8 Answers8

18

I ported a simple function (original C version written by Jukka “Yucca” Korpela) to Python for formatting numbers according to SI standards. I use it often, for example, to set tick labels on plots, etc.

You can install it with:

pip install si-prefix

The source is available on GitHub.

Example usage:

from si_prefix import si_format

print si_format(.5)
# 500.0m  (default precision is 1)

print si_format(.01331, precision=2)
# 13.31m

print si_format(1331, precision=2)
# 1.33k

print si_format(1331, precision=0)
# 1k
naitsirhc
  • 5,274
  • 2
  • 23
  • 16
  • This doesn't seem to have a way to go REVERSE (from SI string to number) – K.S. Oct 05 '22 at 23:21
  • 1
    @K.S., please check out the `si_prefix.si_parse()` function (see [here](https://github.com/cfobel/si-prefix/blob/274fdf47f65d87d0b7a2e3c80f267db63d042c59/si_prefix/__init__.py#L224-L263) for the definition). This function was added in version [0.4](https://github.com/cfobel/si-prefix/releases/tag/v0.4). – naitsirhc Oct 07 '22 at 13:14
7

QuantiPhy is a new package that converts to and from numbers with SI scale factors. It is often a better choice that the unit packages such as Unum and Magnitude that are heavier and focused on the units rather than the scale factors.

QuantiPhy provides Quantity, which is an object that combines a number with its unit of measure (the units are optional). When creating a quantity you can use SI unit prefixes. Once you have a Quantity you can use it in expressions, where it acts as a float. Or you can convert it to a string, in which case it uses the SI unit prefixes by default.

>>> from quantiphy import Quantity

# convert strings to quantities
>>> duration = Quantity('0.12 ks')
>>> print(duration)
120 s

# convert to other units when rendering to a string
>>> print(duration.render(scale='min'))
2 min

# quantities act like floats in expressions
>>> rate = 1/duration
>>> print(rate)
0.008333333333333333

# convert floats to quantities
>>> rate = Quantity(rate, 'Hz')
>>> print(rate)
8.3333 mHz

# can be used in format strings
>>> print(f'Duration = {duration:<12.3} Rate = {rate}')
Duration = 120 s        Rate = 8.3333 mHz

By default QuantiPhy uses the natural prefix when rendering to a string, which is probably what you want. But you can force it to render to a specific prefix using scaling:

>>> mass = Quantity('1000 g')
>>> print(mass)
1 kg

>>> print(mass.render(show_si=False))
1e3 g

>>> print(mass.render(show_si=False, scale=(1e-12, 'pg')))
1e9 pg

In this case you must turn off SI unit prefixes to avoid getting multiple prefixes: '1 npg'.

A more natural example might be where you are converting units:

>>> l = Quantity('2um')                                                      
>>> print(l.render(scale='Å'))                                               
20 kÅ                                                                        

>>> print(f'{l:sÅ}')                                                         
20 kÅ

The last example shows that you can place your desired units in the format string after the type and the conversion will be done for you automatically.

August West
  • 351
  • 3
  • 7
6

Dictionaries

If you don't want to use any 3rd-party library like the ones listed below, you can actually implement your own parsing function.

Use a dictionary to match up the prefixes to their values. I've done it for you already:

_prefix = {'y': 1e-24,  # yocto
           'z': 1e-21,  # zepto
           'a': 1e-18,  # atto
           'f': 1e-15,  # femto
           'p': 1e-12,  # pico
           'n': 1e-9,   # nano
           'u': 1e-6,   # micro
           'm': 1e-3,   # mili
           'c': 1e-2,   # centi
           'd': 1e-1,   # deci
           'k': 1e3,    # kilo
           'M': 1e6,    # mega
           'G': 1e9,    # giga
           'T': 1e12,   # tera
           'P': 1e15,   # peta
           'E': 1e18,   # exa
           'Z': 1e21,   # zetta
           'Y': 1e24,   # yotta
    }

Then you can use regex (as described by my answer here) to search or parse the input and use the dictionary for getting the appropriate value.


Unum

Unum is well finished and thoroughly documented library.

Pros:

  • allows you to define arbitrary units (magnitude only supports user-defined units as long as they are a combination of the base units).

Cons:

  • doesn't handle prefixes well
  • clutters your namespace with all its unit definitions (you end up with variables named M, S etc. in your namespace)

Magnitude

You can also use Magnitude, another library. It supports all the kinds of SI unit prefixes you're talking about, plus it'll handle the parsing as well. From the site:

A physical quantity is a number with a unit, like 10 km/h. Units are specified as strings. They can be any of the SI units, plus a bunch of non-SI, bits, dollars, and any combination of them. They can include the standard SI prefixes.
...
All standard prefixes are understood, from yocto to yotta and from kibi to exbi.

Community
  • 1
  • 1
Yatharth Agarwal
  • 4,385
  • 2
  • 24
  • 53
  • Regex doesn't sound like the best way to detect which prefix to use. Doesn't log(abs(value)) give you the size of your value in a form you can use to select a prefix. – Jonathan Hartley Nov 12 '13 at 11:22
4

I don't know if this is the best answer but it is working in my case. Feel free to verify my solution. I am working for first time with Python and constructive criticism is welcome... along with positive feedback :D
This is my code:

class Units:
def __init__(self):
    global si;
    si = {
          -18 : {'multiplier' : 10 ** 18, 'prefix' : 'a'},
          -17 : {'multiplier' : 10 ** 18, 'prefix' : 'a'},
          -16 : {'multiplier' : 10 ** 18, 'prefix' : 'a'},
          -15 : {'multiplier' : 10 ** 15, 'prefix' : 'f'},
          -14 : {'multiplier' : 10 ** 15, 'prefix' : 'f'},
          -13 : {'multiplier' : 10 ** 15, 'prefix' : 'f'},
          -12 : {'multiplier' : 10 ** 12, 'prefix' : 'p'},
          -11 : {'multiplier' : 10 ** 12, 'prefix' : 'p'},
          -10 : {'multiplier' : 10 ** 12, 'prefix' : 'p'},
          -9 : {'multiplier' : 10 ** 9, 'prefix' : 'n'},
          -8 : {'multiplier' : 10 ** 9, 'prefix' : 'n'},
          -7 : {'multiplier' : 10 ** 9, 'prefix' : 'n'},
          -6 : {'multiplier' : 10 ** 6, 'prefix' : 'u'},
          -5 : {'multiplier' : 10 ** 6, 'prefix' : 'u'},
          -4 : {'multiplier' : 10 ** 6, 'prefix' : 'u'},
          -3 : {'multiplier' : 10 ** 3, 'prefix' : 'm'},
          -2 : {'multiplier' : 10 ** 2, 'prefix' : 'c'},
          -1 : {'multiplier' : 10 ** 1, 'prefix' : 'd'},
           0 : {'multiplier' : 1, 'prefix' : ''},
           1 : {'multiplier' : 10 ** 1, 'prefix' : 'da'},
           2 : {'multiplier' : 10 ** 3, 'prefix' : 'k'},
           3 : {'multiplier' : 10 ** 3, 'prefix' : 'k'},
           4 : {'multiplier' : 10 ** 3, 'prefix' : 'k'},
           5 : {'multiplier' : 10 ** 3, 'prefix' : 'k'},
           6 : {'multiplier' : 10 ** 6, 'prefix' : 'M'},
           7 : {'multiplier' : 10 ** 6, 'prefix' : 'M'},
           8 : {'multiplier' : 10 ** 6, 'prefix' : 'M'},
           9 : {'multiplier' : 10 ** 9, 'prefix' : 'G'},
          10 : {'multiplier' : 10 ** 9, 'prefix' : 'G'},
          11 : {'multiplier' : 10 ** 9, 'prefix' : 'G'},
          12 : {'multiplier' : 10 ** 12, 'prefix' : 'T'},
          13 : {'multiplier' : 10 ** 12, 'prefix' : 'T'},
          14 : {'multiplier' : 10 ** 12, 'prefix' : 'T'},
          15 : {'multiplier' : 10 ** 15, 'prefix' : 'P'},
          16 : {'multiplier' : 10 ** 15, 'prefix' : 'P'},
          17 : {'multiplier' : 10 ** 15, 'prefix' : 'P'},
          18 : {'multiplier' : 10 ** 18, 'prefix' : 'E'},
          }

def convert(self, number):
    # Checking if its negative or positive
    if number < 0:
        negative = True;
    else:
        negative = False;

    # if its negative converting to positive (math.log()....)
    if negative:
        number = number - (number*2);

    # Taking the exponent
    exponent = int(math.log10(number));

    # Checking if it was negative converting it back to negative
    if negative:
        number = number - (number*2);

    # If the exponent is smaler than 0 dividing the exponent with -1
    if exponent < 0:
        exponent = exponent-1;
        return [number * si[exponent]['multiplier'], si[exponent]['prefix']]; 
    # If the exponent bigger than 0 just return it
    elif exponent > 0:
        return [number / si[exponent]['multiplier'], si[exponent]['prefix']]; 
    # If the exponent is 0 than return only the value
    elif exponent == 0:
        return [number, ''];


And this is how it works:

c1 = +1.189404E-010
fres = -4.07237500000000E+007;
ls = +1.943596E-005;

units = sci.Units();
rValue, rPrefix = units.convert(c1);
print rValue;
print rPrefix;

print units.convert(fres);
print units.convert(ls);

And the response is:

118.9404
p
[-40.72375, 'M']
[19.435959999999998, 'u']

I don't know if anyone will find this helpful or not. I hope you do. I've posted here so the people who want help to see it also to give them an idea maybe they can optimize it :)

Pentarex
  • 130
  • 1
  • 9
  • 2
    Improvement suggestion: You use 'd' as the prefix for both deci and deca, which is both confusing and incorrect. Deca should have the prefix "da". – alkanen May 03 '19 at 08:43
3

I know this is an old thread, but I'd just like to throw out a reference to a python library I wrote which handles all manner of prefix unit conversion handling

Here's the major feature list:

Tim Bielawa
  • 6,935
  • 2
  • 17
  • 11
1

Since the question is not yet flagged as answered, I'll give you here my solution, which is largely inspired from si-prefix, but I wanted to limit the number of dependencies of my package.

You can use the following function :

def format_prefix(value, precision=2, unit="m"):
    """
   Formats a numerical value with an appropriate SI prefix.

   Args:
   - value: A float representing the numerical value to be formatted.
   - precision: An integer representing the number of decimal places to include in the formatted value (default: 2).
   - unit: A string representing the unit of measurement to be appended to the formatted value (default: "m").

   Returns:
   - A string representing the formatted value with the appropriate SI prefix and unit.

   Raises:
   - ValueError: If the exponent is out of range of the available prefixes.
    """
    SI_PREFIX_UNITS = u"yzafpnµm kMGTPEZY"
    negative = False
    digits = precision + 2

    if value == 0.:
        expof10 = 0
    else:
        if value < 0.:
            value = -value
            negative = True
        expof10 = int(np.log10(value))
        if expof10 > 0:
            expof10 = (expof10 // 3) * 3
        else:
            expof10 = (-expof10 + 3) // 3 * (-3)

        value *= 10 ** (-expof10)

        if value >= 1000.:
            value /= 1000.0
            expof10 += 3
        elif value >= 100.0:
            digits -= 2
        elif value >= 10.0:
            digits -= 1

        if negative:
            value *= -1
    expof10 = int(expof10)
    prefix_levels = (len(SI_PREFIX_UNITS) - 1) // 2
    si_level = expof10 // 3

    if abs(si_level) > prefix_levels:
        raise ValueError("Exponent out range of available prefixes.")
    return f"{round(value*10**digits)/10**digits} "+SI_PREFIX_UNITS[si_level + prefix_levels].strip()+unit

The function outputs a string like this :

>>> format_prefix(123687.2e-9, unit="m")
'123.69 µm'
>>> format_prefix(123687.2e-9, precision=12, unit="rad")
'123.6872 µrad'
y3t1
  • 195
  • 1
  • 8
0

@naitsirhc, thanks for your package. i have added a little function idea to use your package

import pandas as pd
import collections
Measure = collections.namedtuple('Measure', 'SLOT TEXT AVG HIGH LAST LOW SIGMA SWEEPS')
d=[Measure(SLOT='1', TEXT='CH1,AMPLITUDE', AVG='584.4782173493248E-3', HIGH='603.9744119119119E-3', LAST='594.125218968969E-3', LOW='561.1797735235235E-3', SIGMA='5.0385410346638E-3', SWEEPS='237996'), Measure(SLOT='2', TEXT='CH1,FREQUENCY', AVG='873.9706607717992E+6', HIGH='886.1564731675113E+6', LAST='873.9263571643770E+6', LOW='854.8833348698727E+6', SIGMA='4.382200567330E+6', SWEEPS='20705739'), Measure(SLOT='3', TEXT='CH4,PERIOD', AVG='1.1428492411436E-9', HIGH='1.1718844685593E-9', LAST='1.1432428766843E-9', LOW='1.1261916413092E-9', SIGMA='6.6735923746950E-12', SWEEPS='20680921'), Measure(SLOT='4', TEXT='CH4,FREQUENCY', AVG='875.0358282079155E+6', HIGH='887.9483414008331E+6', LAST='874.780693212961E+6', LOW='853.3264385945507E+6', SIGMA='5.0993358972092E+6', SWEEPS='20681008')]

from si_prefix import si_format

import si_prefix
si_prefix.SI_PREFIX_UNITS="yzafpnum kMGTPEZY"

def siSuffixNotation(element):
    try:
        ret=float(element)
        return str(si_format(ret)).replace(' ','')
    except ValueError:
        return element

df=pd.DataFrame(d)

df.T.applymap(siSuffixNotation) #<= nice pretty print output table
                    0              1           2              3
SLOT              1.0            2.0         3.0            4.0
TEXT    CH1,AMPLITUDE  CH1,FREQUENCY  CH4,PERIOD  CH4,FREQUENCY
AVG            584.5m         874.0M        1.1n         875.1M
HIGH           604.0m         885.6M        1.2n         887.9M
LAST           586.5m         874.2M        1.1n         874.9M
LOW            561.2m         854.9M        1.1n         854.1M
SIGMA            5.0m           4.4M        6.7p           5.1M
SWEEPS         191.5k          16.7M       16.6M          16.6M

Thanks to you, i can know have a pretty print table as i like it. (i don't like space between the number and the suffix, and i do not like the unicode type, i prefer u for micro) ++

douardo
  • 697
  • 6
  • 6
0

You can use Prefixed, which has a float type with additional formatting options.

You can create float-like numbers by including the prefix

>>> from prefixed import Float

>>> Float('2k')
Float(2000.0)

prefixed.Float is a subclass of float, so you can use it just like a float, but when you want to output, it supports additional format specifiers.

num = Float('2k')
>>> f'{num}'
'2000.0'
>>> f'{num:.2h}'
'2.00k'

Binary prefixes are also supported and some additional formatting options. See the docs for more info.

aviso
  • 2,371
  • 1
  • 14
  • 15