Julia vs NumPy array comparison

I’m trying Julia for a personal project after working in Python for school. With NumPy
a = np.arange(0,10); np.array_equal(a,a.T) gives True as the result. In Julia, a = range(0, length=10); a == transpose(a) or a == a' give false, and a .== transpose(a) gives a matrix with true or 1 on the diagonal. Could someone explain what’s going on here? I’m unclear about the differences in the statements, and why the comparisons give different results. Thanks!

I don’t know about numpy, but in Julia a = range(0, length=10) is a one-dimensional column vector, its transpose is a row vector, and they can’t be equal because they have different shapes. With a .== a' you’re roughly checking whether they have equal corresponding elements, the dot is used for broadcasting

1 Like

ndarray.T or the equivalent numpy.transpose mean something different from transpose in Julia. NumPy “transposes” the axes of a multidimensional array and reverses the shape, so it would do nothing to a vector:

>>> a = np.arange(0, 10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a.T
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In Julia, a vector is conventionally treated as surrogate for a matrix with 1 column, which is a column vector in the linear algebra sense. transpose represents a matrix transpose, so it would turn a vector into a row vector:

julia> a = range(0, length=10)
0:9

julia> transpose(a)
1×10 transpose(::UnitRange{Int64}) with eltype Int64:
 0  1  2  3  4  5  6  7  8  9

Julia’s transpose doesn’t even work on 3- or higher dimensional arrays, and the erroring method suggests permutedims instead, thought that can still turn vectors into row vectors if the permutation isn’t specified.

2 Likes

1. NumPy: a 1D array has no row/column orientation

In NumPy:

import numpy as np

a = np.arange(0, 10)
a.shape          # (10,)
a.T.shape        # (10,)   <-- transpose is a no-op for 1D arrays
np.array_equal(a, a.T)     # True

A NumPy 1D array has shape (n,), not (n,1) or (1,n), so .T cannot swap axes (there’s only one axis). That’s why a and a.T are equal.

2. Julia: transpose/' gives a row vector object

In Julia, a Vector is 1D, but Julia’s transpose (and a', the adjoint) are defined with linear algebra semantics: they turn a column vector into a row vector (as a lazy wrapper type, not necessarily copying data). Therefore, the shapes/axes don’t match:

a = collect(range(0, length=10))  # Vector{Int}
a == transpose(a)   # false
a == a'             # false

a is a Vector with axes (1:10,), while transpose(a) behaves like a 1×10 row object with axes (1:1, 1:10). Since == for arrays requires matching axes and equal elements, this returns false.

3. Why a .== transpose(a) makes a matrix with a true diagonal

Dot operators broadcast elementwise. Here you’re broadcasting a length-10 vector against a 1×10 row vector, so Julia expands them to a 10×10 result.

4. == checks axes/shape and compares elements

Julia’s == compares values (and for arrays, also requires matching axes). Pointer/object identity is ===.

If you want something closer to NumPy’s np.array_equal(a, b) (notably treating NaN values in the same positions as equal), isequal(a, b) is typically the closest match:

isequal(a, b)  # matches NaN with NaN in the same positions

Going into more detail about (Julia’s) broadcasting, what’s happening here is that in this broadcast of Arrays with shapes (10,) and (1, 10) Julia is first adding implicit trailing singleton dimensions until the number of axes match ((10, 1) and (1, 10)). Then the singleton dimensions are implicitly filled to matching (10, 10) (i.e. in the broadcasting a behaves as if it is a Matrix b of shape (10, 10) with b[i, j] == b[i, 1] == a[i] for all j). Finally, the operation (==) is applied elementwise, resulting in a (10, 10) binary matrix.

Except for the slightly different semantics of np.transpose and Julia’s transpose, and accounting for the difference between row and column major ordering (so that a np.shape (10,) will be expanded using leading singleton dimensions to (1, 10)), NumPy works the same (though you need to know that == is broadcasting and hence distinct from np.array_equal)

>>> a[:, None] == a  # (10, 1) and (10,)
array([[ True, False, False, False, False, False, False, False, False,
        False],
       [False,  True, False, False, False, False, False, False, False,
        False],
       [False, False,  True, False, False, False, False, False, False,
        False],
       [False, False, False,  True, False, False, False, False, False,
        False],
       [False, False, False, False,  True, False, False, False, False,
        False],
       [False, False, False, False, False,  True, False, False, False,
        False],
       [False, False, False, False, False, False,  True, False, False,
        False],
       [False, False, False, False, False, False, False,  True, False,
        False],
       [False, False, False, False, False, False, False, False,  True,
        False],
       [False, False, False, False, False, False, False, False, False,
         True]])

(Also, welcome to the Julia community! :slight_smile: )