Different slices give different inequalities for same elements

Question:

import numpy as np

a = np.array([.4], dtype='float32')
b = np.array([.4, .6])

print(a > b)
print(a > b[0], a > b[1])
print(a[0] > b[0], a[0] > b[1])
[ True False]
[False] [False]
True False

What’s the deal? Yes, b.dtype == 'float64', but so are its slices b[0] & b[1], and a remains 'float32'.

Note: I’m asking why this occurs, not how to circumvent it, which I know (e.g. cast both to 'float64').

Asked By: OverLordGoldDragon

||

Answers:

As I’ve noted in another answer, type casting in numpy is pretty complicated, and this is the root cause of the behaviour you are seeing. The documents linked in that answer make it clear that scalars(/0d arrays) and 1d arrays differ in type conversions, since the latter aren’t considered value by value.

The first half of the problem you already know: the problem is that type conversion happens differently for your two cases:

>>> (a + b).dtype
dtype('float64')

>>> (a + b[0]).dtype
dtype('float32')

>>> (a[0] + b[0]).dtype
dtype('float64')

There’s also a helper called numpy.result_type() that can tell you the same information without having to perform the binary operation:

>>> np.result_type(a, b)
dtype('float64')
>>> np.result_type(a, b[0])
dtype('float32')
>>> np.result_type(a[0], b[0])
dtype('float64')

I believe we can understand what’s happening in your example if we consider the type conversion tables:

>>> from numpy.testing import print_coercion_tables
can cast
[...]

In these tables, ValueError is '!', OverflowError is '@', TypeError is '#'

scalar + scalar
+ ? b h i l q p B H I L Q P e f d g F D G S U V O M m 
? ? b h i l q l B H I L Q L e f d g F D G # # # O ! m 
b b b h i l q l h i l d d d e f d g F D G # # # O ! m 
h h h h i l q l h i l d d d f f d g F D G # # # O ! m 
i i i i i l q l i i l d d d d d d g D D G # # # O ! m 
l l l l l l q l l l l d d d d d d g D D G # # # O ! m 
q q q q q q q q q q q d d d d d d g D D G # # # O ! m 
p l l l l l q l l l l d d d d d d g D D G # # # O ! m 
B B h h i l q l B H I L Q L e f d g F D G # # # O ! m 
H H i i i l q l H H I L Q L f f d g F D G # # # O ! m 
I I l l l l q l I I I L Q L d d d g D D G # # # O ! m 
L L d d d d d d L L L L Q L d d d g D D G # # # O ! m 
Q Q d d d d d d Q Q Q Q Q Q d d d g D D G # # # O ! m 
P L d d d d d d L L L L Q L d d d g D D G # # # O ! m 
e e e f d d d d e f d d d d e f d g F D G # # # O ! # 
f f f f d d d d f f d d d d f f d g F D G # # # O ! # 
d d d d d d d d d d d d d d d d d g D D G # # # O ! # 
g g g g g g g g g g g g g g g g g g G G G # # # O ! # 
F F F F D D D D F F D D D D F F D G F D G # # # O ! # 
D D D D D D D D D D D D D D D D D G D D G # # # O ! # 
G G G G G G G G G G G G G G G G G G G G G # # # O ! # 
S # # # # # # # # # # # # # # # # # # # # # # # O ! # 
U # # # # # # # # # # # # # # # # # # # # # # # O ! # 
V # # # # # # # # # # # # # # # # # # # # # # # O ! # 
O O O O O O O O O O O O O O O O O O O O O O O O O ! # 
M ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! 
m m m m m m m m m m m m m m # # # # # # # # # # # ! m 

scalar + neg scalar
[...]

