Skip to main content

Gamma Lake

High-performance feature store built on Delta Lake with optional Ray parallelism

Build Status codecov License PyPI

Overview

Gamma Lake is a feature store built on Delta Lake with optional Ray parallelism, designed for efficient storage, versioning, and retrieval of time-indexed features backed by Polars DataFrames.

It was built to solve the real-world pain points that teams encounter when using flat Parquet files for ML feature storage: expensive column additions, no versioning, and painful cross-team collaboration.

Why Gamma Lake?

The most common alternative — storing features in per-day Parquet files — breaks down quickly:

Problem Gamma Lake's answer
Adding a new feature group rewrites all existing files Gamma Lake writes one new Delta table per feature group — O(1) cost regardless of existing data
Multiple teams can't write features in parallel Each feature group is an independent Delta table — concurrent writes don't conflict
No versioning or audit trail Delta Lake's transaction log provides full time-travel and version history
Cross-group reads require expensive joins Gamma Lake maintains a master index; reads are aligned horizontal concatenations
Experimental features pollute production data Features carry owner and version metadata; read() accepts a filtered metadata frame

See performance for benchmark data comparing Gamma Lake against per-day Parquet at scale.

Key Features

  • Sortable index — a master index table ensures all feature groups stay aligned for efficient range-filtered reads
  • Parallel reads and writes — Ray remote functions parallelise across feature groups for large-scale workloads
  • Feature versioning — every add_features() call records owner and version; read any historical version by filtering the metadata frame
  • Multiple signal types — standard features, as-of features, sparse features, and runtime-computed features
  • Native Delta Lake storage — ACID transactions, schema enforcement, and time-travel out of the box
  • Local or cloudbase_path accepts a local directory, an on-prem object store path, or an s3:// URI
  • Configurable compressionzstd by default; pluggable via compression field

Quick Start

import tempfile
from datetime import datetime, timezone

import polars as pl
import pyarrow as pa
from ccflow import ArrowSchema

from gammalake import GammaFeatureLake

# 1. Create and initialise a feature lake
with tempfile.TemporaryDirectory() as tmp:
    lake = GammaFeatureLake(base_path=tmp, run_on_ray_cluster=False)
    lake.initialize(
        ArrowSchema.make(
            pa.schema([("timestamp", pa.timestamp("us", tz="UTC")), ("symbol", pa.large_string())])
        )
    )

    # 2. Build a toy feature DataFrame (timestamp × symbol × features)
    now = datetime(2024, 1, 1, tzinfo=timezone.utc)
    df = pl.DataFrame({
        "timestamp": pl.Series([now]).cast(pl.Datetime("us", "UTC")),
        "symbol":    ["AAPL"],
        "momentum":  [0.42],
        "vol_20d":   [0.18],
    })

    # 3. Write features
    lake.add_features(df, owner="my-team")

    # 4. Read features back
    result = lake.read(["momentum", "vol_20d"])
    print(result)

    # 5. Inspect metadata
    print(lake.feature_metadata_frame().collect())

Run the full annotated demo: examples/quickstart.py

How Gamma Lake Works

The index and feature groups

Gamma Lake assumes each feature row has a unique, strictly ordered tuple key. In finance, (timestamp, symbol) is a common choice. A lake stores one master index Delta table and one Delta table per feature group:

Gamma Lake index and feature groups

Dense feature groups persist rows in canonical index order over their covered range. Missing rows inside that range are stored explicitly for alignment, while a group may remain shorter when the index grows only beyond its latest update. Reads never need keyed joins: Polars pads any missing trailing values during horizontal concatenation. Delta files are not assumed to be physically ordered; each source is sorted by the configured sort keys before assembly.

As-of and sparse feature tables are intentionally not padded. Their read paths first align them to the bounded master index with as-of and left joins, respectively, before they enter the same positional assembly path.

Reading features

A read:

  1. Resolves the minimum set of feature-group tables containing the requested features.
  2. Applies range and supported sort-key predicates to the master index and each source.
  3. Sorts each source by the same key order.
  4. Takes keys from the master index, drops duplicate keys from feature groups, and concatenates the feature values horizontally.

Reading aligned feature groups using the index

This positional concatenation is the central Gamma Lake invariant: it avoids repeated key hashing and multi-way joins as feature groups accumulate.

Feature groups do not need to end at the same index value:

Partially updated feature groups

Polars pads shorter groups with implicit trailing nulls during horizontal concatenation:

Concatenated feature groups with implicit nulls

The nulls are not stored in the shorter Delta tables. A useful mental model separates persisted values from the implicit aligned tail:

Persisted and implicit Gamma Lake values

Adding new rows and feature groups

When incoming keys extend beyond the current range, Gamma Lake writes the feature values and appends the keys to the master index once. Untouched dense groups may remain shorter because their missing trailing values are implicit. Adding or updating a dense group writes explicit alignment rows for gaps inside that group's covered range.

For example, appending values that continue a partially updated group requires no other table changes:

New values that continue an existing feature group

Feature group after a straightforward append

If an append skips index values inside the group's covered range, Gamma Lake stores null alignment rows for those internal gaps:

New values with gaps in the covered range

Feature group padded across internal gaps

Independent updates to multiple groups can run together:

Simultaneous updates to multiple feature groups

Feature groups after simultaneous updates

The resulting read remains positionally aligned:

Concatenated result after simultaneous updates

New keys can also fall inside the existing master-index range:

New index values inside the existing range

Groups covering that range receive explicit alignment rows where needed. Groups whose covered prefix ends before the new key remain untouched:

