Can I use eachrow, eachcol, etc. to change array's values?

Can I use eachrow, eachcol, etc. to change array’s values?

For example,

x = rand(5, 5)
for c in eachcol(x)
    for el in c
        if el < maximum(x)/2
            el = 0.0
        end
    end
end
display(x)

does nothing, and

x = rand(5, 5)
for c in eachcol(x)
    for el in c
        if el < maximum(x)
            el .= 0.0
        end
    end
end
display(x)

throws

ERROR: MethodError: no method matching copyto!(::Float64, ::Base.Broadcast.Broadcasted{…})

Closest candidates are:
  copyto!(::AbstractArray, ::Base.Broadcast.Broadcasted{<:Base.Broadcast.AbstractArrayStyle{0}})
   @ Base broadcast.jl:959
  copyto!(::AbstractArray, ::Base.Broadcast.Broadcasted)
   @ Base broadcast.jl:956
  copyto!(::AbstractArray, ::Any)
   @ Base abstractarray.jl:940

Stacktrace:
 [1] materialize!
   @ ./broadcast.jl:914 [inlined]
 [2] materialize!(dest::Float64, bc::Base.Broadcast.Broadcasted{…})
   @ Base.Broadcast ./broadcast.jl:911
 [3] top-level scope

Or what would be the idiomatic way to do this?

This wouldn’t work even if c were an ordinary array. If you do

c = [1,2,3,4]
el = c[2]

then el = 17 doesn’t change the original array, and el .= 17 is an error because el is not mutable. Please see the difference between assignment and mutation.

Whereas if you do:

for i in eachindex(c)
    if c[i] < maximum(x)
        c[i] = 0
    end
end

that will work, because an assignment c[i] = 0 (equivalent to setindex!(c, 0, i)) mutates the contents of the array c.

And this will work with eachcol too, because eachcol(x) returns a sequence of views of x.

7 Likes

This could work:

x = rand(5, 5)
for c in eachcol(x)
    c[c .< maximum(x) / 2] .= 0
end
display(x)

This uses a Bool array to index the column, which is very typical in python, less so in Julia but it works. Once you iterate through the individual values in the column, though, you’re down to plain bits types (numbers), which are passed by value, not by reference, hence the e variable doesn’t refer to the memory location in your original x matrix, overwriting its values doesn’t change x itself.

That being said, you don’t even need the eachcol here, as Julia is ace when it comes to matrices:

x = rand(5, 5)
x[x .< maximum(x) / 2] .= 0
display(x)
3 Likes

Note, by the way, that calling maximum(x) inside your innermost loop is horribly inefficient because computing the maximum involves traversing the whole array x, and you are repeating that for every loop iteration — you really want to compute maximum(x) once outside the loops.

Thanks for your replies. I thought I would be able to figure it out on a simple example, but I still cannot make it work, although

c = rand(4)
for i in eachindex(c)
    if c[i] < maximum(c) / 2
        c[i] = 0
    end
end

works fine.

Here is what I want to do eventually:
I have a matrix of type Any with the leftmost columns and topmost rows serving as headers. For each row, I want to nullify elements based on some simple conditions: for example, nullify every subsequent row element that is less that the current one multiplied by 2. Of course, I have to ignore the heading columns.

This is the simplest example, and I’m not sure how I can use boolean index masks here if I want to filter, say, by the name in the top column and values and something else.

Here is my attempt at filtering:

function filter!(table)
    for r in eachrow(table[2:end, :])
        for i in eachindex(r)
            subr = r[i+1:end]
            for j in eachindex(subr)
                if r[i] > 0 && subr[j] < r[i] * 2
                    println("It is less")
                    setindex!(subr, 0, j)
                else
                    println("Not less")
                end
            end
        end
    end
    return table
end

A = [[Inf "b" "c" "d"]; [-1 2 3 10]; [-2 3 5 10]]
TARGET = [[Inf "b" "c" "d"]; [-1 2 0 10]; [-2 3 0 10]]

# nothing changed
display(filter!(A))
display(A)

Is there limit in nesting of iterators? Or is slicing here should be done via some non-oblious macros to allow mutation?

I’d say that’s your problem right there, are you maybe looking for

?

2 Likes

There the problem is that slices in Julia create new arrays. Thus, here:

    for r in eachrow(table[2:end, :])

you are creating a new array when doing table[2:end, :], and thus then forward you won´t be modifying the original table.

Then, here,

            subr = r[i+1:end]

the same happens.

Use @view(table[2:end, :]) and @view(r[i+1:end]) and your function will mutate the original arrays. A shortcut to that is adding @views function... for the whole function.

1 Like

It seems to be a nice wrapper, thanks for the recommendation.

Thanks for the answers!

In my case, the solution is simply to ditch this machinery and do it the old-school way, ignoring the linter warnings

function filter!(table)
    for i in 2:size(table, 1)
        for j in 1:size(table, 2)
            current = table[i, j]
            for k in (j+1):size(table, 2)
                if current > 0 && table[i, k] < current * 2
                    table[i, k] = 0
                end
            end
        end
    end
    return table
end

Indeed. Those warnings are there so that people try to write code that works for arrays with indices that do not go from 1 to the length of the array, or the size in each dimension (OffsetArrays.jl in particular).

But if you know your arrays do not have such complications, you don’t need to worry.