Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Analysis with Python (pandas, polars, DuckDB)  Module 6 – Combining data: concat and joins

Nearest-key and range joins: merge_asof

Join each row to the latest earlier record with pandas merge_asof, put values into ranges such as tariff slabs, and do the same in polars and DuckDB.

  • Advanced
  • 35 minutes
  • Examples run with Python 3.14.8, pandas 3.0.6, polars 1.44.2 and duckdb 1.5.6
  • By MySmartCoPilot
Free core, Pro depth: 11 of 35 minutes free
Free: Direction selects the eligible ordered key · A ping searches only its own train's timetable · Interval closure decides where a value belongs · Check yourself. With a pass: 3 in-depth parts and 3 interview answers, about 24 minutes.

What you will learn

  • Join each event to the latest earlier record with merge_asof
  • Match within groups with by= and cap the gap with tolerance
  • Map values to ranges such as tariff slabs with cut or an IntervalIndex
  • Translate the same joins to polars join_asof and DuckDB ASOF JOIN

Before you start

Some joins have no equal key to match on. A train’s position report at 06:20 belongs with the last station the train was due to pass before 06:20, and a meter reading of 250 units belongs with the tariff slab whose range contains 250. A nearest-key join: A join that selects a right-side partner by ordered key distance under a chosen direction, group and eligibility rule. attaches to each row the row of another table whose key is closest in one direction, usually the latest one at or before it. Range joins attach the interval that contains a value. pandas writes nearest-key joins with pd.merge_asof, and range joins with pd.cut or an IntervalIndex, because merge only matches equal keys. This lesson builds both on invented railway and electricity data, then shows the same joins in polars and DuckDB.

Direction selects the eligible ordered key

merge_asof works like a left join in which “equal” is replaced by “nearest in one direction”. For every left row it searches the right frame’s key column and takes one row:

T202 pings select the latest eligible earlier due station; 05:50 has none, while 06:20, 07:05 and 07:40 take XRD, YLM and ZTP.Train T202 · backwardPing timeLatest due stop05:5006:2007:0507:40No earlierdue timeXRD06:00YLM06:45ZTP07:30Selected due time ≤ ping timeForward chooses the next keyNearest compares key distanceSchedule match ≠ observed location

Backward selection matches each ping to its prior due time

Text description of the diagram

Two narrow columns show each ping and its backward timetable match. The 05:50 ping has no earlier due time. The 06:20 ping selects XRD due at 06:00, 07:05 selects YLM due at 06:45, and 07:40 selects ZTP due at 07:30.

Every selected due key is at or before its ping key. Forward instead selects the next eligible key, while nearest compares key distance. These are scheduled relationships; the matches do not establish the train's observed position.

  • direction="backward", the default, takes the latest right key at or before the left key.
  • direction="forward" takes the first right key at or after it.
  • direction="nearest" takes whichever of those two is closer.
  • by= adds an exact match on other columns first, so a train only looks at its own timetable.
  • tolerance= caps how far away a match may be, so an hours-old record is not passed off as current.

Analysts need nearest-key joins whenever one table records events and the other records states that change over time: the exchange rate in force at each payment, the price list valid on each order date, the last known status of a parcel at each scan. Every left row stays in the result once, as in a left join, so a nearest-key join never fans out. The price is a strict rule: both frames must be sorted by their key, or pandas raises a ValueError (pandas.merge_asof). Follow T202’s 06:20 key back to 06:00; key ordering alone does not establish information availability.

Interactive · stage by stage Ordered-key selection Follow T202's 06:20 key back to 06:00; key ordering alone does not establish information availability.
Ordered-key selection, step 3 of 3. Showing Ping, Backward candidates, Selected partner. In focus: Selected partner.PingT202 at 06:20Backward candidatesXRD due 06:00Selected partnerXRD; gap 20 minutes

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Ping. The left row supplies its train and event time.
  2. Backward candidates. Only eligible right keys at or before the event are candidates under backward direction.
  3. Selected partner. The latest eligible due time is 06:00. This scheduled relationship does not establish observed train position.

Key idea

Backward limits the selected key; event-time knowledge also depends on when the state became available.

Checkpoint

Check: Ordered-key selection

  1. One question What is the backward matching gap for T202's 06:20 ping and XRD's 06:00 due time?

    Type a number.

    Show the answer: One question

    Answer: 20 minutes

    06:20 − 06:00 is twenty minutes.

A ping searches only its own train’s timetable

