Skip to content

Commit df261cd

Browse files
voidstackloopclaude
andcommitted
Add bounded provider request timeouts and a binary vs. multi-class classification benchmark
Recovered a real fix from an orphaned prior session on this same task (its process exited without a completion record; verified independently before keeping it): none of the provider SDKs default to a request timeout safe for an unattended run -- google-genai's is unbounded, confirmed by hand to hang forever on a stalled connection. Every client now applies BUFFDATA_REQUEST_TIMEOUT (default 120s), with 4 new tests covering the default, the env override, and every client family. New: benchmarks/benchmark_classification_matrix.py runs the dirty-data recovery methodology across 19 real, independently-sourced datasets (10 binary, 9 multi-class, 3-20 classes), split by classification shape. Finding: multi-class recovery is unreliable at 1,000 rows (-0.5 points average, 5 of 9 datasets negative) but reaches parity with binary by 3,000 rows (+4.5 vs +4.8 points) -- a proxy-classifier noise effect from fewer examples per class at higher cardinality, not a real difference in how BuffData's cleaning performs. Real classification-accuracy measurement (via DatasetClassifier directly, mirroring the standalone `classify` command) is built and resumable but not yet run -- this API key's gemini-3.7-flash free-tier quota (20 requests/day) was exhausted partway through today's work. Full writeup in docs/classification-benchmark.md. Deliberately left out of this commit: an orphaned, unrun rewrite of benchmark_scale_matrix.py (would silently break the already-published 10k/25k/50k scale numbers in README.md), and two other orphaned scripts (benchmark_classification_correctness.py, benchmark_multilabel*.py) whose completed runs show 0.0 accuracy across nearly every dataset -- not a credible finding, almost certainly a measurement bug, not verified or fixed here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 02a3d08 commit df261cd

9 files changed

Lines changed: 5986 additions & 5 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,17 @@ your account before choosing between them at scale.
232232
- [gemini-3.5-flash-lite report](benchmarks/results-scale-matrix-gemini/REPORT.md)
233233
- [gemini-3.7-flash report](benchmarks/results-scale-matrix-gemini-3.7-flash/REPORT.md)
234234

