GLM: 负二项回归#

注意

本笔记本使用了一些非 PyMC 依赖库,因此需要专门安装才能运行。请打开下面的下拉菜单以获取更多指导。

额外的依赖安装说明

为了运行此笔记本(在本地或 Binder 上),您不仅需要安装可用的 PyMC 以及所有可选依赖项,还需要安装一些额外的依赖项。有关 PyMC 本身安装的建议,请参阅安装

您可以使用您偏好的包管理器安装这些依赖项,我们在此处提供 pip 和 conda 命令作为示例。

$ pip install seaborn

请注意,如果您想(或需要)从笔记本内部而不是命令行安装软件包,您可以通过运行 pip 命令的变体来安装软件包

import sys

!{sys.executable} -m pip install seaborn

您不应运行 !pip install,因为它可能会将软件包安装在不同的环境中,即使安装了也无法从 Jupyter notebook 中使用。

另一种替代方法是使用 conda

$ conda install seaborn

当使用 conda 安装科学 Python 软件包时,我们建议使用 conda forge

import arviz as az
import numpy as np
import pandas as pd
import pymc as pm
import seaborn as sns

from scipy import stats
RANDOM_SEED = 8927
rng = np.random.default_rng(RANDOM_SEED)

%config InlineBackend.figure_format = "retina"
az.style.use("arviz-darkgrid")

本笔记本紧密跟随 Jonathan Sedar 的 GLM 泊松回归示例(该示例又受到 Ian Osvald 的一个项目 的启发),不同之处在于,此处的数据是负二项分布而不是泊松分布。

负二项回归用于建模方差高于均值的计数数据。负二项分布 可以被认为是泊松分布,其速率参数是伽马分布的,因此可以调整速率参数以解释增加的方差。

生成数据#

与泊松回归示例中一样,我们假设打喷嚏以某个基线速率发生,并且饮酒、不服用抗组胺药或两者兼有都会增加其频率。

泊松数据#

首先,让我们看一下泊松回归示例中的一些泊松分布数据。

# Mean Poisson values
theta_noalcohol_meds = 1  # no alcohol, took an antihist
theta_alcohol_meds = 3  # alcohol, took an antihist
theta_noalcohol_nomeds = 6  # no alcohol, no antihist
theta_alcohol_nomeds = 36  # alcohol, no antihist

# Create samples
q = 1000
df_pois = pd.DataFrame(
    {
        "nsneeze": np.concatenate(
            (
                rng.poisson(theta_noalcohol_meds, q),
                rng.poisson(theta_alcohol_meds, q),
                rng.poisson(theta_noalcohol_nomeds, q),
                rng.poisson(theta_alcohol_nomeds, q),
            )
        ),
        "alcohol": np.concatenate(
            (
                np.repeat(False, q),
                np.repeat(True, q),
                np.repeat(False, q),
                np.repeat(True, q),
            )
        ),
        "nomeds": np.concatenate(
            (
                np.repeat(False, q),
                np.repeat(False, q),
                np.repeat(True, q),
                np.repeat(True, q),
            )
        ),
    }
)
df_pois.groupby(["nomeds", "alcohol"])["nsneeze"].agg(["mean", "var"])
均值 方差
nomeds 酒精
1.047 1.127919
2.986 2.960765
5.981 6.218858
35.929 36.064023

由于泊松分布随机变量的均值和方差相等,因此样本均值和方差非常接近。

负二项数据#

现在,假设数据集中的每个受试者都患有流感,从而增加了他们打喷嚏的方差(并导致少数不幸的人每天打喷嚏超过 70 次)。如果平均打喷嚏次数保持不变但方差增加,则数据可能服从负二项分布。

# Gamma shape parameter
alpha = 10


def get_nb_vals(mu, alpha, size):
    """Generate negative binomially distributed samples by
    drawing a sample from a gamma distribution with mean `mu` and
    shape parameter `alpha', then drawing from a Poisson
    distribution whose rate parameter is given by the sampled
    gamma variable.

    """

    g = stats.gamma.rvs(alpha, scale=mu / alpha, size=size)
    return stats.poisson.rvs(g)


# Create samples
n = 1000
df = pd.DataFrame(
    {
        "nsneeze": np.concatenate(
            (
                get_nb_vals(theta_noalcohol_meds, alpha, n),
                get_nb_vals(theta_alcohol_meds, alpha, n),
                get_nb_vals(theta_noalcohol_nomeds, alpha, n),
                get_nb_vals(theta_alcohol_nomeds, alpha, n),
            )
        ),
        "alcohol": np.concatenate(
            (
                np.repeat(False, n),
                np.repeat(True, n),
                np.repeat(False, n),
                np.repeat(True, n),
            )
        ),
        "nomeds": np.concatenate(
            (
                np.repeat(False, n),
                np.repeat(False, n),
                np.repeat(True, n),
                np.repeat(True, n),
            )
        ),
    }
)
df
nsneeze 酒精 nomeds
0 2
1 1
2 2
3 2
4 3
... ... ... ...
3995 28
3996 63
3997 19
3998 34
3999 40

4000 rows × 3 columns

