Skip to content

Commit 2e1cd9c

Browse files
zzylolclaude
andauthored
polars: tag Hydra baseline subset keys by column (#74) (#195)
* polars: tag Hydra baseline subset keys by column (#74) The polars exact baselines keyed a label subset by its `;`-joined values, asap_sketchlib 0.2.2's subkey. 0.3.0's Hydra tags each value with its column (`label0:a;label1:b`), so on label columns that share a value domain the baseline merged a key1 group with the same key2 group and scored nonzero against itself. subset_key now builds the 0.3.0 key over hydra_shared's `label{i}` schema (escaping `\`, `:` and `;`), empty labels keep their column, and every lookup goes through it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhcWJExZcmVqS6r6rEtjis * polars: write subset key tags without a temporary String; fix test comment Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhcWJExZcmVqS6r6rEtjis --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 3b9e3db commit 2e1cd9c

4 files changed

Lines changed: 58 additions & 24 deletions

File tree

‎configs/datagen/hydra_columns.yaml‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,10 @@
3030
# comparable as `d` changes.
3131
#
3232
# The columns here share the default alphabet, so their value domains overlap.
33-
# The sketch is not affected: asap_sketchlib 0.3.0 keys each subset by column
34-
# (`key1:abc`), so a key1 value never collides with the same key2 value. The
35-
# `polars` exact baselines still build untagged subset keys (the 0.2.2
36-
# encoding, `polars_shared.rs`), so on this file they merge the key1 and key2
37-
# groups and do not score zero (#74; docs/rqe_optimizer_hydra.md §5.2). Give
38-
# each column its own `string.alphabet` and the baselines drop to exactly zero.
33+
# Neither side aliases them: asap_sketchlib 0.3.0 keys each subset by column
34+
# (`label0:abc`), and the `polars` exact baselines build the same tagged keys
35+
# (`polars_shared::subset_key`), so a key1 value never matches the same key2
36+
# value and the baselines score exactly zero (#74).
3937

4038
column_num: 3
4139
column_label: [key1, key2, value]

‎configs/datagen/hydra_kll_disjoint_labels.yaml‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
# `hydra_columns_f64.yaml` with a separate alphabet per label column, so a key1
2-
# value can never render the same as a key2 value (#74). The sketch keys each
3-
# subset by column (asap_sketchlib 0.3.0), so it doesn't need this; the
4-
# `polars` baseline still builds untagged subset keys, and on the shared
5-
# alphabet it merges a key1 group with the matching key2 group. Here the
6-
# baseline scores zero and any rank error is the sketch's. `export_rqe_optimizer_costs.sh` priced `hydra-kll` on it
7-
# until #174; HydraKLL has no saturation curve, so nothing measures it now.
2+
# value can never render the same as a key2 value. Since #74 the sketch and the
3+
# `polars` baseline both key subsets by column, so the shared alphabet no longer
4+
# aliases either; this file stays as the disjoint-domain case.
5+
# `export_rqe_optimizer_costs.sh` priced `hydra-kll` on it until #174; HydraKLL
6+
# has no saturation curve, so nothing measures it now.
87

98
column_num: 3
109
column_label: [key1, key2, value]

‎sketch-bench/src/wrappers/hydra_kll/polars.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ impl<T: QuantileValue + PolarsColumnItem> PolarsSubpopQuantile<T> {
4747
/// rank interval contains `phi * n`, which is what the rank-error
4848
/// comparator scores against, so an exact answer scores zero.
4949
pub fn estimate_subpop_quantile(&self, labels: &[&str], phi: f64) -> f64 {
50-
let Some(values) = self.sorted.get(&labels.join(";")) else {
50+
let Some(values) = self.sorted.get(&prefix_key(labels)) else {
5151
return f64::NAN;
5252
};
5353
if values.is_empty() {

‎sketch-bench/src/wrappers/polars_shared.rs‎

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use crate::wrappers::hll::CardinalityValue;
66
use ::polars::prelude::*;
77
use std::collections::HashMap;
8+
use std::fmt::Write as _;
89

910
/// the resulting key→count table for O(1) per-key queries.
1011
/// Shared by `cms/polars` and `countsketch/polars`.
@@ -262,28 +263,39 @@ impl<T: PolarsColumnItem> PolarsQuantileCore<T> {
262263
}
263264
}
264265

265-
/// The `;`-joined key of one label subset, in column order: the subset key
266-
/// asap_sketchlib 0.2.2's `Hydra::update` built, so the baseline matches a
267-
/// value in any label column. 0.3.0's Hydra keys a subset by column, so the
268-
/// two differ when label columns share a value domain.
266+
/// The key of the label subset `mask` picks out of `parts`, in column order,
267+
/// each value tagged with its column (`label0:a;label2:c`, with `\`, `:` and `;`
268+
/// escaped): asap_sketchlib 0.3.0's subkey over the `label{i}` schema
269+
/// `hydra_shared` declares. So a value in one column never matches the same
270+
/// value in another (#74), in the baseline as in the sketch.
269271
pub fn subset_key(parts: &[&str], mask: usize) -> String {
270272
let mut out = String::new();
271273
for (j, part) in parts.iter().enumerate() {
272274
if (mask >> j) & 1 == 1 {
273275
if !out.is_empty() {
274276
out.push(';');
275277
}
276-
out.push_str(part);
278+
let _ = write!(out, "label{j}:");
279+
for ch in part.chars() {
280+
if matches!(ch, '\\' | ':' | ';') {
281+
out.push('\\');
282+
}
283+
out.push(ch);
284+
}
277285
}
278286
}
279287
out
280288
}
281289

290+
/// The key a probe of the leading label columns `labels` reads.
291+
pub fn prefix_key(labels: &[&str]) -> String {
292+
subset_key(labels, (1 << labels.len()) - 1)
293+
}
294+
282295
/// Expand one record into `(subset_key, value)` rows, one per non-empty subset
283-
/// of its labels. Empty label parts are dropped, as asap_sketchlib 0.2.2's
284-
/// Hydra did (0.3.0's keeps them in their column).
296+
/// of its labels. An empty label keeps its column, as 0.3.0's Hydra does.
285297
pub fn fan_out<V: Clone>(key: &str, value: &V, keys: &mut Vec<String>, values: &mut Vec<V>) {
286-
let parts: Vec<&str> = key.split(';').filter(|s| !s.is_empty()).collect();
298+
let parts: Vec<&str> = key.split(';').collect();
287299
for mask in 1..(1usize << parts.len()) {
288300
keys.push(subset_key(&parts, mask));
289301
values.push(value.clone());
@@ -350,7 +362,7 @@ impl<T: PolarsFrequencyItem> PolarsSubpopFrequencyCore<T> {
350362

351363
pub fn query(&self, labels: &[&str], value: &T) -> f64 {
352364
self.counts
353-
.get(&(labels.join(";"), value.count_key()))
365+
.get(&(prefix_key(labels), value.count_key()))
354366
.copied()
355367
.unwrap_or(0) as f64
356368
}
@@ -415,7 +427,7 @@ impl<T: CardinalityValue> PolarsSubpopCardinalityCore<T> {
415427
}
416428

417429
pub fn query(&self, labels: &[&str]) -> f64 {
418-
self.distinct.get(&labels.join(";")).copied().unwrap_or(0) as f64
430+
self.distinct.get(&prefix_key(labels)).copied().unwrap_or(0) as f64
419431
}
420432

421433
pub fn memory_bytes(&self) -> usize {
@@ -484,7 +496,7 @@ impl<T: CardinalityValue> PolarsSubpopVectorCore<T> {
484496
}
485497

486498
pub fn query(&self, labels: &[&str]) -> f64 {
487-
self.exact.get(&labels.join(";")).copied().unwrap_or(0.0)
499+
self.exact.get(&prefix_key(labels)).copied().unwrap_or(0.0)
488500
}
489501

490502
pub fn memory_bytes(&self) -> usize {
@@ -521,3 +533,28 @@ fn grouped_values<T: CardinalityValue>(
521533
.expect("polars group_by collect"),
522534
)
523535
}
536+
537+
#[cfg(test)]
538+
mod tests {
539+
use super::*;
540+
541+
#[test]
542+
fn subset_keys_tag_each_value_with_its_column() {
543+
assert_eq!(subset_key(&["a", "b", "c"], 0b101), "label0:a;label2:c");
544+
assert_eq!(subset_key(&["x:y;z", ""], 0b11), r"label0:x\:y\;z;label1:");
545+
assert_eq!(prefix_key(&["a", "b"]), "label0:a;label1:b");
546+
}
547+
548+
/// #74: the column-0 group `a` holds 2 records (`a;a`, `a;b`). The old
549+
/// untagged key counted 3: `a;a`'s `a` once per column.
550+
#[test]
551+
fn a_value_shared_by_two_columns_is_counted_once() {
552+
let mut core = PolarsSubpopFrequencyCore::<i64>::default();
553+
core.update(&("a;a".to_string(), 7));
554+
core.update(&("a;b".to_string(), 7));
555+
core.finalize();
556+
assert_eq!(core.query(&["a"], &7), 2.0);
557+
assert_eq!(core.query(&["a", "a"], &7), 1.0);
558+
assert_eq!(core.query(&["b"], &7), 0.0);
559+
}
560+
}

0 commit comments

Comments
 (0)