Repository navigation
Design: Point-in-Time (AsOf) Joins #6498
desmondcheongzx
started this conversation in
Roadmaps
Replies: 3 comments
|
@universalmind303 raised a good follow-up offline: a top-level # Instead of:
df = df1.join_asof(df2, on="timestamp").join_asof(df3, on=...).join_asof(df4, ...)
# This:
df = daft.align_timeseries(
df1,
df2,
df3,
on="timestamp",
*other_dfs
)Common pattern in feature engineering when aligning N feature tables to a single label table. Implementation would just fold over |
0 replies
|
Hey @desmondcheongzx, I was interested in #2911 and wanted asof and inequality join support in daft. I started working on both of them and have working prototypes. Would it be possible for me to contribute? |
0 replies
|
Hi @paa1750, yes we're accepting contributions, please open up separate PRs for them! |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Tracking issue: #1400
What is a Point-in-Time Join?
A point-in-time (asof) join matches each row in the left table to the most recent row in the right table whose timestamp is at or before the left row's timestamp, within the same entity group. The temporal equivalent of "give me the last known value as of this moment."
Row
B @ 10:30does not matchB @ 11:00even though that row exists - it is in the future relative to the observation. And it does not require an exact match on09:00either. It finds the most recent preceding value.Motivation
ML feature engineering requires constructing training datasets by joining labels with precomputed features. Feature tables are updated at different cadences by different pipelines, so exact-timestamp joins produce mostly nulls. What you actually need is the most recent feature value at or before each observation time.
This is the standard operation in feature stores (Feast, Tecton, Hopsworks) and the biggest gap blocking users from adopting Daft for ML feature engineering workflows. Pandas has
merge_asofbut it's single-node. Spark has no first-class implementation (rewrites to correlated subqueries, that then decorrelates to a hash join + window function aggregate). Polars and DuckDB have dedicated operators but are single-node only. Daft can be the first distributed DataFrame engine with a native, performant asof join.Prior Art
ASOF JOIN ... ON eq AND ineq>=,>,<=,<df.join_asof(on, by, strategy)pd.merge_asof(on, by, direction)pyspark.pandas.merge_asof()(private)DuckDB hash-partitions both sides on equality keys into buckets, sorts each bucket on the asof key, then runs a two-pointer merge that stops at the first match per left row. Also has a "loop join" fallback for tiny probe tables (< 64 rows). No distributed execution.
Polars uses a single-pass O(M+N) merge on pre-sorted data without
bykeys. Withbykeys, it hash-partitions the right side, then does per-group asof scans. Parallelized across threads.Spark has no dedicated physical operator. The
RewriteAsOfJoinoptimizer rule rewrites asof joins into correlated scalar subqueries withMIN_BYaggregation - does not exploit sortedness for O(M+N) merge. Tempo (Databricks Labs) uses a union + window function workaround instead.Proposed API
Usage
Design decisions
how="asof"onjoin(). Asof joins have fundamentally different semantics from equi-joins (inequality matching, direction, tolerance). Extendingjoin()would force every optimizer rule that pattern-matches onJoinnodes to add asof guard clauses. A separateAsofJoinlogical plan node is invisible to existing optimizer passes.on= asof key,by= equality keys. This contradicts Daft'sjoin()whereonmeans equality keys. Butjoin_asofis a different method with its own docstring, and the Polars/Pandas convention is strong enough that users from either library will map instantly. The asof key is the defining parameter of the operation - it should be the primary positional argument.prefix/suffixcarried over from Daft's existingjoin()for column name conflict resolution.backwardonly.forward,nearest,tolerance, andallow_exact_matchesare followups.Implementation Plan
Phase 1: Core kernel + native runner
New
asof_joinkernel at theRecordBatchlevel. The algorithm is a modified two-pointer merge on data sorted by(by_keys, asof_key):Structurally similar to the existing
merge_inner_join()state machine inmerge_join.rs- the main difference is asymmetric timestamp comparison and tracking "best match so far" instead of emitting all equality matches.Effort estimate: <1 week
Phase 2: Distributed execution (hash partitioned)
Hash-partition both sides on the equality keys (
by), then run the local asof kernel per partition. This completely avoids cross-partition lookups because all rows for a given entity are co-located. Reuses existing hash repartitioning infrastructure.Effort estimate: ~1 week
Phase 3: Extended functionality
forwarddirection (first right row whereright.ts >= left.ts)nearestdirection (closest by absolute distance)toleranceparameter (max allowed time delta)allow_exact_matches=False(strictly less/greater than)Each is an incremental change to the kernel.
Effort estimate: <1 week
Phase 4: Distributed execution (range partitioned)
For the no-equality-keys case or extreme entity skew, range partition by
(by_keys, asof_key)with overlapping right partitions at boundaries. The key insight: we only need a 1-row overlap per partition boundary (prepend the last right row from partition i-1 to partition i) to produce correct results. This is strictly better than Tempo's approach, which duplicates an entire fraction of the data.Effort estimate: 1-2 weeks
Follow-ups (not scoped)
AsofJoinnodes. Individual asof joins cannot swap left/right, but independent asof joins can be reordered by cardinality.ASOF JOIN ... ON ... AND ...in Daft SQL.All reactions