Library / SDK
elixir-explorer/explorer avatar
elixir-explorer/explorer

Explorer: series and dataframes for Elixir, with Polars underneath

Series (one-dimensional) and dataframes (two-dimensional) for fast and elegant data exploration in Elixir

1,298 stars155 forksElixirMIT

At a glance

What is it?
Explorer brings one-dimensional series and two-dimensional dataframes to Elixir, backed by NIF bindings to Polars, with a deliberately constrained API modelled on dplyr and a set of typed dtypes. The trade-offs are explicit: immutable returns, a precompiled native binary, and a project that describes itself as aiming for elegance rather than for the fastest library around.
Who is it for?
Adopt Explorer if you want tabular data work inside an Elixir application, want the ergonomics of the tidyverse verbs without leaving the BEAM, and can accept a native dependency and an API that refuses to grow. Do not adopt it if your pipelines need remote or distributed dataframes, since the remote backends are described as forthcoming, and do not adopt it for a language where a mature dataframe library already exists.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 25 days ago.
What is it written in?
Mainly Elixir, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A dataframe library that says it is not trying to be the fastest

Explorer brings series and dataframes for fast data exploration to Elixir. The design notes in the README are unusually candid about the goal, and the aim is stated as bridging the best of many worlds: the elegance of dplyr, the speed of Polars, and the joy of Elixir. The README adds that the aim is not to have the fastest dataframe library around, though it helps that the default backend is built on Polars, which it links to a benchmark.

The default backend is NIF bindings to the Polars library, and the backends are pluggable, giving a uniform API whether the data is in memory or, as the README puts it, on forthcoming remote databases or even Spark dataframes. The first and default backend is the Polars one.

The library sits in a family of tools that reach outside the BEAM for the heavy lifting. Explorer integrates with external databases through ADBC, and it connects directly to file storages such as S3. For file formats it accepts CSV, Parquet, NDJSON and Arrow IPC, and the README says reading from files is by far the most common way to load a dataframe.

The repository shows the shape of the implementation, with a native/ directory, a rust-toolchain.toml, a mix.exs and lib/ for the Elixir side, plus notebooks/, datasets/, a flake.nix and a RELEASE.md.

The dtype list is the real API contract

The first feature listed is simply typed series, and the list is worth reading closely because it is the boundary of what the library will hold.

The supported types are :binary, :boolean, :category, :date, :datetime, :duration, floats of 32 and 64 bits written as {:f, size}, integers of 8, 16, 32 and 64 bits written as {:s, size} and {:u, size}, :null, :string, :time, :list and :struct.

Two details in that list matter. The distinction between signed and unsigned integers, and the explicit bit widths, means the library is precise about numeric representation rather than treating integers as one type, which matters when data comes from Parquet or Arrow IPC where the width is part of the file. And :category is a first-class dtype, which tells you categorical data is a designed-for case rather than an afterthought encoded as strings.

A series is defined as similar to a list but guaranteed to contain items of one dtype only, with the important caveat that nil values are permitted in series of any dtype. A dataframe is a way to represent one or more series together and work with them as a whole, with the single restriction that all the series share the same size.

So the contract is: one dtype per column, equal lengths across columns, and nulls everywhere. That is a narrow contract, and it is the reason the operations can be fast.

Constrained on purpose, and nothing is ever mutated

The second headline feature is a powerful but constrained and opinionated API, so you spend less time looking for the right function and more time doing data manipulation. That phrasing comes directly from the README, and it is a design statement rather than a boast.

The API is heavily influenced by Tidy Data and borrows much of its design from dplyr, and the README quotes three principles from the dplyr documentation: that constraining your options helps you think about your data manipulation challenges, that simple verbs correspond to the most common tasks, and that efficient backends mean you spend less time waiting for the computer.

The Elixir-ness comes through in how the data is handled. The README says you will not see the underlying data mutated, even if that would be the most efficient implementation, and that Explorer functions will always return a new dataframe or series. In a system with a Rust backend over shared memory, mutating in place would be faster, and the library declines to do it. That is a deliberate cost paid for a mental model that matches the rest of the language.

