- BrainTools - https://www.braintools.ru -

Зависимости, которых не существует: как фильтрация искажает данные и что такое коллайдеры

Бабушкин телевизор работает уже сорок лет. Дед ездит на жигулях, видевших мороженое за пять копеек. Отцовский перфоратор пережил десяток ремонтов – и хоть бы что. А твой новый ноутбук умер на третий год, смартфон не держит заряд к концу дня, и Cloudflare лёг третий раз за месяц.
Раньше делали на совесть. Это же очевидно.

Нет.

Почему же нет? Ответ кроется буквально у нас под носом. Если понаблюдать за своим бытом, то сразу можно заметить, что есть у людей привычка выбрасывать нерабочие и ненужные вещи, вследствие которой нас окружают только те, что хоть как-то функционируют. Таким образом мы совершенно внезапно, в своём обиходе, встречаемся с одной из фундаментальных ошибок статистики – Collider bias.

Для выяснения деталей, давайте на время из быта перейдём в область формализма.

Причинно – следственный анализ


Нет ничего проще, чем увидев древнюю, чудом уцелевшую детальку кивнуть головой и многозначительно сказать: “Ну, вещь”.

Куда сложнее ответить на вопрос: “Является ли её сохранность следствием качества из былых времён?”.

Или же: “Каковы причины качества найденной детальки?”

Заметили слова “следствие”, “причина”? Именно о них мы и продолжим разговор.

Причинно-следственный анализ – отвечает на вопрос почему что-то происходит и что можно сделать чтобы изменить результат.

Казалось бы – всё просто. Берём x. Берём y. Строим график зависимости y от x. Если график идёт вправо и вверх – значит, чем больше “x” возьмёшь тем больше “y” получишь. Задача решена.

Была бы, если бы мы изучали линейные функции. В области аналитики данных реального мира встретить линейные зависимости – счастье и редкость. В большинстве случаев мы даже не можем утверждать, что связь между переменными существует. Конфаундеры, обратные зависимости и прочие чудеса статистики не дают нам возможности делать простых выводов. Все осветить в рамках одной статьи трудно, однако нам сейчас важно одно: связи между переменными бывают ложными. И один из самых коварных способов получить такую ложную связь — коллайдер.

Коллайдер – переменная, являющаяся следствием других переменных. То есть та, в которой два и более причинных пути сходятся. Зачастую коллайдерами становятся разного рода результаты: последствия действий, выводы, искомые значения.

Формально: если есть A и B, то коллайдер это такая переменная C, которая зависит и от A и от B

A ⇨ C ⇦ B

Возвращаясь же к “деталькам”:

Возраст ⇨ Употребление в быту ⇦ Качество

Коллайдеры легко найти вокруг себя. Вот более заезженный пример:

Ум ⇨ Поступление в ВШЭ ⇦ Богатые родители

Или же:

Талант ⇨ Успех ⇦ Трудолюбие

Опасность коллайдера в том, что его интуитивно хочется учесть в анализе — и именно это открывает ложную связь там, где её нет.

Одним из вариантов “учёта в анализе” можно назвать условие по переменной. Чем же оно опасно в случае с коллайдером, и почему на нём нельзя уславливаться?

Прежде чем мы рассмотрим, как так получается я позволю себе сделать ряд утверждений без доказательств. Предположим у нас всё же есть данные из примеров выше. Мы построили график и сделали выводы:
Среди абитуриентов, поступивших в ВШЭ богатые тупее умных.
Среди успешных людей талантливые – ленивые, трудолюбивые – бесталанны.

При этом взяв статистику по популяции мы таких выводов больше сделать не сможем (или они будут сильно менее выраженными).

Обратите внимание [1], что фраза “Среди” в данном случае и является ранее упомянутой условностью. Однако всё ещё не понятно а что происходит с данными. Почему ранее несвязные переменные становятся отрицательно скоррелированными?
Откуда же появляется ложная связь? Давайте разбираться.

Одним из наиболее честных вариантов демонстрации статистических ошибок я вижу в их симуляции на случайных числах. Внутри случайных чисел нет закономерностей, корреляции, по закону больших чисел, стремятся к нулю и появление закономерностей даст нам чёткий сигнал о нарушении их свойств.

Симуляция эффекта


Проведём простой эксперимент на Python. Для симуляции используем 1000 точек, значения A, B от 0 до 100 и пороги для определения коллайдера = 50

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from statsmodels.nonparametric.smoothers_lowess import lowess


NUMBER_OF_VALUES = 1000
A_RANGE = (0, 100)
B_RANGE = (0, 100)
A_THRESHOLD = 50
B_THRESHOLD = 50

Мы возьмём 2 множества случайных чисел, A и B. Очевидно что связи между ними быть не может.

