By: Shuhei Kadowaki
Re-posted from: https://aviatesk.github.io/posts/effects-analysis/index.html
Quick intro to the new effect analysis of Julia compiler
By: Shuhei Kadowaki
Re-posted from: https://aviatesk.github.io/posts/effects-analysis/index.html
Quick intro to the new effect analysis of Julia compiler
Re-posted from: https://bkamins.github.io/julialang/2022/07/22/regex.html
During JuliaCon 2022 I gave a tutorial on DataFrames.jl.
You can find its recording on YouTube and all source code on GitHub.
This post is a follow up to one of the questions that I got during
the workshop. The topic of the discussion was applying the same function to
many columns of a data frame. Since the question is quite technical I will
first give you a brief introduction to the topic and next dive deep into the
issue.
I hope that this will be a useful material even for people that do not use
DataFrames.jl as we will explore the consequences of the
avoid type piracy rule that the Julia Manual recommends.
The post was written under Julia 1.7.2, DataFrames.jl 1.3.4.
Let us create a sample data frame first:
julia> using DataFrames
julia> df = DataFrame(a1=1:2, b1=3:4, a2=5:6)
2×3 DataFrame
Row │ a1 b1 a2
│ Int64 Int64 Int64
─────┼─────────────────────
1 │ 1 3 5
2 │ 2 4 6
Next I define a simple function that allows us to inspect what arguments
it received:
julia> inspect(x...) = Ref(x)
inspect (generic function with 1 method)
In this function I wrap a tuple of arguments in Ref as in DataFrames.jl
Ref protects the wrapped value against being expanded.
Let us check this function in action on some simple examples of combine
transformation (if you do not know the operation specification syntax please
check my tutorial on DataFrames.jl I have linked above for an
introduction):
julia> combine(df,
:a1 => inspect,
r"a" => inspect,
Cols(endswith("1")) => inspect)
1×3 DataFrame
Row │ a1_inspect a1_a2_inspect a1_b1_inspect
│ Tuple… Tuple… Tuple…
─────┼────────────────────────────────────────────────
1 │ ([1, 2],) ([1, 2], [5, 6]) ([1, 2], [3, 4])
julia> combine(df,
AsTable(:a1) => inspect,
AsTable(r"a") => inspect,
AsTable(Cols(endswith("1"))) => inspect)
1×3 DataFrame
Row │ a1_inspect a1_a2_inspect a1_b1_inspect
│ Tuple… Tuple… Tuple…
────┼─────────────────────────────────────────────────────────────────────
1 │ ((a1=[1, 2],),) ((a1=[1, 2], a2=[5, 6]),) ((a1=[1, 2], b1=[3, 4]),)
What we can see in these examples is the following:
r"a" picks all columns whose names match this regular expression"a");Cols(endswith("1")) picks all columns whose names meet the endswith("1")"1");AsTable then they get passed a singleNamedTuple.It is crucially important to understand at this point that r"a" and
Cols(endswith("1")) column selectors do not get resolved before being passed
to combine:
julia> r"a" => inspect
r"a" => inspect
julia> Cols(endswith("1")) => inspect
Cols{Tuple{Base.Fix2{typeof(endswith), String}}}((Base.Fix2{typeof(endswith), String}(endswith, "1"),)) => inspect
What I mean by resolved is that the expression will get its meaning (i.e. will
determine which columns it actually selects, inside the combine function in
the context of the data frame that is passed as a first argument to combine).
Having seen these basic examples, let us check how one can apply a given
function to multiple columns individually. You can do it e.g. like this:
julia> combine(df, :a1 => inspect, :a2 => inspect)
1×2 DataFrame
Row │ a1_inspect a2_inspect
│ Tuple… Tuple…
─────┼────────────────────────
1 │ ([1, 2],) ([5, 6],)
but there is an easier way. You can do it like this:
julia> combine(df, [:a1, :a2] .=> inspect)
1×2 DataFrame
Row │ a1_inspect a2_inspect
│ Tuple… Tuple…
─────┼────────────────────────
1 │ ([1, 2],) ([5, 6],)
The point is that combine (and other functions in DataFrames.jl) accept
vectors and matrices of operation specification syntax expressions and above
I create such a vector using broadcasting with .=>.
Let us check:
julia> [:a1, :a2] .=> inspect
2-element Vector{Pair{Symbol, typeof(inspect)}}:
:a1 => inspect
:a2 => inspect
julia> combine(df, [:a1 => inspect, :a2 => inspect])
1×2 DataFrame
Row │ a1_inspect a2_inspect
│ Tuple… Tuple…
─────┼────────────────────────
1 │ ([1, 2],) ([5, 6],)
Having the information I have shared above we are now ready to face the pirates.
We have seen above that r"a" and Cols(endswith("1")) do not get resolved
before they get passed to combine. Therefore how can we apply the inspect
function to all columns selected by them?
The basic approach is to use the names function like this:
julia> names(df, r"a")
2-element Vector{String}:
"a1"
"a2"
julia> names(df, r"a") .=> inspect
2-element Vector{Pair{String, typeof(inspect)}}:
"a1" => inspect
"a2" => inspect
julia> combine(df, names(df, r"a") .=> inspect)
1×2 DataFrame
Row │ a1_inspect a2_inspect
│ Tuple… Tuple…
─────┼────────────────────────
1 │ ([1, 2],) ([5, 6],)
This method works, but it is a bit heavy-handed, as it requires us to use
the names function that needs the df as its first argument. This duplication
of information is not optimal.
Therefore we are tempted to skip the names(df, r"a") and just write
r"a" .=> inspect. Let us see what it gives us:
julia> combine(df, r"a" .=> inspect)
1×1 DataFrame
Row │ a1_a2_inspect
│ Tuple…
─────┼──────────────────
1 │ ([1, 2], [5, 6])
julia> r"a" .=> inspect
r"a" => inspect
Unfortunately this is not what we expected. The reason is that, as you can see,
r"a" is treated by broadcasting as a scalar. Here we need to note that
the r"a" .=> inspect is resolved before the value of this expression is
passed to combine, so evaluation of this expression cannot be made by Julia
in the context of the df data frame.
Let us check what happens if we use the same approach with
Cols(endswith("1")) .=> inspect:
julia> combine(df, Cols(endswith("1")) .=> inspect)
1×2 DataFrame
Row │ a1_inspect b1_inspect
│ Tuple… Tuple…
─────┼────────────────────────
1 │ ([1, 2],) ([3, 4],)
This time we got what we wanted. It seems that this expression had to be
resolved only after it got passed to combine. Let us inspect it:
julia> Cols(endswith("1")) .=> inspect
DataAPI.BroadcastedSelector{Cols{Tuple{Base.Fix2{typeof(endswith), String}}}}(Cols{Tuple{Base.Fix2{typeof(endswith), String}}}((Base.Fix2{typeof(endswith), String}(endswith, "1"),))) => inspect
We see some strange DataAPI.BroadcastedSelector type. Its role is exactly to
delay the final resolution of broadcasted operation only after the expression
is processed in combine. When combine sees a value of type
DataAPI.BroadcastedSelector it does its post-processing in the context
of the df data frame to give us the desired result.
So how does this relate to type piracy? The answer is:
Cols(endswith("1")) .=> inspectCols selectorr"a", because the Regex type is defined inr"a" is handled by broadcasting becauseSo what do we learn from the today’s post?
Cols, Not, Between, and All, have;If you want to learn what exactly happens when you pass
Cols(endswith("1")) .=> inspect to combine
you can check it here and here.
Re-posted from: https://bkamins.github.io/julialang/2022/07/22/juliacon2022.html
During JuliaCon 2022 I will run a
tutorial on DataFrames.jl.
In the tutorial I will focus on ways you can write transformation operations
using the select/transform/combine functions and the operation
specification syntax.
In this post I want to give you a preview of the topics I am going to cover
in my tutorial.
The post was written under Julia 1.7.2, DataFrames.jl 1.3.4,
and DataFramesMeta.jl 0.12.
If you are new to DataFrames.jl then you probably wonder what
operation specification syntax is. Fortunately it is quite easy.
If you want to transform data using one of the select/transform/combine
functions you can specify the transformation you want to perform using the
following general syntax:
[source columns] => [function] => [output columns]
For example if you write :a => mean => :b you will get a mean of column :a
and store it in output data frame in column :b (visually: input column :a is
passed to the mean function whose output is passed to output column :b).
Additionally, if you prefer assignment style of specifying operations you can
use DataFramesMeta.jl package that allows you to write the same as just
:b = mean(:a). To use DataFramesMeta.jl you need to prefix the appropriate
function name with @ (to turn it into a macro).
Let me show you a minimal working example of such a transformation.
We compute mean of column :val by groups defined by the :id column:
julia> using DataFramesMeta
julia> using Statistics
julia> df = DataFrame(id=[1, 2, 1, 2, 1, 2], val=1:6)
6×2 DataFrame
Row │ id val
│ Int64 Int64
─────┼──────────────
1 │ 1 1
2 │ 2 2
3 │ 1 3
4 │ 2 4
5 │ 1 5
6 │ 2 6
julia> combine(groupby(df, :id), :val => mean => :mean_val)
2×2 DataFrame
Row │ id mean_val
│ Int64 Float64
─────┼─────────────────
1 │ 1 3.0
2 │ 2 4.0
julia> @combine(groupby(df, :id), :mean_val = mean(:val))
2×2 DataFrame
Row │ id mean_val
│ Int64 Float64
─────┼─────────────────
1 │ 1 3.0
2 │ 2 4.0
This is a simple example of what operation specification syntax
can do. In this post let me give you a more complex example (I explain
all the details of how it works in my upcoming tutorial).
In this StackOverflow question the user wanted to analyze iris data
set and get 25% and 75% quantiles of the Sepal.Length column.
The R code that the StackOverflow user provided was:
> library(dplyr)
> iris %>%
+ group_by(Species) %>%
+ summarise(
+ quantile(Sepal.Length, c(.25, .75)) %>%
+ matrix(nrow = 1) %>%
+ as.data.frame() %>%
+ setNames(paste0("Sepal.Length", c(.25, .75)))
+ )
# A tibble: 3 x 3
Species Sepal.Length0.25 Sepal.Length0.75
<fct> <dbl> <dbl>
1 setosa 4.8 5.2
2 versicolor 5.6 6.3
3 virginica 6.22 6.9
The question was how to achieve the same with DataFrames.jl and
DataFramesMeta.jl?
Here is a solution. First we need to load the iris dataset (in the code I take
advantage of the fact that this dataset is bundled into DataFrames.jl
installation folders):
julia> using CSV
julia> iris = CSV.read(joinpath(dirname(pathof(DataFrames)),
"..", "docs", "src", "assets", "iris.csv"),
DataFrame)
150×5 DataFrame
Row │ SepalLength SepalWidth PetalLength PetalWidth Species
│ Float64 Float64 Float64 Float64 String15
─────┼──────────────────────────────────────────────────────────────────
1 │ 5.1 3.5 1.4 0.2 Iris-setosa
2 │ 4.9 3.0 1.4 0.2 Iris-setosa
⋮ │ ⋮ ⋮ ⋮ ⋮ ⋮
149 │ 6.2 3.4 5.4 2.3 Iris-virginica
150 │ 5.9 3.0 5.1 1.8 Iris-virginica
146 rows omitted
Now we are ready to use the combine function and the operation specification
syntax:
julia> combine(groupby(iris, :Species),
:SepalLength =>
(x -> [quantile(x, [0.25, 0.75])]) =>
string.("SepalLength", [0.25, 0.75]))
3×3 DataFrame
Row │ Species SepalLength0.25 SepalLength0.75
│ String15 Float64 Float64
─────┼───────────────────────────────────────────────────
1 │ Iris-setosa 4.8 5.2
2 │ Iris-versicolor 5.6 6.3
3 │ Iris-virginica 6.225 6.9
With DataFramesMeta.jl you would write this using the @combine macro as
follows (I additionally show here how to use operation chaining with @chain):
julia> @chain iris begin
groupby(:Species)
@combine($["SepalLength0.25", "SepalLength0.75"] = [quantile(:SepalLength, [0.25, 0.75])])
end
3×3 DataFrame
Row │ Species SepalLength0.25 SepalLength0.75
│ String15 Float64 Float64
─────┼───────────────────────────────────────────────────
1 │ Iris-setosa 4.8 5.2
2 │ Iris-versicolor 5.6 6.3
3 │ Iris-virginica 6.225 6.9
The operation specification syntax was designed to allow doing simple
transformations in an easy way, but at the same to also support quite complex
operations, like the one we did on the iris data frame.
If you would like to hear a detailed explanation of how to write such code
please join me during the upcoming workshop.
You can find all the examples that I will use during the workshop in the
accompanying GitHub repository.