Skip to content
Open
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
2 changes: 1 addition & 1 deletion benchmarks/tdigest/compress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ fn initial_buffer(bencher: Bencher) {
fn unmerged_tail(bencher: Bencher) {
let values = values(3_280);
let mut digest = build_mut_digest(&values[..1_640]);
black_box(digest.rank(0.5));
black_box(digest.rank(0.5).unwrap().unwrap());
for &value in &values[1_640..] {
digest.update(value);
}
Expand Down
8 changes: 4 additions & 4 deletions benchmarks/tdigest/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ fn merge(bencher: Bencher) {
let values = values(200_000);
let mut left = build_mut_digest(&values[..100_000]);
let mut right = build_mut_digest(&values[100_000..]);
black_box(left.rank(0.5));
black_box(right.rank(0.5));
black_box(left.rank(0.5).unwrap().unwrap());
black_box(right.rank(0.5).unwrap().unwrap());

bencher
.counter(ItemsCount::new(values.len()))
Expand Down Expand Up @@ -65,7 +65,7 @@ fn small_partials(bencher: Bencher) {
let partials = partial_digests(64, SMALL_ROWS_PER_PARTIAL)
.into_iter()
.map(|mut digest| {
black_box(digest.rank(0.0));
black_box(digest.rank(0.0).unwrap().unwrap());
digest
})
.collect::<Vec<_>>();
Expand All @@ -86,7 +86,7 @@ fn partials(bencher: Bencher) {
let partials = partial_digests_with(DEFAULT_DIGEST_K, 64, ROWS_PER_PARTIAL)
.into_iter()
.map(|mut digest| {
black_box(digest.rank(0.0));
black_box(digest.rank(0.0).unwrap().unwrap());
digest
})
.collect::<Vec<_>>();
Expand Down
5 changes: 4 additions & 1 deletion datasketches/src/kll/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@
//! let mut sketch = KllSketch::<i64>::new(200).unwrap();
//! sketch.update(1);
//! sketch.update(2);
//! let q = sketch.quantile(0.5, SearchCriteria::Inclusive).unwrap();
//! let q = sketch
//! .quantile(0.5, SearchCriteria::Inclusive)
//! .unwrap()
//! .unwrap();
//! assert!((1..=2).contains(&q));
//! ```

Expand Down
60 changes: 35 additions & 25 deletions datasketches/src/kll/sketch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,10 @@ impl<T: Clone + Ord> KllSketch<T> {

/// Returns the normalized rank of the given item.
///
/// # Errors
///
/// Returns an error if the sketch is empty.
pub fn rank(&self, item: &T, criteria: SearchCriteria) -> Result<f64, Error> {
/// Returns `None` if the sketch is empty.
pub fn rank(&self, item: &T, criteria: SearchCriteria) -> Option<f64> {
if self.is_empty() {
return Err(Error::invalid_argument("cannot query an empty sketch"));
return None;
}
let inclusive = criteria == SearchCriteria::Inclusive;
let mut weight = 0u64;
Expand All @@ -221,60 +219,72 @@ impl<T: Clone + Ord> KllSketch<T> {
.count() as u64;
weight += count << level;
}
Ok(weight as f64 / self.n as f64)
Some(weight as f64 / self.n as f64)
}

/// Returns the quantile for the given normalized rank.
///
/// Returns `Ok(None)` if the sketch is empty.
///
/// # Errors
///
/// Returns an error if the sketch is empty or `rank` is outside `[0.0, 1.0]`.
pub fn quantile(&self, rank: f64, criteria: SearchCriteria) -> Result<T, Error> {
if self.is_empty() {
return Err(Error::invalid_argument("cannot query an empty sketch"));
}
/// Returns an error if `rank` is outside `[0.0, 1.0]`.
pub fn quantile(&self, rank: f64, criteria: SearchCriteria) -> Result<Option<T>, Error> {
if !(0.0..=1.0).contains(&rank) {
return Err(Error::invalid_argument(format!(
"rank must be in [0.0, 1.0], got {rank}"
)));
}
if self.is_empty() {
return Ok(None);
}
self.sorted_view().quantile(rank, criteria)
}

/// Returns approximate quantiles for the given normalized ranks.
///
/// The sorted view is built once for the whole batch.
///
/// Returns `Ok(None)` if the sketch is empty.
///
/// # Errors
///
/// Returns an error if the sketch is empty or any rank is outside `[0.0, 1.0]`.
pub fn quantiles(&self, ranks: &[f64], criteria: SearchCriteria) -> Result<Vec<T>, Error> {
/// Returns an error if any rank is outside `[0.0, 1.0]`.
pub fn quantiles(
&self,
ranks: &[f64],
criteria: SearchCriteria,
) -> Result<Option<Vec<T>>, Error> {
self.sorted_view().quantiles(ranks, criteria)
}

/// Returns the approximate CDF for the given split points.
///
/// Returns `Ok(None)` if the sketch is empty.
///
/// # Errors
///
/// Returns an error if the sketch is empty or the split points are not unique and strictly
/// increasing.
pub fn cdf(&self, split_points: &[T], criteria: SearchCriteria) -> Result<Vec<f64>, Error> {
if self.is_empty() {
return Err(Error::invalid_argument("cannot query an empty sketch"));
}
/// Returns an error if the split points are not unique and strictly increasing.
pub fn cdf(
&self,
split_points: &[T],
criteria: SearchCriteria,
) -> Result<Option<Vec<f64>>, Error> {
self.sorted_view().cdf(split_points, criteria)
}

/// Returns the approximate PMF for the given split points.
///
/// Returns `Ok(None)` if the sketch is empty.
///
/// # Errors
///
/// Returns an error if the sketch is empty or the split points are not unique and strictly
/// increasing.
pub fn pmf(&self, split_points: &[T], criteria: SearchCriteria) -> Result<Vec<f64>, Error> {
if self.is_empty() {
return Err(Error::invalid_argument("cannot query an empty sketch"));
}
/// Returns an error if the split points are not unique and strictly increasing.
pub fn pmf(
&self,
split_points: &[T],
criteria: SearchCriteria,
) -> Result<Option<Vec<f64>>, Error> {
self.sorted_view().pmf(split_points, criteria)
}

Expand Down
91 changes: 62 additions & 29 deletions datasketches/src/kll/sorted_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,10 @@ impl<T: Clone + Ord> SortedView<T> {

/// Returns the approximate normalized rank of `item`.
///
/// # Errors
///
/// Returns an error if the view is empty.
pub fn rank(&self, item: &T, criteria: SearchCriteria) -> Result<f64, Error> {
/// Returns `None` if the view is empty.
pub fn rank(&self, item: &T, criteria: SearchCriteria) -> Option<f64> {
if self.is_empty() {
return Err(Error::invalid_argument("cannot query an empty view"));
return None;
}
let index = if criteria == SearchCriteria::Inclusive {
upper_bound(&self.entries, item)
Expand All @@ -80,25 +78,27 @@ impl<T: Clone + Ord> SortedView<T> {
};

if index == 0 {
return Ok(0.0);
return Some(0.0);
}
Ok(self.entries[index - 1].cumulative_weight as f64 / self.total_weight as f64)
Some(self.entries[index - 1].cumulative_weight as f64 / self.total_weight as f64)
}

/// Returns the approximate quantile for `rank`.
///
/// Returns `Ok(None)` if the view is empty.
///
/// # Errors
///
/// Returns an error if the view is empty or `rank` is outside `[0.0, 1.0]`.
pub fn quantile(&self, rank: f64, criteria: SearchCriteria) -> Result<T, Error> {
if self.is_empty() {
return Err(Error::invalid_argument("cannot query an empty view"));
}
/// Returns an error if `rank` is outside `[0.0, 1.0]`.
pub fn quantile(&self, rank: f64, criteria: SearchCriteria) -> Result<Option<T>, Error> {
if !(0.0..=1.0).contains(&rank) {
return Err(Error::invalid_argument(format!(
"rank must be in [0.0, 1.0], got {rank}"
)));
}
if self.is_empty() {
return Ok(None);
}

let weight = if criteria == SearchCriteria::Inclusive {
(rank * self.total_weight as f64).ceil() as u64
Expand All @@ -111,53 +111,86 @@ impl<T: Clone + Ord> SortedView<T> {
upper_bound_by_weight(&self.entries, weight)
};

Ok(self.entries[index.min(self.entries.len() - 1)].item.clone())
Ok(Some(
self.entries[index.min(self.entries.len() - 1)].item.clone(),
))
}

/// Returns approximate quantiles for all `ranks`.
///
/// Returns `Ok(None)` if the view is empty.
///
/// # Errors
///
/// Returns an error if the view is empty or any rank is outside `[0.0, 1.0]`.
pub fn quantiles(&self, ranks: &[f64], criteria: SearchCriteria) -> Result<Vec<T>, Error> {
/// Returns an error if any rank is outside `[0.0, 1.0]`.
pub fn quantiles(
&self,
ranks: &[f64],
criteria: SearchCriteria,
) -> Result<Option<Vec<T>>, Error> {
for &rank in ranks {
if !(0.0..=1.0).contains(&rank) {
return Err(Error::invalid_argument(format!(
"rank must be in [0.0, 1.0], got {rank}"
)));
}
}
if self.is_empty() {
return Err(Error::invalid_argument("cannot query an empty view"));
return Ok(None);
}
ranks
let quantiles = ranks
.iter()
.map(|&rank| self.quantile(rank, criteria))
.collect()
.map(|&rank| {
self.quantile(rank, criteria)
.map(|quantile| quantile.expect("checked non-empty view"))
})
.collect::<Result<_, _>>()?;
Ok(Some(quantiles))
}

/// Returns the approximate cumulative distribution over `split_points`.
///
/// Returns `Ok(None)` if the view is empty.
///
/// # Errors
///
/// Returns an error if the view is empty or the split points are invalid.
pub fn cdf(&self, split_points: &[T], criteria: SearchCriteria) -> Result<Vec<f64>, Error> {
/// Returns an error if the split points are invalid.
pub fn cdf(
&self,
split_points: &[T],
criteria: SearchCriteria,
) -> Result<Option<Vec<f64>>, Error> {
check_split_points(split_points)?;
if self.is_empty() {
return Err(Error::invalid_argument("cannot query an empty view"));
return Ok(None);
}
check_split_points(split_points)?;
let mut ranks = Vec::with_capacity(split_points.len() + 1);
for item in split_points {
ranks.push(self.rank(item, criteria)?);
ranks.push(self.rank(item, criteria).expect("checked non-empty view"));
}
ranks.push(1.0);
Ok(ranks)
Ok(Some(ranks))
}

/// Returns the approximate probability mass over `split_points`.
///
/// Returns `Ok(None)` if the view is empty.
///
/// # Errors
///
/// Returns an error if the view is empty or the split points are invalid.
pub fn pmf(&self, split_points: &[T], criteria: SearchCriteria) -> Result<Vec<f64>, Error> {
let mut buckets = self.cdf(split_points, criteria)?;
/// Returns an error if the split points are invalid.
pub fn pmf(
&self,
split_points: &[T],
criteria: SearchCriteria,
) -> Result<Option<Vec<f64>>, Error> {
let Some(mut buckets) = self.cdf(split_points, criteria)? else {
return Ok(None);
};
for index in (1..buckets.len()).rev() {
buckets[index] -= buckets[index - 1];
}
Ok(buckets)
Ok(Some(buckets))
}
}

Expand Down
4 changes: 3 additions & 1 deletion datasketches/src/req/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@
//! sketch.update(ReqFloat::<f64>::new(value)?);
//! }
//!
//! let median = sketch.quantile(0.5, SearchCriteria::Inclusive)?;
//! let median = sketch
//! .quantile(0.5, SearchCriteria::Inclusive)?
//! .expect("the sketch is non-empty");
//! assert_eq!(median.into_inner(), 2.0);
//! # Ok::<(), datasketches::error::Error>(())
//! ```
Expand Down
Loading