Байесовская статистика и вероятностное программирование

Сегодня мы окунюмся в байесовскую статистику и рассмотрим концепция вероятностного программирования Python. Это большая и сложная тема, более-менее глубокое изучение которой невозможно в рамках двух пар, поэтому я, как всегда, оставлю ссылки на материалы, которые использовал для подготовки, основными из которых будут эти три книги:

Bayesian Analysis with Python [1]
Bayesian Methods for Hackers [2]
Think Bayes: Bayesian Statistics in Python [3]

В сегодняшнем занятия будут использоваться новая библиотека — pymc3.

!pip3 install pymc3 # установить библиотеку

Следует отметить, что сейчас (февраль 2019 года) активно идёт разработка новой, четвёртой, версии библиотеки PyMC, API которой претерпел значительные изменения из-за перехода с Theano на TensorFlow, которые теперь используется для всех вычислений. По этой причине данный туториал, вероятно, потеряет актуальность через некоторое время.

Введение в байесовский статистический вывод

Если данные говорят с вами, значит вы — байесовец.

Philip A. Schrodt, Seven deadly sins of contemporary quantitative political analysis [4]

Формально, теорема Байеса – это просто математическая формула. Однако её значение гораздо глубже. Теорема Байеса подводит нас к тому, что необходимо иначе взглянуть на процесс выдвижения и проверки идей.

Nate Silver, The Signal and the Noise: Why So Many Predictions Fail — but Some Don't [5]

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

Сторонники классического подхода исходят из того, что истинные параметры модели не случайны, а аппроксимирующие их оценки случайны, поскольку они являются функциями наблюдений, содержащих случайный элемент. Параметры модели считаются не случайными из-за того, что классическое определение вероятности исходит из предположения равновозможности как объективного свойства изучаемых явлений, основанного на их реальной симметрии. На такое представление о вероятности повлияло то, что в начале своего развития теория вероятности применялась прежде всего для анализа азартных игр. Суждение вида «вероятность выпадения шестёрки при бросании игрального кубика равняется 1/6» основывается на том, что любая из шести граней при подбрасывании на удачу не имеет реальных преимуществ перед другими, и это не подлежит формальному определению. Таким образом, вероятностью случайного события AA в её классическом понимании будет называться отношение числа несовместимых (не могущих произойти одновременно) и равновозможных элементарных событий mm к числу всех возможных элементарных событий nn: P(A)=\frac{m}{n}

Однако такое определение наталкивается на некоторые непреодолимые препятствия, связанные с тем, что не все явления подчиняются принципу симметрии. Например, из соображений симметрии невозможно определить вероятность наступления дождливой погоды. Для преодоления подобных трудностей был предложен статистический или частотный способ приближенной оценки неизвестной вероятности случайного события, основанный на длительном наблюдении над проявлением или не проявлением события AA при большом числе независимых испытаний и поиске устойчивых закономерностей числа проявлений этого события. Если в результате достаточно многочисленных наблюдений замечено, что частота события AA колеблется около некоторой постоянной, то мы скажем, что это событие имеет вероятность. Данный тип вероятности был выражен Р. Мизесом в следующей математической формуле: p=\lim_{x\to\infty}\frac{\mu}{n}, где μ\mu — количество успешных испытаний, nn — количество всех испытаний. Вероятность здесь понимается как частота успешных исходов я является чисто объективной мерой, поскольку зависит лишь от точного подсчёта отношения количества успешных и неуспешных событий.

Основываясь на этом подходе, статистика занималась созданием вероятностных моделей, которые включали в себя параметры, которые, как предполагалось, связаны с характеристиками исследуемой выборки. Параметры никогда не могут быть известны с абсолютной точностью до тех пор, пока мы не исследуем всю генеральную совокупность. До тех пор всегда существует вероятность отклонить гипотезу, когда она на самом деле верна, т. е. совершить ошибку первого рода. Для обозначения вероятности такой ошибки частотники используют понятие уровня значимости α\alpha. Именно вероятность ошибки первого рода частотники ставят во главу анализа, определяя вероятность события. После каждого своего утверждения они обычно добавляют «... на доверительном уровне в 95%», подразумевая, что исследователь допускает вероятность ошибки в пяти процентах случаев (при α=0,05\alpha = 0,05).

Иногда параметры вообще невозможно интерпретировать применительно к реальной жизни, поскольку модели редко бывают абсолютно верными. Модели, как мы надеемся, — это некоторые полезные приближения к истине, на основании которых можно делать прогнозы. Тем не менее прежде всего классическое статистическое исследование сосредоточено на оценке параметров, а не на предсказании.