The macro layer is the other half of the ergonomics, and it is where the dplyr resemblance becomes literal. Columns are referred to by name and functions are used without being defined, which the README attributes to the Explorer.Query features and calls the main reason the Explorer.DataFrame module has to be required.

Installing, and forcing a local build of the native code

The quick start has two forms. Inside an Elixir script or a Livebook, you install the package with Mix.install:

elixir
Mix.install([
  {:explorer, "~> 0.13.0"}
])

In the mix.exs of an application, it goes in the dependency list instead:

elixir
def deps do
  [
    {:explorer, "~> 0.13.0"}
  ]
end

Explorer downloads a precompiled version of its native code on installation, which is the difference between a one-line install and a Rust toolchain on every developer machine. When that precompiled binary is not what you want, the README gives the local build route, with the environment variable EXPLORER_BUILD set to 1 and the rustler dependency added:

elixir
  {:explorer, "~> 0.13.0", system_env: %{"EXPLORER_BUILD" => "1"}},
  {:rustler, ">= 0.0.0"}

The last line of the install section is the one to keep: if necessary, clean up before rebuilding with mix deps.clean explorer. A stale native build is a common source of confusion when the Rust side changes.

The README also points to an extensive guide you can play with in Livebook, Ten Minutes to Explorer, which is the fastest way to judge whether the API feels right before writing anything.

Reading the API through a mountains dataframe

The guided tour in the README builds a small dataframe and then filters it, and the shape of the API is clearer from that example than from a function list.

A series is created from a list, and inspecting it shows the backend and the dtype:

elixir
fruits = Explorer.Series.from_list(["apple", "mango", "banana", "orange"])

which is displayed as a Polars[4] string series, and Explorer.Series.sort on it produces the same series ordered alphabetically. A dataframe is then created with Explorer.DataFrame.new, which the README says is neat for small experiments, passing either series or lists:

elixir
mountains = Explorer.DataFrame.new(name: ["Everest", "K2", "Aconcagua"], elevation: [8848, 8611, 6962])

The result is a Polars[3 x 2] frame with a string name column and an s64 elevation column, which can be printed as a table with Explorer.DataFrame.print/2.

The filter is where the macro layer earns its place:

elixir
require Explorer.DataFrame, as: DF

DF.filter(mountains, elevation > mean(elevation))

The require is needed to load the macro features of the module, and the shorter alias keeps the examples readable. The comparison refers to the column by name and uses a function that was never defined, which returns the two mountains above the mean elevation.

The DuckDB alternative, named in the README itself

The README includes a boxed note pointing at a different project for a different backend choice. If you would prefer a dataframe API backed by DuckDB rather than Polars, it says to check out the Dux project.

That is a rare and useful thing for a README to do. The difference in approach is stated in one line: the same ergonomic API over a different engine. Explorer defaults to Polars through NIF bindings, which means the data crosses into native code inside the BEAM process and the engine is a Rust library designed for in-process columnar work. Dux is, in the README's words, a dataframe API backed by DuckDB instead of Polars.

The choice follows from what you are doing. A Polars-backed library is a good fit for data that lives in memory and moves through a small number of verbs, and a DuckDB-backed one is a good fit when the work is closer to running queries over files and databases. The README itself notes that Explorer also integrates with external databases through ADBC and can read from S3, so the boundary between the two approaches is not entirely sharp.

For anyone choosing today, the advice that follows from the README's own framing is to read both and pick the engine, because the API shape you learn is close to the same in each case.

Where Explorer is the wrong tool

The first wrong-tool case is a workload that needs the remote backends. The pluggable backend idea covers in-memory work now and promises remote databases and Spark dataframes as forthcoming, so a pipeline that expects to query a large distributed store through this API is betting on a feature that is not in the version you would install.

