use apache_datasketches::tuple::generic::{
tuple_jaccard_similarity, TupleAnotB, TupleIntersection, TupleSketch, TupleSketchBuilder,
TupleSummary, TupleUnionBuilder,
};
#[derive(Clone, Debug)]
struct Activity {
sessions: u32,
revenue_cents: u64,
largest_order_cents: u64,
countries: Vec<String>,
}
struct Event<'a> {
revenue_cents: u64,
country: &'a str,
}
impl TupleSummary for Activity {
type Update = Event<'static>;
fn create(event: &Event<'static>) -> Self {
Activity {
sessions: 1,
revenue_cents: event.revenue_cents,
largest_order_cents: event.revenue_cents,
countries: vec![event.country.to_string()],
}
}
fn union_combine(&mut self, other: &Self) {
self.sessions += other.sessions;
self.revenue_cents += other.revenue_cents;
self.largest_order_cents = self.largest_order_cents.max(other.largest_order_cents);
self.countries.extend(other.countries.iter().cloned());
self.countries.sort();
self.countries.dedup();
}
fn intersection_combine(&mut self, other: &Self) {
self.sessions = self.sessions.min(other.sessions);
self.revenue_cents = self.revenue_cents.min(other.revenue_cents);
self.largest_order_cents = self.largest_order_cents.min(other.largest_order_cents);
self.countries.retain(|c| other.countries.contains(c));
}
}
fn main() {
let mut january: TupleSketch<Activity> = TupleSketchBuilder::new().lg_k(12).build().unwrap();
for user in 0..10_000u64 {
january.update_u64(
user,
&Event {
revenue_cents: 250 + (user % 100),
country: if user % 2 == 0 { "GB" } else { "US" },
},
);
}
let mut february: TupleSketch<Activity> = TupleSketchBuilder::new().lg_k(12).build().unwrap();
for user in 5_000..15_000u64 {
february.update_u64(
user,
&Event {
revenue_cents: 400,
country: "US",
},
);
}
println!("January unique users: {:.0}", january.get_estimate());
println!("February unique users: {:.0}", february.get_estimate());
let mut union = TupleUnionBuilder::<Activity>::new()
.lg_k(12)
.build()
.unwrap();
union.update(&january);
union.update(&february);
let combined = union.get_result(true);
println!("Users across both months: {:.0}", combined.get_estimate());
let retained_revenue: u64 = combined.entries().map(|(_, a)| a.revenue_cents).sum();
let biggest_order = combined
.entries()
.map(|(_, a)| a.largest_order_cents)
.max()
.unwrap_or(0);
println!(
"Estimated total revenue: {:.2} (from {} retained entries, theta = {:.4})",
(retained_revenue as f64 / combined.get_theta()) / 100.0,
combined.get_num_retained(),
combined.get_theta()
);
println!(
"Largest single order seen: {:.2}",
biggest_order as f64 / 100.0
);
let mut intersection = TupleIntersection::<Activity>::new();
intersection.update(&january);
intersection.update(&february);
match intersection.get_result(true) {
Ok(returning) => {
println!("Returning users: {:.0}", returning.get_estimate());
if let Some((_, activity)) = returning.entries().next() {
println!(
" e.g. one returning user: {} session(s), countries seen in both months: {:?}",
activity.sessions, activity.countries
);
}
}
Err(e) => println!("No intersection result: {e}"),
}
let churned = TupleAnotB::<Activity>::new().compute(&january, &february, true);
println!("Churned after January: {:.0}", churned.get_estimate());
let similarity = tuple_jaccard_similarity(&january, &february);
println!(
"Audience overlap (Jaccard): {:.3} (range [{:.3}, {:.3}])",
similarity.estimate, similarity.lower_bound, similarity.upper_bound
);
}