GLMM (3): 카운트 결과 — 포아송 및 음이항 혼합 모델

턴 수, 클릭 수, 방문 횟수 등 카운트 반복 측정 데이터 분석

카운트 결과 변수(0, 1, 2, …)가 반복 측정되는 데이터에 GLMM을 적용한다. 포아송 혼합 모델의 구조와 과산포 문제, 음이항 혼합 모델로의 확장, Zero-Inflation 처리, 그리고 세션당 턴 수 분석 실무 예시를 다룬다.

Statistics
Longitudinal Data
GLMM
저자

Kwangmin Kim

공개

2026년 03월 07일

1 GLMM (3): 포아송 및 음이항 혼합 모델

1.1 카운트 데이터의 특성

카운트 데이터는 세 가지 특성을 가진다:

  1. 비음수: 0 이상의 정수
  2. 오른쪽 꼬리: 대부분 작은 값, 가끔 큰 값
  3. 평균-분산 관계: 포아송 → \(\text{Var}=\mu\), 실제로는 \(\text{Var}>\mu\) (과산포)
결과 변수 예시 분포 선택
세션당 턴 수 0, 1, 2, 3, … 포아송 (과산포 시 NB)
클릭 수 0, 0, 0, 3, 0, … Zero-inflated 포아송
하루 방문 횟수 1, 2, 5, 10, … 포아송 또는 NB
에러 발생 횟수 0, 0, 1, 0, … Zero-inflated 포아송

1.2 포아송 혼합 모델

1.2.1 수식

\[\log(E[Y_{ij} \mid u_i]) = \beta_0 + \beta_1 X_{ij} + u_i\]

\[Y_{ij} \mid u_i \sim \text{Poisson}(\lambda_{ij}), \quad \lambda_{ij} = \exp(\beta_0 + \beta_1 X_{ij} + u_i)\]

\[u_i \sim N(0, \sigma^2_u)\]

계수 해석: \(e^{\beta_1}\) = 비율비(Rate Ratio, RR) — X가 1 증가할 때 카운트 기대값이 몇 배가 되는가

1.2.2 예시: 세션당 턴 수 분석

import numpy as np
import pandas as pd

np.random.seed(42)
n_users, n_weeks = 500, 8
user_ids = np.repeat(range(n_users), n_weeks)
weeks    = np.tile(range(1, n_weeks + 1), n_users)
personalized = (weeks >= 5).astype(int)

# 사용자별 랜덤 절편 (대화 참여도 개인차)
u_i = np.repeat(np.random.normal(0, 0.45, n_users), n_weeks)

log_lambda = 0.70 + 0.20 * personalized + 0.02 * weeks + u_i
turn_count = np.random.poisson(np.exp(log_lambda))

df = pd.DataFrame({
    "user_id": user_ids, "week": weeks,
    "personalized": personalized, "turn_count": turn_count,
    "segment": np.repeat(
        np.random.choice(["SI","MIEP","N"], n_users, p=[0.6,0.3,0.1]),
        n_weeks
    )
})

print(df["turn_count"].describe())
print(f"\n개인화 전 평균 턴: {df[df.personalized==0]['turn_count'].mean():.2f}")
print(f"개인화 후 평균 턴: {df[df.personalized==1]['turn_count'].mean():.2f}")
count    4000.000
mean        2.351
std         1.701
min         0.000
25%         1.000
50%         2.000
75%         3.000
max        14.000
dtype: float64

개인화 전 평균 턴: 2.04
개인화 후 평균 턴: 2.72
library(lme4)

m_pois <- glmer(
  turn_count ~ personalized + week + segment + (1 | user_id),
  data=df, family=poisson(link="log")
)
summary(m_pois)
Random effects:
 Groups   Name        Variance Std.Dev.
 user_id  (Intercept)  0.197   0.444

Fixed effects:
               Estimate Std.Error z-value Pr(>|z|)
(Intercept)     0.678     0.044    15.4   <0.001
personalized    0.198     0.029     6.8   <0.001   ← log RR
week            0.021     0.007     3.0    0.003
segmentMIEP     0.482     0.052     9.3   <0.001
segmentN       -0.162     0.071    -2.3    0.022
# 비율비 (Rate Ratio)
exp(fixef(m_pois))

# personalized: exp(0.198) = 1.22
# → 개인화 시 세션당 평균 턴 수 22% 증가
# → (같은 사용자 기준, Conditional RR)

1.3 과산포 (Overdispersion) 문제

포아송의 가정: \(E[Y] = \text{Var}[Y] = \lambda\)

실제 데이터는 대부분 \(\text{Var}[Y] > E[Y]\) (과산포).

1.3.1 과산포 탐지

library(DHARMa)

simres <- simulateResiduals(m_pois)
testDispersion(simres)
DHARMa dispersion test:
  dispersion = 1.42, p = 0.003
  → 유의한 과산포 존재
  → 음이항 모델로 전환 필요

과산포 원인: - 측정되지 않은 이질성 (관측되지 않은 개인차) - Zero가 예상보다 많음 (Zero-Inflation) - 군집 내 상관 (랜덤 효과로 일부 해결)


1.4 음이항 혼합 모델 (Negative Binomial GLMM)

과산포를 직접 모델에 포함:

