| Signature | Description |
|---|---|
enum class asof_policy : unsigned char { // Default: // For each lhs value, find the greatest rhs value that is <= lhs value. // This is the classic "last known value" lookup used in time series work // (align trade prices to the most recent quote available at that time). // backward = 1, // For each lhs value, find the smallest rhs value that is >= lhs value. // Useful for "next event" lookups (e.g. next scheduled earnings date). // forward = 2, // For each lhs value, find the rhs value whose absolute distance to the // lhs value is smallest. Ties are broken in favour of backward (i.e. the // earlier rhs row). // nearest = 3, }; |
Policy for asof_join(): controls how each lhs value is matched to the nearest rhs value when an exact match does not exist. |
| Signature | Description | Parameters |
|---|---|---|
template<typename RHS_T, typename ... Ts> DataFrame asof_join(const RHS_T &rhs, asof_policy ap = asof_policy::backward, IndexType tolerance = IndexType { }) const; |
For every row in self (lhs), find the nearest row in rhs by index and combine their columns. The result carries lhs's index as its own index. Unlike join_by_index, which requires exact index matches, asof_join performs a nearest-neighbour lookup so it is well-suited for irregularly-sampled time series (tick data vs quotes, signals vs bars, etc.). Only lhs rows are kept (this is a left-join by design: every lhs row appears exactly once in the result). Both DataFrames must be sorted ascending by their index before calling this method. The implementation uses binary search (O(N log M)) and never modifies either input. Columns present in both frames are disambiguated with "lhs."/"rhs." prefixes, following the same convention used by join_by_index. |
RHS_T: Type of the rhs DataFrame Ts: List all the types of all data columns. A type should be specified in the list only once. rhs: rhs DataFrame ap: As-of join policy. See above tolerance: Optional upper bound on |lhs_idx - rhs_idx|. When the nearest rhs index is further away than tolerance, the rhs columns for that lhs row are filled with NaN. A tolerance of 0 (the default) disables the check and always propagates the nearest value regardless of distance. |
static void test_asof_join() { std::cout << "\nTesting asof_join( ) ..." << std::endl; using MyDataFrame = StdDataFrame<unsigned long>; MyDataFrame lhs, rhs; lhs.load_index(std::vector<unsigned long>{ 1, 3, 5, 7, 9 }); lhs.load_column<double>("price", std::vector<double>{ 1.1, 3.3, 5.5, 7.7, 9.9 }); rhs.load_index(std::vector<unsigned long>{ 2, 4, 6 }); rhs.load_column<double>("quote", std::vector<double>{ 20.0, 40.0, 60.0 }); // Test backward // { const auto result { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::backward) }; const auto &ridx { result.get_index() }; assert(ridx.size() == 5); assert(ridx[0] == 1 && ridx[1] == 3 && ridx[2] == 5 && ridx[3] == 7 && ridx[4] == 9); // lhs price column should be present unmodified // const auto &price { result.get_column<double>("price") }; assert(price[0] == 1.1 && price[4] == 9.9); // rhs quote column: lhs=1 has no rhs before it -> NaN // const auto "e { result.get_column<double>("quote") }; assert(std::isnan(quote[0])); // lhs=1: no rhs <= 1 assert(quote[1] == 20.0); // lhs=3: rhs=2 -> 20 assert(quote[2] == 40.0); // lhs=5: rhs=4 -> 40 assert(quote[3] == 60.0); // lhs=7: rhs=6 -> 60 assert(quote[4] == 60.0); // lhs=9: rhs=6 -> 60 (last carry) } // Test foreward // { const auto result { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::forward) }; const auto &ridx { result.get_index() }; assert(ridx.size() == 5); assert(ridx[0] == 1 && ridx[1] == 3 && ridx[2] == 5 && ridx[3] == 7 && ridx[4] == 9); const auto "e { result.get_column<double>("quote") }; // lhs price column should be present unmodified // const auto &price { result.get_column<double>("price") }; assert(price[0] == 1.1 && price[4] == 9.9); assert(quote[0] == 20.0); // lhs=1: rhs=2 -> 20 assert(quote[1] == 40.0); // lhs=3: rhs=4 -> 40 assert(quote[2] == 60.0); // lhs=5: rhs=6 -> 60 assert(std::isnan(quote[3])); // lhs=7: no rhs >= 7 assert(std::isnan(quote[4])); // lhs=9: no rhs >= 9 } // Test nearest // { const auto result { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::nearest) }; const auto &ridx { result.get_index() }; assert(ridx.size() == 5); assert(ridx[0] == 1 && ridx[1] == 3 && ridx[2] == 5 && ridx[3] == 7 && ridx[4] == 9); const auto "e { result.get_column<double>("quote") }; // lhs price column should be present unmodified // const auto &price { result.get_column<double>("price") }; assert(price[0] == 1.1 && price[4] == 9.9); assert(quote[0] == 20.0); // lhs=1: only forward rhs=2 assert(quote[1] == 20.0); // lhs=3: tie → backward -> rhs=2 assert(quote[2] == 40.0); // lhs=5: tie → backward -> rhs=4 assert(quote[3] == 60.0); // lhs=7: only backward -> rhs=6 assert(quote[4] == 60.0); // lhs=9: only backward -> rhs=6 } // Test exact match // { MyDataFrame lhs, rhs; lhs.load_index(std::vector<unsigned long>{ 3, 5, 8 }); lhs.load_column<double>("x", std::vector<double>{ 3.0, 5.0, 8.0 }); rhs.load_index(std::vector<unsigned long>{ 2, 5, 9 }); rhs.load_column<double>("y", std::vector<double>{ 200.0, 500.0, 900.0 }); // Backward // auto rb { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::backward) }; const auto &yb { rb.get_column<double>("y") }; assert(yb[0] == 200.0); // lhs=3: rhs=2 (back) assert(yb[1] == 500.0); // lhs=5: exact rhs=5 assert(yb[2] == 500.0); // lhs=8: rhs=5 (back, next is 9 which is >8) // forward // auto rf { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::forward) }; const auto &yf { rf.get_column<double>("y") }; assert(yf[0] == 500.0); // lhs=3: rhs=5 (fwd) assert(yf[1] == 500.0); // lhs=5: exact rhs=5 assert(yf[2] == 900.0); // lhs=8: rhs=9 (fwd) } // Test tolerance // { const unsigned long tol { 1UL }; const auto result { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::backward, tol) }; const auto &ridx { result.get_index() }; assert(ridx.size() == 5); assert(ridx[0] == 1 && ridx[1] == 3 && ridx[2] == 5 && ridx[3] == 7 && ridx[4] == 9); // lhs price column should be present unmodified // const auto &price { result.get_column<double>("price") }; assert(price[0] == 1.1 && price[4] == 9.9); const auto "e { result.get_column<double>("quote") }; assert(std::isnan(quote[0])); // lhs=1: no rhs <= 1 -> NaN assert(quote[1] == 20.0); // lhs=3: dist 1 <= tol assert(quote[2] == 40.0); // lhs=5: dist 1 <= tol assert(quote[3] == 60.0); // lhs=7: dist 1 <= tol assert(std::isnan(quote[4])); // lhs=9: dist 3 > tol -> NaN } // Empty rhs // { MyDataFrame lhs, rhs; lhs.load_index(std::vector<unsigned long>{ 1, 2, 3 }); lhs.load_column<double>("price", std::vector<double>{ 1.0, 2.0, 3.0 }); rhs.load_index(std::vector<unsigned long>{ }); rhs.load_column<double>("quote", std::vector<double>{ }); const auto result { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::backward) }; const auto &ridx { result.get_index() }; assert(ridx.size() == 3); assert(ridx[0] == 1 && ridx[1] == 2 && ridx[2] == 3); // lhs price column should be present unmodified // const auto &price { result.get_column<double>("price") }; assert(price[0] == 1.0 && price[1] == 2.0 && price[2] == 3.0); const auto "e { result.get_column<double>("quote") }; for (const auto &v : quote) assert(std::isnan(v)); } // Same column names // { MyDataFrame lhs, rhs; lhs.load_index(std::vector<unsigned long>{ 1, 3, 5 }); lhs.load_column<double>("value", std::vector<double>{ 10.0, 30.0, 50.0 }); rhs.load_index(std::vector<unsigned long>{ 2, 4 }); rhs.load_column<double>("value", std::vector<double>{ 20.0, 40.0 }); const auto result { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::backward) }; assert(result.has_column("lhs.value")); assert(result.has_column("rhs.value")); const auto &lv { result.get_column<double>("lhs.value") }; const auto &rv { result.get_column<double>("rhs.value") }; assert(lv[0] == 10.0 && lv[1] == 30.0 && lv[2] == 50.0); assert(std::isnan(rv[0])); // lhs=1: no rhs <= 1 assert(rv[1] == 20.0); // lhs=3: rhs=2 } // Single elements // { { // Single lhs, single rhs, exact MyDataFrame lhs, rhs; lhs.load_index(std::vector<unsigned long>{ 5 }); lhs.load_column<double>("x", std::vector<double>{ 1.0 }); rhs.load_index(std::vector<unsigned long>{ 5 }); rhs.load_column<double>("y", std::vector<double>{ 99.0 }); const auto r { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::backward) }; assert(r.get_column<double>("y")[0] == 99.0); } { // Single lhs, single rhs, lhs before rhs (backward → NaN) MyDataFrame lhs, rhs; lhs.load_index(std::vector<unsigned long>{ 3 }); lhs.load_column<double>("x", std::vector<double>{ 1.0 }); rhs.load_index(std::vector<unsigned long>{ 5 }); rhs.load_column<double>("y", std::vector<double>{ 99.0 }); const auto r { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::backward) }; assert(std::isnan(r.get_column<double>("y")[0])); } { // Single lhs, single rhs, lhs before rhs (forward → value) MyDataFrame lhs, rhs; lhs.load_index(std::vector<unsigned long>{ 3 }); lhs.load_column<double>("x", std::vector<double>{ 1.0 }); rhs.load_index(std::vector<unsigned long>{ 5 }); rhs.load_column<double>("y", std::vector<double>{ 99.0 }); const auto r { lhs.asof_join<MyDataFrame, double>(rhs, asof_policy::forward) }; assert(r.get_column<double>("y")[0] == 99.0); } } }