Частотный подход доминировал в XX веке, придя на смену другому пониманию вероятности, связанном с именем английского математика Томаса Байеса. Сущность байесовского подхода составляют три элемента: априорная вероятность, исходные статистические данных данные, постаприорная вероятность.

Байесовская статистика начинает построение своей модели при помощи понятия априорной вероятности, с помощью которой описывается текущее состояние наших знаний, относительно параметров распределения. Априорная вероятность, таким образом, — это степень нашей уверенности в том, что исследуемый параметр примет то или иное значение ещё до начала сбора исходных статистических данных. На этом основании байесовское понимание вероятности относят к группе субъективистских трактовок вероятности. Чаще всего предполагается, что для оценки степени уверенности необходимо привлечь экспертов, чьё субъективное свидетельство позволит избежать действительной многократной реализации интересующего нас эксперимента.

Следующий элемент — это исходные статистические данные. По мере их поступления статистик пересчитывает распределение вероятностей анализируемого параметра, переходя от априорного распределения к апостериорному, используя для этого формулу Байеса: P(\theta|y)=\frac{P(y|\theta)P(\theta)}{P(y)}, где

  • P(θ)P(\theta) —— априорная вероятность гипотезы θ\theta, выражает наши знания о значениях параметров, предшествующие построению модели.
  • P(yθ)P(y|\theta) —— апостериорная вероятность, представляет собой распределение вероятностей для параметра в нашей модели, рассчитанное с учётом априорных знаний и правдоподобия новых.
  • P(θy)P(\theta|y) —— правдоподобие, вероятность наблюдать данные yy при истинности гипотезы θ\theta
  • P(y)P(y) —— свидетельство (evidence, marginal likelihood), полная вероятность гипотезы θ\theta. Другими словами — вероятность наблюдения данных, усредненая по всем возможным значениям, которые могут принимать параметры. Можно рассматривать её как плотность распределения всех объектов, но, по сути, она нам не очень нужна: выбор максимальной вероятности принадлежности к классам не зависит от этого значения. Можно расценивать этот член как некоторый нормирующий коэффициент. Без него формула станет даже проще:

p(θy)p(yθ)p(θ)p(\theta|y)∝ p(y|\theta)p(\theta)