На данный момент одна запись нашего датасетта выглядит как значение A и B. Это могли бы быть выраженности различных симптомов пациента, зарплата и KPI сотрудника, успех и трудолюбие. Но в нашем случае просто A и B.

a = np.random.uniform(*A_RANGE, NUMBER_OF_VALUES)
b = np.random.uniform(*B_RANGE, NUMBER_OF_VALUES)

data = pd.DataFrame({'a': a, 'b': b})

А также создадим третью переменную значение которой будет зависеть от наших двух раннее сгенерированных переменных. Сие есть прямое определение коллайдера. Создавать переменную flag мы будем с помощью условия “ИЛИ”. Или A больше некоторого порога, или B больше некоторого порога. Если запись в нашем датасетте удовлетворяющее хотя бы одному из условий, то Flag – “да” если ни одному, то – “нет”.

data['flag'] = (data['a'] > A_THRESHOLD) | (data['b'] > B_THRESHOLD)  # Условие "или"!

Посчитаем корреляцию среди всех данных. Это настоящие свойства данных A, B.

print(f"Корреляция между A и B: {data['a'].corr(data['b']):.2f}")
Корреляция между A и B: 0.01

Закономерно, она приблизительно равна нулю. Данные случайны и независимы.

А теперь условимся по коллайдеру, и посмотрим корреляцию для тех значений чей flag – “да”.

print(f"Корреляция между только теми A и B где истинный флаг: {data[data['flag']]['a'].corr(data[data['flag']]['b']):.2f}")
Корреляция между только теми A и B где истинный флаг: -0.34

Корреляция около – 0,3. Это значительная корреляция, говорящая об умеренной связности между переменными, и она может привести к неверным выводам, несоответствующим действительности.

Давайте визуализируем данные и посмотрим с чем связанно появление ложной корреляции.

sns.scatterplot(data=data, x='a', y='b', hue='flag', palette={True: 'red', False: 'blue'}, alpha=0.5)

sns.regplot(data=data, x='a', y='b', scatter=False, color='black', label='General Trend')

plt.legend()
plt.show()
Зависимости, которых не существует: как фильтрация искажает данные и что такое коллайдеры - 1

Красным показаны данные удовлетворяющее хотя бы одному условию. Синим – ни одному. Чёрная линия – линия тренда на всех данных. Она показывает тенденции развития величины. Так как величина случайна линия тренда горизонтальна. Данные по закону больших чисел компенсируют сами себя.

Давайте же применим условие и “вырежем” данные не удовлетворяющее условию.

sns.lmplot(data=data[data['flag']], x='a', y='b', 
           scatter_kws={'alpha': 0.5}, line_kws={'color': 'Black'})
Зависимости, которых не существует: как фильтрация искажает данные и что такое коллайдеры - 2

Линия тренда наклонилась, вслед за искусственно созданной нами отрицательной зависимостью. Это видно по оставшимся данным, вырезав точки, не удовлетворяющие обоим условиям, мы нарушили “равновесие”, и правая половина графика “перевесила” левую. Отсюда и отрицательная корреляция и ложная связность.

Это и есть проявление смещения, вследствие фиксации коллайдера. Так, если бы мы без задней мысли анализировали эти данные, то получили что то вроде такого графика:

filtered = data[data['flag']]

smoothed = lowess(filtered['b'], filtered['a'], frac=0.8)

plt.figure()
plt.plot(smoothed[:, 0], smoothed[:, 1], color='black', linewidth=2)
plt.xlabel('a')
plt.ylabel('b')
plt.tight_layout()
plt.show()
Зависимости, которых не существует: как фильтрация искажает данные и что такое коллайдеры - 3

Но ни направление графика, ни его сглаженные изгибы не отражают природы данных.


Возможно, вы справедливо заметите, что данные не отражают реальных ситуаций. Мало кто анализирует абсолютно случайные, равномерные величины, ведь в этом попусту нет практической пользы (они сущностно представляют и себя белый шум). Поэтому заменим равномерное распределение нормальным, гауссовским. Оно встречается в природе повсеместно.

Вторая проблема данных – жёсткие условия отсечения. Сделаем их вероятностными, как в жизни. Не все же кто предрасположен к раку обязательно от него умрут. Есть ещё грузовик, кирпич на крыше и т.д.

Что ж, давайте посмотрим, как collider bias проявит себя на данных, значительно приближенных к реальности.

a = np.random.normal(loc = 50, scale = 5, size = NUMBER_OF_VALUES)
b = np.random.normal(loc = 30, scale = 5, size = NUMBER_OF_VALUES)

