Как посчитать Jaccard similarity в SQL

Закрепи формулу jaccard similarity в Карьернике
Запомнить надолго — 5 коротких сессий с задачами на эту тему. Бесплатно
Тренировать jaccard similarity в Telegram

Зачем Jaccard

Jaccard similarity (или Jaccard index) — мера схожести двух множеств. В аналитике пригождается для:

  • overlap между двумя сегментами / маркетинговыми кампаниями
  • похожесть пользователей по купленным товарам (для lookalike или коллабфильтрации)
  • дедупликация: документы с Jaccard > 0.8 — почти копии
  • A/B на пересечении аудиторий

В отличие от cosine, Jaccard игнорирует частоту — только наличие.

Формула

Jaccard(A, B) = |A ∩ B| / |A ∪ B|

Диапазон [0, 1]. 0 — нет пересечения, 1 — множества равны.

|A ∪ B| можно посчитать через |A| + |B| − |A ∩ B| — пригодится в SQL.

Jaccard двух множеств

Пример: насколько пользователи из campaign A пересекаются с campaign B.

WITH set_a AS (
    SELECT DISTINCT user_id FROM events WHERE campaign = 'spring_sale'
),
set_b AS (
    SELECT DISTINCT user_id FROM events WHERE campaign = 'spring_promo'
),
metrics AS (
    SELECT
        (SELECT COUNT(*) FROM set_a) AS size_a,
        (SELECT COUNT(*) FROM set_b) AS size_b,
        (SELECT COUNT(*) FROM set_a INNER JOIN set_b USING (user_id)) AS intersection
)
SELECT
    size_a,
    size_b,
    intersection,
    size_a + size_b - intersection AS union_size,
    intersection::NUMERIC / NULLIF(size_a + size_b - intersection, 0) AS jaccard
FROM metrics;

Audience overlap

Все пары сегментов:

WITH user_segments AS (
    SELECT DISTINCT user_id, segment_id
    FROM segment_assignments
    WHERE assigned_at >= CURRENT_DATE - INTERVAL '30 days'
),
pair_stats AS (
    SELECT
        a.segment_id AS seg_a,
        b.segment_id AS seg_b,
        COUNT(*) AS intersection
    FROM user_segments a
    JOIN user_segments b USING (user_id)
    WHERE a.segment_id < b.segment_id
    GROUP BY a.segment_id, b.segment_id
),
seg_sizes AS (
    SELECT segment_id, COUNT(*) AS size
    FROM user_segments
    GROUP BY segment_id
)
SELECT
    p.seg_a,
    p.seg_b,
    sa.size AS size_a,
    sb.size AS size_b,
    p.intersection,
    p.intersection::NUMERIC
    / NULLIF(sa.size + sb.size - p.intersection, 0) AS jaccard
FROM pair_stats p
JOIN seg_sizes sa ON sa.segment_id = p.seg_a
JOIN seg_sizes sb ON sb.segment_id = p.seg_b
ORDER BY jaccard DESC
LIMIT 50;

Высокий Jaccard между двумя сегментами = они почти про одно и то же, маркетинг будет дублироваться.

Закрепи формулу jaccard similarity в Карьернике
Запомнить надолго — 5 коротких сессий с задачами на эту тему. Бесплатно
Тренировать jaccard similarity в Telegram

Похожие пользователи

Найти пользователей, похожих на user 42 по купленным товарам:

WITH target_items AS (
    SELECT DISTINCT product_id FROM purchases WHERE user_id = 42
),
candidate_items AS (
    SELECT user_id, product_id FROM purchases WHERE user_id <> 42
),
overlap AS (
    SELECT
        c.user_id,
        COUNT(*) FILTER (WHERE c.product_id IN (SELECT product_id FROM target_items)) AS intersection,
        COUNT(DISTINCT c.product_id) AS user_size
    FROM candidate_items c
    GROUP BY c.user_id
),
target_size AS (
    SELECT COUNT(*) AS size FROM target_items
)
SELECT
    o.user_id,
    o.intersection,
    o.intersection::NUMERIC
    / NULLIF(t.size + o.user_size - o.intersection, 0) AS jaccard
FROM overlap o
CROSS JOIN target_size t
WHERE o.intersection > 0
ORDER BY jaccard DESC
LIMIT 20;

Частые ошибки

Ошибка 1. Не дедуплицировать. Если events содержит одного юзера несколько раз, intersection и size раздуются, Jaccard сломается. SELECT DISTINCT — обязательно.

Ошибка 2. Деление пустого на пустое. Если оба множества пустые, формула 0/0. NULLIF(..., 0) или фильтр снаружи.

Ошибка 3. Игнорировать частоту, когда она важна. Jaccard говорит «купил» / «не купил». Если важно, что юзер купил товар 10 раз, лучше cosine или weighted similarity.

Ошибка 4. Сравнивать множества разного размера. Jaccard для маленького множества против большого почти всегда низкий, даже при полном включении. Используйте overlap coefficient |A ∩ B| / min(|A|, |B|) если нужна именно вложенность.

Ошибка 5. Cross join без фильтра. Pairs всех сегментов = N² комбинаций. На 1000 сегментов это миллион пар. Фильтруйте a.segment_id < b.segment_id, чтобы не считать дважды.

Связанные темы

FAQ

Jaccard vs cosine?

Jaccard — наличие/отсутствие. Cosine — учитывает частоту (вектор). Для бинарных данных — Jaccard, для счётчиков — cosine.

Jaccard для документов?

Да: разбиваете на n-grams (shingling), считаете Jaccard как 1−Jaccard distance. Используется в near-duplicate detection.

Какой Jaccard «похожие»?

Зависит от домена. В recsys 0.1+ уже сигнал. В дедупе документов: > 0.8 — почти копии.

Производительность на больших множествах?

MinHash — приближённый Jaccard с константной памятью. Если множества миллионы — без MinHash тяжело.

Можно ли отрицательный Jaccard?

Нет, всегда в [0, 1]. Если получился < 0 — ошибка в SQL (например, union неправильно посчитан).