Enjoying Julia For Data Science? Please share us with a friend and follow us on Twitter at @JuliaForDataSci.
Why Animations are Great
The ability to communicate results is an under-appreciated skill in data science. An important analysis can be unheard or misunderstood if it's not presented well. Let's borrow the model for data science projects proposed by R for Data Science, in which Communicate is the final step.
Animations tell a story that static images are unable to tell by adding an extra dimension (often time). They are also more engaging to an audience (here is one of many social marketing blogs on the topic of engagement from video vs. static images). Getting your audience to pay attention is a part of communicating your results, so animations are a great tool.
Animations with Plots.jl
Plots.jl is a Julia package that provides a unified syntax for multiple plotting backends. It also provides some super simple and powerful utilities for creating animations in the gif format. There are several ways to create animations in Plots, with varying levels of complexity. We recommend using Pluto (see our Pluto introduction here) to make Plots animations because they'll appear in the notebook.
The simplest way is the @gif macro.
Place @gif in front of a for loop that generates a plot in each iteration. Each plot will be saved as a single frame in the animation.
using Plots
@gif for i in 1:50
plot(sin, 0, i * 2pi / 10)
end
You can add "flags" such as every n to only save a frame every n images. or when <condition> to only save certain frames.
@gif for i in 1:50
plot(sin, 0, i * 2pi / 10)
end when i > 30
To control frames-per-second, use @animate.
Works just like @gif, but creates a Plots.Animation rather than a gif directly.
anim = @animate for i in 1:50
Random.seed!(123)
scatter(cumsum(randn(i)), ms=i, lab="", alpha = 1 - i/50,
xlim=(0,50), ylim=(-5, 7))
end
You can then use the gif function on your Animation.
gif(anim, fps=50)
For the most control, use Plots.Animation directly.
Save each frame explicitly with frame.
a = Animation()
for i in 1:10
plt = bar(1:i, ylim=(0,10), xlim=(0,10), lab="")
frame(a, plt)
end
gif(a)
🚀 That's It!
You now know how to make some cool animations with Julia and Plots.jl.
Enjoying Julia For Data Science? Please share us with a friend and follow us on Twitter at @JuliaForDataSci.
Before I start let me comment that exactly one year ago this blog has been
started. I hope to keep posting weekly updates on the Julia language, and
especially its ecosystem for data science, so:
Now let us go back to business.
The 1.1 release of the DataFrames.jl package introduced a small fix of how
the subset function works. Today I will discuss its design and compare it
to the filter function.
In this post I am using Julia 1.6.1 and DataFrames.jl 1.1.0.
The design of filter
The filter function is defined in Julia Base. Therefore in DataFrames.jl we
add methods to it. Let us start with the contract for filter(f, a) then:
Return a copy of collection a, removing elements for which f is false.
The function f is passed one argument.
How do we translate this into DataFrames.jl realm? We have to cases.
If a is an AbstractDataFrame then we treat it as a collection of rows.
Therefore f will get one row of data and we expect it to return a Bool value.
As a result of the operation we produce a DataFrame (unless view keyword
argument is true in which case we return a SubDataFrame).
As you can see the style is that you pass a Pair or column name and a
predicate function (i.e. a function that produces Bool). This has two
benefits. Firstly, the operation is type stable (thus faster). Secondly, in the row -> row.a != 2 we define a new anonymous function with each call of filter, which causes compilation (unless the operation is wrapped in a
function or we predefine the predicate function).
The second case is when a is a GroupedDataFrame. In this case f will get
one group and should return a Bool value again. The result will be a GroupedDataFrame with groups appropriately removed:
julia> gdf = groupby(df, :a)
GroupedDataFrame with 3 groups based on key: a
First Group (1 row): a = 1
Row │ a
│ Int64
─────┼───────
1 │ 1
⋮
Last Group (1 row): a = 3
Row │ a
│ Int64
─────┼───────
1 │ 3
julia> filter(sdf -> sdf.a != [2], gdf)
GroupedDataFrame with 2 groups based on key: a
First Group (1 row): a = 1
Row │ a
│ Int64
─────┼───────
1 │ 1
⋮
Last Group (1 row): a = 3
Row │ a
│ Int64
─────┼───────
1 │ 3
A Pair version is also supported:
julia> filter(:a => !=([2]), gdf)
GroupedDataFrame with 2 groups based on key: a
First Group (1 row): a = 1
Row │ a
│ Int64
─────┼───────
1 │ 1
⋮
Last Group (1 row): a = 3
Row │ a
│ Int64
─────┼───────
1 │ 3
A crucial thing to note is that this time the predicate gets a data frame (or
its column/columns).
In summary — the filter function (apart from the view keyword argument and
a special Pair syntax that improves the performance) works exactly like
the Julia Base contract requires.
Before we move forward you might notice that the Pair syntax for the AbstractDataFrame case is different than the same syntax for select, transform, and combine functions, where always a whole column is passed.
Indeed there is a small inconsistency. It was left for user convenience
and consistency with Julia Base.
On the other hand subset is fully consistent with the rest of DataFrames.jl
ecosystem, so let us move to it now.
The design of subset
The subset function is designed for filtering of rows in a way consistent
with the select, transform, and combine functions. The contract for
the subset(df, args...) function is:
Return a copy of data frame df containing only rows for which all values
produced by transformation(s) args for a given row are true.
If instead of a df data frame you pass a GroupedDataFrame the rules are
the same, but the difference is that they apply to the parent of the GroupedDataFrame. So this leads us to a list of differences from filter, as
in subset:
the AbstactDataFrame/GroupedDataFrame argument goes first;
you are allowed do pass multiple conditions on which you want to perform row selection;
always works on whole columns;
always filters rows;
the transformation is expected to return a vector (not a scalar Bool — remember
we are filtering rows so the length of the vector must match the number of rows);
by default always produces a data frame.
The additional differences follow the available keyword arguments:
all transformations must produce vectors containing true or false; however,
optionally missing is allowed if skipmissing=true (this option is not available in filter);
for GroupedDataFrame case if ungroup=false the resulting data frame is
re-grouped based on the same grouping columns as the source GroupedDataFrame
(but by default a data frame is returned).
The view keyword argument works like in filter and allows you to produce
a SubDataFrame instead of a DataFrame.
Here you can see that we had to wrap predicates in ByRow to make sure
that a vector of Bool is produce by the filtering conditions. Otherwise
you would get an error:
julia> subset(df2, :a => ==(1))
ERROR: ArgumentError: functions passed to `subset` must return an AbstractVector.
(By the way: this is a thing that was changed in DataFrames.jl 1.1 release;
previously unintentionally returning scalar Bool was allowed which was error
prone, as the comparison was made against a whole vector — not its elements.)
The second key thing to remember is that subset filters rows always,
also in GroupedDataFrame case:
julia> gdf2 = groupby(df2, :a)
GroupedDataFrame with 3 groups based on key: a
First Group (2 rows): a = 1
Row │ a b
│ Int64 Int64
─────┼──────────────
1 │ 1 1
2 │ 1 4
⋮
Last Group (2 rows): a = 3
Row │ a b
│ Int64 Int64
─────┼──────────────
1 │ 3 3
2 │ 3 6
julia> subset(gdf2, :b => (x -> x .== maximum(x)))
3×2 DataFrame
Row │ a b
│ Int64 Int64
─────┼──────────────
1 │ 1 4
2 │ 2 5
3 │ 3 6
This is often very useful if we want to filter rows by some within-group condition,
like in the example above.
Finally, let me show the skipmissing keyword argument at work:
julia> df3 = DataFrame(a=[1, missing, 3, 4])
4×1 DataFrame
Row │ a
│ Int64?
─────┼─────────
1 │ 1
2 │ missing
3 │ 3
4 │ 4
julia> subset(df3, :a => ByRow(isodd))
ERROR: ArgumentError: missing was returned in condition number 1 but only true or false are allowed; pass skipmissing=true to skip missing values
julia> subset(df3, :a => ByRow(isodd), skipmissing=true)
2×1 DataFrame
Row │ a
│ Int64?
─────┼────────
1 │ 1
2 │ 3
Conclusions
In summary both filter and subset are useful, but in
different contexts. The basic rules are:
if you have multiple conditions to apply use subset;
if you want to easily handle missing values use subset;
if you have a single predicate that takes a single row (or a scalar)
and returns Bool and want to filter a data frame use filter
(this saves you typing ByRow in subset);
if you have a single predicate that returns Bool and want to filter
whole groups of a GroupedDataFrame (as opposed to rows) use filter.
The things are unfortunately a bit complex, but we provide them for user
convenience as both filter and subset are useful in different contexts.
Before I finish let me highlight that there are also in-place filter! and subset! variants of these functions.
I use Windows for gaming. It's been a long time since I've last done any serious development work on my Windows machine and yet I still spent a good chunk of money on building out a beefy machine for my efforts to learn machine learning. It has taken me a few months to finally sit down and get this machine ready for anything other than gaming.
With the recent announcement of GUI support for WSLg I got really excited to try out WSL and see how good the GPU support actually is, but that's not the main reason. I've been shying away from developing on Windows because I'm used to a *NIX environment. WSL gives you that, but up until recently you wouldn't have been able to interact with any GPU – and this all changed with this announcement!
You can watch the video below to see what's coming for WSL2.
The first half of this article will show you how to get everything set up and in the second half we'll set up CUDA.jl in Julia. Since the latter part is about CUDA I'll assume that you have a compatible nVidia GPU.
Installation
Here's a summary of what we need to go through to prepare our environment:
Update Windows 10 to latest release on the dev channel
Install nVidia CUDA drivers
Install Ubuntu 20.04 in WSL2
Install Linux CUDA packages
🎉
Windows 10 Insider Preview
At the time of writing all of the features are only available through the Windows Insider Program. The Windows Insider Program allows you to receive new Windows features before the hit the main update line. The program is split into three channels: Dev, Beta and Release Preview.
To receive the update with WSLg and GPU support we will need to switch to the dev channel.
Note: The dev channel comes with some rough edges and potential for system instability. Be mindful of this when you switch and make sure you have backups!
After installing all downloaded updates you should end up with OS Build 21364 or higher. You can check your OS Build by running winver in PowerShell/cmd.
Run winver using the Windows Run command
With this all set we can hop on to install the latest WSL2 compatible CUDA drivers.
CUDA drivers
NVIDIA are providing special CUDA drivers for Windows 10 WSL. The link below will take you to the download page. It's required to sign up for the NVIDIA Developer Program, which is free.
Follow the setup wizard (I chose the express installation which keeps existing settings in place).
Note: There's also a documentation page provided by NVIDIA around setting up your GPU for WSL. I found the docs to be outdated and not working on my machine.
Installing Ubuntu 20.04 LTS with WSL2
Before we can proceed with installing Ubuntu I advise to update the WSL kernel by running:
wsl --update
In case you're like me and don't enjoy the default terminal Windows comes with I suggest you install Windows Terminal from the Microsoft Store. This terminal comes is a lot more pleasant to use than either cmd or the PowerShell terminal ever were.
It's also a good idea to set WSL to default to version 2:
wsl --set-default-version 2
Finally let's install Ubuntu with:
wsl --install --distribution Ubuntu-20.04
In case you were wondering what other distributions are available you can simply run: wsl --list --online.
Ubuntu 20.04 LTS
With Ubuntu installed we have a couple of final steps. First we will add an upstream repo to apt for getting the latest CUDA builds directly from NVIDIA:
You may pick a different target directory for your installation of Julia or use a version manager like asdf-vm.
Install Cuda.jl
At this point simply run julia in your terminal and you should be dropped into the Julia REPL. I assume you've worked with Julia before and know how to operate it's package manager.
Hit ] to enter pkg mode and install CUDA with:
activate --temp
add CUDA
From here hit backspace and import CUDA. CUDA.jl provides a useful function called functional which will confirm that we've done everything right (well, that's the hope at least, right?).
using CUDA
CUDA.functional()
Ensuring Julia CUDA.jl is functional inside WSL2
You can additionally run CUDA.versioninfo() to get a more detailed breakdown of the supported features on your GPU.
At this point you should have a working installation with WSL2, Ubuntu 20.04, Julia and CUDA.jl. In case you're new to CUDA.jl I suggest you follow the excellent introduction to GPU programming by JuliaGPU or jump in at the deep end with FluxML's GPU support.
If you like articles like this one, please consider subscribing to my free newsletter where at least once a week I send out my latest work covering Julia, Python, Machine Learning and other tech.