0

I would like to "round" (not exact a mathematical rounding) the elements of a numpy array in the following way:

Given a numpy NxN or NxM 2D array with digit between 0.00001 to 9.99999 like

 a=np.array([[1.232, 1.872,2.732,0.123],
             [0.0019, 0.025, 1.854, 0.00017],
             [1.457, 0.0021, 2.34 , 9.99],
             [1.527, 3.3, 0.012 , 0.005]]
    )

I would like basically to "round" this numpy array by selecting the first non-zero digit (irregardless of the digit that follows the first non-zero digit) of each element giving the output:

output =np.array([[1.0, 1.0, 2.0, 0.1],
                 [0.001, 0.02, 1.0, 0.0001],
                 [1.0, 0.002, 2 , 9.0],
                 [1, 3, 0.01 , 0.005]]
        )

thanks for any help!

norok2
  • 25,683
  • 4
  • 73
  • 99
DiMa
  • 63
  • 4
  • Use numpy.around – tstanisl Oct 23 '19 at 21:31
  • in numpy around does not work properly because the when I set the values numpy.around(a, decimals=0) the numbers 0.0019 , 0.025 , 0.00017 etc they become all 0 instead of 0.001, 0.02, 0.0001 – DiMa Oct 23 '19 at 21:38
  • Possible duplicate of [Rounding to significant figures in numpy](https://stackoverflow.com/questions/18915378/rounding-to-significant-figures-in-numpy) – norok2 Oct 23 '19 at 22:34

3 Answers3

1

You could use np.logspace and np.seachsorted to determine the order of magnitude of each element and then floor divide and multiply back

po10 = np.logspace(-10,10,21)
oom = po10[po10.searchsorted(a)-1]
a//oom*oom
# array([[1.e+00, 1.e+00, 2.e+00, 1.e-01],
#        [1.e-03, 2.e-02, 1.e+00, 1.e-04],
#        [1.e+00, 2.e-03, 2.e+00, 9.e+00],
#        [1.e+00, 3.e+00, 1.e-02, 5.e-03]])
Paul Panzer
  • 51,835
  • 3
  • 54
  • 99
  • Would this be faster / numerically more stable than just computing the logarithm? – norok2 Oct 23 '19 at 22:17
  • 1
    @norok2 Depends on problem size. The logspace has to be computed only once and since it has only a handful of entries the searchsorted is certainly faster than taking the log and power. You could even push it further and instead of the logspace 10^-10, 10^-9, 10^-8, use all possible outcomes ,i.e. xpo10 = 1 x 10^-10, 2 x 10^-10, 3 x 10^-10, ... 9 x 10^-10, 1 x 10^-9, 2 x 10^-9, etc. That is still a very manageable number of terms and we wouldn't have to do any arithmetic anymore just `xpo10[xpo10.searchsorted(a)-1]` – Paul Panzer Oct 23 '19 at 22:32
1

What you would want to do is to keep a fixed number of significant figures.

This functionality is not integrated into NumPy.

To get only the 1 significant figure, you could look into either @PaulPanzer or @darcamo answers (assuming that you only have positive values).

If you want something that works a specified number of significant figures, you could use something like:

def significant_figures(arr, num=1):
    # : compute the order of magnitude
    order = np.zeros_like(arr)  
    mask = arr != 0
    order[mask] = np.floor(np.log10(np.abs(arr[mask])))
    del mask  # free unused memory
    # : compute the corresponding precision
    prec = num - order - 1
    return np.round(arr * 10.0 ** prec) / 10.0 ** prec


print(significant_figures(a, 1))
# [[1.e+00 2.e+00 3.e+00 1.e-01]
#  [2.e-03 2.e-02 2.e+00 2.e-04]
#  [1.e+00 2.e-03 2.e+00 1.e+01]
#  [2.e+00 3.e+00 1.e-02 5.e-03]]

print(significant_figures(a, 2))
# [[1.2e+00 1.9e+00 2.7e+00 1.2e-01]
#  [1.9e-03 2.5e-02 1.9e+00 1.7e-04]
#  [1.5e+00 2.1e-03 2.3e+00 1.0e+01]
#  [1.5e+00 3.3e+00 1.2e-02 5.0e-03]]

EDIT

For truncated output use np.floor() instead of np.round() just before the return.

norok2
  • 25,683
  • 4
  • 73
  • 99
0

First get the powers of 10 for each number in the array with

powers = np.floor(np.log10(a))

In your example this gives us

array([[ 0.,  0.,  0., -1.],
       [-3., -2.,  0., -4.],
       [ 0., -3.,  0.,  0.],
       [ 0.,  0., -2., -3.]])

Now, if we divide the i-th element in the array by 10**power_i we essentially move each number non-zero element in the array to the first position. Now we can simple take the floor to remove the other non-zero digits and then multiply the result by 10**power_i to get back to the original scale.

The complete solution is then only the code below

powers = np.floor(np.log10(a))
10**powers * np.floor(a/10**powers)

What about numbers greater than or equal to 10?

For this you can simply take np.floor of the original value in the array. We can do this easily with a mask. You can modify the answer as below

powers = np.floor(np.log10(a))
result = 10**powers * np.floor(a/10**powers)

mask = a >= 10
result[mask] = np.floor(a[mask])

You can also use a mask to avoid computing the powers and logarithm for numbers that will just be replaced later.

darcamo
  • 3,294
  • 1
  • 16
  • 27
  • Thanks! works very nice and simple. Is there a way to kind of generalize this procedure with numbers bigger than 10 , let's say if it's 12.32 , 15.78 , 121.34 to get out 12 , 15 , 121? – DiMa Oct 23 '19 at 21:54
  • Note that here `a` may not have non-positve values. – norok2 Oct 23 '19 at 22:16
  • I have edited the answer with a section to make it work with numbers >= 10. – darcamo Oct 24 '19 at 02:51