Well, If you work with time-series data like stocks, sensor logs, user clickstreams,

you have definitely faced the

Alignment Problem.

Example: You have two datasets.

  • Dataset A: User clicks (Timestamp: 10:00:01)
  • Dataset B: Server CPU logs (Timestamp: 10:00:05)

Now, you want to know:

“What was the CPU usage when the user clicked?”

Standard left_join or merge, you won’t get anything , right?

Why? Because 10:00:01 != 10:00:05. Computers are literal. Not human. They look for exact matches.

I know, I know.

To fix this, most beginners write complex for loops or round timestamps to the nearest minute (which clearly destroys accuracy)

There is a better way.

It is built directly into Pandas and Polars, and it is called the AsOf Join.

What is an AsOf Join?

Think of asof_join as a "Left Join + Time Travel."

It takes a row from your left table, looks at the right table, and finds the closest match that is not in the future (or strictly in the past, depending on your settings).

It asks: “What was the state of the world AS OF this specific timestamp?”

Okay, another example:

The Real World Scenario: High Frequency Trading (Simplified)

Imagine you are analyzing stock prices.

  • Trades: happen randomly when someone buys.
  • Quotes: happen randomly when the price changes.

You want to know the “Quote” price right before a “Trade” happened to see if you got a good deal.

import pandas as pd
# 1. Trades: The events we care about
trades = pd.DataFrame({
'time': pd.to_datetime(['09:30:00', '09:30:05', '09:30:12']),
'ticker': ['AAPL', 'AAPL', 'AAPL'],
'quantity': [100, 50, 200]
})
# 2. Quotes: The reference data (Notice the times DON'T match)
quotes = pd.DataFrame({
'time': pd.to_datetime(['09:29:55', '09:30:04', '09:30:14']),
'ticker': ['AAPL', 'AAPL', 'AAPL'],
'bid_price': [150.00, 150.10, 150.20]
})

If you run trades.merge(quotes, on='time'), you get an empty dataframe. The times never touch.

Now, we want to attach the most recent quote to each trade.

# The Magic Function
result = pd.merge_asof(
trades,
quotes,
on='time',
by='ticker', # Match the stock symbol exactly
direction='backward' # Look for the closest PAST timestamp
)
print(result)

Output:

time ticker  quantity  bid_price
0 2023-01-01 09:30:00 AAPL 100 150.0 (Matches 09:29:55)
1 2023-01-01 09:30:05 AAPL 50 150.1 (Matches 09:30:04)
2 2023-01-01 09:30:12 AAPL 200 150.1 (Matches 09:30:04, NOT 14)

Look at row 2: The trade happened at :12. The next quote is at :14.

A standard “nearest neighbor” search might accidentally grab the future quote (:14).
But direction=’backward’ also respects the laws of physics
You can’t trade on a price that hasn’t happened yet.

If you are dealing with millions of rows, Pandas might choke.

Polars does this natively and incredibly fast.

import polars as pl
# In Polars, tables MUST be sorted by the key for this to work
trades_pl = pl.from_pandas(trades).sort("time")
quotes_pl = pl.from_pandas(quotes).sort("time")
result = trades_pl.join_asof(
quotes_pl,
on="time",
by="ticker",
strategy="backward"
)

The Superpower: Tolerance

Sometimes, “close enough” isn’t good enough.

You don’t want to match a trade today with a price from 3 weeks ago just because it was the “last known price.”

So, you can set a tolerance.

pd.merge_asof(
trades,
quotes,
on='time',
tolerance=pd.Timedelta('2 seconds')
)

If the nearest record is older than 2 seconds, it returns NaN.

This is crucial for IoT sensor data where "stale" data is worse than "no" data.

Why You Should Use This

  • No More Resampling: You don’t have to round everything to the nearest minute and lose precision.
  • Performance: It is implemented in C (Pandas) and Rust (Polars). It is infinitely faster than applying a custom function row-by-row.
  • Clean Code: It turns 20 lines of “messy timestamp logic” into 1 line of readable code.

Next time you find yourself rounding timestamps just to make a .join() work, stop. You need an AsOf Join.

Have you read how I read my articles faster?

A message from our Founder

Hey, Sunil here. I wanted to take a moment to thank you for reading until the end and for being a part of this community.

Did you know that our team run these publications as a volunteer effort to over 3.5m monthly readers? We don’t receive any funding, we do this to support the community. ❤️

If you want to show some love, please take a moment to follow me on LinkedIn, TikTok, Instagram. You can also subscribe to our weekly newsletter.

And before you go, don’t forget to clap and follow the writer️!