\[Y_{ij} \mid u_i \sim \text{NB}(\mu_{ij}, \theta)\]

\[\log(\mu_{ij}) = \beta_0 + \beta_1 X_{ij} + u_i\]

\(\theta\): 분산 파라미터. \(\text{Var}[Y] = \mu + \mu^2/\theta\) (\(\theta \to \infty\) 이면 포아송)

library(glmmTMB)  # lme4보다 NB 모델에 강력

m_nb <- glmmTMB(
  turn_count ~ personalized + week + segment + (1 | user_id),
  data=df,
  family=nbinom2  # NB Type II: Var = μ + μ²/θ
)
summary(m_nb)
Random effects:
 Groups   Name        Variance Std.Dev.
 user_id  (Intercept)  0.192   0.438

Conditional model:
               Estimate Std.Error z-value Pr(>|z|)
(Intercept)     0.681     0.045    15.1   <0.001
personalized    0.199     0.031     6.4   <0.001
week            0.020     0.008     2.5    0.012
segmentMIEP     0.479     0.055     8.7   <0.001
segmentN       -0.159     0.074    -2.1    0.034

Dispersion:
  theta (분산 파라미터) = 4.82
  → 과산포 보정됨

1.5 Zero-Inflated 모델

데이터에 0이 지나치게 많은 경우:

일반 포아송 기대 0 비율: 20%
실제 0 비율: 45%  → Zero-Inflation

원인: “구조적 0” (절대 전환 안 하는 사용자) + “카운트 0” (이번 주에 우연히 0)

library(glmmTMB)

# Zero-Inflated 포아송
m_zip <- glmmTMB(
  turn_count ~ personalized + week + segment + (1 | user_id),
  ziformula = ~1 + segment,  # 구조적 0의 확률 모델
  data=df,
  family=poisson
)

# Zero-Inflated 음이항
m_zinb <- glmmTMB(
  turn_count ~ personalized + week + segment + (1 | user_id),
  ziformula = ~1 + segment,
  data=df,
  family=nbinom2
)

# 모델 비교
AIC(m_pois, m_nb, m_zip, m_zinb)
AIC 비교:
m_pois:  8234   ← 기본 포아송
m_nb:    8108   ← 음이항 (과산포 보정)
m_zip:   8142   ← Zero-inflated 포아송
m_zinb:  8095   ← Zero-inflated 음이항 (최선)

→ m_zinb 선택

1.6 Offset: 노출량 차이 보정

측정 기간이나 노출량이 사람마다 다를 때 offset으로 보정.

예시: 세션 길이가 사람마다 다른데 턴 수를 비교

# 세션 길이(분)을 offset으로 추가
df$log_duration <- log(df$session_duration_min)

m_offset <- glmer(
  turn_count ~ personalized + week + segment +
               offset(log_duration) +  # 세션 길이 보정
               (1 | user_id),
  data=df, family=poisson
)

# 이제 계수 해석: "같은 세션 길이 대비 턴 수 비율"

1.7 포아송 vs 음이항 vs ZIP 선택 가이드

Step 1: 포아송 혼합 모델 적합
         ↓
Step 2: 과산포 검정 (DHARMa::testDispersion)
         ↓ 과산포 있음
Step 3: 음이항 혼합 모델 적합
         ↓
Step 4: Zero-Inflation 검정 (DHARMa::testZeroInflation)
         ↓ Zero-Inflation 있음
Step 5: Zero-Inflated 음이항 (ZINB) 적합
         ↓
Step 6: AIC/BIC로 최종 모델 선택

1.8 Python에서의 카운트 GLMM

# Python에서 포아송 GLMM은 제한적
# statsmodels는 Poisson GLMM 미지원
# 대안 1: rpy2로 R lme4 호출
import rpy2.robjects as ro
from rpy2.robjects import pandas2ri
pandas2ri.activate()

ro.globalenv['df_r'] = df
ro.r('''
library(lme4)
m <- glmer(turn_count ~ personalized + week + segment + (1|user_id),
           data=df_r, family=poisson)
print(summary(m))
''')

# 대안 2: bambi (베이지안 GLMM, Python)
import bambi as bmb

model = bmb.Model(
    "turn_count ~ personalized + week + segment + (1|user_id)",
    df,
    family="poisson"
)
results = model.fit(draws=1000, chains=2)

1.9 결과 보고 형식

[방법]
세션당 턴 수(카운트)의 개인화 효과를 분석하기 위해
사용자별 랜덤 절편을 포함한 음이항 혼합 모델을 적용하였다.
과산포 검정 결과(dispersion=1.42, p=0.003) 음이항 분포를 선택하였다.

[결과]
개인화 적용 시 세션당 평균 턴 수가 1.22배 증가하였다
(Conditional RR = 1.22, 95% CI [1.15, 1.29], p < 0.001).
사용자 간 기본 참여도 차이도 유의하였다(σ²_u = 0.192).
MIEP 세그먼트는 SI 대비 1.61배 더 많은 턴을 보였다(RR = 1.61, p < 0.001).

다음: 09-mixed-model-gam-intro.qmd — GAM/GAMM: 비선형 관계와 종단 데이터

Subscribe

Enjoy this blog? Get notified of new posts by email: