← the table

A Thousand Words for Red · 3,001 shades

A Thousand Words for Red

Lip makeup uses a sixth of the colour wheel. Brands sell the same colour and reach for unrelated words.

open the piece

“A Thousand Words for Red” is a scrollytelling data story about the gap between what a lipstick is called and what colour it actually is — whether a name like “Espresso” or “Pillow Talk” carries any real information about the shade. It plots 3,001 real Sephora lip shades, each drawn in its own colour measured directly off the product swatch, and ends with a small in-browser model that lets you type any lipstick name and watch it try, and mostly fail, to guess the colour. The finding is that the failure is the point: a name buys only about nine per cent over guessing the average lipstick, and Sephora’s own plain warehouse descriptions predict colour far better than its poetic marketing names.

How it was made

The data

The dataset is 3,001 lip shades across 99 brands, scraped from Sephora.com. Because Sephora publishes no colour value anywhere, each shade’s true colour was measured from its own 36×36 swatch image rather than read from a field. The scrape drove a real Chrome browser to earn its access cookies, then pulled shade data from a hidden catalogue API, parsing it out of a JSON blob embedded in each product page.

Built with

  • Canvas
  • Python
  • scikit-learn
  • Playwright

The front end is pure HTML5 canvas with no chart library — all 3,001 dots are drawn in their own measured hex with multiply blending, and each scene locks to the paragraph explaining it rather than to a raw scroll fraction. Behind it, a Python pipeline scraped the shades, measured each swatch’s colour in CIELAB with Pillow and NumPy, and fit the name-to-colour model with scikit-learn.

Problems, and how I solved them

  1. The problemSephora sits behind Akamai’s bot manager, which fingerprints the browser, so curl, headless Chromium and the newer APIs all came back “Access Denied.”

    The fixInstead of faking a browser, a real non-headless Chrome earned the cookies once, then every further request was issued as an in-page fetch that inherited the browser’s identity and hit an older, un-blocked catalogue endpoint.

  2. The problemAn auto-restart loop mistook Sephora’s throttling for crashes and kept instantly retrying, which escalated into a full IP ban that knocked even the user’s own browser offline.

    The fixThe supervisor was deleted and retries set to zero, replaced by a deliberately human rhythm of six products then sixty seconds of silence, which drew no further blocks.

  3. The problemColour had to be read from noisy 36×36 swatch JPEGs where compression, border rings and stray highlight pixels could bias the reading toward white.

    The fixThe sampler crops to the middle of each swatch to dodge the border and takes the median of each colour channel, so a few bright pixels are outvoted; a known red validated at #CE2840.

  4. The problemAn early result claimed women’s names carry a real colour signal at p = 0.004 — a tempting but false finding.

    The fixIt traced entirely to the word “rose,” which is a flower, a colour and a name at once; removing such ambiguous words collapsed the signal to nothing, and a guard comment now stops them being re-added.