See It in Action
The examples use prices, a small table of daily prices for two stocks, defined in the quick start. Every output on this site comes from running the code.
Compute within Groups
Daily returns per stock, in date order. Every row is kept:
(dt prices
:by [:sym]
:within-order [(asc :date)]
:set {:ret #dt/e (- (win/ratio :price) 1)})
_unnamed [8 5]:
| :date | :sym | :price | :volume | :ret |
|------------|------|-------:|--------:|------------:|
| 2024-01-02 | AAPL | 100.0 | 1200 | |
| 2024-01-03 | AAPL | 102.0 | 1500 | 0.02000000 |
| 2024-01-04 | AAPL | 99.0 | | -0.02941176 |
| 2024-01-05 | AAPL | 104.0 | 1800 | 0.05050505 |
| 2024-01-02 | MSFT | 200.0 | 900 | |
| 2024-01-03 | MSFT | 198.0 | 1100 | -0.01000000 |
| 2024-01-04 | MSFT | 202.0 | 1000 | 0.02020202 |
| 2024-01-05 | MSFT | 207.0 | 1300 | 0.02475248 |
Chain Steps
Each dt call returns a dataset, so steps chain with ->. Returns, then their mean and volatility per stock, then a sort:
(-> prices
(dt :by [:sym] :within-order [(asc :date)]
:set {:ret #dt/e (- (win/ratio :price) 1)})
(dt :by [:sym] :agg {:mean-ret #dt/e (mn :ret) :vol #dt/e (sd :ret)})
(dt :order-by [(desc :mean-ret)]))
_unnamed [2 3]:
| :sym | :mean-ret | :vol |
|------|-----------:|-----------:|
| AAPL | 0.01369776 | 0.04032943 |
| MSFT | 0.01165150 | 0.01888828 |
Compare across Stocks
Group by date instead of by stock to work across the cross-section. Each return minus that day's average return:
(-> prices
(dt :by [:sym] :within-order [(asc :date)]
:set {:ret #dt/e (- (win/ratio :price) 1)})
(dt :by [:date]
:set {:excess #dt/e (stat/demean :ret)}
:select [:date :sym :ret :excess]
:order-by [(asc :date)]))
_unnamed [8 4]:
| :date | :sym | :ret | :excess |
|------------|------|------------:|------------:|
| 2024-01-02 | AAPL | | |
| 2024-01-02 | MSFT | | |
| 2024-01-03 | AAPL | 0.02000000 | 0.01500000 |
| 2024-01-03 | MSFT | -0.01000000 | -0.01500000 |
| 2024-01-04 | AAPL | -0.02941176 | -0.02480689 |
| 2024-01-04 | MSFT | 0.02020202 | 0.02480689 |
| 2024-01-05 | AAPL | 0.05050505 | 0.01287629 |
| 2024-01-05 | MSFT | 0.02475248 | -0.01287629 |
Match by Time
An as-of join gives each trade the latest quote for its symbol at or before the trade's time:
(join trades quotes :on [:sym :time] :how :asof)
_unnamed [3 4]:
| :sym | :time | :qty | :bid |
|------|------:|-----:|------:|
| AAPL | 10 | 100 | 99.5 |
| AAPL | 25 | 50 | 100.5 |
| MSFT | 30 | 75 | 201.0 |
The Core Idea
Two keywords decide what a query does to the rows. :set adds columns and keeps every row; :agg collapses rows into summaries. :by splits the rows into groups first. Together they give four operations:
:by:by:set:aggAround that core, :where filters rows first, and :select, :order-by and :take shape the result. The docs explain each step.
Why Datajure
Datajure is a syntax layer, not an engine. It compiles #dt/e expressions to vectorized operations on tech.ml.dataset, so results are standard datasets that work with tablecloth, Clerk, Clay and the rest of the Scicloj ecosystem.
Its design borrows from whichever library got a given idea right:
- R's data.table:
DT[i, j, by]becomes(dt ds :where i :set-or-agg j :by by), without in-place changes. - Python's Polars: expressions are values you can store and combine, and comparisons with missing values are safe.
- q/kdb+: time-series tools such as
win/ratio,win/mavg,win/ema,win/fillsandxbar, plus as-of and window joins. - Julia's DataFramesMeta.jl:
#dt/eplays the role of@transformand@subset, using a Clojure reader tag.
Where to Go Next
Datajure v1
Datajure v2 (March 2026) is a rewrite. Datajure v1 (1.x) was a query syntax that routed to several backends: tech.ml.dataset, tablecloth, Clojask and Geni. Its code is on the v1 branch, its documentation is in this site's history, and posts from that time describe its design. Thanks to YANG Ming-Tian for the original v1 implementation.
Found a bug? Please open an issue. Datajure is distributed under the Apache License, Version 2.0.