235+
### Binary vs. multi-class classification benchmark
236+
237+
A narrower question across 19 more real datasets (10 binary, 9 multi-class): does
238+
recovery hold the same way for both classification shapes, and how accurate is
239+
BuffData's own classify stage on each? See
240+
[docs/classification-benchmark.md](docs/classification-benchmark.md) for the full
241+
write-up -- recovery is done (binary and multi-class converge to comparable recovery
242+
by 3,000 rows, though multi-class needs that scale to get past proxy-classifier noise
243+
that binary doesn't hit until much smaller samples); real classification-accuracy
244+
numbers are queued behind this API key's `gemini-3.7-flash` daily quota.
245+
235246
BuffData is a provider-neutral Python CLI and SDK for turning raw or inconsistent
236247
datasets into safer, higher-quality AI training data. It validates structure,
237248
redacts PII locally, removes duplicates, scores and refines records, quarantines

benchmarks/benchmark_classification_matrix.py

Lines changed: 493 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
Resuming: found 11 dataset(s) already recorded in benchmarks/results-classification-matrix/results.json
2+
=== yelp_polarity (fancyzhx/yelp_polarity) ===
3+
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
4+
/home/saldev/projects/buffdata/.venv/lib/python3.12/site-packages/torch/autograd/graph.py:979: UserWarning: CUDA initialization: The NVIDIA driver on your system is too old (found version 12070). Please update your GPU driver by downloading and installing a new version from the URL: http://www.nvidia.com/Download/index.aspx Alternatively, go to: https://pytorch.org to install a PyTorch version that has been compiled with your version of the CUDA driver. (Triggered internally at /__w/pytorch/pytorch/c10/cuda/CUDAFunctions.cpp:119.)
5+
return Variable._execution_engine.run_backward( # Calls into the C++ engine to run the backward pass
6+
[yelp_polarity @ 1000] clean_raw acc=0.8387
7+
[yelp_polarity @ 1000] clean_optimized acc=0.8387
8+
[yelp_polarity @ 1000] dirty_raw acc=0.7840
9+
[yelp_polarity @ 1000] dirty_optimized acc=0.8387
10+
[yelp_polarity @ 3000] clean_raw acc=0.8727
11+
[yelp_polarity @ 3000] clean_optimized acc=0.8727
12+
[yelp_polarity @ 3000] dirty_raw acc=0.8027
13+
[yelp_polarity @ 3000] dirty_optimized acc=0.8727
14+
--- yelp_polarity done in 51.0s (checkpoint saved) ---
15+
=== amazon_polarity (fancyzhx/amazon_polarity) ===
16+
[amazon_polarity @ 1000] clean_raw acc=0.7660
17+
[amazon_polarity @ 1000] clean_optimized acc=0.7660
18+
[amazon_polarity @ 1000] dirty_raw acc=0.7133
19+
[amazon_polarity @ 1000] dirty_optimized acc=0.7660
20+
[amazon_polarity @ 3000] clean_raw acc=0.8233
21+
[amazon_polarity @ 3000] clean_optimized acc=0.8233
22+
[amazon_polarity @ 3000] dirty_raw acc=0.7773
23+
[amazon_polarity @ 3000] dirty_optimized acc=0.8233
24+
--- amazon_polarity done in 132.2s (checkpoint saved) ---
25+
=== imdb (stanfordnlp/imdb) ===
26+
[imdb @ 1000] clean_raw acc=0.8147
27+
[imdb @ 1000] clean_optimized acc=0.8147
28+
[imdb @ 1000] dirty_raw acc=0.7433
29+
[imdb @ 1000] dirty_optimized acc=0.8147
30+
[imdb @ 3000] clean_raw acc=0.8560
31+
[imdb @ 3000] clean_optimized acc=0.8607
32+
[imdb @ 3000] dirty_raw acc=0.7680
33+
[imdb @ 3000] dirty_optimized acc=0.8607
34+
--- imdb done in 38.6s (checkpoint saved) ---
35+
=== rotten_tomatoes (cornell-movie-review-data/rotten_tomatoes) ===
36+
[rotten_tomatoes @ 1000] clean_raw acc=0.6513
37+
[rotten_tomatoes @ 1000] clean_optimized acc=0.6513
38+
[rotten_tomatoes @ 1000] dirty_raw acc=0.6467
39+
[rotten_tomatoes @ 1000] dirty_optimized acc=0.6513
40+
[rotten_tomatoes @ 3000] clean_raw acc=0.7013
41+
[rotten_tomatoes @ 3000] clean_optimized acc=0.7013
42+
[rotten_tomatoes @ 3000] dirty_raw acc=0.6713
43+
[rotten_tomatoes @ 3000] dirty_optimized acc=0.7013
44+
--- rotten_tomatoes done in 13.5s (checkpoint saved) ---
45+
=== sst2 (SetFit/sst2) ===
46+
Repo card metadata block was not found. Setting CardData to empty.
47+
[sst2 @ 1000] clean_raw acc=0.6480
48+
[sst2 @ 1000] clean_optimized acc=0.6480
49+
[sst2 @ 1000] dirty_raw acc=0.6540
50+
[sst2 @ 1000] dirty_optimized acc=0.6480
51+
[sst2 @ 3000] clean_raw acc=0.7540
52+
[sst2 @ 3000] clean_optimized acc=0.7520
53+
[sst2 @ 3000] dirty_raw acc=0.7020
54+
[sst2 @ 3000] dirty_optimized acc=0.7520
55+
--- sst2 done in 10.0s (checkpoint saved) ---
56+
=== subj (SetFit/subj) ===
57+
Repo card metadata block was not found. Setting CardData to empty.
58+
[subj @ 1000] clean_raw acc=0.8300
59+
[subj @ 1000] clean_optimized acc=0.8300
60+
[subj @ 1000] dirty_raw acc=0.7840
61+
[subj @ 1000] dirty_optimized acc=0.8300
62+
[subj @ 3000] clean_raw acc=0.9067
63+
[subj @ 3000] clean_optimized acc=0.9067
64+
[subj @ 3000] dirty_raw acc=0.8260
65+
[subj @ 3000] dirty_optimized acc=0.9067
66+
--- subj done in 16.8s (checkpoint saved) ---
67+
=== tweet_eval_irony (cardiffnlp/tweet_eval/irony) ===
68+
[tweet_eval_irony @ 1000] clean_raw acc=0.6160
69+
[tweet_eval_irony @ 1000] clean_optimized acc=0.6160
70+
[tweet_eval_irony @ 1000] dirty_raw acc=0.5827
71+
[tweet_eval_irony @ 1000] dirty_optimized acc=0.6160
72+
[tweet_eval_irony @ 2834] clean_raw acc=0.6053
73+
[tweet_eval_irony @ 2834] clean_optimized acc=0.6053
74+
[tweet_eval_irony @ 2834] dirty_raw acc=0.5787
75+
[tweet_eval_irony @ 2834] dirty_optimized acc=0.6053
76+
--- tweet_eval_irony done in 9.4s (checkpoint saved) ---
77+
=== tweet_eval_hate (cardiffnlp/tweet_eval/hate) ===
78+
[tweet_eval_hate @ 1000] clean_raw acc=0.5173
79+
[tweet_eval_hate @ 1000] clean_optimized acc=0.5220
80+
[tweet_eval_hate @ 1000] dirty_raw acc=0.5353
81+
[tweet_eval_hate @ 1000] dirty_optimized acc=0.5220
82+
[tweet_eval_hate @ 3000] clean_raw acc=0.5740
83+
[tweet_eval_hate @ 3000] clean_optimized acc=0.5647
84+
[tweet_eval_hate @ 3000] dirty_raw acc=0.5813
85+
[tweet_eval_hate @ 3000] dirty_optimized acc=0.5647
86+
--- tweet_eval_hate done in 12.8s (checkpoint saved) ---
87+
=== cr (SetFit/CR) ===
88+
Repo card metadata block was not found. Setting CardData to empty.
89+
[cr @ 1000] clean_raw acc=0.7451
90+
[cr @ 1000] clean_optimized acc=0.7451
91+
[cr @ 1000] dirty_raw acc=0.7132
92+
[cr @ 1000] dirty_optimized acc=0.7451
93+
[cr @ 2458] clean_raw acc=0.7598
94+
[cr @ 2458] clean_optimized acc=0.7708
95+
[cr @ 2458] dirty_raw acc=0.7145
96+
[cr @ 2458] dirty_optimized acc=0.7708
97+
--- cr done in 7.1s (checkpoint saved) ---
98+
=== amazon_counterfactual (SetFit/amazon_counterfactual_en) ===
99+
Repo card metadata block was not found. Setting CardData to empty.
100+
[amazon_counterfactual @ 1000] clean_raw acc=0.8173
101+
[amazon_counterfactual @ 1000] clean_optimized acc=0.8173
102+
[amazon_counterfactual @ 1000] dirty_raw acc=0.7873
103+
[amazon_counterfactual @ 1000] dirty_optimized acc=0.8173
104+
[amazon_counterfactual @ 1054] clean_raw acc=0.8120
105+
[amazon_counterfactual @ 1054] clean_optimized acc=0.8120
106+
[amazon_counterfactual @ 1054] dirty_raw acc=0.7633
107+
[amazon_counterfactual @ 1054] dirty_optimized acc=0.8120
108+
--- amazon_counterfactual done in 5.3s (checkpoint saved) ---
109+
=== ag_news (fancyzhx/ag_news) ===
110+
[ag_news @ 1000] clean_raw acc=0.7193
111+
[ag_news @ 1000] clean_optimized acc=0.7193
112+
[ag_news @ 1000] dirty_raw acc=0.6873
113+
[ag_news @ 1000] dirty_optimized acc=0.7193
114+
[ag_news @ 3000] clean_raw acc=0.8120
115+
[ag_news @ 3000] clean_optimized acc=0.8120
116+
[ag_news @ 3000] dirty_raw acc=0.7420
117+
[ag_news @ 3000] dirty_optimized acc=0.8120
118+
--- ag_news done in 18.0s (checkpoint saved) ---
119+
=== dbpedia_14 (fancyzhx/dbpedia_14) ===
120+
[dbpedia_14 @ 1000] clean_raw acc=0.7733
121+
[dbpedia_14 @ 1000] clean_optimized acc=0.7733
122+
[dbpedia_14 @ 1000] dirty_raw acc=0.7940
123+
[dbpedia_14 @ 1000] dirty_optimized acc=0.7733
124+
[dbpedia_14 @ 3000] clean_raw acc=0.9173
125+
[dbpedia_14 @ 3000] clean_optimized acc=0.9173
126+
[dbpedia_14 @ 3000] dirty_raw acc=0.8233
127+
[dbpedia_14 @ 3000] dirty_optimized acc=0.9173
128+
--- dbpedia_14 done in 35.9s (checkpoint saved) ---
129+
=== yahoo_answers_topics (community-datasets/yahoo_answers_topics) ===
130+
[yahoo_answers_topics @ 1000] clean_raw acc=0.4153
131+
[yahoo_answers_topics @ 1000] clean_optimized acc=0.4153
132+
[yahoo_answers_topics @ 1000] dirty_raw acc=0.4373
133+
[yahoo_answers_topics @ 1000] dirty_optimized acc=0.4153
134+
[yahoo_answers_topics @ 3000] clean_raw acc=0.5500
135+
[yahoo_answers_topics @ 3000] clean_optimized acc=0.5500
136+
[yahoo_answers_topics @ 3000] dirty_raw acc=0.5040
137+
[yahoo_answers_topics @ 3000] dirty_optimized acc=0.5500
138+
--- yahoo_answers_topics done in 68.7s (checkpoint saved) ---
139+
=== emotion (dair-ai/emotion) ===
140+
[emotion @ 1000] clean_raw acc=0.4015
141+
[emotion @ 1000] clean_optimized acc=0.4015
142+
[emotion @ 1000] dirty_raw acc=0.4175
143+
[emotion @ 1000] dirty_optimized acc=0.4015
144+
[emotion @ 3000] clean_raw acc=0.7079
145+
[emotion @ 3000] clean_optimized acc=0.6961
146+
[emotion @ 3000] dirty_raw acc=0.5976
147+
[emotion @ 3000] dirty_optimized acc=0.6961
148+
--- emotion done in 14.5s (checkpoint saved) ---
149+
=== tweet_eval_emotion (cardiffnlp/tweet_eval/emotion) ===
150+
[tweet_eval_emotion @ 1000] clean_raw acc=0.4289
151+
[tweet_eval_emotion @ 1000] clean_optimized acc=0.4289
152+
[tweet_eval_emotion @ 1000] dirty_raw acc=0.4390
153+
[tweet_eval_emotion @ 1000] dirty_optimized acc=0.4289
154+
[tweet_eval_emotion @ 1176] clean_raw acc=0.4919
155+
[tweet_eval_emotion @ 1176] clean_optimized acc=0.4661
156+
[tweet_eval_emotion @ 1176] dirty_raw acc=0.4797
157+
[tweet_eval_emotion @ 1176] dirty_optimized acc=0.4661
158+
--- tweet_eval_emotion done in 4.6s (checkpoint saved) ---
159+
=== tweet_eval_sentiment (cardiffnlp/tweet_eval/sentiment) ===
160+
[tweet_eval_sentiment @ 1000] clean_raw acc=0.4440
161+
[tweet_eval_sentiment @ 1000] clean_optimized acc=0.4440
162+
[tweet_eval_sentiment @ 1000] dirty_raw acc=0.4407
163+
[tweet_eval_sentiment @ 1000] dirty_optimized acc=0.4440
164+
[tweet_eval_sentiment @ 3000] clean_raw acc=0.4687
165+
[tweet_eval_sentiment @ 3000] clean_optimized acc=0.4687
166+
[tweet_eval_sentiment @ 3000] dirty_raw acc=0.4627
167+
[tweet_eval_sentiment @ 3000] dirty_optimized acc=0.4687
168+
--- tweet_eval_sentiment done in 8.4s (checkpoint saved) ---
169+
=== 20_newsgroups (SetFit/20_newsgroups) ===
170+
Repo card metadata block was not found. Setting CardData to empty.
171+
[20_newsgroups @ 1000] clean_raw acc=0.3333
172+
[20_newsgroups @ 1000] clean_optimized acc=0.3247
173+
[20_newsgroups @ 1000] dirty_raw acc=0.3173
174+
[20_newsgroups @ 1000] dirty_optimized acc=0.3247
175+
[20_newsgroups @ 3000] clean_raw acc=0.5500
176+
[20_newsgroups @ 3000] clean_optimized acc=0.5373
177+
[20_newsgroups @ 3000] dirty_raw acc=0.4647
178+
[20_newsgroups @ 3000] dirty_optimized acc=0.5373
179+
--- 20_newsgroups done in 29.2s (checkpoint saved) ---
180+
=== trec_coarse (SetFit/TREC-QC) ===
181+
Repo card metadata block was not found. Setting CardData to empty.
182+
[trec_coarse @ 516] clean_raw acc=0.6728
183+
[trec_coarse @ 516] clean_optimized acc=0.6728
184+
[trec_coarse @ 516] dirty_raw acc=0.6975
185+
[trec_coarse @ 516] dirty_optimized acc=0.6728
186+
[trec_coarse @ 516] clean_raw acc=0.6728
187+
[trec_coarse @ 516] clean_optimized acc=0.6728
188+
[trec_coarse @ 516] dirty_raw acc=0.6975
189+
[trec_coarse @ 516] dirty_optimized acc=0.6728
190+
--- trec_coarse done in 7.2s (checkpoint saved) ---
191+
=== tweet_sentiment_extraction (SetFit/tweet_sentiment_extraction) ===
192+
Repo card metadata block was not found. Setting CardData to empty.
193+
[tweet_sentiment_extraction @ 1000] clean_raw acc=0.5140
194+
[tweet_sentiment_extraction @ 1000] clean_optimized acc=0.5140
195+
[tweet_sentiment_extraction @ 1000] dirty_raw acc=0.5120
196+
[tweet_sentiment_extraction @ 1000] dirty_optimized acc=0.5140
197+
[tweet_sentiment_extraction @ 3000] clean_raw acc=0.6047
198+
[tweet_sentiment_extraction @ 3000] clean_optimized acc=0.6073
199+
[tweet_sentiment_extraction @ 3000] dirty_raw acc=0.5480
200+
[tweet_sentiment_extraction @ 3000] dirty_optimized acc=0.6073
201+
--- tweet_sentiment_extraction done in 7.9s (checkpoint saved) ---
202+
Results: benchmarks/results-classification-matrix/results.json
203+
Report: benchmarks/results-classification-matrix/REPORT.md
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Binary vs. multi-class classification benchmark
2+
3+
Model: `gemini-3.7-flash` | Scales: [1000, 3000] | Seeds: [17, 29, 43] | Classification sample size: 40
4+
5+
## Classification accuracy
6+
7+
Not run this pass (`--skip-classification`).
8+
9+
## Dirty-data recovery (local only, no LLM)
10+
11+
"Rows" is the actual balanced training size used, which can be smaller than the scale header: `_max_balanced_count` caps every request at `smallest_class_size × num_classes` so a stratified sample never asks a rare class for more rows than it has (see trec_coarse below, whose rarest coarse class has only 86 examples -- both scale requests land on the same capped, and therefore identical, result).
12+
13+
### Scale 1000
14+
15+
| Dataset | Kind | Rows | Dirty raw acc | Dirty optimized acc | Recovery | Clean delta |
16+
|---|---|---:|---:|---:|---:|---:|
17+
| yelp_polarity | binary | 1000 | 0.7840 | 0.8387 | +0.0547 | +0.0000 |
18+
| amazon_polarity | binary | 1000 | 0.7133 | 0.7660 | +0.0527 | +0.0000 |
19+
| imdb | binary | 1000 | 0.7433 | 0.8147 | +0.0713 | +0.0000 |
20+
| rotten_tomatoes | binary | 1000 | 0.6467 | 0.6513 | +0.0047 | +0.0000 |
21+
| sst2 | binary | 1000 | 0.6540 | 0.6480 | -0.0060 | +0.0000 |
22+
| subj | binary | 1000 | 0.7840 | 0.8300 | +0.0460 | +0.0000 |
23+
| tweet_eval_irony | binary | 1000 | 0.5827 | 0.6160 | +0.0333 | +0.0000 |
24+
| tweet_eval_hate | binary | 1000 | 0.5353 | 0.5220 | -0.0133 | +0.0047 |
25+
| cr | binary | 1000 | 0.7132 | 0.7451 | +0.0319 | +0.0000 |
26+
| amazon_counterfactual | binary | 1000 | 0.7873 | 0.8173 | +0.0300 | +0.0000 |
27+
| ag_news | multi | 1000 | 0.6873 | 0.7193 | +0.0320 | +0.0000 |
28+
| dbpedia_14 | multi | 1000 | 0.7940 | 0.7733 | -0.0207 | +0.0000 |
29+
| yahoo_answers_topics | multi | 1000 | 0.4373 | 0.4153 | -0.0220 | +0.0000 |
30+
| emotion | multi | 1000 | 0.4175 | 0.4015 | -0.0160 | +0.0000 |
31+
| tweet_eval_emotion | multi | 1000 | 0.4390 | 0.4289 | -0.0102 | +0.0000 |
32+
| tweet_eval_sentiment | multi | 1000 | 0.4407 | 0.4440 | +0.0033 | +0.0000 |
33+
| 20_newsgroups | multi | 1000 | 0.3173 | 0.3247 | +0.0073 | -0.0087 |
34+
| trec_coarse | multi | 516 † | 0.6975 | 0.6728 | -0.0247 | +0.0000 |
35+
| tweet_sentiment_extraction | multi | 1000 | 0.5120 | 0.5140 | +0.0020 | +0.0000 |
36+
37+
### Scale 3000
38+
39+
| Dataset | Kind | Rows | Dirty raw acc | Dirty optimized acc | Recovery | Clean delta |
40+
|---|---|---:|---:|---:|---:|---:|
41+
| yelp_polarity | binary | 3000 | 0.8027 | 0.8727 | +0.0700 | +0.0000 |
42+
| amazon_polarity | binary | 3000 | 0.7773 | 0.8233 | +0.0460 | +0.0000 |
43+
| imdb | binary | 3000 | 0.7680 | 0.8607 | +0.0927 | +0.0047 |
44+
| rotten_tomatoes | binary | 3000 | 0.6713 | 0.7013 | +0.0300 | +0.0000 |
45+
| sst2 | binary | 3000 | 0.7020 | 0.7520 | +0.0500 | -0.0020 |
46+
| subj | binary | 3000 | 0.8260 | 0.9067 | +0.0807 | +0.0000 |
47+
| tweet_eval_irony | binary | 2834 † | 0.5787 | 0.6053 | +0.0267 | +0.0000 |
48+
| tweet_eval_hate | binary | 3000 | 0.5813 | 0.5647 | -0.0167 | -0.0093 |
49+
| cr | binary | 2458 † | 0.7145 | 0.7708 | +0.0564 | +0.0110 |
50+
| amazon_counterfactual | binary | 1054 † | 0.7633 | 0.8120 | +0.0487 | +0.0000 |
51+
| ag_news | multi | 3000 | 0.7420 | 0.8120 | +0.0700 | +0.0000 |
52+
| dbpedia_14 | multi | 3000 | 0.8233 | 0.9173 | +0.0940 | +0.0000 |
53+
| yahoo_answers_topics | multi | 3000 | 0.5040 | 0.5500 | +0.0460 | +0.0000 |
54+
| emotion | multi | 3000 | 0.5976 | 0.6961 | +0.0985 | -0.0118 |
55+
| tweet_eval_emotion | multi | 1176 † | 0.4797 | 0.4661 | -0.0136 | -0.0257 |
56+
| tweet_eval_sentiment | multi | 3000 | 0.4627 | 0.4687 | +0.0060 | +0.0000 |
57+
| 20_newsgroups | multi | 3000 | 0.4647 | 0.5373 | +0.0727 | -0.0127 |
58+
| trec_coarse | multi | 516 † | 0.6975 | 0.6728 | -0.0247 | +0.0000 |
59+
| tweet_sentiment_extraction | multi | 3000 | 0.5480 | 0.6073 | +0.0593 | +0.0027 |
60+
61+
† capped below the requested scale by the smallest class's available rows.

0 commit comments

Comments
 (0)