I just released Query.jl
version v0.7.1. The v0.7.x series includes a number of smaller improvements
and some major new experimental features. This post describes and explains
all new features included in this release.
Enable {} everywhere
{}
is the syntax for named tuples in Query.jl.
In previous versions this syntax only worked in @select
statements,
and it only worked at the top level (for example you couldn’t create a
named tuple that has a field that is itself a named tuple). The new release
now enables the {}
syntax everywhere in queries.
This is especially handy for @group
statements. Take the following
query as an example:
using DataFrames, Query
df = DataFrame(sex=[:male, :female, :female, :female], age=[21., 30., 45., 34.], children=[2,2,1,2])
@from i in df begin
@group i.age by {i.sex, i.children} into g
@select {g.key.sex, g.key.children, age=mean(g)}
@collect DataFrame
end
Here we have data about four individuals: their sex, their age and how
many children they have. We then group this data by sex and children, and
compute the average age for each group. The expression by {i.sex, i.children}
creates a named tuple as the grouping key, which is handy later on, because
we can now access the individual fields of the group key by their name.
Experimental standalone commands
I also added number of new standalone commands that enable a user experience
that is inspired by both the method syntax of the LINQ query operators
and dplyr. I am still tinkering with the precise details of this API, so
things might change going forward (hence “experimental”).
Here is a code example that highlights some of the new features and how
they play together with some of the other packages that I’ve created
over the last year:
using Dataverse
df = load("http://www.david-anthoff.com/blog/data/exampledata.csv") |>
@groupby(_.Children) |>
@select({Children=_.key,Age=mean(_..Age)}) |>
@orderby(_.Children) |>
@tee(save("output.csv")) |>
@where(_.Age>30) |>
@tee(save("output.feather")) |>
DataFrame
First, this uses the unregistered Dataverse.jl
package (I’m still looking for a better name, ideas welcome!). That package
pulls together a set of data packages that play nicely together and adds
a number of small experimental features on top of Query.jl.
The query starts out by loading a CSV file from a URL. The load
function
is from the FileIO.jl package.
You can use that function to load files from web addresses or local disc,
and it works for CSV, Feather, Excel, Stata, SPSS and SAS files.
The next thing to note is that this query uses the pipe operator |>
to build up a data processing pipeline. Essentially you pass your data
through a series of manipulation commands and chain those together via
the |>
operator.
The @groupby
macro call is the first new standalone query command
introduced in this version of Query.jl.
The argument to the @groupby
macro is an anonymous function that selects
the key by which the source data should be grouped. The normal julia
syntax for this would be @groupby(i->i.Children)
. In the example
above I use another experimental syntax in the new Query.jl
version: when you write an expression that contains the _
symbol,
that expression is translated into _ -> your_original_expression
. This
syntax is just a short-cut to writing anonymous functions. In the context
of querying table-like sources, it typically stands for the current row
in query commands. The syntax _.Children
here therefor extracts the
value of the Children
column for each row, and rows are grouped by
that value.
The next command is a @select
macro call. It takes an anonymous function
that projects each element from the source stream. The @groupby
macro
creates a stream of groups, so each element that the @select
macro sees
will be an element of type Grouping
. A Grouping
element has one
field key
that holds the value of the group key for that group. In
this example I am accessing that value via the _.key
expression in
the @select
call. Any Grouping
element is also at the same time
an array of the elements that were grouped. In our example, the
Grouping
element is an array of the rows that make up a single group.
In this example I want to compute the average age for each group. The problem
here is that _
will be an array of rows, not an array of the age column
for each group. But I need to pass just a vector of values to the mean
function, not a vector of rows. The standard julia way to achieve this
would be to use a generator expression like mean(i.Age for i in _)
.
Because these kind of summary operations for groups are so common in data
analysis, I also added another new experimental syntax to Query.jl
in this version that simplifies the syntax for this common pattern. The
syntax a..b
is a shortcut for map(i->i.b,a)
in this version, and
that enables the concise syntax mean(_..Age)
that is used in our example.
The next command @orderby
is relatively simple: it takes an anonymous
function that extracts the key by which things should be sorted from each
source element. So in this example we are sorting the dataset by the
Children
column at this stage.
The @tee
macro is another experimental feature. It mimics the
tee shell command. It
takes the thing that is piped into it and pipes it into both the argument
of the @tee
macro, and then passes the input unmodified to the next
stage in the general pipeline. In the example I use this to save the intermediate
dataset we have created so far into a CSV file, but then pass the data
on to further data manipulation commands.
The save
function is once again from the FileIO.jl
package. It currently supports saving tabular data as CSV and Feather files.
The @where
command filters the source dataset: only elements for
which the anonymous function that is passed to the @where
command returns
true
are passed on to the next stage. I am using the _
syntax once
again here to create the anonymous function.
The next @tee
command just saves the now filtered dataset to a Feather
file.
The final line of this query pipes the result into a DataFrame
. Note
that we never allocated or used any of the table types like DataFrame
or DataTable
in the query so far. We could have easily concluded the
query with a save
call and actually never materialized the data into
a DataFrame
if we just wanted to load a file, manipulate it and save
it back to disc again. But if we intend to use the data for further
in-memory manipulation it is of course convenient to store it in a
DataFrame
(or any of the other supported table types, see the list
in IterableTables.jl).
You can find a bit more information about these new experimental features
in the documentation here.
I am really interested in feedback on these new features! They are obviously
not complete at this point, but I hope you can get a general feel for
the direction, and any comments on that and anything else would be most
welcome. I track both the issues over at Query.jl
and the discussion on the julia forum.
One important final point: this new syntax will eventually augment the syntax
we already have in Query.jl.
The @from
macro will not go away! In fact, one can easily combine the
two styles, for example in the following way:
using Dataverse
df = load("http://www.david-anthoff.com/blog/data/exampledata.csv") |>
@query(i, begin
@group i by i.Children into g
@select {Children=g.key, Age=mean(g..Age)}
end) |>
@where(_.Age>30) |>
DataFrame
Various smaller changes
Query.jl now uses the iterable
tables interface definition in TableTraits.jl.
This change should be entirely transparent to users, it just amounts
to some reorganization of packages in the background.
The release also includes a number of performance improvements and bug fixes.
Thanks
Thanks to Florian for help with the
..
syntax, and to Keno and Steven
for pointing me to the @tee
name.
This post is being discussed here.