Суть формулы в том, что она позволяет переставить причину и следствие: по известному факту события вычислить вероятность того, что оно было вызвано данной причиной. Эту формулу также называют формулой обратной вероятности. Процесс пересмотра вероятностей, связанных с высказываниями, по мере поступления новой информации составляет существо машинного обучения и является одним из возможных способов формализации и операционализации следующего тезиса: «степень нашей разумной уверенности в некотором утверждении (касающемся, например, неизвестного численного значения интересующего нас параметра) возрастает и корректируется по мере пополнения имеющейся у нас информации относительно исследуемого явления» (Айвазян 2008). В частотном подходе данный тезис интерпретируется в свойстве состоятельности оценки неизвестного параметра: чем больше объём выборки, на основании которой мы строим свою оценку, тем большей информацией об этом параметре мы располагаем и тем ближе к истине наше заключение. Специфика байесовского подхода к интерпретации этого тезиса основана на том, что вероятность, понимаемая как количественное значение степени разумной уверенности в справедливости некоторого утверждения, пересматривается по мере изменения информации, касающейся этого утверждения. Поэтому в данном подходе вероятность всегда есть условная вероятность, при условии нынешнего состояния информации (в русле классического подхода исследователь скорее склонен рассматривать совместную вероятность.

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

Так или иначе, в 1980-х годах, стало ясно, что частотный подход к статистическому выводу не достаточно хорошо подходит для анализа нелинейных отношений в больших объёмах данных, производимых сложными системами при моделировании процессов реального мира. Для преодоления этих ограничений частотники создали нелинейные версии параметрических методов, такие как множественный нелинейный регрессионный анализ.

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

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

С другой стороны, и у байесовского метода имеются несколько недостатков. Одним из них является необходимость привлекать для расчёта априорные данные, которые могут быть недоступны. А если они и доступны, то, как отмечалось выше, часто носят субъективный характер. Другой недостаток — сложность вычислений. В вышеописанном примере для вычисления байесовской вероятности нам необходимо было вычислить частотную вероятность, полную вероятность, и, наконец, собственно байесовскую вероятность. Сложность байесовских вычислений частично объясняет тот факт, что байесовские методы вновь обрели популярность с развитием вычислительной техники. Следующий недостаток байесовского метода — неинтуитивность, непонятность его результатов для обыденного сознания. Именно на этой неинтуитивности построен знаменитый парадокс Монти Холла, который легко решает с помощью формулы Байеса.

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

Байесовская статистика концептуально очень проста: у нас есть некоторые данные, которые являются фиксированными, в том смысле, что мы не можем изменить то, что мы измерили, и у нас есть параметры, значения которых представляют для нас интерес, и, следовательно, мы исследуем их вероятные значения.

Все неопределенности, которые мы имеем, моделируются с использованием вероятностей. В других статистических парадигмах существуют разные типы неизвестных величин; в байесовских рамках все, что неизвестно, рассматривается одинаково. Если мы не знаем количество, мы назначаем ему распределение вероятностей. Затем теорема Байеса используется для преобразования предшествующего распределения вероятности p(θ)p (θ) (что мы знаем о данной проблеме до наблюдения данных) в апостериорное распределение p(θD)p (θ | D) (то, что мы знаем после наблюдения данных). Другими словами, байесовская статистика является формой обучения.

Статистический вывод

  • Доверительный интервал
    • Байесовский: credible interval
    • Частотный: confidence interval
  • Результат
    • Байесовский: Оценка уверенности в виде вероятности, предсказание
    • Частотный: Вердикт о правильности или неправильности гипотезы при заданном уровне доверительного интервала, уровне значимости
  • Выборка
    • Частотный: Фиксирована
    • Байесовский: Имеет некоторое распределение
  • Оцениваемый параметр
    • Байесовский: Случайная величина с некоторым заданным априорным распределением
    • Частотный: Фиксирован

Вероятностное программирование

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

Бросание монетки — частотный подход

В большинстве учебников по теории вероятностей изучение начинается с бросания монетки. То же самое и в байесовской статистики, только тут всё куда интересней. Пусть кто-то бросает монету и мы наблюдаем исходы: 1 — выпал орёл, 0 — выпала решка. Наша задача оценить вероятность выпадения орла (т.е. θ\theta в данном случае — это количество выпадений орла). Если любите формулы и интегралы, читайте объяснение Николенко или Дьяконова, а ещё лучше целый курс.

Эта случайная величина — результаты бросания монетки — подчиняется распределению Бернулли. Мы также знаем, что последовательность независимых случайных величин Y=X1+X2++XnY=X_{1}+X_{2}+\ldots +X_{n}, имеющих одинаковое распределение Бернулли, имеет биномиальное распределение с параметрами nn и pp, где pp — вероятность успеха в nn испытаниях. Это записывается в виде: YBin(n,p).Y\sim {\mathrm {Bin}}(n,p).

Функция вероятности данной случайной величины задаётся формулой Бернулли:

Pnk=Cnkpk(1p)nk,P_{n}^{k}=C_{n}^{k}p^{k}(1-p)^{n-k}, где kk — количество успешных исходов.

Давайте запрограммируем формулу на Python.

%matplotlib inline import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import pymc3 as pm from scipy import stats from math import factorial import arviz as az import matplotlib.pyplot as plt #%config InlineBackend.figure_format = 'svg' plt.rcParams['figure.figsize'] = (10, 6) SEED = 12 def num_of_successes(n, k): return factorial(n)/(factorial(k) * factorial(n - k)) def probability_of_success(p, n, k): C_kn = num_of_successes(n, k) return C_kn * (p**k) * (1 - p)**(n - k)

Формула Бернулли позволяет ответить на вопрос, какова вероятность в 10 бросках монеты получить 9 «орлов», если монека честная (вероятность «орла» составляет 50%).

probability_of_success(p=0.5, n=10, k=9)
0.009765625

Как видно, вероятность достаточно небольшая.

Предположим, некоторый игрок утверждает, что он побеждает не менее, чем в 5 играх из 6. Однако, его аккаунт показал, что в последних 10 игр он выиграл только 4. Какова вероятность, что игрок сказал правду о своих навыках? Попытайтесь решить задачу сами, а потом .

Вероятность успешных испытаний для бросков монетки моделируется при помощи биноминального распределения. Это дискретное распределение показывает вероятность успеха в серии из N испытаний при фиксированном значении θ, если все испытания независимы друг от друга. В терминах байесовской статистики это будет правдоподобие p(yθ)p(y|\theta). Следующий код генерирует 9 биномиальных распределений; у каждого есть своя легенда, где показаны параметры nn — количество испытаний и p—вероятность успеха в каждом испытании.

n_params = [1, 4, 12] p_params = [0.25, 0.5, 0.75] f, ax = plt.subplots(len(n_params), len(p_params), sharex=True, sharey=True) for i in range(3): for j in range(3): n = n_params[i] p = p_params[j] y = [probability_of_success(p=p, n=n, k=i) for i in range(n+1)] ax[i,j].vlines(range(0, n + 1), 0, y, colors='b', lw=5) ax[i,j].set_ylim(0, 1) ax[i,j].plot(0, 0, label="n = {:3.2f}\np ={:3.2f}".format(n, p), alpha=0) ax[i,j].legend(fontsize=10) ax[2,1].set_xlabel('$\\theta$', fontsize=14) ax[1,0].set_ylabel('$p(y|\\theta)$', fontsize=14);

Выглядит чудесно, но в реальной жизни мы не знаем, чему равен θ\theta. Однако это не проблема для байесовского подхода, где каждый раз, когда мы не знаем значение параметра, необходимо придумать для него априорное распределение, а затем подобрать его параметры.

В нашем примере в роли априорного распределения будет выступать бета-распределение. Оно выражает нашу веру в то, что θ\theta принимает именно такое значение, какое декларируется. Мы выбрали бета-распределение, поскольку оно является сопряжённым к Биномиальному распределению, т.е. может принимать тот же вид, но при других параметрах. Также, если функцию правдоподобия для распределения Бернулли умножить на плотность бета-распределения, то мы снова получим бета-распределение. Таким образом, переходя от априорного распределения к апостериорному вид распределения не меняется, что удобно.

n_trials = [0, 1, 2, 3, 4, 8, 16, 32, 50, 150] data = [0, 1, 1, 1, 1, 4, 6, 9, 13, 48] theta_real = 0.35 beta_params = [(1, 1), (20, 20), (1, 4)] x = np.linspace(0, 1, 200) for idx, N in enumerate(n_trials): if idx == 0: plt.subplot(4, 3, 2) else: plt.subplot(4, 3, idx+3) y = data[idx] for (a_prior, b_prior) in beta_params: # это получилось после перемножения биноминального на бета p_theta_given_y = stats.beta.pdf(x, a_prior + y, b_prior + N - y) plt.fill_between(x, 0, p_theta_given_y, alpha=0.7) plt.xlabel('θ') plt.axvline(theta_real, ymax=0.3, color='k') plt.plot(0, 0, label=f'{N:4d} trials\n{y:4d} heads', alpha=0) plt.legend() plt.tight_layout()

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

Тут на помощь приходит семплирование методом MCMC.

MCMC

MCMC генерирует сэмплы из апостериорного распределения вероятностей, создавая обратимую цепь Маркова, равновесное распределение которой – это целевое апостериорное распределение [6].

  1. Задаётся начальное значение параметра
  2. Перемещаемся из этой позиции в какое-то другое место (каким-либо образом меняем значение параметра).
  3. Если полученное распределение с новым параметром объясняет данные лучше, чем при предыдущем параметре (т.е. вероятность данных выше), то двигаемся в этом направлении дальше.

Обычно, движение осуществляется согласно алгоритму Метрополиса — Гастингса.

PyMC3

PyMC3 позволяет выводить апостериорное распределение без непосредственного вывода формул.

np.random.seed(12) n_experiments = 4 theta_real = 0.35 data = stats.bernoulli.rvs(p=theta_real, size=n_experiments)

θBeta(α,β)θ ∼ Beta (α , β ) yBin(n=1,p=θ)y ∼ Bin(n = 1, p = θ )

with pm.Model() as flip_coin: # a priori θ = pm.Beta('θ', alpha=1., beta=1.) # likelihood y = pm.Bernoulli('y', p=θ, observed=data) trace = pm.sample(1000, random_seed=12, njobs=2)
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [θ]
Sampling 2 chains: 100%|██████████| 3000/3000 [00:01<00:00, 1594.16draws/s]
The acceptance probability does not match the target. It is 0.8999423548983662, but should be close to 0.8. Try to increase the number of tuning steps.
pm.model_to_graphviz(flip_coin)
%3 cluster4 4 θ θ ~ Beta y y ~ Bernoulli θ->y

The function summary provides a text-format summary of the posterior. We get the mean, standard deviation, and the HPD intervals:

pm.summary(trace)
mean sd mc_error hpd_2.5 hpd_97.5 n_eff Rhat
θ 0.324482 0.174799 0.006748 0.028 0.652839 537.154337 1.008212

Since we are approximating the posterior with a finite number of samples, the first thing we need to do is to check if we have a reasonable approximation. There are several tests we can run, some are visual and some quantitative. These tests try to find problems with our samples but they cannot prove we have the correct distribution; they can only provide evidence that the sample seems reasonable. If we find problems with the sample, the solutions are:

  • Increase the number of samples.
  • Remove a number of samples from the beginning of the trace. This is know as burn-in.
  • Re-parametrize the model, that is express the model in a different but equivalent way.
  • Transform the data.
pm.traceplot(trace, lines={'θ':theta_real}, skip_first=100);

The left side shows our marginal posterior – for each parameter value on the x-axis we get a probability on the y-axis that tells us how likely that parameter value is. KDE plots should look like smooth curves. Often, as the number of data increases, the distribution of each parameter will tend to become Gaussian-like; this is due to the law of the large numbers. Of course, this is not always true.

The plot on the right should look like white noise; we are looking for good mixing. This plot shows individual sampled values at each step during the sampling. We should not see any recognizable pattern, we should not see a curve going up or down, instead we want a curve meandering around a single value.

A quantitative way to check for convergence is by using the Gelman-Rubin test. The idea of this test is to compare the variance between chains with the variance within chains, so of course we need more than one chain for this test to work. Ideally, we should expect a value of R=1R=1. As an empirical rule, we will be ok with a value &lt; 1.1; higher values are signaling a lack of convergence:

pm.gelman_rubin(trace)
{'θ': 1.0082118915125144}

We can also visualize the RR for every parameter together with the mean, 50% HPD (Highest Posterior Density) and 95% HPD for each parameter distribution using the function forestplot. An HPD is the shortest interval containing a given portion of the probability density.

pm.forestplot(trace);
pm.plot_posterior(trace, kde_plot=True, rope=[.3, .4]);

An ideal sample will lack autocorrelation, that is, a value at one point should be independent of the values at other points.

pm.autocorrplot(trace);
pm.effective_n(chain)
{'θ': 501.6890060156433}

Assignment

Using PyMC3, change the parameters of the prior beta distribution to match those of the previous chapter and compare the results to the previous chapter. Replace the beta distribution with a uniform one in the interval [0,1]. Are the results equivalent to beta(α =1,β =1)? Is the sampling slower, faster, or the same? What about using a larger interval such as [-1, 2]?

Байесовский подход для сравнения групп

Одной из частных задач в анализе данных является сравнение групп. Пример такой задачи можно посмотреть в секции «Анализ данных для маркетологов», где в последней задаче надо было сравнить выручку, полученную с пользователей Айфонов и Айпадов. Там это было сделано при помощи двувыборочного t-test'а, который является типичным методом частотного подхода. Суть этого теста в том, чтобы проверить, значимо ли различаются средние значения в двух выборках, а его результатом является вероятность того, что средние различаются незначимо (нулевая гипотеза). Если эта вероятность меньше определённого значения (p-value), обычно 0.050.05, мы можем говорить, что с вероятностью не менее 95% средние величины в двух выборках различаются значимо.

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

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

tips = pd.read_csv("https://nagornyy.me/datasets/tips.csv") sns.violinplot(x='day', y='tip', data=tips);
/usr/local/lib/python3.7/site-packages/scipy/stats/stats.py:1713: FutureWarning: Using a non-tuple sequence for multidimensional indexing is deprecated; use `arr[tuple(seq)]` instead of `arr[seq]`. In the future this will be interpreted as an array index, `arr[np.array(seq)]`, which will result either in an error or a different result.
return np.add.reduce(sorted[indexer] * weights, axis=axis) / sumval

Модель для этой проблемы почти такая же, как и раньше; единственное отличие состоит в том, что теперь параметры µ и σ будут векторами вместо скалярных случайных величин. Другими словами, каждый раз, когда мы производим выборку из наших априорных значений, мы получим четыре значения для μ и четыре значения σ вместо одного, как в нашей предыдущей модели. При помощи PyMC3 это очень удобно воплотить: для задания арпиорного распределения нам нужно передать аргумент shape, а для вероятности нам нужно правильно индексировать μ и σ, и поэтому мы создаем переменную idx:

tip = tips['tip'].values idx = pd.Categorical(tips['day'], categories=['Thur', 'Fri', 'Sat', 'Sun']).codes groups = len(np.unique(idx)) with pm.Model() as comparing_groups: μ = pm.Normal('μ', mu=0, sd=10, shape=groups) σ = pm.HalfNormal('σ', sd=10, shape=groups) # объясните, почему так y = pm.Normal('y', mu=μ[idx], sd=σ[idx], observed=tip) trace_cg = pm.sample(5000) pm.traceplot(trace_cg)
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [σ, μ]
Sampling 2 chains: 100%|██████████| 11000/11000 [00:29<00:00, 375.84draws/s]
array([[,
],
[,
]],
dtype=object)

Оценим различие между группами при помощи Cohen's d, который рассчитывается как разница между средними, деленными корень из дредних квадратов отклонений и очень популярен, например, в психологии, и Probability of Superiority вероятности того, что показатель одной случайной выборки больше, чем показатель другой случайной выборки из той же генеральной совокупности.

dist = stats.norm() _, ax = plt.subplots(3, 2, figsize=(14, 8), constrained_layout=True) comparisons = [(i, j) for i in range(4) for j in range(i+1, 4)] pos = [(k, l) for k in range(3) for l in (0, 1)] for (i, j), (k, l) in zip(comparisons, pos): means_diff = trace_cg['μ'][:, i] - trace_cg['μ'][:, j] d_cohen = (means_diff / np.sqrt((trace_cg['σ'][:, i]**2 + trace_cg['σ'][:, j]**2) / 2)).mean() ps = dist.cdf(d_cohen/(2**0.5)) az.plot_posterior(means_diff, ref_val=0, ax=ax[k, l]) ax[k, l].set_title(f'$\mu_{i}-\mu_{j}$') ax[k, l].plot( 0, label=f"Cohen's d = {d_cohen:.2f}\nProb sup = {ps:.2f}", alpha=0) ax[k, l].legend()

Чтобы интерпретировать эти графики, надо сравнить значение 0 (нет разницы) с интервалом HPD. Согласно предыдущему рисунку, у нас есть только один случай, когда 95% HPD исключают 0, разницу в чаевых между четвергом и воскресеньем. Для всех остальных примеров мы не можем исключить разницу в 0 (в соответствии с критериями HPD). Но даже для этого случая средняя разница составляет ≈0,5 доллара.

Линейная регрессия в байесовском стиле

Обычно линейная регрессия выглядит так: Y=Xβ+ϵY = X\beta + \epsilon

Байесовцы выражают эту модель в терминах распределения вероятностей. Вышеупомянутая линейная регрессия может быть переписана как:

YN(Xβ,σ2)Y \sim \mathcal{N}(X \beta, \sigma^2)

Таким образом, мы рассматриваем YY как случайную переменную (или случайный вектор), элементы которой распределены нормально. Среднее значение этого нормального распределения обеспечивается нашим линейным предиктором с дисперсией σ2\sigma^2.

Хотя это, по сути, одна и та же модель, есть два критических преимущества байесовского подхода:

  • Наличие априорного распределения: Мы можем количественно оценить любые предварительные знания, которые у нас могут быть, выбрав соответствующие априорные распределения. Например, если мы думаем, что дисперсия, вероятно, будет небольшой, мы бы выбрали априоное распределение, где вероятность небольших значений выше.
  • Количественная оценка неопределенности: мы не получаем единственную оценку 𝛽, как указано выше, но вместо этого получаем полное апостериорное распределение о том, насколько вероятны различные значения. Например, с небольшим количеством данных наша неопределенность для 𝛽 будет очень высокой, и мы получим очень широкие постериорные распределения для для этого параметра.

Сгенерируем 200 точек, которые описываются уравнением y=2x+1+ϵy=2x+1+\epsilon, и нанесём их и прямую на график.

size = 200 true_intercept = 1 true_slope = 2 x = np.linspace(0, 1, size) # y = a + b*x true_regression_line = true_intercept + true_slope * x # add noise y = true_regression_line + np.random.normal(scale=.5, size=size) data = dict(x=x, y=y) fig = plt.figure(figsize=(7, 7)) ax = fig.add_subplot(111, xlabel='x', ylabel='y', title='Generated data and underlying model') ax.plot(x, y, 'x', label='sampled data') ax.plot(x, true_regression_line, label='true regression line', lw=2.) plt.legend(loc=0);
with pm.Model() as simple_linear_model: # Define priors sigma = pm.HalfCauchy('sigma', beta=10, testval=1) intercept = pm.Normal('Intercept', 0, sd=20) x_coeff = pm.Normal('x', 0, sd=20) # Define likelihood likelihood = pm.Normal('y', mu=intercept + x_coeff * x, sd=sigma, observed=y) # Inference! trace = pm.sample(1000) # draw 3000 posterior samples using NUTS sampling
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [x, Intercept, sigma]
Sampling 2 chains: 100%|██████████| 3000/3000 [00:07<00:00, 400.94draws/s]
pm.model_to_graphviz(simple_linear_model)
%3 cluster200 200 Intercept Intercept ~ Normal y y ~ Normal Intercept->y x x ~ Normal x->y sigma sigma ~ HalfCauchy sigma->y
pm.traceplot(trace, skip_first=100);

In the GLM we thus do not only have one best fitting regression line, but many. A posterior predictive plot takes multiple samples from the posterior (intercepts and slopes) and plots a regression line for each of them. Here we are using the plot_posterior_predictive_glm() convenience function for this.

plt.figure(figsize=(7, 7)) plt.plot(x, y, 'x', label='data') pm.plot_posterior_predictive_glm(trace, samples=100, label='posterior predictive regression lines') plt.plot(x, true_regression_line, label='true regression line', lw=3., c='y') plt.title('Posterior predictive regression lines') plt.legend(loc=0) plt.xlabel('x') plt.ylabel('y');
pm.forestplot(trace);

Многоуровневые регрессионные модели в байесовском стиле

Сегодня мы опять будем использовать набор данных о радоне. В этом наборе данных количество радона было измерено среди разных домохозяйств в нескольких штатах. Известно, что радон является основной причиной рака легких у некурящих. Считается, что его уровень больше в домах с подвалом, а также его содержание зависит от типа почвы. В своей модели, как и раньше, мы попытаемся учесть все эти различия факторы.

radon = pd.read_csv("https://raw.githubusercontent.com/pymc-devs/pymc3/master/pymc3/examples/data/radon.csv", index_col=0)
radon[['county', 'log_radon', 'floor']].head()
county log_radon floor
0 AITKIN 0.832909 1.0
1 AITKIN 0.832909 0.0
2 AITKIN 1.098612 0.0
3 AITKIN 0.095310 0.0
4 ANOKA 1.163151 0.0
from pymc3 import Model, sample, Normal, HalfCauchy, Uniform, model_to_graphviz floor = radon.floor.values log_radon = radon.log_radon.values n_counties = len(radon.county.unique()) county_names = radon.county.unique() county_idx = radon.county_code.values

Чтобы подчеркнуть эффект иерархической линейной регрессии, мы сначала построим неиерархическую байесовскую модель, в которой для каждого округа будет задан свой собственный свободный коэффициент. radoni,c=αc+βcfloori,c+ϵcradon_{i, c} = \alpha_{c} + \beta_{c}*\text{floor}_{i, c} + \epsilon_c Фактически, это будет просто набор отдельных моделей. Поскольку у нас нет предварительной информации о том, каким может быть перехват или регрессия, мы будем использовать нормальное распределение с центром в 0 с широким стандартным отклонением. Мы предполагаем, что измерения обычно распределены с шумом, который мы смоделируем при помощи равномерного распределения.

with pm.Model() as unpooled_model: # Independent parameters for each county a = pm.Normal('a', 0, sd=100, shape=n_counties) b = pm.Normal('b', 0, sd=100, shape=n_counties) # Model error eps = pm.HalfCauchy('eps', 5) # Model prediction of radon level # a[county_idx] translates to a[0, 0, 0, 1, 1, ...], # we thus link multiple household measures of a county # to its coefficients. radon_est = a[county_idx] + b[county_idx]*radon.floor.values # Data likelihood y = pm.Normal('y', radon_est, sd=eps, observed=radon.log_radon) with unpooled_model: unpooled_trace = pm.sample(3000)
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [eps, b, a]
Sampling 2 chains: 100%|██████████| 7000/7000 [00:32<00:00, 212.17draws/s]
model_to_graphviz(unpooled_model)
%3 cluster85 85 cluster919 919 b b ~ Normal y y ~ Normal b->y a a ~ Normal a->y eps eps ~ HalfCauchy eps->y

Вместо того, чтобы создавать отдельные модели, иерархическая модель создает параметры на уровне округов, благодаря чему округа рассматриваются не по отдельности. αcN(μα,σα2)\alpha_{c} \sim \mathcal{N}(\mu_{\alpha}, \sigma_{\alpha}^2) βcN(μβ,σβ2)\beta_{c} \sim \mathcal{N}(\mu_{\beta}, \sigma_{\beta}^2) Эти распределения впоследствии используются, чтобы влиять на распределение 𝛼 и 𝛽 каждого округа. Таким образом, мы предполагаем, что свободный коэффициент 𝛼 и угловой коэффициент 𝛽 происходят из нормального распределения, сосредоточенного вокруг их соответствующего среднего по группе μ\mu с некоторым стандартным отклонением σ2\sigma^2, значения (или, скорее, исходные значения), которое мы также оцениваем. Вот почему это называется многоуровневым моделированием.

with pm.Model() as hierarchical_model: # Hyperpriors for group nodes mu_a = pm.Normal('mu_a', mu=0., sd=100**2) sigma_a = pm.HalfCauchy('sigma_a', 5) mu_b = pm.Normal('mu_b', mu=0., sd=100**2) sigma_b = pm.HalfCauchy('sigma_b', 5) # Intercept for each county, distributed around group mean mu_a # Above we just set mu and sd to a fixed value while here we # plug in a common group distribution for all a and b (which are # vectors of length n_counties). a = pm.Normal('a', mu=mu_a, sd=sigma_a, shape=n_counties) # Intercept for each county, distributed around group mean mu_a b = pm.Normal('b', mu=mu_b, sd=sigma_b, shape=n_counties) # Model error eps = pm.HalfCauchy('eps', 5) radon_est = a[county_idx] + b[county_idx] * radon.floor.values # Data likelihood radon_like = pm.Normal('radon_like', mu=radon_est, sd=eps, observed=radon.log_radon)
model_to_graphviz(hierarchical_model)
%3 cluster85 85 cluster919 919 sigma_b sigma_b ~ HalfCauchy b b ~ Normal sigma_b->b sigma_a sigma_a ~ HalfCauchy a a ~ Normal sigma_a->a mu_a mu_a ~ Normal mu_a->a eps eps ~ HalfCauchy radon_like radon_like ~ Normal eps->radon_like mu_b mu_b ~ Normal mu_b->b b->radon_like a->radon_like
with hierarchical_model: hierarchical_trace = pm.sample(draws=2000, n_init=1000)
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [eps, b, a, sigma_b, mu_b, sigma_a, mu_a]
Sampling 2 chains: 100%|██████████| 5000/5000 [00:32<00:00, 153.82draws/s]
There were 9 divergences after tuning. Increase `target_accept` or reparameterize.
The estimated number of effective samples is smaller than 200 for some parameters.
pm.traceplot(hierarchical_trace);

Величина muamu_a показывает средние уровни радона в группах. mubmu_b говорит, что отсутствие фундамента значительно снижает уровни радона (график плотности лежит ниже нуля). Мы также можем увидеть, посмотрев на график aa, что между округами довольно много различий в уровнях радона (каждый цвет соответствует одному округу); Различная ширина связана с тем, насколько мы уверены в каждой оценке параметров - чем больше измерений в округе, тем выше будет наша достоверность.

selection = ['CASS', 'CROW WING', 'FREEBORN'] fig, axis = plt.subplots(1, 3, figsize=(12, 6), sharey=True, sharex=True) axis = axis.ravel() for i, c in enumerate(selection): c_data = radon.loc[radon.county == c] c_data = c_data.reset_index(drop = True) c_index = np.where(county_names==c)[0][0] z = list(c_data['county_code'])[0] xvals = np.linspace(-0.2, 1.2) for a_val, b_val in zip(unpooled_trace['a'][1000:, c_index], unpooled_trace['b'][1000:, c_index]): axis[i].plot(xvals, a_val + b_val * xvals, 'b', alpha=.1) axis[i].plot(xvals, unpooled_trace['a'][1000:, c_index].mean() + unpooled_trace['b'][1000:, c_index].mean() * xvals, 'b', alpha=1, lw=2., label='individual') for a_val, b_val in zip(hierarchical_trace['a'][1000:][z], hierarchical_trace['b'][1000:][z]): axis[i].plot(xvals, a_val + b_val * xvals, 'g', alpha=.1) axis[i].plot(xvals, hierarchical_trace['a'][1000:][z].mean() + hierarchical_trace['b'][1000:][z].mean() * xvals, 'g', alpha=1, lw=2., label='hierarchical') axis[i].scatter(c_data.floor + np.random.randn(len(c_data))*0.01, c_data.log_radon, alpha=1, color='k', marker='.', s=80, label='original data') axis[i].set_xticks([0,1]) axis[i].set_xticklabels(['basement', 'no basement']) axis[i].set_ylim(-1, 4) axis[i].set_title(c) if not i%3: axis[i].legend() axis[i].set_ylabel('log radon level')

На приведенном выше графике мы более пристально взглянем на три округа. Толстые линии представляют собой среднюю оценку линии регрессии обычно (синяя) и иерархической модели (зеленая). Более тонкие линии представляют собой линии регрессии для раных значений параметров и дают нам представление о том, насколько изменчивы оценки.

Глядя на округ «CASS», мы видим, что неиерархическая оценка сильно смещена: поскольку данные этого округа содержат только домохозяйства с подвалом, регрессионная модель даёт бессмысленный результат, показывая, что даже в доме без подвала следует ожидать уровни радона как в радоновой шахте.

В двух других округах неиерархическая модель, по-видимому, более сильно, чем иерархическая модель, реагирует на наличие выбросов в наборе данных. Учёт многоуровневости ограничивает коэффициенты, и мы получаем более корректные оценки.

Самостоятельное задание

Изучая классические, не байесовские многоуровневые модели, мы пытались найти различия в ценностях людей из разных стран в базе данные European Social Survey. Попробуйте сделать тоже самое с опорой на байесовский подход и проведите необходимые диагностики. Описание всех переменных из ESS доступно в протоколе исследования, сами данные можно скачать по ссылке.

import pyreadstat ess, _ = pyreadstat.read_sav('ESS6.sav') ess.head()

Комментарии