GroupBy & Aggregations¶
CupOfTea features a high-performance, cache-conscious GroupBy engine powered by primitive open-addressing hash tables, single-pass chunk accumulation, and direct columnar vector emission. It allows segmenting massive datasets by one or more keys and computing multiple summary metrics with minimal GC overhead.
🏗️ Architecture & Engine Design¶
Traditional JVM dataframe libraries group rows by building intermediate maps such as Map<List<Object>, List<Integer>> or Map<Key, RowSet>. On datasets with millions of rows, this produces:
1. Millions of temporary List<Object> / Tuple composite key allocations.
2. Millions of boxed objects (Integer, Double, etc.) and LinkedHashMap.Entry nodes.
3. Severe memory fragmentation, pointer chasing, and heavy GC pauses.
4. Multiple cache-unfriendly passes over source columns for each aggregate metric.
CupOfTea solves this with three core architectural components introduced in Issue #210:
flowchart TD
subgraph Input ["Columnar Input"]
V1["Key Column Vector(s)"]
V2["Value Column Vector(s)"]
end
subgraph HashEngine ["Primitive Group Hash Engine"]
HT["GroupHashTable<br/>• Flat int[] table (open addressing, linear probing)<br/>• Flat int[] hashes & int[] groupIds<br/>• Zero-allocation key lookup via row index"]
end
subgraph AccumEngine ["Single-Pass Chunk Accumulation"]
ACC["GroupAccumulators (Min / Max / Sum / Count / Mean)<br/>• Direct primitive array updates (double[], long[], int[])<br/>• Parallel thread-local accumulation<br/>• Zero-allocation chunk merging"]
end
subgraph Output ["Direct Columnar Vector Emission"]
OUT["DoubleColumnVector / IntColumnVector / StringColumnVector<br/>(No intermediate row HashMaps or object boxing)"]
end
V1 --> HT
HT --> ACC
V2 --> ACC
ACC --> OUT
1. Primitive Open-Addressing Hash Table (GroupHashTable)¶
- Uses power-of-2 capacity with linear probing and flat primitive arrays:
int[] table,int[] hashes, andint[] groupIds. - Keys are compared directly against previous rows in the source column vectors using their integer row index. No composite key objects (
List<Object>orTuple) are allocated on key lookups. - Highly optimized single-key specializations (
String,Integer,Long,Double) bypassing general array lookups. - Memory footprint is only ~12 bytes per slot in flat arrays.
2. Single-Pass Chunk Accumulation (GroupAccumulator)¶
- Standard aggregations (
count,sum,mean/avg,min,max,listAgg) implement theGroupAccumulatorinterface. - Each chunk data buffer is traversed once: for each row, the group ID is identified from
GroupHashTableand all active accumulators update their internal primitive arrays (double[],long[],int[]) directly in CPU L1/L2 cache. - In multi-threaded execution (
df.asParallel()), worker threads accumulate independently on data chunks without lock contention, followed by a fast chunk merge.
3. Direct Columnar Vector Emission¶
- Output grouped vectors (
DoubleColumnVector,IntColumnVector,LongColumnVector,FloatColumnVector,StringColumnVector) are allocated once with the exact cardinality (numGroups). - Accumulators populate internal primitive buffers directly by group ID index without boxing (
double -> Double) and without creating per-rowHashMap<String, Object>containers.
👥 Grouping Data¶
To start a grouped query, call df.groupBy(...):
import com.cupoftea.CupOfTea;
import com.cupoftea.table.GroupBy;
CupOfTea df = CupOfTea.read().csv("sales.csv");
// Single-key grouping
GroupBy byDept = df.groupBy("department");
// Multi-key grouping
GroupBy byRegionAndDept = df.groupBy("region", "department");
📈 Aggregation Functions¶
The com.cupoftea.table.Aggregations class provides a rich suite of aggregate builders:
| Function | Alias | Single-Pass | Description |
|---|---|---|---|
Aggregations.count() |
— | ✅ | Total rows per group |
Aggregations.count("col") |
— | ✅ | Count non-null values in column |
Aggregations.sum("col") |
— | ✅ | Numeric sum (preserves int, long, double, float, BigDecimal) |
Aggregations.mean("col") |
average(), avg() |
✅ | Arithmetic mean (double / BigDecimal) |
Aggregations.min("col") |
minimum() |
✅ | Minimum value (supports primitives and Comparable) |
Aggregations.max("col") |
maximum() |
✅ | Maximum value (supports primitives and Comparable) |
Aggregations.listAgg("col", sep) |
aggregate() |
✅ | String concatenation joined by delimiter |
Aggregations.first("col") |
— | ❌ | First non-null value in group |
Aggregations.last("col") |
— | ❌ | Last non-null value in group |
Aggregations.median("col") |
— | ❌ | Median value using linear-time Introselect |
Aggregations.quantile("col", q) |
— | ❌ | Quantile q (0.0 to 1.0) |
Aggregations.variance("col") |
var() |
❌ | Sample variance (two-pass numerically stable) |
Aggregations.standardDeviation("col") |
std() |
❌ | Sample standard deviation |
Aggregations.skewness("col") |
— | ❌ | Fisher-Pearson skewness coefficient |
Aggregations.kurtosis("col") |
— | ❌ | Excess kurtosis |
💡 Practical Examples¶
1. High-Performance Multi-Metric Aggregation (1BRC Pattern)¶
Compute minimum, mean, maximum, and total counts over millions of records in a single pass:
import com.cupoftea.CupOfTea;
import com.cupoftea.table.Aggregations;
CupOfTea measurements = CupOfTea.read().csv("measurements.txt");
CupOfTea result = measurements.asParallel()
.groupBy("station")
.aggregate(
Aggregations.min("temperature").as("min_temp"),
Aggregations.mean("temperature").as("mean_temp"),
Aggregations.max("temperature").as("max_temp"),
Aggregations.count().as("total_readings")
);
result.print();
2. Multi-Column Grouping with Custom Aliases¶
CupOfTea summary = df.groupBy("region", "category")
.aggregate(
Aggregations.count().as("order_count"),
Aggregations.sum("revenue").as("total_revenue"),
Aggregations.avg("revenue").as("avg_ticket"),
Aggregations.median("delivery_days").as("median_delivery")
);
3. String Aggregation (listAgg)¶
Concatenate group strings with a custom delimiter:
CupOfTea tagsByArticle = df.groupBy("article_id")
.aggregate(
Aggregations.listAgg("tag", ", ").as("tags")
);
⚡ Parallel Execution (df.asParallel())¶
When grouping large DataFrames, calling .asParallel() partitions the underlying columnar vectors across worker threads.
Each worker thread builds a thread-local GroupHashTable and updates its thread-local GroupAccumulator instances:
// Executes parallel multi-core aggregation using the common ForkJoinPool
CupOfTea fastSummary = df.asParallel()
.groupBy("customer_id")
.aggregate(
Aggregations.sum("amount").as("total_spent"),
Aggregations.count().as("trans_count")
);
// Or provide a custom ExecutorService
CupOfTea customPoolSummary = df.parallel(customExecutor)
.groupBy("customer_id")
.aggregate(Aggregations.sum("amount").as("total_spent"));
🛡️ Null Value Semantics¶
Consistent with SQL and pandas standards:
- Missing/null values are automatically skipped during accumulation (sum, mean, min, max, count(col)).
- Aggregations.count() counts all rows in the group regardless of nulls.
- Aggregations.count("col") counts only non-null occurrences in that column.
- If all values in a group for a given column are null, sum, mean, min, and max return null.