array + scalar
+ ? b h i l q p B H I L Q P e f d g F D G S U V O M m 
? ? b h i l q l B H I L Q L e f d g F D G # # # O ! m 
b b b b b b b b b b b b b b e f d g F D G # # # O ! m 
h h h h h h h h h h h h h h f f d g F D G # # # O ! m 
i i i i i i i i i i i i i i d d d g D D G # # # O ! m 
l l l l l l l l l l l l l l d d d g D D G # # # O ! m 
q q q q q q q q q q q q q q d d d g D D G # # # O ! m 
p l l l l l l l l l l l l l d d d g D D G # # # O ! m 
B B B B B B B B B B B B B B e f d g F D G # # # O ! m 
H H H H H H H H H H H H H H f f d g F D G # # # O ! m 
I I I I I I I I I I I I I I d d d g D D G # # # O ! m 
L L L L L L L L L L L L L L d d d g D D G # # # O ! m 
Q Q Q Q Q Q Q Q Q Q Q Q Q Q d d d g D D G # # # O ! m 
P L L L L L L L L L L L L L d d d g D D G # # # O ! m 
e e e e e e e e e e e e e e e e e e F F F # # # O ! # 
f f f f f f f f f f f f f f f f f f F F F # # # O ! # 
d d d d d d d d d d d d d d d d d d D D D # # # O ! # 
g g g g g g g g g g g g g g g g g g G G G # # # O ! # 
F F F F F F F F F F F F F F F F F F F F F # # # O ! # 
D D D D D D D D D D D D D D D D D D D D D # # # O ! # 
G G G G G G G G G G G G G G G G G G G G G # # # O ! # 
S # # # # # # # # # # # # # # # # # # # # # # # O ! # 
U # # # # # # # # # # # # # # # # # # # # # # # O ! # 
V # # # # # # # # # # # # # # # # # # # # # # # O ! # 
O O O O O O O O O O O O O O O O O O O O O O O O O ! # 
M ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! 
m m m m m m m m m m m m m m # # # # # # # # # # # ! m

[...]

The above is part of the current promotion tables for value-based promotion. It denotes how differing types contribute to a result type when pairing two numpy objects of a given kind (see the first column and first row for the specific types). The types are to be understood according to the single-character dtype specifications (below "One-character strings"), in particular np.dtype('f') corresponds to np.float32 (f for C-style float) and np.dtype('d') (d for C-style double) to np.float64 (see also np.typename('f') and the same for 'd').

I have noted two items in boldface in the above tables:

scalar f + scalar d –> d
array f + scalar d –> f

Now let’s look at your cases. The premise is that you have an 'f' array a and a 'd' array b. The fact that a only has a single element doesn’t matter: it’s a 1d array with length 1 rather than a 0d array.

  1. When you do a > b you are comparing two arrays, this is not denoted in the above tables. I’m not sure what the behaviour is here; my guess is that a gets broadcast to b‘s shape and then its type is cast to 'd'. The reason I think this is that np.can_cast(a, np.float64) is True and np.can_cast(b, np.float32) is False. But this is just a guess, a lot of this machinery in numpy is not intuitive to me.

  2. When you do a > b[0] you are comparing a 'f' array to a 'd' scalar, so according to the above you get a 'f' array. That’s what (a + b[0]).dtype told us. (When you use a > b[0] you don’t see the conversion step, because the result is always a bool.)

  3. When you do a[0] > b[0] you are comparing a 'f' scalar to a 'd' scalar, so according to the above you get a 'd' scalar. That’s what (a[0] + b[0]).dtype told us.

So I believe this is all consistent with the quirks of type conversion in numpy. While it might seem like an unfortunate corner case with the value of 0.4 in double and single precision, this feature goes deeper and the problem serves as a big red warning that you should be very careful when mixing different dtypes.

The safest course of action is to convert your types yourself in order to control what happens in your code. Especially since there’s discussion about reconsidering some aspects of type promotion.


As a side note (for now), there’s a work-in-progress NEP 50 created in May 2021 that explains how confusing type promotion can be when scalars are involved, and plans to simplify some of the rules eventually. Since this also involves breaking changes, its implementation in NumPy proper won’t happen overnight.

Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.