billylanchantin
Explorer - Series (1D) and dataframes (2D) for fast and elegant data exploration in Elixir
The Explorer library has been out for some time. But we just released the latest version (see below), and we thought we’d start posting updates to the forum.
Wait, Explorer is new to me. What are Series and DataFrames?
Explorer is a DataFrame library for Elixir.
DataFrame libraries are common in languages which have a focus on data manipulation, including:
If you’d like a more in-depth tutorial, there’s an excellent LiveBook called Ten Minutes to Explorer that you can play with:
But we’ll provide a quick overview here.
Briefly, you can think of a DataFrame like an in-memory table. Its purpose is to facilitate common data exploration and analysis tasks. As such, it’s a column-oriented table.
Column-oriented tables
If you’re unfamiliar with column-oriented tables, suppose you have a table of pet data like this:
| type | age | color |
|---|---|---|
| cat | 5 | black |
| dog | 2 | brown |
| dog | 3 | brindle |
A row-oriented organization of that data might look like this in Elixir:
rows = [
[type: "cat", age: 5, color: "black"],
[type: "dog", age: 2, color: "brown"],
[type: "dog", age: 3, color: "brindle"],
]
It matches the original table fairly one-to-one. But the column-oriented version might instead look like:
columns = [
type: ["cat", "dog", "dog"],
age: [5, 2, 3],
color: ["black", "brown", "brindle"]
]
It has same information, but “transposed”.
Column-orientation is beneficial if you’re asking questions that require a lot of number-crunching like “What’s the average age of all pets?”. In the row-oriented version, finding the average age would require first looking through the entire contents of the table to collect the relevant data. But in the column-oriented version, those values have already been co-located in memory.
Series and DataFrames: columns and tables
In dataframe parlance, a “series” is a single column and a “dataframe” is a collection of named series, aka a table.
Our example above would look like this:
type = Explorer.Series.from_list(["cat", "dog", "dog"])
age = Explorer.Series.from_list([5, 2, 3])
color = Explorer.Series.from_list(["black", "brown", "brindle"])
df = Explorer.DataFrame.new(type: type, age: age, color: color)
# #Explorer.DataFrame<
# Polars[3 x 3]
# type string ["cat", "dog", "dog"]
# age s64 [5, 2, 3]
# color string ["black", "brown", "brindle"]
# >
Some things to note:
- Each series has a corresponding data type or “dtype”, e.g.
typehas the dtypestring. - The word “Polars” appears. That indicates that this dataframe is using the backend powered by the fantastic Polars library (the default backend).
And if we really did want to know the average age of the pets, that would look like this:
Explorer.Series.mean(df["age"])
# 3.3333333333333335
Features and design
Preiminaries out of the way, here are Explorer’s high-level features:
-
Simply typed series:
:binary,:boolean,:category,:date,:datetime,:duration, floats of 32 and 64 bits ({:f, size}), integers of 8, 16, 32 and 64 bits ({:s, size},{:u, size}),:null,:string,:time,:list, and:struct. -
A powerful but constrained and opinionated API, so you spend less time looking for the right function and more time doing data manipulation.
-
Support for CSV, Parquet, NDJSON, and Arrow IPC formats
-
Integration with external databases via ADBC and direct connection to file storages such as S3
-
Pluggable backends, providing a uniform API whether you’re working in-memory or (forthcoming) on remote databases or even Spark dataframes.
-
The first (and default) backend is based on NIF bindings to the blazing-fast polars library.
The API is heavily influenced by Tidy Data and borrows much of its design from dplyr.
The philosophy is heavily influenced by this passage from dplyr’s documentation:
By constraining your options, it helps you think about your data manipulation challenges.
It provides simple “verbs”, functions that correspond to the most common data manipulation tasks, to help you translate your thoughts into code.
It uses efficient backends, so you spend less time waiting for the computer.
The aim here isn’t to have the fastest dataframe library around (though it certainly helps that we’re building on Polars, one of the fastest).
Instead, we’re aiming to bridge the best of many worlds:
- the elegance of dplyr
- the speed of polars
- the joy of Elixir
That means you can expect the guiding principles to be ‘Elixir-ish’. For example, you won’t see the underlying data mutated, even if that’s the most efficient implementation. Explorer functions will always return a new dataframe or series.
Links:
- HexDocs: Explorer — Explorer v0.11.1
- GitHub:
Acknowledgements
Explorer is an extensive library and there’s much more we could say. But for now, we’d just like to thank the dozens of contributors who’ve added wonderful improvements over the years. ![]()
Most Liked
billylanchantin
Explorer - version 0.8
Explorer has released version 0.8!
Added
-
Add
explode/2toExplorer.DataFrame. This function is useful to expand the contents of a{:list, inner_dtype}series into a “inner_dtype” series. -
Add the new series functions
all?/1andany?/1, to work with boolean series. -
Add support for the “struct” dtype. This new dtype represents the struct dtype from Polars/Arrow.
-
Add
map/2andmap_with/2to theExplorer.Seriesmodule.
This change enables the usage of theExplore.Queryfeatures in a series. -
Add
sort_by/2andsort_with/2to theExplorer.Seriesmodule.
This change enables the usage of the lazy computations and theExplorer.Querymodule. -
Add
unnest/2toExplorer.DataFrame. It works by taking the fields of a “struct” - the new dtype - and transform them into columns. -
Add pairwise correlation -
Explorer.DataFrame.correlation/2- to calculate the correlation between numeric columns inside a data frame. -
Add pairwise covariance -
Explorer.DataFrame.covariance/2- to calculate the covariance between numeric columns inside a data frame. -
Add support for more integer dtypes. This change introduces new signed and unsigned integer dtypes:
{:s, 8},{:s, 16},{:s, 32}{:u, 8},{:u, 16},{:u, 32},{:u, 64}.
The existing
:integerdtype is now represented as{:s, 64}, and it’s still the default dtype for integers. But series and data frames can now work with the new dtypes. Short names for these new dtypes can be used in functions likeExplorer.Series.from_list/2. For example,{:u, 32}can be represented with the atom:u32.This may bring more interoperability with Nx, and with Arrow related things, like ADBC and Parquet.
-
Add
ewm_standard_deviation/2andewm_variance/2toExplorer.Series.
They calculate the “exponentially weighted moving” variance and standard deviation. -
Add support for
:skip_rows_after_headeroption for the CSV reader functions. -
Support
{:list, numeric_dtype}forExplorer.Series.frequencies/1. -
Support pins in
cond, inside the context ofExplorer.Query. -
Introduce the
:nulldtype. This is a special dtype from Polars and Apache Arrow to represent “all null” series. -
Add
Explorer.DataFrame.transpose/2to transpose a data frame.
Changed
-
Rename the functions related to sorting/arranging of the
Explorer.DataFrame.
Nowarrange_withis namedsort_with, andarrangeissort_by.The
sort_by/3is a macro and it is going to work using theExplorer.Querymodule. On the other side, thesort_with/2uses a callback function. -
Remove unnecessary casts to
{:s, 64}now that we support more integer dtypes.
It affects some functions, like the following in theExplorer.Seriesmodule:argsortcountrankday_of_week,day_of_year,week_of_year,month,year,hour,minute,secondabscliplengthsslicen_distinctfrequencies
And also some functions from the
Explorer.DataFramemodule:mutate- mostly because of series changessummarise- mostly because of series changesslice
Fixed
-
Fix inspection of series and data frames between nodes.
-
Fix cast of
:stringseries to{:datetime, any()} -
Fix mismatched types in
Explorer.Series.pow/2, making it more consistent. -
Normalize sorting options.
-
Fix functions with dtype mismatching the result from Polars.
This fix is affecting the following functions:quantile/2in the context of a lazy seriesmode/1inside a summarisationstrftime/2in the context of a lazy seriesmutate_with/2when creating a column from aNaiveDateTimeorExplorer.Duration.
Contributors
Thank you to everyone who opend up a PR:
- @billylanchantin
- @cigrainger
- @costaraphael
- @cristineguadelupe
- @Jhonatannunessilva
- @kellyfelkins
- @lkarthee
- @philss
And thank you to the first-time contributors!:
- @JonGretar made their first contribution in Support for skip_rows_after_header option in reading csv files by JonGretar · Pull Request #782 · elixir-explorer/explorer · GitHub
- @rtvu made their first contribution in Added Series.ewm_std/2 and Series.ewm_var/2 by rtvu · Pull Request #778 · elixir-explorer/explorer · GitHub
Changelogs
Full Changelog: Comparing v0.7.2...v0.8.0 · elixir-explorer/explorer · GitHub
Official Changelog: Changelog — Explorer v0.11.1
billylanchantin
[Release] Explorer - v0.10
Added
-
Add support for the decimals data type.
Decimals dtypes are represented by the
{:decimal, precision, scale}tuple,
where precision can be a positive integer from 0 to 38, and is the maximum number
of digits that can be represented by the decimal. The scale is the number of
digits after the decimal point.With this addition, we also added the
:decimalpackage as a new dependency.
TheExplorer.Series.from_list/2function accepts decimal numbers from that
package as values *%Decimal{}.This version has a small number of operations, but is a good foundation.
-
Allow the usage of queries and lazy series outside callbacks and macros.
This is an improvement to functions that were originally designed to accept callbacks.
With this change you can now reuse lazy series across different “queries”.
See theExplorer.Querydocs for details.The affected functions are:
Explorer.DataFrame.filter_with/2Explorer.DataFrame.mutate_with/2Explorer.DataFrame.sort_with/2Explorer.DataFrame.summarise_with/2
-
Allow accessing the dataframe inside query.
-
Add “lazy read” support for Parquet and NDJSON from HTTP(s).
-
Expose more options for
Explorer.Series.cut/3andExplorer.Series.qcut/3.
These options were available in Polars, but not in our APIs.
Fixed
-
Fix creation of series where a
nilvalue inside a list * for a{:list, any()}dtype -
could result in an incompatible dtype. This fix will prevent panics for list of lists with
nilentries. -
Fix
Explorer.DataFrame.dump_ndjson/2when date time is in use. -
Fix
Explorer.Series.product/1for lazy series. -
Accept
%FSS.HTTP.Entry{}structs in functions likeExplorer.DataFrame.from_parquet/2. -
Fix encode of binaries to terms from series of the
{:struct, any()}dtype.
In case the inner fields of the struct had any binary (:binarydtype), it was
causing a panic.
Changed
- Change the defaults of the functions
Explorer.Series.cut/3andExplorer.Series.qcut/3
to not have “break points” column in the resultant dataframe.
So the:include_breaksis nowfalseby default.
Contributors
- @audacioustux
- @billylanchantin
- @ceyhunkerti
- @mhanberg
- @mooreryan
- @philss
- @preciz
New Contributors
- @audacioustux made their first contribution in fix typo cuontry ~> country by audacioustux · Pull Request #979 · elixir-explorer/explorer · GitHub
- @ceyhunkerti made their first contribution in LazyFrame from http endpoint by ceyhunkerti · Pull Request #993 · elixir-explorer/explorer · GitHub
- @mooreryan made their first contribution in Fix return type in `DataFrame.dtypes/1` typespec (#1002) by mooreryan · Pull Request #1003 · elixir-explorer/explorer · GitHub
- @preciz made their first contribution in Fix typos by preciz · Pull Request #984 · elixir-explorer/explorer · GitHub
Changelogs
- Full changelog: Comparing v0.9.2...v0.10.0 · elixir-explorer/explorer · GitHub
- Official changelog: Changelog — Explorer v0.11.1
billylanchantin
billylanchantin
[Release] Explorer - v0.11.0
Version 0.11.0 has been released!
This one is mostly minor improvements and bugfixes. One notable change is that the DataFrame print format was altered to save vertical space and to hint when rows were hidden.
Before:
iex> Explorer.Datasets.iris() |> Explorer.DataFrame.print()
+-----------------------------------------------------------------------+
| Explorer DataFrame: [rows: 150, columns: 5] |
+--------------+-------------+--------------+-------------+-------------+
| sepal_length | sepal_width | petal_length | petal_width | species |
| <f64> | <f64> | <f64> | <f64> | <string> |
+==============+=============+==============+=============+=============+
| 5.1 | 3.5 | 1.4 | 0.2 | Iris-setosa |
+--------------+-------------+--------------+-------------+-------------+
| 4.9 | 3.0 | 1.4 | 0.2 | Iris-setosa |
+--------------+-------------+--------------+-------------+-------------+
| 4.7 | 3.2 | 1.3 | 0.2 | Iris-setosa |
+--------------+-------------+--------------+-------------+-------------+
| 4.6 | 3.1 | 1.5 | 0.2 | Iris-setosa |
+--------------+-------------+--------------+-------------+-------------+
| 5.0 | 3.6 | 1.4 | 0.2 | Iris-setosa |
+--------------+-------------+--------------+-------------+-------------+
After:
iex> Explorer.Datasets.iris() |> Explorer.DataFrame.print()
+--------------------------------------------------------------------------+
| Explorer DataFrame: [rows: 150, columns: 5] |
+--------------+-------------+--------------+-------------+----------------+
| sepal_length | sepal_width | petal_length | petal_width | species |
| <f64> | <f64> | <f64> | <f64> | <string> |
+==============+=============+==============+=============+================+
| 5.1 | 3.5 | 1.4 | 0.2 | Iris-setosa |
| 4.9 | 3.0 | 1.4 | 0.2 | Iris-setosa |
| 4.7 | 3.2 | 1.3 | 0.2 | Iris-setosa |
| … | … | … | … | … |
| 6.2 | 3.4 | 5.4 | 2.3 | Iris-virginica |
| 5.9 | 3.0 | 5.1 | 1.8 | Iris-virginica |
+--------------+-------------+--------------+-------------+----------------+
See here for details on the new format:
Added
Explorer.DataFrame.estimated_size/1- Estimates memory size of a DataFrameExplorer.DataFrame.to_table_string/2- Represents a DataFrame as a string
for printingExplorer.Series.degrees/1- Converts radians to degreesExplorer.Series.radians/1- Converts degrees to radians:quote_styleoption to CSV functions
Fixed
- Fix bug where
:regionwas incorrectly required in%FSS.S3.Entry{} - Fix trigonometric functions to not raise on f32
- Fix warning from
:table_rexdependency when printing - Fix formatting of
Explorer.DataFrame.mutate_with/2options Explorer.Series.fill_missing/2now works for all integer and float dtypesExplorer.Series.frequencies/1now works for{:list, _}dtype- Fix inefficiency with categorization
- Fix typespecs
Explorer.DataFrame.select/2Explorer.DataFrame.ungroup/1Explorer.Seriesfunctions that may return lazy series
Changed
- Printing a DataFrame looks different
- Adds a row of
…to indicate there are hidden rows. Includes a new option
limit_dots: :bottom | :splitto specify how to do this. - Drops the row separators except when composite dtypes are present.
- Allows you to pass through valid options to
TableRex.render!/2. This
gives you a little more flexibility in case you don’t like the defaults.
- Adds a row of
Explorer.DataFrame.print/1now documents its default:limitof 5 rowsExplorer.DataFrame.concat_rows/1has improved error messages- Accessing a DataFrame with a range now raises if the range is out of bounds
New Contributors
- @szajbus made their first contribution in
Make region truly optional by szajbus · Pull Request #1030 · elixir-explorer/explorer · GitHub - @viniciussbs made their first contribution in
Add Regex as a valid column type in typespec by viniciussbs · Pull Request #1037 · elixir-explorer/explorer · GitHub - @pejrich made their first contribution in
added error message text to exploring_explorer.livemd by pejrich · Pull Request #1040 · elixir-explorer/explorer · GitHub - @jdbarillas made their first contribution in
Add quote_style option to csv IO functions by jdbarillas · Pull Request #1049 · elixir-explorer/explorer · GitHub - @petrkozorezov made their first contribution in
Enable `Series.frequencies` over `{:list, _}` type by petrkozorezov · Pull Request #1083 · elixir-explorer/explorer · GitHub
Full Changelog
The full changelog includes all contributions with individual attributions. Special shoutout to @mhanberg for helping out with some gnarly version wrangling!
billylanchantin
[Release] Explorer - v0.11.1
This is a small one, mostly because we broke printing for lazy dataframes
(thanks for the patch, @mhanberg!). But it comes with a few improvements too.
Of particular note is that Explorer.DataFrame.group_by is no longer stable by default. Before, groups would always be returned in their original order which is nice but has a non-trivial performance penalty. Now groups are returned in a random order for a performance boost (thanks, @petrkozorezov!). But you can always set group_by(..., stable: true) if stability is needed.
Added
Explorer.DataFrame.dump_ipc_schemaExplorer.DataFrame.dump_ipc_record_batchExplorer.Series.cumulative_count:stableoption forExplorer.DataFrame.group_by
Fixed
- Fix printing lazy data frame with new default print options (as of v0.11.0)
- Fix mutate docs formatting
New Contributors
- @WolfDan made their first contribution in Add dump arrow schema and batchrecord by WolfDan · Pull Request #1103 · elixir-explorer/explorer · GitHub
Full Changelog
The full changelog includes all contributions with individual attributions.