Two invented trains send position pings, and the timetable says when each is due at each station. Each ping is matched with the last station its own train was due to pass:

The last station each train was due at, for every ping Python · train_asof.py
import pandas as pd

pings = pd.read_csv("rail/pings.csv", parse_dates=["ping_at"])
timetable = pd.read_csv("rail/timetable.csv", parse_dates=["due_at"])

matched = pd.merge_asof(
    pings.sort_values("ping_at"),
    timetable.sort_values("due_at"),
    left_on="ping_at",
    right_on="due_at",
    by="train",  # match within the same train only
)
matched["minutes_since"] = ((matched["ping_at"] - matched["due_at"]).dt.total_seconds() // 60).astype("Int64")

hhmm = {c: matched[c].dt.strftime("%H:%M") for c in ["ping_at", "due_at"]}
print(matched.assign(**hhmm).sort_values(["train", "ping_at"]).to_string(index=False))

Output

train ping_at station due_at  minutes_since
 T101   06:05     AMP  06:00              5
 T101   06:41     BLG  06:30             11
 T101   07:30     CDP  07:10             20
 T101   08:10     DVN  07:45             25
 T202   05:50     NaN    NaN           <NA>
 T202   06:20     XRD  06:00             20
 T202   07:05     YLM  06:45             20
 T202   07:40     ZTP  07:30             10

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 train_asof.py

Train T101’s 07:30 ping gets CDP, due at 07:10: its time is 20 minutes after the scheduled stop. A timetable match does not establish the train’s observed position or that it actually passed the station. The T202 ping at 05:50 comes before its first due time and gets no station: NaN, as a left join gives a row without a partner. The same pings with each direction:

backward, forward and nearest for the same pings Python · asof_directions.py
import pandas as pd

pings = pd.read_csv("rail/pings.csv", parse_dates=["ping_at"]).sort_values("ping_at")
timetable = pd.read_csv("rail/timetable.csv", parse_dates=["due_at"]).sort_values("due_at")

table = pings[["train", "ping_at"]].copy()
for direction in ["backward", "forward", "nearest"]:
    m = pd.merge_asof(pings, timetable, left_on="ping_at", right_on="due_at", by="train", direction=direction)
    table[direction] = m["station"].to_numpy()

table["ping_at"] = table["ping_at"].dt.strftime("%H:%M")
print(table.sort_values(["train", "ping_at"]).to_string(index=False))

Output

train ping_at backward forward nearest
 T101   06:05      AMP     BLG     AMP
 T101   06:41      BLG     CDP     BLG
 T101   07:30      CDP     DVN     DVN
 T101   08:10      DVN     NaN     DVN
 T202   05:50      NaN     XRD     XRD
 T202   06:20      XRD     YLM     XRD
 T202   07:05      YLM     ZTP     YLM
 T202   07:40      ZTP     NaN     ZTP

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 asof_directions.py

The three directions of merge_asof (train T202's 06:20 ping)
Criterion backwardforwardnearest
Takes The latest key at or beforeThe first key at or afterThe closer of the two
T202 at 06:20 XRD, due 06:00YLM, due 06:45XRD (20 min against 25)
No partner when No eligible earlier keyNo eligible later keyNo eligible key within the rules
When to choose Price, rate or status in force at a timeThe next scheduled eventMatching two clocks that drift

Backward chooses the latest eligible key at or before the event. That models a rate in force only when the key’s meaning and availability rules support it; a backdated correction published later can still introduce information that was unknown at the event. Forward answers “what comes next”, and nearest suits two sensors whose clocks drift a little in both directions. Compare the 06:00 and 06:45 candidates within T202 before selecting a direction.

Interactive · stage by stage Timetable membership Compare the 06:00 and 06:45 candidates within T202 before selecting a direction.
Timetable membership, step 3 of 3. Showing Event group, Direction choice, Start of the series. In focus: Start of the series.Event groupT202Direction choice06:00 or 06:45Start of the series05:50 has no past stop

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Event group. The exact train match restricts the candidate timetable rows.
  2. Direction choice. At 06:20, backward selects XRD and forward selects YLM; nearest chooses XRD because twenty is less than twenty-five.
  3. Start of the series. The earlier ping remains present with no backward partner. Its real location is still unknown.

Key idea

A close key from the wrong group is still the wrong partner.

Checkpoint

Check: Timetable membership

  1. One question How many minutes separate T202's 06:20 ping from its next due station at 06:45?

    Type a number.

    Show the answer: One question

    Answer: 25 minutes

    06:45 − 06:20 is twenty-five minutes.

Interval closure decides where a value belongs

Range joins match a value with the interval that contains it. Electricity tariffs are a typical case: the rate depends on which slab a reading falls into. Tax brackets, age bands in a survey, delivery zones by distance and discount tiers by basket value are the same problem with other labels.

Five meter readings, four tariff slabs Python · slab_lookup.py
import pandas as pd

meters = pd.DataFrame({"meter": ["M1", "M2", "M3", "M4", "M5"], "units": [40, 100, 101, 250, 640]})

edges = [0, 100, 200, 500, float("inf")]
names = ["(0, 100]", "(100, 200]", "(200, 500]", "(500, inf)"]
meters["slab"] = pd.cut(meters["units"], bins=edges, labels=names)

print(meters.to_string(index=False))

Output

meter  units       slab
   M1     40   (0, 100]
   M2    100   (0, 100]
   M3    101 (100, 200]
   M4    250 (200, 500]
   M5    640 (500, inf)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 slab_lookup.py

pd.cut takes the slab edges as bins and gives each interval a label. Its intervals are right-closed interval: An interval that includes its upper endpoint. In (a, b], values equal to b are included and values equal to a are excluded. by default, so a reading of exactly 100 lands in the first slab and 101 in the second (pandas.cut). An IntervalIndex holds the same intervals as an index and can look values up directly, and SQL engines such as DuckDB write range joins as a join on an inequality, units > lo AND units <= hi. Which side of the boundary is closed decides real money, so write it down with the slab table.

Run SQL on CSV & Excel Files Try a slab lookup on your own CSV with an inequality join in SQL, in the browser.

Write continuous boundaries as (0, 100] and (100, 200]. The label “101 to 200” would misdescribe a fractional reading such as 100.4, even though the numeric interval contains it. Follow exactly 100 and fractional 100.4 across the shared edges and identify their containing intervals.

Interactive · stage by stage Continuous interval edges Follow exactly 100 and fractional 100.4 across the shared edges and identify their containing intervals.
Continuous interval edges, step 3 of 3. Showing Edges, Right closure, Match one range. In focus: Match one range.Edges0, 100, 200, 500Right closure(0, 100]Match one rangelo < units ≤ hi

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Edges. The tariff bands share numeric boundaries rather than separate whole-number labels.
  2. Right closure. Exactly 100 belongs to the first band; 100.4 belongs to the next band.
  3. Match one range. Use cut, IntervalIndex or an inequality join under the same closure rule.

Key idea

Continuous interval labels should state the actual inclusive and exclusive edges.

Checkpoint

Check: Continuous interval edges

  1. One question With default right-closed bins [0, 100, 200], does 100 belong to the first interval?

    Choose one answer.

    Show the answer: One question

    Answer: Yes; the first interval includes its upper endpoint

    The inequality signs are part of the range's meaning.

Check yourself

Practice that adapts to you

Practise this lesson

8 questions about 12 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 8 Train T202 is due at XRD at 06:00 and at YLM at 06:45. Which station does merge_asof with its default direction give a ping at 06:20?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: XRD, the latest due time at or before 06:20

    The default, direction="backward", takes the last right row whose key is less than or equal to the left key. direction="forward" would give YLM, and direction="nearest" picks XRD here because 20 minutes is less than 25.

  2. Question 2 of 8 What must you do to both frames before calling pd.merge_asof?

    Choose one answer.

    Show the answer: Question 2 of 8

    Answer: Sort them by the key they are matched on

    Pandas merge_asof requires both time keys to be globally ascending. Sorting by train then time can restart the global key; by= restricts identity and does not replace this ordering requirement.

  3. Question 3 of 8 What does by="train" add to a merge_asof of pings with a timetable?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: A ping is matched only with timetable rows of the same train

    by= adds an exact-match condition: the nearest key is searched only among right rows whose by column equals the left row's. Without it, a ping could take another train's station.

  4. Question 4 of 8 pd.cut(units, bins=[0, 100, 200]) with its defaults. Which interval does a reading of exactly 100 fall into?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: (0, 100], because the intervals are closed on the right

    pd.cut builds intervals closed on the right by default (right=True), so 100 belongs to (0, 100]. With right=False it would fall into [100, 200).

  5. Question 5 of 8 Which of these can attach the range a value falls into?

    Choose every answer that is right.

    Show the answer: Question 5 of 8

    Answer:

    • pd.cut with bins and labels
    • A SQL join on an inequality such as units > lo AND units <= hi
    • IntervalIndex.get_indexer

    cut and an IntervalIndex look a value up among intervals, and an inequality join does the same in SQL. An ordinary merge needs equal keys, so a reading of 250 would never match a slab row.

  6. Question 6 of 8 A state row is effective from 09:00 but was published at 11:00. Is a backward join on effective time alone safe for a report of information known at 10:00?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: No; the publication or knowledge-availability rule must also be enforced

    The meaning of the time key determines what a backward result can establish.

  7. Question 7 of 8 Another train has a closer due time. Which argument prevents T202's ping taking that station?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: by="train"

    Nearest distance and exact group membership are separate conditions.

  8. Question 8 of 8 A report labels the second continuous band 101 to 200. Where does 100.4 belong under (100, 200]?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: The second band; change the label to express its true edges

    Labels must describe the continuous boundary rule rather than imply an unrequested rounding.

merge_asof in depth: tolerance, exact matches and sorting rules Pro: opens with a Pro, Premium or Ultimate pass

A nearest-key join is only as good as its limits: how stale a match may be, whether an exact tie counts, and what the inputs must look like. This part shows each limit with recorded output, reproduces the errors merge_asof raises, and finishes with an exercise on exchange rates.

In this part:

  • Tolerance and exact-match rules limit eligible partners
  • Pandas requires globally ordered compatible time keys
  • Correction order and rate age determine priced payments

3 diagrams · 3 runnable examples (Python) · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Range joins: slab boundaries, IntervalIndex and SQL, with a bill Pro: opens with a Pro, Premium or Ultimate pass

Range joins go wrong at the edges of the ranges. This part pushes awkward readings through pd.cut, an IntervalIndex and DuckDB, reproduces a boundary that matches twice and a gap that matches nothing, and works out a slab-wise bill.

In this part:

  • Boundary cases test the tariff’s exact interval contract
  • One strict inequality prevents shared-edge double matches
  • Slab-wise charges and whole-reading charges are different

3 diagrams · 3 runnable examples (Python) · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

The same joins in polars and DuckDB, and a silent wrong answer Pro: opens with a Pro, Premium or Ultimate pass

polars and DuckDB both have nearest-key joins, and on sorted input they agree with pandas row for row. This part shows that agreement, then reproduces the case where polars returns a wrong station without an error.

In this part:

  • Equivalent rules agree on the eight recorded pings
  • An unchecked grouped order can select a future station
  • Engine choice includes ordering and failure behavior

3 diagrams · 2 runnable examples (Python) · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    What do the three directions of merge_asof mean, and which one answers "what was in force at that time"?

    Backward takes the latest eligible right key at or before the left key; forward takes the first at or after it; nearest chooses the smaller distance. Backward models a state in force only when the time key and information-availability contract support that interpretation. An unmatched left row remains with missing right values. Group equality restricts candidates to the intended identity, and eligibility rules can remove distant candidates. Pandas requires global ascending order by the time key, while other engines have their own ordering contracts.

  2. Mid to senior

    Convert 3 million card payments in foreign currencies to rupees at the rate in force when each was paid. Rates are published once per working day per currency. Design the join, and say what happens on weekends, before the first rate and when a published rate is corrected.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Fresher to mid

    A billing job attaches tariff slabs to meter readings with a SQL join on units BETWEEN lo AND hi. Customers whose readings are exactly 100, 200 or 500 units complain about their bills. What went wrong, and how do you redesign and test the slab join?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    A team ports a pandas merge_asof of GPS pings to timetables, by train, to polars join_asof for speed. The tests pass, but in production a few pings get stations that are due in the future. Explain how that can happen and how you would make the port safe.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A nearest-key join attaches to each row the closest row of another table in one direction; pandas writes it as merge_asof, which keeps every left row once.
  • Sort pandas inputs globally by the time key, use by= for exact group identity, and choose direction, availability and eligibility rules from the event-state contract.
  • Range joins attach the interval that contains a value: pd.cut and an IntervalIndex in pandas, an inequality join in SQL. Decide which end of each interval is closed, and write it down.

Terms in this lesson

nearest-key join
A join that selects a right-side partner by ordered key distance under a chosen direction, group and eligibility rule.
right-closed interval
An interval that includes its upper endpoint. In (a, b], values equal to b are included and values equal to a are excluded.

Corrections

  • Added concept figures and practice, clarified join and schema contracts, and corrected meter alignment; explanations remain in their original reading parts.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.