Notebook integration
Run Ibex inside Python or R notebook workflows
Quickly switch between Python and Ibex cells in a notebook. The results from Ibex can seamlessly be used by later cells in the notebook.
Use Ibex as a DSL inside a real notebook workflow
The ibex_pyarrow bridge evaluates Ibex code and returns
a pyarrow.Table. The ibex_ipython extension
adds %%ibex, %ibexfile, and
%ibexreset, so notebooks can bind pandas or Arrow
tables and ordinary Python scalars into Ibex, keep expensive table
loads alive across cells, and keep plotting in Python.
Scalar bindings currently support Python int,
float, bool, str,
datetime.date, and datetime.datetime,
with the last two mapped to Ibex Date and
Timestamp.
This keeps Ibex focused on joins, aggregations, reshaping, and time-series work, while reusing pandas, matplotlib, and the rest of the notebook ecosystem for presentation.
Build the bridge with
cmake --build build-release --parallel --target ibex_pyarrow,
then start IPython from the repository root and load the extension.
# IPython / Jupyter
%load_ext ibex_ipython
import pandas as pd
trades = pd.DataFrame({
"symbol": ["AAPL", "AAPL", "MSFT"],
"qty": [10, 15, 7],
"px": [101.2, 101.5, 299.8],
})
offset = 10
%%ibex --bind trades=trades --bind offset=offset --as pandas --out grouped
trades[
select { total_qty = sum(qty + offset), avg_px = mean(px) },
by symbol,
order symbol
];
grouped.plot(kind="bar", x="symbol", y="total_qty");
Load once, reuse across notebook cells
%%ibex keeps table-valued let bindings in
a hidden Ibex session. Large CSV or parquet loads do not need to be
repeated in every cell.
%%ibex --quiet
import "csv";
let train = read_csv("../../kaggle/data/train.csv", "<empty>");
%%ibex --as pandas --out bucket_summary
train[select { rows = count() }, by seconds_in_bucket, order seconds_in_bucket];
%ibexreset # clear the hidden session
Use Ibex through native source or a lazy dplyr plan
The in-repo ibex package evaluates inline Ibex queries
or .ibex files from R. Results come back as a
data.frame by default, so the common
ggplot2 path is immediate. For lower-level interop,
format = "nanoarrow" returns the Arrow-backed result.
ibex_tbl() also records a supported subset of dplyr as
an immutable native plan. show_query() reveals the
generated Ibex, while collect() returns a tibble.
Unsupported R calls cross one explicit fallback boundary and then
remain in local dplyr; R closures never execute on Ibex workers.
Like the Python notebook path, ibex supports a
persistent Ibex session, so large CSV or parquet loads can happen
once and then be reused across later calls.
For literate reports, ibex can also register a real
{ibex} knitr engine inside R Markdown, so Ibex chunks
and ordinary R / ggplot2 chunks can share one named
session.
R input bindings are available by copy for named
data.frame tables and named scalar lists. Scalars
currently support R integer, double,
logical, character, Date,
and POSIXct.
# R
library(ibex)
library(ggplot2)
ibex::register_knitr_engines()
sess <- create_session(
plugin_paths = c("build-release/tools")
)
session_eval(
sess,
'
import "csv";
let iris = read_csv("data/iris.csv", "");
'
)
summary <- session_eval(
sess,
'
iris[
select { mean_sl = mean(Sepal_Length), n = count() },
by Species,
order Species
];
'
)
ggplot(summary, aes(Species, mean_sl)) +
geom_col()
query <- ibex_tbl(trades, fallback = "error") |>
filter(price > 10) |>
group_by(symbol) |>
summarise(total = sum(price), .groups = "drop")
show_query(query)
result <- collect(query)
Read delimited files with or without headers
The bundled CSV plugin supports custom delimiters and headerless
files. When has_header is false, Ibex
synthesizes column names col1, col2, and so on.
SQLite is also available through the optional ADBC plugin, so the same pipeline can start from a SQL query instead of a flat file when you want database-side staging first.
import "csv";
let raw = read_csv("examples/measurements.txt", "", ";", false)
[select { station = col1, temp = col2 }];
Generate feature families without leaving Ibex
Braced select and update blocks support a
compile-time map form. It expands from earlier string-list
let bindings into ordinary named fields before lowering,
so runtime execution stays unchanged.
Use backtick aliases with ${name} interpolation and
get(name) inside the map body to turn compile-time names
into column references.
columns(table_expr) returns a one-column metadata table
named name, so generated schemas can stay in Ibex
instead of being rebuilt in notebook host code.
let trades = Table {
symbol = ["AAPL", "AAPL", "GOOG"],
price = [150.0, 155.0, 140.0],
fee = [1.0, 1.5, 2.0]
};
let measures = ["price", "fee"];
trades[select {
symbol,
map m in measures => `avg_${m}` = mean(get(m))
}, by symbol];
let price_cols = ["ask_price", "bid_price", "wap"];
let book = Table {
ask_price = [101.0, 103.0],
bid_price = [99.0, 100.0],
wap = [100.0, 101.5]
};
book[update {
map (i, a) in price_cols, (j, b) in price_cols
where i > j
=> `${a}_${b}_imb` = (get(a) - get(b)) / (get(a) + get(b))
}];
let generated = columns(book);
generated[filter name != "row_id"];