Feature groups after inserting a new index value

The read result combines stored values with implicit trailing nulls:

Read result after inserting a new index value

Stored and implicit null values after the update

Backfills can therefore be expensive. Adding a new secondary-key value across historical primary-key periods may require alignment updates to many feature groups; adding it only for future periods does not.

If incoming rows overlap existing rows, overlap_mode="copy" creates a new versioned Delta table, while overlap_mode="merge" performs an in-place Delta merge. Both paths preserve the alignment invariant.

Consider an update to a partially populated feature group:

Partially populated feature group before an overlap

Overlapping feature values

The default copy mode creates a new version while retaining the previous table:

Versioned copy of an overlapping feature group

Merge mode instead updates the current physical table:

Merged overlapping feature group

Default reads select the latest feature version. Copy mode preserves earlier physical tables that can be selected by filtering the metadata frame. Merge mode reuses the current table and does not retain pre-merge values as a separate feature version.

Lazy predicate pushdown

Local lazy reads use polars-io-tools to coordinate supported sort-key predicates into the bounded index and every feature source before positional concatenation. Runtime-computed reads defer downstream predicates until after computation to preserve neighbour context. As-of reads retain the right-hand history required for carry-forward semantics.

Projection-only aggregations remain correct, but a requested feature group that is later projected away must still read one column to preserve its row count during horizontal concatenation.

Parallel I/O

With run_on_ray_cluster=True, Gamma Lake dispatches feature-group operations independently through Ray. Group writes and the single master-index update run in parallel. Metadata becomes visible only after the feature and index writes succeed. Install Ray support with pip install "gamma-lake[ray]".

Metadata and versioning

Every feature write records its name, version, owner, table address, signal type, and optional feature parameters. read() selects the latest version by default; pass a filtered metadata frame to select an owner or historical version explicitly.

Concept Implementation
Master index One Delta table containing every configured sort-key tuple
Dense feature group Canonically ordered rows; missing trailing index values implicit
As-of or sparse group Sparse physical table aligned to the index during reads
New index rows Extend the index once; preserve aligned prefixes
Overlapping rows Versioned copy or Delta merge
Cross-group assembly Predicate-distributed positional horizontal concatenation
Parallel I/O Ray task per feature group
Versioning Append-only metadata records owner and version per feature column

Documentation

Document Description
Architecture Index design, feature groups, and append/merge semantics
Best practices Recommended versioning, update, and parallel-write patterns
Performance Benchmark comparison against per-day Parquet files
docs/benchmarking.md Running the portable ASV read/write benchmarks
examples/quickstart.py Runnable end-to-end demo

Core API

from gammalake import GammaFeatureLake

lake = GammaFeatureLake(base_path="s3://my-bucket/features")

# One-time setup
lake.initialize(schema)

# Writing
lake.add_features(df, owner="team-a")          # standard features
lake.add_targets(df, owner="team-a")           # target/label columns
lake.add_as_of_features(df, params, owner=...) # point-in-time safe features
lake.add_sparse_features(df, owner=...)        # sparse / infrequently-updated features

# Runtime features require an explicit opt-in because stored expressions are executable.
trusted_lake = GammaFeatureLake(base_path="s3://my-bucket/features", enable_runtime_computed_features=True)
trusted_lake.add_runtime_computed_features(exprs, ...)                     # arbitrary Polars expressions
trusted_lake.add_runtime_transforms(["feature_a"], ["abs", "reciprocal"])  # named row-local transforms

# Reading
lake.read(["feature_a", "feature_b"])                        # latest versions
lake.read(["feature_a"], start="2023-01-01", end="2024-01-01")  # date range
lake.read(lake.feature_metadata_frame()                      # specific owner/version
          .filter(pl.col("owner") == "team-a").collect())

# Metadata inspection
lake.feature_metadata_frame().collect()   # features, versions, owners
lake.table_metadata_frame().collect()     # Delta table details
lake.index_frame().collect()              # master index

Installation

gamma-lake can be installed via pip or conda, the two primary package managers for the Python ecosystem.

To install gamma-lake via pip, run this command in your terminal:

pip install gamma-lake

This installs the local execution dependencies without Ray. To enable distributed execution:

pip install "gamma-lake[ray]"

To install gamma-lake via conda, run this command in your terminal:

conda install gamma-lake -c conda-forge

Getting Started

See our wiki!

Development

Check out the contribution guide for more information.

License

Apache-2.0 — see LICENSE for details.

Release files for gamma-lake 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gamma-lake 0.2.0
File Size Uploaded
gamma_lake-0.2.0.tar.gz 69.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for gamma-lake 0.2.0
File Interpreter ABI Platform
gamma_lake-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 143.7 kB

Release files / gamma_lake-0.2.0.tar.gz

Download URL gamma_lake-0.2.0.tar.gz
Size 69.6 kB
Tags Source
SHA-256 checksum
How to use checksums
072f9af31cdd05feed599ded7a45edb90c975a82930298581db1268e08b356d8
BLAKE2b-256 checksum
How to use checksums
ad3604cd883a4b31954f85858a1659bff836c91ae420651dd608ea8619765bec
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.4

Release files / gamma_lake-0.2.0-py3-none-any.whl

Download URL gamma_lake-0.2.0-py3-none-any.whl
Size 74.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6e7fa5fa9ce19100118ebd860c1e3fa3bd51abe9b1bf818a235e158b50cef820
BLAKE2b-256 checksum
How to use checksums
ba8c990943e7c05fb4f639c7efc080ec8131d4e1eff21b35a3abc26bbe2458bb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.4

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page