Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Build output
build/
build*/

# Editor and tooling
.cache/
Expand Down
2 changes: 1 addition & 1 deletion data_processing/include/backend/data/filters/timegap.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace filters {

inline constexpr std::int64_t kMaxGapSeconds = 5 * 60;
inline constexpr std::int64_t kMaxGapSeconds = 60 * 60;

std::vector<geolife::Segment> split_by_time_gap(const geolife::Segment &segment,
std::int64_t max_gap_s);
Expand Down
6 changes: 3 additions & 3 deletions engine/configs/geolife.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"query_count": 0
},
"range": {
"query_count": 500,
"query_count": 20000,
"box_size_m": 2000.0,
"window_seconds": 604800,
"min_results": 5,
Expand All @@ -26,12 +26,12 @@
"max_gap_seconds": 60.0
},
"where_at_scoring": {
"hit_radius_m": 100.0,
"hit_radius_m": 150.0,
"min_tolerance_m": 25.0,
"max_tolerance_m": 500.0
},
"join": {
"query_count": 0,
"mode": "Self",
"eps_m": 500.0
}
}
8 changes: 6 additions & 2 deletions engine/include/backend/engine/evaluator/combined.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@

namespace evaluator {

// 0 = the floor (endpoints only), 1 = perfect. Query types average their own scores and combined
// averages the types, skipping NaN.
// 0 = the floor (endpoints only), 1 = perfect, without clipping. Combined averages
// one primary score per type: recall@k, range F1, where-at within-radius hit rate, join F1.
// Missing primaries are skipped, never replaced with a diagnostic score.
// The radius is configured by hit_radius_m (100 m for the research protocol).
// Average-hit remains a diagnostic and does not contribute to the combined score.
struct NormalizedScores {
static constexpr double missing = std::numeric_limits<double>::quiet_NaN();

double knn_recall_at_k = missing;
double knn_kendalls_tau_topk = missing;
double range_recall = missing;
double range_f1 = missing;
Comment thread
Kristofferjoha marked this conversation as resolved.
Resolved
double where_at_within_radius = missing;
double where_at_average_hit = missing;
double join_f1 = missing;
Expand Down
3 changes: 3 additions & 0 deletions engine/include/backend/engine/evaluator/evaluator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "backend/engine/query/knn.hpp"
#include "backend/engine/query/range_query.hpp"

#include <limits>
#include <span>

namespace evaluator {
Expand All @@ -26,6 +27,7 @@ struct Scoretable {
double range_recall;
WhereAtScores where_at;
JoinScores join;
double range_f1 = std::numeric_limits<double>::quiet_NaN();
};

struct EvaluationResult {
Expand All @@ -44,6 +46,7 @@ class Evaluator {
// Each runs only its own evaluation
KnnScores get_knn_scores() const;
double get_range_recall() const;
double get_range_f1() const;
WhereAtScores get_where_at_scores() const;
JoinScores get_join_scores() const;

Expand Down
14 changes: 11 additions & 3 deletions engine/include/backend/engine/evaluator/join.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@

namespace evaluator {

// Averages over queries. Precision skips queries with an empty simplified result, recall those
// with an empty reference, F1 those where both are empty. NaN if no query counts.
// HeldOut mode averages over queries. Self mode uses global unordered-pair micro scores.
// Self precision/recall are NaN for empty prediction/reference respectively; F1 is NaN
// only when both pair sets are empty, otherwise 2*TP/(predicted+reference).
// HeldOut: averages over queries. Precision skips queries with an empty simplified result, recall
// those with an empty reference, F1 those where both are empty. NaN if no query counts.
struct JoinScores {
double f1 = std::numeric_limits<double>::quiet_NaN();
double precision = std::numeric_limits<double>::quiet_NaN();
double recall = std::numeric_limits<double>::quiet_NaN();
double eps_m = std::numeric_limits<double>::quiet_NaN();
std::size_t query_count = 0;
std::size_t query_count = 0; // held-out queries only; zero for Self
query::JoinMode mode = query::JoinMode::HeldOut;
std::size_t candidate_pairs = 0;
std::size_t reference_pairs = 0;
std::size_t predicted_pairs = 0;
std::size_t true_positive_pairs = 0;
};

JoinScores evaluate_join(const core::TrajectoryDb &compressed, const query::JoinWorkload &workload);
Expand Down
4 changes: 4 additions & 0 deletions engine/include/backend/engine/evaluator/range.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace evaluator {

struct RangeEvaluation {
double recall = std::numeric_limits<double>::quiet_NaN();
double f1 = std::numeric_limits<double>::quiet_NaN();
std::vector<std::vector<std::int32_t>> answers;
};

Expand All @@ -26,4 +27,7 @@ namespace evaluator::range {
// Share of original that also appears in simplified. NaN if original is empty.
double recall(const std::vector<std::size_t> &original, const std::vector<std::size_t> &simplified);

// F1 over trajectory membership; NaN when both result sets are empty.
double f1(const std::vector<std::size_t> &original, const std::vector<std::size_t> &simplified);

} // namespace evaluator::range
16 changes: 13 additions & 3 deletions engine/include/backend/engine/query/join.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@

namespace query {

enum class JoinMode { HeldOut, Self };

struct JoinConfig {
std::size_t query_count = 10; // 0 = use every trajectory in the query pool
double eps_m = 500.0; // join threshold in metres, must be positive
std::size_t query_count = 10; // 0 = use every trajectory in the query pool
double eps_m = 500.0; // join threshold in metres, must be positive
JoinMode mode = JoinMode::HeldOut; // Self ignores query_count and the query pool
};

struct JoinQuery {
Expand All @@ -20,17 +23,24 @@ struct JoinQuery {
};

struct JoinWorkload {
// In Self mode this stores original endpoints/IDs for validation, not query inputs.
core::TrajectoryDb queries;
std::vector<JoinQuery> entries;
double eps_m = 0.0;
JoinMode mode = JoinMode::HeldOut;
// Self entries contain only indices j > i; both operands are evaluated compressed.
};

// Samples query_count trajectories from query_pool (the held-out split) and stores their join
// result in original as the reference answer
// result in original as the reference answer. Self mode delegates to generate_self_join.
JoinWorkload generate_join_queries(const core::TrajectoryDb &original,
const core::TrajectoryDb &query_pool,
const JoinConfig &config = {}, std::uint64_t seed = 42);

// All unordered database pairs, endpoint-pruned, with reference truth on originals.
// No held-out queries or sampling. Nonempty trajectories must retain their endpoints/IDs/order.
JoinWorkload generate_self_join(const core::TrajectoryDb &original, double eps_m = 500.0);

std::vector<std::size_t> join(const core::TrajectoryDb &trajectories,
const core::TrajectoryView &query, double eps);

Expand Down
8 changes: 4 additions & 4 deletions engine/src/evaluator/combined.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ NormalizedScores normalize_scores(const Scoretable &scores, const Scoretable &fl
result.knn_kendalls_tau_topk =
normalize(scores.knn.kendalls_tau_topk, floor.knn.kendalls_tau_topk);
result.range_recall = normalize(scores.range_recall, floor.range_recall);
result.range_f1 = normalize(scores.range_f1, floor.range_f1);
result.where_at_within_radius =
normalize(scores.where_at.within_radius, floor.where_at.within_radius);
result.where_at_average_hit =
normalize(scores.where_at.average_hit, floor.where_at.average_hit);
result.join_f1 = normalize(scores.join.f1, floor.join.f1);

result.knn = mean_ignoring_nan({result.knn_recall_at_k, result.knn_kendalls_tau_topk});
result.range = result.range_recall;
result.where_at =
mean_ignoring_nan({result.where_at_within_radius, result.where_at_average_hit});
result.knn = result.knn_recall_at_k;
result.range = result.range_f1;
result.where_at = result.where_at_within_radius;
result.join = result.join_f1;

result.combined = mean_ignoring_nan({result.knn, result.range, result.where_at, result.join});
Expand Down
7 changes: 6 additions & 1 deletion engine/src/evaluator/evaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ EvaluationResult Evaluator::evaluate() const {
return {.scores = {.knn = knn.scores,
.range_recall = range.recall,
.where_at = get_where_at_scores(),
.join = get_join_scores()},
.join = get_join_scores(),
.range_f1 = range.f1},
.knn = std::move(knn.answers),
.range = std::move(range.answers)};
}
Expand All @@ -39,6 +40,10 @@ double Evaluator::get_range_recall() const {
return evaluate_range(m_simplified, m_workloads.range).recall;
}

double Evaluator::get_range_f1() const {
return evaluate_range(m_simplified, m_workloads.range).f1;
}

WhereAtScores Evaluator::get_where_at_scores() const {
return evaluate_where_at(m_simplified, m_workloads.where_at, m_where_at_config);
}
Expand Down
56 changes: 49 additions & 7 deletions engine/src/evaluator/join.cpp
Comment thread
Kristofferjoha marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <cmath>
#include <cstddef>
#include <limits>
#include <stdexcept>
#include <vector>

namespace evaluator {
Expand Down Expand Up @@ -34,13 +35,35 @@ double average(const std::vector<double> &values) {

JoinScores evaluate_join(const core::TrajectoryDb &compressed,
const query::JoinWorkload &workload) {
const bool self = workload.mode == query::JoinMode::Self;
if (self) {
if (compressed.size() != workload.queries.size() ||
workload.entries.size() != compressed.size())
throw std::invalid_argument("self join: database size does not match reference");
for (std::size_t i = 0; i < compressed.size(); ++i) {
const auto a = compressed.trajectory(i);
const auto b = workload.queries.trajectory(i);
if (a.size() == 0 || b.size() == 0 || a.trajectory_id != b.trajectory_id ||
a.x.front() != b.x.front() || a.y.front() != b.y.front() ||
a.x.back() != b.x.back() || a.y.back() != b.y.back())
throw std::invalid_argument(
"self join: IDs/order or endpoints do not match reference");
}
}
const std::size_t n = workload.entries.size();
std::vector<Counts> totals(n);
const double nan = std::numeric_limits<double>::quiet_NaN();
std::vector<double> f1(n, nan), precision(n, nan), recall(n, nan);
std::vector<double> f1, precision, recall;
if (!self) {
f1.assign(n, nan);
precision.assign(n, nan);
recall.assign(n, nan);
}

core::parallel_for(n, [&](std::size_t i) {
const auto &entry = workload.entries[i];
const auto query = workload.queries.trajectory(entry.query_index);
const auto query = self ? compressed.trajectory(entry.query_index)
: workload.queries.trajectory(entry.query_index);

// Simplification keeps endpoints, so the original candidates cover every possible match
Counts counts;
Expand All @@ -52,6 +75,9 @@ JoinScores evaluate_join(const core::TrajectoryDb &compressed,
++counts.hits;
}

totals[i] = counts;
if (self)
return;
const auto hits = static_cast<double>(counts.hits);
const auto found = static_cast<double>(counts.found);
const auto truth = static_cast<double>(entry.reference.size());
Expand All @@ -63,11 +89,27 @@ JoinScores evaluate_join(const core::TrajectoryDb &compressed,
f1[i] = 2.0 * hits / (found + truth);
});

return {.f1 = average(f1),
.precision = average(precision),
.recall = average(recall),
.eps_m = workload.eps_m,
.query_count = n};
JoinScores result{.f1 = self ? nan : average(f1),
.precision = self ? nan : average(precision),
.recall = self ? nan : average(recall),
.eps_m = workload.eps_m,
.query_count = self ? 0 : n,
.mode = workload.mode};
for (std::size_t i = 0; i < n; ++i) {
result.candidate_pairs += workload.entries[i].candidates.size();
result.reference_pairs += workload.entries[i].reference.size();
result.predicted_pairs += totals[i].found;
result.true_positive_pairs += totals[i].hits;
}
if (self) {
const double hits = static_cast<double>(result.true_positive_pairs);
const double found = static_cast<double>(result.predicted_pairs);
const double truth = static_cast<double>(result.reference_pairs);
result.precision = found == 0 ? nan : hits / found;
result.recall = truth == 0 ? nan : hits / truth;
result.f1 = found + truth == 0 ? nan : 2.0 * hits / (found + truth);
}
return result;
}

} // namespace evaluator
23 changes: 22 additions & 1 deletion engine/src/evaluator/range.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ RangeEvaluation evaluate_range(const core::TrajectoryDb &compressed,
result.answers.resize(queries.size());
double sum = 0.0;
std::size_t scored = 0;
std::vector<double> recalls(queries.size());
std::vector<double> recalls(queries.size()), f1s(queries.size());
core::parallel_for(queries.size(), [&](std::size_t i) {
const auto &range_query = queries[i];
const auto simplified = query::range(compressed, range_query.box);
Expand All @@ -24,6 +24,7 @@ RangeEvaluation evaluate_range(const core::TrajectoryDb &compressed,
for (const auto index : simplified)
answer.push_back(compressed.trajectory(index).trajectory_id);
recalls[i] = range::recall(range_query.reference, simplified);
f1s[i] = range::f1(range_query.reference, simplified);
});
for (const double recall : recalls) {
if (!std::isnan(recall)) {
Expand All @@ -33,6 +34,16 @@ RangeEvaluation evaluate_range(const core::TrajectoryDb &compressed,
}
if (scored != 0)
result.recall = sum / static_cast<double>(scored);
sum = 0.0;
scored = 0;
for (const double f1 : f1s) {
if (!std::isnan(f1)) {
sum += f1;
++scored;
}
}
if (scored != 0)
result.f1 = sum / static_cast<double>(scored);
return result;
}

Expand All @@ -52,4 +63,14 @@ double recall(const std::vector<std::size_t> &original,
return static_cast<double>(hits) / static_cast<double>(original.size());
}

double f1(const std::vector<std::size_t> &original, const std::vector<std::size_t> &simplified) {
if (original.empty() && simplified.empty())
return std::numeric_limits<double>::quiet_NaN();
std::size_t hits = 0;
for (const auto i : simplified)
hits += std::ranges::binary_search(original, i);
return 2.0 * static_cast<double>(hits) /
(static_cast<double>(original.size()) + static_cast<double>(simplified.size()));
}

} // namespace evaluator::range
4 changes: 3 additions & 1 deletion engine/src/pipeline/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ void parse_where_at_scoring_config(Config &config, const json &scoring) {
}

void parse_join_config(Config &config, const json &join) {
validate_keys(join, {"query_count", "eps_m"}, "join");
validate_keys(join, {"query_count", "eps_m", "mode"}, "join");
if (join.contains("mode"))
config.join.mode = parse_enum<query::JoinMode>(join["mode"]);
config.join.query_count = join.value("query_count", config.join.query_count);
config.join.eps_m = join.value("eps_m", config.join.eps_m);
}
Expand Down
8 changes: 5 additions & 3 deletions engine/src/pipeline/pipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ void print_scores(const evaluator::Scoretable &scores) {
<< " queries)\n";
std::cout << " where-at: within radius=" << scores.where_at.within_radius
<< ", average hit=" << scores.where_at.average_hit << '\n';
std::cout << " range recall: " << scores.range_recall << '\n';
std::cout << " range F1: " << scores.range_f1 << ", recall: " << scores.range_recall << '\n';
std::cout << " join F1: " << scores.join.f1 << ", precision: " << scores.join.precision
<< ", recall: " << scores.join.recall << '\n';
}
Expand Down Expand Up @@ -100,8 +100,10 @@ void run(const Config &config) {
std::size_t join_pairs = 0;
for (const auto &entry : join.entries)
join_pairs += entry.reference.size();
std::cout << "join: " << join.entries.size() << " queries, eps=" << join.eps_m << " m, "
<< join_pairs << " matching pairs\n";
std::cout << "join " << magic_enum::enum_name(join.mode) << ": " << join.entries.size()
<< (join.mode == query::JoinMode::Self ? " database trajectories, eps="
: " queries, eps=")
<< join.eps_m << " m, " << join_pairs << " matching pairs\n";

if (config.export_results) {
io::export_workloads(config.export_dir, database, range.queries, knn_workload);
Expand Down
Loading
Loading