The second is a project that has already standardised on a different data stack. Explorer deliberately borrows from dplyr, and the DuckDB-based alternative covers a similar ground. If your organisation has chosen one of them, having two dataframe APIs in the same codebase is a cost in training and in tooling, and there is no advantage in having both.

The third is an environment without a Rust toolchain and without a matching precompiled binary. The install path assumes either a precompiled download for your platform or the ability to build Rust locally, and if neither holds, the dependency is heavier than the problem. A team that is deploy-only and cannot compile will find the prebuilt download does the job or will find the library unusable.

There is a fourth, smaller one: workloads where the dtype contract is wrong for the data. One dtype per column and equal lengths across columns is a clean model, and a dataset with genuinely ragged columns has to be normalised before it can enter.

MIT, version 0.13.0, and a release every few months

The project is MIT licensed, with the LICENSE file at the repository root, and it is published on Hex as explorer with the documentation on hexdocs. The repository is not archived, and the last push was on 2026-09-28, the same day as the v0.13.0 release.

The release list shows v0.13.0 on 2026-09-28, v0.12.0 on 2026-07-08 and v0.11.1 on 2025-08-17. So the release list shows v0.13.0 in September 2026 and v0.12.0 in July 2026, and the version is still below 1.0. That is a library in the phase where the API is still moving and the changelog is the honest way to find out what changed, which is why the CHANGELOG.md and RELEASE.md at the root are worth reading before an upgrade.

The repository carries a CODEOWNERS file, a.formatter.exs, a.vscode directory, a flake.nix for a reproducible development environment, a datasets/ directory and a notebooks/ directory. The README says Explorer uses Rust for its default backend implementation, which is consistent with the native/ directory and the rust-toolchain.toml at the root.

Editorial conclusion

Adopt Explorer if you want tabular data work inside an Elixir application, want the ergonomics of the tidyverse verbs without leaving the BEAM, and can accept a native dependency and an API that refuses to grow. Do not adopt it if your pipelines need remote or distributed dataframes, since the remote backends are described as forthcoming, and do not adopt it for a language where a mature dataframe library already exists. Before you commit, read the dtype list to see whether your data fits the supported types, decide whether you want the Polars backend or the DuckDB-backed alternative the README points at, and plan for the case where the precompiled native binary is not shipped for your platform, which is what EXPLORER_BUILD=1 with the rustler dependency exists for.

Frequently asked questions

How do I add Explorer to an Elixir project?

Add {:explorer, "~> 0.13.0"} to the deps list in mix.exs, or call Mix.install([{:explorer, "~> 0.13.0"}]) inside a script or a Livebook. Explorer downloads a precompiled version of its native code on installation.

What backends does Explorer support?

The backends are pluggable, giving a uniform API whether the data is in memory or on forthcoming remote databases or Spark dataframes. The first and default backend is based on NIF bindings to Polars, and the README points at the Dux project for a DuckDB-backed alternative.

What data types can an Explorer series hold?

A series is guaranteed to contain items of one dtype. The listed types are :binary, :boolean, :category, :date, :datetime, :duration, floats of 32 and 64 bits, integers of 8, 16, 32 and 64 bits with signed and unsigned variants, :null, :string, :time, :list and :struct, with nil values permitted in any series.

How do I build Explorer from source instead of using the precompiled binary?

Set the environment variable EXPLORER_BUILD to 1 in the system_env option and add the rustler dependency, as in {:explorer, "~> 0.13.0", system_env: %{"EXPLORER_BUILD" => "1"}} alongside {:rustler, ">= 0.0.0"}. The README suggests cleaning up with mix deps.clean explorer before rebuilding.

Does Explorer mutate dataframes?

No. The README states you will not see the underlying data mutated even where that would be the most efficient implementation, and that Explorer functions will always return a new dataframe or series.

Official sources

  1. elixir-explorer/explorer on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/elixir-explorer-explorer.svg)](https://hysenlabs.com/projects/elixir-explorer-explorer)

Was this useful?