Everything you need to know about Indexing in Julia | by Emmett Boudreau | Towards Data Science

In the world of programming, and furthermore Data Science, it is very common to work with data structures which are collections of smaller elements. As one might expect, these collections need to be interacted with in some way from outside of the collection — to do this, programmers use indexing. Indexing is a vital programming topic, and the depths of it in a specific language is always great knowledge. A lot of times when it comes to familiarity and know-how in a programming language it comes down to knowing calls after using them for some time.
Luckily, in this article I will go over some methods that work very abstractly. This is to say that the type definition that carries the types inside of it is very vague on what kind of types can be passed through that function. In Julia, the mightiest error is the
MethodError
And we can run into these with indexes as well. There is a core way to work with indexes that is unique that Julia and is incredibly easy in my subjective view of my experience with it. Compared to other ecosystems, this method seems to fit in really well. Also if you would like to view a notebook with the code for this article to follow along to, you can get it out of this Github repository here:
github.com
Every type inside of Julia can be indexed, believe it or not. This is the case even for types that are not collections, and we will demonstrate this. However, let us first take a look at the simpler things, such as basic array indexing. We can index using an integer as a 1-dimensional reference point in the array. This is especially useful if the arrays are ordered.
array = [1, 2, 3, 4, 5]array[1]1
Now to be clear here, we are not getting one because we passed one through the index in this example, we are getting one because that is the first value inside of our array. If we were to change the one to a five for example:
array = [5, 2, 3, 4, 5]array[1]5
We can also do indexing via ranges:
array[1:5]5-element Vector{Int64}: 1 2 3 4 5
If this collection were to be a dictionary, we could also index by key (whatever type that might be.) If you would like to learn more about the various data-structures of Julia, you can read into or watch into it here:
towardsdatascience.com
For a comprehensive list of the types to be provided, we can actually question mark a method. The method that indexing calls is the getindex() method from base. Let us call the documentation browser on it:
?(getindex)
We can also set indexes. This will call the setindex!() method, which has an explanation point in it because it will mutate the type we send into it. Let us go ahead and import getindex() and setindex!() directly from Base so that we can extend them:
import Base: getindex, setindex!
Now simply using the typical Julia dispatch approach we can set up any struct to have any sort of index we want.
mutable struct NotACollection    x::Int64end
Our constructed type is not such a collection at all, instead it just has an integer inside of it. It is actually quite a useless constructor. Regardless, we are now going to dispatch it to where indexing anything will just return x. There is only one index, I do not know what you want me to say.
getindex(notcol::NotACollection, y::Int64) = notcol.xnotacol = NotACollection(5)
Now if we call the index of our type that is definitely not a collection, we get the number and no fuss about it:
notacol[1]5
If you were to do this without extending getindex() first, this code would return a MethodError. Now just for fun we will do the same to setindex!():
setindex!(notcol::NotACollection, y::Int64, val::Int64) = notcol.x = ynotacol[1] = 10println(notacol[x])
Now let us look at some useful methods that can be used for working with basic data-structures and mutating their indexes. I would consider these to be essential Julia knowledge, so I am really happy to share some of these awesome base methods with you. These are some of those tools that I know come in handy all the time.
The first method is size(). This method is equivalent to shape() in Python. This will give the dimensions of a given array or matrix. While we have not dove much into the multi-dimensional arrays, size() is going to come in handy a lot for working with arrays that are multi-dimensional. That being said, keeping this method and its usage handy can certainly apply to 1-dimensional and multi-dimensional applications.
nums = [54, 33, 22, 13]size(nums)(4,)
The return type is a two-element tuple, which we can index:
println(typeof(size(nums)))Tuple{Int64}println(size(nums)[1])4
The push!() method is one of the most useful methods in the Julia language. What is great about this method is that it is defined close to the top of the type hierarchy. This means that the types which can be passed into push!() are often very abstract. This abstraction means that a lot of different types can be provided. However, this does put in some nuances that one might need to be aware of. The great thing about Julia though is also that we can change the language.
The push!() method is used to move new elements to a structure. It works with dictionary, an array, and various other data-types — pretty much any data-type you can imagine. The great thing about push!() is you never know if it is going to work until you try, and if it does not work you can always write your own push!() to take care of it. Let’s try it out on our array:
push!(nums, 5)5-element Vector{Int64}: 54 33 22 13  5
I actually wrote a whole article that dives into the push!() method, so if you happen to be interested in reading about this method and all its various nuances, you may read into it here if you desire:
The append!() method is very similar to the push!() method. Like the push!() method, the append!() method adds an element to a given array. However a key difference is that append!() will avoid mutating orders as best as possible, push!() will not. The append!() method is also much more defined and specific for types it should be used with. Typically, it is used for arrays with simple data-types. Let us take a look at an example with our array:
append!(nums, 5)6-element Vector{Int64}: 54 33 22 13  5  5
Note that because our array is of type Vector{Int64}, we can only push Integers through this function. We could also push numbers that can be converted to integers, however in most cases we will need to cast the types before providing them.
append!(nums, 5.5)InexactError: Int64(5.5)
Casting that type yields that it does work:
append!(nums, 5.5)
However, if this structure is going to be containing multiple types, a better approach might be to just cast the entire structure to contain {Any}:
nums = Array{Any}(nums)append!(nums, 5.5)
The filter method in Julia is another one that comes in handy very frequently. This one can especially come to use when using statistics. Think Pandas DataFrame masks, which actually made it into this article of my favorite Pandas features:
towardsdatascience.com
However, filter!() is the equivalent in Julia. Also wow, over a year ago.
There are some slight gripes with me and filter!(), and it is not that I do not like the method — I just feel it is a lot more annoying to use than a lot of other interfaces out there. Julia tends to excel in a lot of things that it does, and make things super easy for a programmer to work with — but this is one instance where I thought picking this up was harder than picking it up in another language, that being Python. Luckily, the syntax for the alteration I wished to make was pretty easy to accomplish and boolean indexing is now available in a package I am currently developing, OddFrames.jl, which you can also view the Github page of here:
github.com
filter((x) -> x == 5, nums)
For this method, we use something called an Anonymous Function as its first arguments, for a more comprehensive look at this concept you may read an article diving into them here:
towardsdatascience.com
We will also touch more on them a bit later in this article, but for now we will simply observe our filtered values:
filter((x) -> x == 5, nums)2-element Vector{Any}: 5 5
Methods like collect are often hard to explain, but collect can be used to quickly take values out of some sort of generator or process. Essentially anything that is capable of returning an iterable. If we had the range
r = 5:10
And we wanted to get an array of five through ten, we would simply call collect on it for that result
collect(r)6-element Vector{Int64}:  5  6  7  8  9 10
The findall!() method is a method that I can very definitively say is incredibly useful. There are kind of a lot of calls for this, so it is a great one to use the ‘ole question mark on:
?(findall)
I actually have used this method a lot inside of OddFrames.jl. For the example code I am about to demonstrate, we are going to be looking at the index_iter.jl file for OddFrames.jl, which you can view by clicking this text.
function getindex(od::AbstractOddFrame, col::Symbol)        pos = findall(x->x==col, od.labels)[1]        return(od.columns[pos])endgetindex(od::AbstractOddFrame, col::String) = od[Symbol(col)]getindex(od::AbstractOddFrame, axis::Int64) = od.columns[axis]function getindex(od::AbstractOddFrame, mask::BitArray)        pos = findall(x->x==0, mask)        od.drop(pos)endgetindex(z::UnitRange) = [od.labels[i] for i in z]
This code is a few getindex() dispatch binds. You might remember getindex() from earlier. OddFrames.jl works with one-dimensional arrays inside of arrays that could be stacked on top of one another, but are not. The findall() method is used twice here. The first instance is when the index is called with a symbol:
od[:column]
In this example, the findall() method returns to us the index of any instance that is equal to such a thing. For some reason, a lot of Julia code I have seen written like this uses no spaces around the operators, and thus I have adopted it — although I do not think this is proper, so let me make this a bit easier to read:
pos = findall(x -> x == col, od.labels)[1]
All we are doing is trying to find a label that is equal to that value, we first use:
x -> x
This tells Julia we want to create an Anonymous Function. Now this x is a variable, and we have defined a function that will return a BitArray. A BitArray is just a type of array that holds boolean true/false values, usually as ones and zeros. Ones and zeros both only take up one bit, so thus this is called a BitArray. Maybe I could have explained that a lot easier by just saying it is a binary array. Anyway, our array is returned directly from this:
x == col
And with that turn, we then iterate through od.labels and see if a value matches. Since labels cannot be the same in an OddFrame, we always know that this will either be one or no value if it is not found on the OddFrame. Given that information, I call the first index at the end, this will just change the type of our value from being an Array{Int64, 1} into an Int64. Then we simply call that index on the column in our return:
function getindex(od::AbstractOddFrame, col::Symbol)        pos = findall(x->x==col, od.labels)[1]        return(od.columns[pos])end
Skipping a bit ahead, we see the same is used for the conditional mask:
function getindex(od::AbstractOddFrame, mask::BitArray)        pos = findall(x->x==0, mask)        od.drop(pos)end
The only difference is that this time findall() is used to collect the positions which need to be dropped, and then the next method on this list is used in the od.drop() method in order to get rid of that value.
For our notebook example, we will get every value equal to 5:
z = findall(x -> x == 5, nums)array = [nums[x] for x in z]
The last method I wanted to take a look at is the deleteat!() method. As touched on previously, we are going to look at the example of this in OddFrames.jl. We will also do another example in the notebook. The OddFrames example is located in the member_func.jl file in the OddFrames repository, once again click this text. The deleteat!() method is used to do just that, delete at a specific index.
I know it someone somewhere keeps reading that as delet eat, just know you are not alone. Maybe we are just hungry.
The method is straightforward to use, just provide a collection and an index:
function _drop(row::Int64, columns::Array)        [deleteat!(col, row) for col in columns]end
This is done with an iterative loop in this instance and mutates the provided vector. Although there is a return from this, it is not meant to be used — perhaps I should return after to make sure this is understood. For our notebook example, we will delete our entire array:
while length(nums) > 1    deleteat!(nums, 1)endprintln(nums)Any[5.5]
Rest in peace.
Of course, there are so many methods for working with these sorts of types, so there is no way I could possibly describe them all, but these are some that I have found incredibly value aside from the regular things like length() and sum(), etc. These are all Julia-specific other than size(), so I think a newer Julia user might find these valuable to reference. There are even horizontal and vertical concatenations, various matrix operations and more and more.
The Julia language has a pretty sweet way of handling iteration. Every language has an approach that I can appreciate, but I do really like this compared to using the Python class default methods or doing these same tasks in R. I find myself often missing these features whenever I use other languages, which tells me they are likely great features.
Thank you for reading, and I am happy that I can share this knowledge with you. These are some of those things that Julia users might not get told about when they first begin, so it is really awesome to bring these methods before you and offer a bit more information on working with iterable items in Julia.