LightGBM + bucket scores
Same Single LightGBM as the perceptual-features variant, plus target-encoded colour-bucket scores looked up trilinearly across an 8x8x8 RGB grid. The signed gender-bucket total is the single highest-gain feature in the entire input vector. New top of the gender column.
Scores
How the bucket scores work
RGB space is divided into an 8x8x8 grid of 32-wide cubes, with bucket centres at RGB values 16, 48, 80, ..., 240. For each session we build a 512-dim signed delta vector that captures both the colours the user picked and the colours they declined:
- Not picked in r1each of the 48 round-1 offered colours that was NOT picked subtracts 0.1 from its bucket
- Picked anywhereeach of the 21 picks (16 r1 + 4 r2 + 1 final) adds 0.1 * 16 * 3 / 21 ~ 0.229 to its bucket
- Balance48 * (-0.1) + 21 * (+0.229) = 0, so every session's vector sums to exactly zero
Per CV fold (StratifiedKFold for gender, KFold for the regression heads, seed 42), we average the discrete delta vectors across the training rows of each class / target value to build per-bucket score grids:
- girly_grid[b]mean delta among training girls
- masc_grid[b]mean delta among training boys
- age_grid[b]mean ((age - mean_age) * delta) across training rows (covariance-style)
- mood_grid[b]same for mood
Lookup uses trilinear interpolation: each colour's contribution is
spread across the 8 bucket centres surrounding it in RGB space, with weights
summing to 1. A colour exactly at a bucket centre lands entirely in that
bucket; a colour in the middle of two adjacent centres splits 50/50; a
colour in the middle of the 8-corner cube splits eighths. The per-session
feature is then smooth_delta @ grid, which is mathematically
equivalent to "look up every event's interpolated grid value, weight by
+0.229 or -0.1, sum across the session".
Critically, the grids are built using only training rows in the current fold, so a session's bucket-derived features never depend on its own label - the standard guard against target-encoding leakage.
What the data found on its own
The buckets with the highest girly-vs-masculine lead, computed from the full dataset:
| RGB centre | Lead | |
|---|---|---|
| (240, 176, 208) | +0.0190 | |
| (208, 176, 208) | +0.0174 | |
| (240, 144, 176) | +0.0154 | |
| (208, 176, 240) | +0.0152 | |
| (208, 144, 208) | +0.0150 |
And the most masculine-leaning:
| RGB centre | Lead | |
|---|---|---|
| (48, 16, 240) | -0.0161 | |
| (48, 48, 240) | -0.0138 | |
| (48, 48, 208) | -0.0136 | |
| (80, 240, 48) | -0.0136 | |
| (16, 48, 176) | -0.0133 |
The data identifies the same gender-coded clusters humans would: pinks and light purples lean girly, bright and dark blues lean masculine. The outlier is a single bright green (80, 240, 48) - a boy colour the prototypes I hand-coded missed.
The age grid is just as crisp. Buckets that most predict above-average age are dark and muted: dark blue (80, 80, 208), dark purple (80, 48, 144), dark green (16, 112, 16). Buckets predicting below-average age are pastels and brights: pale pink (240, 208, 240), light cyan (112, 208, 240), white (240, 240, 240). Kids pick light and saturated colours, adults pick muted ones - and the model figures that out from the colour picks alone, without any hand-coding.
Feature importance
Gain-based importance from a full-data LightGBM on gender. The three gender-bucket features absolutely dominate the top of the list (* = new feature from this experiment, all ranks out of 477).
| Rank | Feature | Gain |
|---|---|---|
| ★ 1 | signed_total | 27,326 |
| ★ 3 | girly_total | 1,789 |
| ★ 6 | masc_total | 1,119 |
signed_total alone carries gain 27,326 - more than 2.5x the
next-best feature (r1_lab_b_std at 10,689) and more than 15x
the next individual bucket feature. The signed difference is the
information-dense one because it captures the gender direction with the
baseline popularity divided out.
Cached lookup table
After CV the script also fits the four grids on the full dataset and
writes them to bucket_scores.npz. Anyone with that file
plus the conversion constants can score a new session without
re-running training: bucket a colour's RGB into the grid, look up the
score with trilinear interpolation, multiply by +0.229 or -0.1 depending
on whether the colour was picked, sum across the session. Four numbers
come out per session - girly_total, masc_total, age_total, mood_total -
and they alone hold most of what the bigger model uses to discriminate.
Configuration
- Feature vector441 base + 33 perceptual (documented here) + 3 gender-bucket totals (gender head only) + 1 age-bucket total (age head) + 1 mood-bucket total (mood head)
- ModelSingle LightGBM, identical config to the Single-LGB baseline
- Bucket grid8x8x8 over RGB, 32-wide buckets, centres at (16, 48, ..., 240) per channel
- Per-session delta values+0.1 * 16 * 3 / 21 ~ +0.229 per pick, -0.1 per round-1 offered-but-not-picked colour
- Score gridper-fold mean delta over training rows of each class (gender) or per-fold covariance of delta with target (age, mood)
- Lookuptrilinear interpolation across the 8 surrounding bucket centres
- CV5-fold StratifiedKFold (gender), KFold (age, mood), seed 42 - same as every other row