df.groupby(["nomeds", "alcohol"])["nsneeze"].agg(["mean", "var"])
均值 方差
nomeds 酒精
0.990 1.137037
2.983 4.030742
6.004 8.658643
35.918 169.018294

与泊松回归示例中一样,我们看到饮酒和/或不服用抗组胺药会在不同程度上增加打喷嚏的频率。与该示例不同,对于 alcoholnomeds 的每种组合,nsneeze 的方差都高于均值。这表明泊松分布不适合该数据,因为泊松分布的均值和方差相等。

可视化数据#

g = sns.catplot(x="nsneeze", row="nomeds", col="alcohol", data=df, kind="count", aspect=1.5)

# Make x-axis ticklabels less crowded
ax = g.axes[1, 0]
labels = range(len(ax.get_xticklabels(which="both")))
ax.set_xticks(labels[::5])
ax.set_xticklabels(labels[::5]);
/opt/homebrew/Caskroom/miniconda/base/envs/pymc-dev/lib/python3.11/site-packages/seaborn/axisgrid.py:123: UserWarning: The figure layout has changed to tight
  self._figure.tight_layout(*args, **kwargs)
/opt/homebrew/Caskroom/miniconda/base/envs/pymc-dev/lib/python3.11/site-packages/seaborn/axisgrid.py:123: UserWarning: The figure layout has changed to tight
  self._figure.tight_layout(*args, **kwargs)
../_images/26e650782d73f95310b3fda053366e4f2a9df307cc35e9ed09f3a2fecc1ed06d.png

负二项回归#

创建 GLM 模型#

COORDS = {"regressor": ["nomeds", "alcohol", "nomeds:alcohol"], "obs_idx": df.index}

with pm.Model(coords=COORDS) as m_sneeze_inter:
    a = pm.Normal("intercept", mu=0, sigma=5)
    b = pm.Normal("slopes", mu=0, sigma=1, dims="regressor")
    alpha = pm.Exponential("alpha", 0.5)

    M = pm.ConstantData("nomeds", df.nomeds.to_numpy(), dims="obs_idx")
    A = pm.ConstantData("alcohol", df.alcohol.to_numpy(), dims="obs_idx")
    S = pm.ConstantData("nsneeze", df.nsneeze.to_numpy(), dims="obs_idx")

    λ = pm.math.exp(a + b[0] * M + b[1] * A + b[2] * M * A)

    y = pm.NegativeBinomial("y", mu=λ, alpha=alpha, observed=S, dims="obs_idx")

    idata = pm.sample()
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [intercept, slopes, alpha]
100.00% [8000/8000 00:10<00:00 Sampling 4 chains, 0 divergences]
Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 11 seconds.

查看结果#

az.plot_trace(idata, compact=False);
../_images/15707fccc92da51bc592954d9b0db8e7507d02bb298a5060840b2681af815c9c.png
# Transform coefficients to recover parameter values
az.summary(np.exp(idata.posterior), kind="stats", var_names=["intercept", "slopes"])
均值 标准差 hdi_3% hdi_97%
截距 0.993 0.033 0.935 1.056
斜率[nomeds] 6.048 0.221 5.635 6.438
斜率[酒精] 3.006 0.115 2.789 3.215
斜率[nomeds:酒精] 1.995 0.086 1.842 2.163
az.summary(idata.posterior, kind="stats", var_names="alpha")
均值 标准差 hdi_3% hdi_97%
alpha 9.909 0.487 9.05 10.887

平均值接近我们在生成数据时指定的数值

  • 基准率是一个常数 1。

  • 饮酒使基准率增加三倍。

  • 不服用抗组胺药使基准率增加 6 倍。

  • 饮酒和不服用抗组胺药使预期速率(如果它们的速率是独立的)翻倍。如果它们是独立的,那么两者都做会将基准率增加 3*6=18 倍,但实际上基准率增加了 3*6*2=36 倍。

最后,nsneeze_alpha 的均值也非常接近其 10 的实际值。

另请参阅 bambi's 负二项示例 以供进一步参考。

作者#

%load_ext watermark
%watermark -n -u -v -iv -w -p pytensor,xarray
Last updated: Mon Oct 16 2023

Python implementation: CPython
Python version       : 3.11.5
IPython version      : 8.15.0

pytensor: 2.16.1
xarray  : 2023.8.0

pymc   : 5.8.1
pandas : 2.1.0
seaborn: 0.13.0
numpy  : 1.25.2
arviz  : 0.16.1
scipy  : 1.11.2

Watermark: 2.4.3

许可声明#

本示例库中的所有笔记本均根据 MIT 许可证 提供,该许可证允许修改和再分发用于任何用途,前提是保留版权和许可声明。

引用 PyMC 示例#

要引用此笔记本,请使用 Zenodo 为 pymc-examples 存储库提供的 DOI。

重要提示

许多笔记本改编自其他来源:博客、书籍…… 在这种情况下,您也应该引用原始来源。

另请记住引用您的代码使用的相关库。

这是一个 bibtex 格式的引用模板

@incollection{citekey,
  author    = "<notebook authors, see above>",
  title     = "<notebook title>",
  editor    = "PyMC Team",
  booktitle = "PyMC examples",
  doi       = "10.5281/zenodo.5654871"
}

渲染后可能如下所示