Repository navigation
clustering #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
clustering #49
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| { | ||
| "data_path": "data/geolife.parquet", | ||
| "data_path": "data/geolife_gap60.parquet", | ||
| "export_results": true, | ||
| "export_dir": "data/output/geolife", | ||
| "seed": 42, | ||
|
|
@@ -34,5 +34,10 @@ | |
| "join": { | ||
| "mode": "Self", | ||
| "eps_m": 500.0 | ||
| }, | ||
| "clustering": { | ||
| "enabled": false, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why enabled false as standard
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is currently cooked. Next pr fixes it. Its split in two, could prob have been a bit smarter |
||
| "eps_m": 150.0, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do join and clustering need different distances? 150 vs 500
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see next pr about cont frechet. 150 works best, this is just a temporary step. join will also be 150
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. alr |
||
| "min_size": 5 | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| #pragma once | ||
|
|
||
| #include "backend/engine/core/trajectory_db.hpp" | ||
| #include "backend/engine/query/route_clustering.hpp" | ||
|
|
||
| #include <cstddef> | ||
| #include <limits> | ||
|
|
||
| namespace evaluator { | ||
|
|
||
| // Reruns the route clustering on the compressed database and compares it with the reference. | ||
| // score is the symmetric macro best-match F1: each cluster's best overlap F1 with any cluster on | ||
| // the other side, averaged per side, then over both sides. NaN without reference clusters, 0 if | ||
| // nothing is predicted. | ||
| struct RouteClusteringScores { | ||
| static constexpr double missing = std::numeric_limits<double>::quiet_NaN(); | ||
|
|
||
| double score = missing; | ||
| std::size_t reference_clusters = 0; | ||
| std::size_t predicted_clusters = 0; | ||
| std::size_t reference_covered = 0; | ||
| std::size_t predicted_covered = 0; | ||
| }; | ||
|
|
||
| query::RouteClusters compressed_route_clusters(const core::TrajectoryDb &compressed, | ||
| const query::RouteClusteringWorkload &workload); | ||
|
|
||
| RouteClusteringScores compare_route_clusters(const query::RouteClusters &reference, | ||
| const query::RouteClusters &predicted); | ||
|
|
||
| RouteClusteringScores evaluate_route_clustering(const core::TrajectoryDb &compressed, | ||
| const query::RouteClusteringWorkload &workload); | ||
|
|
||
| } // namespace evaluator |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| #pragma once | ||
|
|
||
| #include "backend/engine/core/trajectory_db.hpp" | ||
| #include "backend/engine/query/join.hpp" | ||
|
|
||
| #include <cstddef> | ||
| #include <cstdint> | ||
| #include <span> | ||
| #include <utility> | ||
| #include <vector> | ||
|
|
||
| namespace query { | ||
|
|
||
| struct RouteClusteringConfig { | ||
| bool enabled = false; | ||
| double eps_m = 500.0; // neighbour threshold, same test as the self join | ||
| std::size_t min_size = 5; // a cluster needs at least this many trajectories, medoid included | ||
| }; | ||
|
|
||
| using TrajectoryPair = std::pair<std::size_t, std::size_t>; | ||
|
|
||
| // Greedy medoid grouping. Repeatedly takes the unassigned trajectory with the most unassigned | ||
| // neighbours and groups it with them if that gives at least min_size trajectories. Every member | ||
| // neighbours its medoid, so clusters cannot chain. Ties go to the earliest start time, then the | ||
| // smallest start x, then the smallest trajectory ID, which makes the result order independent. | ||
| struct RouteClusters { | ||
| std::vector<std::ptrdiff_t> label; // cluster per trajectory, -1 = unclustered | ||
| std::vector<std::vector<std::size_t>> members; // members[c][0] is the medoid | ||
| }; | ||
|
|
||
| RouteClusters cluster_routes(const core::TrajectoryDb &trajectories, | ||
| std::span<const TrajectoryPair> neighbours, std::size_t min_size); | ||
|
|
||
| // ID and endpoints of an original trajectory. The candidate pairs were pruned by endpoint | ||
| // distance, so a database is only comparable if every one of these matches. | ||
| struct TrajectoryEndpoints { | ||
| std::int32_t trajectory_id = 0; | ||
| double first_x = 0.0, first_y = 0.0, last_x = 0.0, last_y = 0.0; | ||
| bool operator==(const TrajectoryEndpoints &) const = default; | ||
| }; | ||
|
|
||
| TrajectoryEndpoints endpoints_of(const core::TrajectoryView &trajectory); | ||
|
|
||
| struct RouteClusteringWorkload { | ||
| std::vector<TrajectoryPair> candidates; // endpoint-pruned pairs, i < j | ||
| RouteClusters reference; | ||
| std::vector<TrajectoryEndpoints> originals; | ||
| double eps_m = 0.0; | ||
| std::size_t min_size = 0; | ||
| }; | ||
|
|
||
| // Clusters the original database. A self join workload over the same trajectories and eps is | ||
| // reused instead of recomputing the Fréchet tests on the originals. | ||
| RouteClusteringWorkload generate_route_clustering(const core::TrajectoryDb &original, | ||
| const RouteClusteringConfig &config = {}, | ||
| const JoinWorkload *self_join = nullptr); | ||
|
|
||
| } // namespace query |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| #include "backend/engine/evaluator/route_clustering.hpp" | ||
| #include "backend/engine/core/parallel.hpp" | ||
| #include "backend/engine/query/frechet_helper.hpp" | ||
|
|
||
| #include <algorithm> | ||
| #include <cstddef> | ||
| #include <stdexcept> | ||
| #include <unordered_map> | ||
| #include <vector> | ||
|
|
||
| namespace evaluator { | ||
|
|
||
| namespace { | ||
|
|
||
| // Mean over a's clusters of the best overlap F1 with any of b's clusters | ||
| double best_match(const query::RouteClusters &a, const query::RouteClusters &b) { | ||
| double sum = 0.0; | ||
| for (const auto &members : a.members) { | ||
| std::unordered_map<std::ptrdiff_t, std::size_t> overlap; | ||
| for (const auto i : members) { | ||
| if (b.label[i] >= 0) | ||
| ++overlap[b.label[i]]; | ||
| } | ||
| double best = 0.0; | ||
| for (const auto &[cluster, shared] : overlap) { | ||
| const auto size = members.size() + b.members[cluster].size(); | ||
| best = std::max(best, 2.0 * static_cast<double>(shared) / static_cast<double>(size)); | ||
| } | ||
| sum += best; | ||
| } | ||
| return sum / static_cast<double>(a.members.size()); | ||
| } | ||
|
|
||
| std::size_t covered(const query::RouteClusters &clusters) { | ||
| std::size_t total = 0; | ||
| for (const auto &members : clusters.members) | ||
| total += members.size(); | ||
| return total; | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| query::RouteClusters compressed_route_clusters(const core::TrajectoryDb &compressed, | ||
| const query::RouteClusteringWorkload &workload) { | ||
| if (compressed.size() != workload.originals.size()) | ||
| throw std::invalid_argument("route clustering: database size does not match reference"); | ||
| // Simplification keeps endpoints, so the original candidates cover every possible match | ||
| for (std::size_t i = 0; i < compressed.size(); ++i) { | ||
| const auto trajectory = compressed.trajectory(i); | ||
| if (trajectory.size() == 0 || query::endpoints_of(trajectory) != workload.originals[i]) | ||
| throw std::invalid_argument( | ||
| "route clustering: IDs/order or endpoints do not match reference"); | ||
| } | ||
|
|
||
| std::vector<char> matched(workload.candidates.size(), 0); | ||
| core::parallel_for(workload.candidates.size(), [&](std::size_t k) { | ||
| const auto [i, j] = workload.candidates[k]; | ||
| matched[k] = query::frechet_within(compressed.trajectory(i), compressed.trajectory(j), | ||
| workload.eps_m); | ||
| }); | ||
| std::vector<query::TrajectoryPair> neighbours; | ||
| for (std::size_t k = 0; k < matched.size(); ++k) { | ||
| if (matched[k]) | ||
| neighbours.push_back(workload.candidates[k]); | ||
| } | ||
| return query::cluster_routes(compressed, neighbours, workload.min_size); | ||
| } | ||
|
|
||
| RouteClusteringScores compare_route_clusters(const query::RouteClusters &reference, | ||
| const query::RouteClusters &predicted) { | ||
| if (reference.label.size() != predicted.label.size()) | ||
| throw std::invalid_argument("route clustering: label sizes differ"); | ||
|
|
||
| RouteClusteringScores scores; | ||
| scores.reference_clusters = reference.members.size(); | ||
| scores.predicted_clusters = predicted.members.size(); | ||
| scores.reference_covered = covered(reference); | ||
| scores.predicted_covered = covered(predicted); | ||
| if (reference.members.empty()) | ||
| return scores; | ||
|
|
||
| scores.score = | ||
| predicted.members.empty() | ||
| ? 0.0 | ||
| : 0.5 * (best_match(reference, predicted) + best_match(predicted, reference)); | ||
| return scores; | ||
| } | ||
|
|
||
| RouteClusteringScores evaluate_route_clustering(const core::TrajectoryDb &compressed, | ||
| const query::RouteClusteringWorkload &workload) { | ||
| return compare_route_clusters(workload.reference, | ||
| compressed_route_clusters(compressed, workload)); | ||
| } | ||
|
|
||
| } // namespace evaluator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leftover