normal_data = pd.DataFrame({'a': a, 'b': b})
sns.scatterplot(data=normal_data, x='a', y='b',  alpha=0.5)
sns.regplot(data=normal_data, x='a', y='b', scatter=False, color='black', label='General Trend')
Зависимости, которых не существует: как фильтрация искажает данные и что такое коллайдеры - 4

Видим типичную картину нормального распределения, тренд ожидаемо нулевой (параллелен горизонтали).

Разобьём данные на 2 группы используя вероятностный метод.

# Нормализуем значения, приводим к диапазону (0, 1)
a_norm = (normal_data['a'] - normal_data['a'].min())  / (normal_data['a'].max() - normal_data['a'].min()  + 1e-6)
b_norm = (normal_data['b'] - normal_data['b'].min())  / (normal_data['b'].max() - normal_data['b'].min() + 1e-6)


selection_prob = np.maximum(a_norm, b_norm) * 1.5
selection_prob = np.clip(selection_prob, 0, 1)  

normal_data['flag'] = np.random.random(len(normal_data)) < selection_prob

sns.scatterplot(data=normal_data, x='a', y='b', hue='flag', 
                palette={True: 'red', False: 'blue'}, alpha=0.5)
plt.title('Вероятностная выборка коллайдера')
plt.show()

print(f"Корреляция во всём датасете: {normal_data['a'].corr(normal_data['b']):.2f}")
print(f"Корреляция, когда флаг истинный: {normal_data[normal_data['flag']]['a'].corr(normal_data[normal_data['flag']]['b']):.2f}")
Зависимости, которых не существует: как фильтрация искажает данные и что такое коллайдеры - 5
Корреляция во всём датасете: 0.03
Корреляция, когда флаг истинный: -0.09

За счёт добавления вероятностного выбора коллайдера разница между истинной и ложной корреляцией упала, в сравнении с наивным примером выше, и теперь находится на низких значениях. Но даже такие отклонения в области, например, фармакологии, опасны.

Также, стоит учитывать, что в реальности ошибка [2] может давать ещё большее влияние на данные.

sns.lmplot(data=normal_data[normal_data['flag']], x='a', y='b', 
           scatter_kws={'alpha': 0.5}, line_kws={'color': 'Black'})
Зависимости, которых не существует: как фильтрация искажает данные и что такое коллайдеры - 6

Наш график после удаления “неистинных” (на прошлом графике – синих) точек. Линия тренда очевидно наклонилась – очевидный признак смещения.

Выводы


Итак, мы увидели конкретный механизм: стоит обусловиться по коллайдеру — и независимые переменные начинают «тянуть» друг друга. Этот эффект не очевиден, потому что фильтровать данные, кажется, разумным действием. Именно поэтому коллайдер опаснее многих других ошибок — он прячется за здравый смысл. Практический вывод один: прежде чем делить выборку, нарисуй причинную диаграмму и спроси — это коллайдер?


Вернёмся же к телевизорам из начала статьи. Искушённый читатель заметил, что приведённый пример является классическим проявлением Ошибки выжившего. Однако в данных она реализуется именно как коллайдер, с условностью на выживании. Кстати, через тот же механизм реализуются парадокс [3] Берксона и Selection Bias.

Таким образом, на пальцах, мы подтвердили постулат причинно-следственного анализа:

Из корреляции не следует каузация.

Даже красивый график может быть лишь ошибкой анализа.

Притом эффект присутствует везде – от госпитализации в медицине до отбора в образовании. Поэтому понимание подобных механизмов, по моему мнению, может помочь в постановке гипотез, вынесении теорий и формировании стратегий. Возможно, именно принятие во внимание условности по коллайдеру когда-то убережёт вас от ошибочных решений или суждений.

Мой Kaggle Nootebook с кодом симуляции [4], если захотите поэкспериментировать сами.

Спасибо за внимание. Это моя первая статья, поэтому фидбэк был бы крайне полезен.

Так же может быть интересно:

  • DAG, причинные диаграммы

  • Конфаундеры, медиаторы

  • Автокорреляции(как ещё один распространённый пример ошибок)

Рекомендую статьи для углубления:

Автор: Toshi_im

Источник [6]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/27675

URLs in this post:

[1] внимание: http://www.braintools.ru/article/7595

[2] ошибка: http://www.braintools.ru/article/4192

[3] парадокс: http://www.braintools.ru/article/8221

[4] Мой Kaggle Nootebook с кодом симуляции: https://www.kaggle.com/code/gpugobrrr/collider-bias

[5] Причинно-следственный анализ в машинном обучении: https://habr.com/ru/companies/ods/articles/544208/

[6] Источник: https://habr.com/ru/articles/1014586/?utm_campaign=1014586&utm_source=habrahabr&utm_medium=rss

www.BrainTools.ru

Rambler's Top100