tuple_generic/
tuple_generic.rs1use apache_datasketches::tuple::generic::{
8 tuple_jaccard_similarity, TupleAnotB, TupleIntersection, TupleSketch, TupleSketchBuilder,
9 TupleSummary, TupleUnionBuilder,
10};
11
12#[derive(Clone, Debug)]
16struct Activity {
17 sessions: u32,
18 revenue_cents: u64,
19 largest_order_cents: u64,
20 countries: Vec<String>,
21}
22
23struct Event<'a> {
25 revenue_cents: u64,
26 country: &'a str,
27}
28
29impl TupleSummary for Activity {
30 type Update = Event<'static>;
35
36 fn create(event: &Event<'static>) -> Self {
37 Activity {
38 sessions: 1,
39 revenue_cents: event.revenue_cents,
40 largest_order_cents: event.revenue_cents,
41 countries: vec![event.country.to_string()],
42 }
43 }
44
45 fn union_combine(&mut self, other: &Self) {
46 self.sessions += other.sessions;
47 self.revenue_cents += other.revenue_cents;
48 self.largest_order_cents = self.largest_order_cents.max(other.largest_order_cents);
49 self.countries.extend(other.countries.iter().cloned());
50 self.countries.sort();
51 self.countries.dedup();
52 }
53
54 fn intersection_combine(&mut self, other: &Self) {
55 self.sessions = self.sessions.min(other.sessions);
57 self.revenue_cents = self.revenue_cents.min(other.revenue_cents);
58 self.largest_order_cents = self.largest_order_cents.min(other.largest_order_cents);
59 self.countries.retain(|c| other.countries.contains(c));
60 }
61}
62
63fn main() {
64 let mut january: TupleSketch<Activity> = TupleSketchBuilder::new().lg_k(12).build().unwrap();
65 for user in 0..10_000u64 {
66 january.update_u64(
67 user,
68 &Event {
69 revenue_cents: 250 + (user % 100),
70 country: if user % 2 == 0 { "GB" } else { "US" },
71 },
72 );
73 }
74
75 let mut february: TupleSketch<Activity> = TupleSketchBuilder::new().lg_k(12).build().unwrap();
76 for user in 5_000..15_000u64 {
77 february.update_u64(
78 user,
79 &Event {
80 revenue_cents: 400,
81 country: "US",
82 },
83 );
84 }
85
86 println!("January unique users: {:.0}", january.get_estimate());
87 println!("February unique users: {:.0}", february.get_estimate());
88
89 let mut union = TupleUnionBuilder::<Activity>::new()
91 .lg_k(12)
92 .build()
93 .unwrap();
94 union.update(&january);
95 union.update(&february);
96 let combined = union.get_result(true);
97 println!("Users across both months: {:.0}", combined.get_estimate());
98
99 let retained_revenue: u64 = combined.entries().map(|(_, a)| a.revenue_cents).sum();
102 let biggest_order = combined
103 .entries()
104 .map(|(_, a)| a.largest_order_cents)
105 .max()
106 .unwrap_or(0);
107 println!(
108 "Estimated total revenue: {:.2} (from {} retained entries, theta = {:.4})",
109 (retained_revenue as f64 / combined.get_theta()) / 100.0,
110 combined.get_num_retained(),
111 combined.get_theta()
112 );
113 println!(
114 "Largest single order seen: {:.2}",
115 biggest_order as f64 / 100.0
116 );
117
118 let mut intersection = TupleIntersection::<Activity>::new();
120 intersection.update(&january);
121 intersection.update(&february);
122 match intersection.get_result(true) {
123 Ok(returning) => {
124 println!("Returning users: {:.0}", returning.get_estimate());
125 if let Some((_, activity)) = returning.entries().next() {
129 println!(
130 " e.g. one returning user: {} session(s), countries seen in both months: {:?}",
131 activity.sessions, activity.countries
132 );
133 }
134 }
135 Err(e) => println!("No intersection result: {e}"),
136 }
137
138 let churned = TupleAnotB::<Activity>::new().compute(&january, &february, true);
140 println!("Churned after January: {:.0}", churned.get_estimate());
141
142 let similarity = tuple_jaccard_similarity(&january, &february);
144 println!(
145 "Audience overlap (Jaccard): {:.3} (range [{:.3}, {:.3}])",
146 similarity.estimate, similarity.lower_bound, similarity.upper_bound
147 );
148}