Day 19
데이터 분석 모델링
데이터 분석 모델링
군집분석 · A/B 테스트 · ARIMA 종합 실습
뮤직스트림(MusicStream) 시나리오로 배우는
K-Means 군집분석 → A/B 테스트(t-test) → ARIMA 시계열 예측 파이프라인
전체 시나리오
상황: 음악 스트리밍 서비스 뮤직스트림(MusicStream)의 그로스 마케터
신규 가입자 증가세가 둔화되고 있고, 팀장의 지시를 받았습니다.
신규 가입자 증가세가 둔화되고 있고, 팀장의 지시를 받았습니다.
"신규 가입자 증가세가 둔화되고 있어요. 그냥 전체 고객에게 쿠폰 뿌리지 말고, 데이터로 고객을 나눠서 제대로 된 실험을 해보고, 앞으로 어떻게 될지도 예측해봐요."
3단계 파이프라인 요약:
1) 고객 행동 데이터 200명 → K-Means 군집분석 → 이탈 위험 고객 식별
2) 이탈 위험 고객 대상 A/B 테스트 → t-test로 쿠폰 효과 통계 검증
3) 검증된 전략 전사 적용 후 → ARIMA로 구독 유지율 미래 예측
1) 고객 행동 데이터 200명 → K-Means 군집분석 → 이탈 위험 고객 식별
2) 이탈 위험 고객 대상 A/B 테스트 → t-test로 쿠폰 효과 통계 검증
3) 검증된 전략 전사 적용 후 → ARIMA로 구독 유지율 미래 예측
1단계 — 군집 분석 (K-Means)
1단계: 군집 분석 (K-Means Clustering)
군집 분석이란?
군집 분석(Clustering)은 비슷한 특성을 가진 고객끼리 자동으로 묶어주는 비지도 학습 방법입니다. 마케터가 "이 고객은 VIP, 저 고객은 이탈 위험"이라고 일일이 판단하지 않아도, 알고리즘이 데이터를 보고 스스로 그룹을 만들어줍니다.
K-Means 알고리즘 작동 원리
K-Means 4단계 반복:
① K개의 중심점(centroid)을 랜덤으로 찍는다
② 각 데이터를 가장 가까운 중심점 그룹에 배정한다
③ 각 그룹의 평균을 새 중심점으로 업데이트한다
④ 더 이상 변화가 없을 때까지 ②~③을 반복 → 안정적인 K개의 그룹 완성
① K개의 중심점(centroid)을 랜덤으로 찍는다
② 각 데이터를 가장 가까운 중심점 그룹에 배정한다
③ 각 그룹의 평균을 새 중심점으로 업데이트한다
④ 더 이상 변화가 없을 때까지 ②~③을 반복 → 안정적인 K개의 그룹 완성
왜 전처리가 필요할까?
① LabelEncoder — 문자를 숫자로 변환
K-Means는 거리 계산을 기반으로 하기 때문에 숫자만 처리할 수 있습니다. 텍스트 값은 반드시 숫자로 바꿔야 합니다.
변환 예시:
'남성' → 0 / '여성' → 1
'무료' → 0 / '기본' → 1 / '프리미엄' → 2
주의: LabelEncoder는 숫자 크기에 순서가 있다고 해석합니다. 순서가 없는 변수(팝, 재즈, 힙합 같은 장르)에 쓰면 알고리즘이 오해할 수 있어요. 이 실습에서는 이진형·순서형 변수에만 사용합니다.
'남성' → 0 / '여성' → 1
'무료' → 0 / '기본' → 1 / '프리미엄' → 2
주의: LabelEncoder는 숫자 크기에 순서가 있다고 해석합니다. 순서가 없는 변수(팝, 재즈, 힙합 같은 장르)에 쓰면 알고리즘이 오해할 수 있어요. 이 실습에서는 이진형·순서형 변수에만 사용합니다.
② StandardScaler — 단위 통일
| 변수 | 범위 | 스케일링 없으면 |
|---|---|---|
| 월 청취 시간 | 10 ~ 420분 | 이 변수가 결과를 지배 |
| 월 접속 횟수 | 1 ~ 31회 | 영향력 작아짐 |
| 친구 추천 여부 | 0 또는 1 | 거의 무시됨 |
StandardScaler는 모든 변수를 평균 0, 표준편차 1로 맞춰줍니다. 단위가 다른 변수들을 공평하게 비교할 수 있게 됩니다.
③ 엘보우(Elbow) 방법 — 최적 K 찾기
K를 1부터 늘려가며 Inertia(군집 내 거리 합)을 계산합니다.
그래프가 꺾이는 지점(팔꿈치) = 최적 K → 이 실습에서는 K = 3
그래프가 꺾이는 지점(팔꿈치) = 최적 K → 이 실습에서는 K = 3
데이터셋 1: 고객 행동 데이터 (200명)
| 컬럼명 | 설명 | 타입 | 전처리 |
|---|---|---|---|
| 고객ID | 고객 고유 번호 | 텍스트 | 분석에 사용 안 함 |
| 성별 | 남성 / 여성 | 범주형 | LabelEncoder |
| 연령대 | 20대 / 30대 / 40대 / 50대이상 | 범주형 | LabelEncoder |
| 구독플랜 | 무료 / 기본 / 프리미엄 (순서 있음) | 범주형 | LabelEncoder |
| 선호장르 | 팝 / 힙합 / 재즈 / 클래식 / 인디 | 범주형 | LabelEncoder |
| 월청취시간 | 한 달 총 청취 시간 (분) | 수치형 | StandardScaler |
| 월접속횟수 | 한 달 앱 접속 횟수 | 수치형 | StandardScaler |
| 구독유지개월 | 현재까지 구독 유지 개월 수 | 수치형 | StandardScaler |
| 최근접속일수 | 마지막 앱 접속이 며칠 전인지 | 수치형 | StandardScaler |
| 플레이리스트수 | 저장한 플레이리스트 수 | 수치형 | StandardScaler |
| 친구추천여부 | 친구에게 추천했는지 (1=예, 0=아니오) | 이진형 | StandardScaler |
실습 코드
1단계: 데이터 불러오기 + EDA
Python 데이터 로드 + 기초 탐색
# CSV 파일 불러오기
df_고객 = pd.read_csv('dataset1_고객행동.csv')
# 수치형 컬럼 기초 통계 확인
수치형_탐색 = ['월청취시간', '월접속횟수', '구독유지개월', '최근접속일수', '플레이리스트수']
print(df_고객[수치형_탐색].describe().round(1))
# 범주형 컬럼 분포 확인
print(df_고객['구독플랜'].value_counts())
print(df_고객['선호장르'].value_counts())
# 결측치 확인
print(df_고객.isnull().sum())
# 결측치 처리: 수치형 컬럼은 평균값으로 채우기
결측치_컬럼 = ['월청취시간', '월접속횟수', '구독유지개월', '최근접속일수', '플레이리스트수']
for col in 결측치_컬럼:
평균값 = df_고객[col].mean()
df_고객[col] = df_고객[col].fillna(round(평균값))
fillna(round(평균값)) — 결측치(NaN)를 해당 컬럼의 평균값으로 채웁니다. round()로 반올림해서 정수로 맞춥니다.
2단계: 전처리 — LabelEncoder + StandardScaler
Python 전처리: 문자→숫자 변환 + 스케일 통일
df_proc = df_고객.copy() # 원본 보존을 위해 복사본 사용
# ── LabelEncoder: 텍스트 → 숫자 변환 ──────────────────────────
범주형_컬럼 = ['성별', '연령대', '구독플랜', '선호장르']
label_encoders = {} # 나중에 역변환하고 싶을 때를 위해 저장
for col in 범주형_컬럼:
le = LabelEncoder()
df_proc[col] = le.fit_transform(df_proc[col])
label_encoders[col] = le
# 역변환 예시: label_encoders['성별'].inverse_transform([0, 1])
# ── StandardScaler: 스케일 통일 ───────────────────────────────
feature_cols = [
'성별', '연령대', '구독플랜', '선호장르',
'월청취시간', '월접속횟수', '구독유지개월',
'최근접속일수', '플레이리스트수', '친구추천여부'
]
X = df_proc[feature_cols]
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
fit_transform(X) — fit(평균·표준편차 계산) + transform(실제 변환)을 한 번에 수행합니다.
이후 predict 데이터(새 데이터)에는 반드시 transform만 사용해야 합니다. (fit을 다시 하면 기준이 달라져서 틀립니다.)
이후 predict 데이터(새 데이터)에는 반드시 transform만 사용해야 합니다. (fit을 다시 하면 기준이 달라져서 틀립니다.)
3단계: 엘보우 방법으로 최적 K 찾기
Python 엘보우 방법 — K=1~10 Inertia 계산
inertia_list = []
for k in range(1, 11):
# KMeans 객체 생성
km = KMeans(n_clusters=k, random_state=42, n_init=10)
# 스케일링된 데이터로 K-Means 학습 (중심점이 수렴할 때까지 반복)
km.fit(X_scaled)
# 학습 후 나온 inertia(군집 내 거리 합) 저장
inertia_list.append(km.inertia_)
# 그래프: K=3 지점에서 꺾임(팔꿈치) 확인
plt.plot(range(1, 11), inertia_list, 'o-', color='#2E86AB')
plt.axvline(x=3, color='#E84855', linestyle='--', label='선택한 K=3')
n_init=10 — 초기 중심점을 10번 다르게 설정해서 가장 좋은 결과를 채택합니다. 결과의 안정성을 높여줍니다.
inertia_ — 군집 내 거리 합. 값이 작을수록 군집이 촘촘합니다. K가 커질수록 줄어들지만, 꺾이는 지점 이후엔 줄어드는 폭이 작아집니다.
inertia_ — 군집 내 거리 합. 값이 작을수록 군집이 촘촘합니다. K가 커질수록 줄어들지만, 꺾이는 지점 이후엔 줄어드는 폭이 작아집니다.
4단계: K-Means 모델 학습 + 군집 프로파일링
Python K=3으로 군집 학습 + 클러스터별 평균 분석
K = 3
kmeans = KMeans(n_clusters=K, random_state=42, n_init=10)
# fit_predict = 학습(fit) + 군집 번호 예측(predict) 동시에
df_고객['클러스터'] = kmeans.fit_predict(X_scaled)
# 클러스터별 수치형 컬럼 평균 확인 (프로파일링)
수치형_cols = ['월청취시간', '월접속횟수', '구독유지개월', '최근접속일수', '플레이리스트수']
print(df_고객.groupby('클러스터')[수치형_cols].mean())
# 클러스터 이름 매핑 (평균 지표 보고 직접 해석해서 지정)
cluster_names = {
2: '충성 고객 - 높은 청취시간, 지속 유지 필요',
0: '중간 고객 - 보통 활동, 참여 유도 필요',
1: '이탈 위험 - 접속 감소, 즉각 리텐션 필요'
}
df_고객['고객등급'] = df_고객['클러스터'].map(cluster_names)
groupby('클러스터').mean() — 클러스터별로 묶어서 각 지표의 평균을 계산합니다. 이 결과를 보고 "어떤 클러스터가 이탈 위험인지"를 사람이 직접 해석해서 이름을 붙입니다.
핵심 해석 포인트:
최근접속일수가 가장 큰 클러스터 → 오래 안 들어온 것 → 이탈 위험
월청취시간이 가장 큰 클러스터 → 많이 사용하는 것 → 충성 고객
핵심 해석 포인트:
최근접속일수가 가장 큰 클러스터 → 오래 안 들어온 것 → 이탈 위험
월청취시간이 가장 큰 클러스터 → 많이 사용하는 것 → 충성 고객
5단계: PCA 시각화
Python PCA로 10차원 → 2차원 압축 후 산점도
# PCA: 10개 변수를 2개 축으로 압축해서 그래프로 그리기
pca = PCA(n_components=2, random_state=42)
X_2d = pca.fit_transform(X_scaled)
# 설명력 확인: 두 축이 전체 정보의 몇 %를 커버하는지
설명력 = pca.explained_variance_ratio_
# 결과: 주성분1 48.3% + 주성분2 12.5% = 합계 60.9%
PCA(주성분 분석) — 10개 변수를 2개 숫자로 압축하는 기법입니다. 10차원 데이터를 눈으로 볼 수 없으니, 2차원으로 줄여서 시각화합니다.
압축 과정에서 일부 정보는 손실됩니다. 이번 실습에서 2개 축으로 전체 정보의 약 60.9%를 설명했습니다.
압축 과정에서 일부 정보는 손실됩니다. 이번 실습에서 2개 축으로 전체 정보의 약 60.9%를 설명했습니다.
군집 분석 결과
클러스터 0 (62명): 월 청취 219분 / 접속 17회 / 최근 접속 7일 전 / 구독 15개월 / 기본 플랜 클러스터 1 (85명): 월 청취 57분 / 접속 5회 / 최근 접속 29일 전 / 구독 4개월 / 무료 플랜 ← 이탈 위험 클러스터 2 (53명): 월 청취 272분 / 접속 22회 / 최근 접속 2일 전 / 구독 18개월 / 프리미엄 ← 충성 고객
마케팅 인사이트
- 클러스터 1 (이탈 위험, 85명): 최근접속일수 29일 → 한 달 가까이 안 들어온 고객. 즉각 A/B 테스트 대상으로 선정.
- 클러스터 2 (프리미엄 충성, 53명): 친구 추천 비율 79% → 자발적 바이럴 가능성 높음. 추천 프로그램 강화 고려.
- 클러스터 0 (기본 충성, 62명): 기본 플랜 + 보통 활동 → 프리미엄 업그레이드 유도 캠페인 대상.
2단계 — A/B 테스트
2단계: A/B 테스트 (t-test)
A/B 테스트란?
두 가지 전략 중 어떤 것이 더 효과적인지 데이터로 비교하는 실험 방법입니다.
"느낌"이 아닌 숫자와 통계로 의사결정합니다.
"느낌"이 아닌 숫자와 통계로 의사결정합니다.
왜 랜덤 배정이 중요한가?
랜덤으로 나누지 않으면 두 그룹 간 차이가 전략 때문인지, 원래 고객 성향 때문인지 알 수 없습니다.
잘못된 예: A그룹=원래 활발한 고객 / B그룹=원래 조용한 고객
→ 쿠폰 효과 없어도 A그룹 재접속률이 더 높게 나옴 → 잘못된 결론!
올바른 예: A, B 그룹 모두 랜덤으로 섞어서 나누기
→ 두 그룹의 고객 성향이 비슷해짐 → 차이가 있다면 전략 때문!
잘못된 예: A그룹=원래 활발한 고객 / B그룹=원래 조용한 고객
→ 쿠폰 효과 없어도 A그룹 재접속률이 더 높게 나옴 → 잘못된 결론!
올바른 예: A, B 그룹 모두 랜덤으로 섞어서 나누기
→ 두 그룹의 고객 성향이 비슷해짐 → 차이가 있다면 전략 때문!
t-test와 p값 개념
t 통계량: 두 그룹 평균 차이 ÷ 데이터의 변동성. 절댓값이 클수록 두 그룹 차이가 뚜렷합니다.
p값: "귀무가설이 맞는 상황에서 지금과 같은 결과가 우연히 나올 확률". 작을수록 우연이 아닐 가능성이 높습니다.
기준 p < 0.05: "100번 실험하면 5번 이하로만 이런 결과가 우연히 나온다"는 의미. 통계학에서 오랫동안 사용해온 관례적 기준입니다.
p값: "귀무가설이 맞는 상황에서 지금과 같은 결과가 우연히 나올 확률". 작을수록 우연이 아닐 가능성이 높습니다.
기준 p < 0.05: "100번 실험하면 5번 이하로만 이런 결과가 우연히 나온다"는 의미. 통계학에서 오랫동안 사용해온 관례적 기준입니다.
데이터셋 2: A/B 테스트 실험 결과 데이터
| 컬럼명 | 설명 | 타입 |
|---|---|---|
| 고객ID | 이탈 위험 고객 고유 번호 | 텍스트 |
| 그룹 | A (리마인드 이메일) / B (30% 할인 쿠폰) | 범주형 |
| 발송일 | 이메일/쿠폰 발송 날짜 | 날짜 |
| 발송채널 | email / push / sms | 범주형 |
| 쿠폰할인율 | 할인율 (A그룹은 0%) | 수치형 |
| 재접속여부 | 7일 이내 앱 재접속 (1=재접속, 0=미접속) | 이진형 |
| 재접속까지일수 | 발송 후 재접속까지 걸린 일수 (미접속은 NaN) | 수치형 |
| 구독갱신여부 | 실험 후 구독 갱신 여부 (1=갱신, 0=미갱신) | 이진형 |
실습 코드
1단계: 데이터 로드 + 결측치 처리
Python A/B 데이터 로드 + 결측치 처리
df_실험 = pd.read_csv('dataset2_AB테스트결과.csv')
# 결측치 처리: 재접속여부 · 구독갱신여부는 최빈값으로 채우기
# (재접속까지일수의 NaN은 미접속 고객이므로 그대로 둠)
df_실험['재접속여부'] = df_실험['재접속여부'].fillna(df_실험['재접속여부'].mode()[0])
df_실험['구독갱신여부'] = df_실험['구독갱신여부'].fillna(df_실험['구독갱신여부'].mode()[0])
mode()[0] — 최빈값(가장 많이 나오는 값)을 반환합니다. [0]을 붙이는 이유는 mode()가 Series를 반환하기 때문에 첫 번째 값을 꺼내는 것입니다.
2단계: A/B 그룹 분리 + 재접속률 계산
Python 그룹 분리 + 재접속률 계산
A그룹 = df_실험[df_실험['그룹'] == 'A'].copy()
B그룹 = df_실험[df_실험['그룹'] == 'B'].copy()
# 0과 1로 된 컬럼은 .mean()이 곧 비율(퍼센트)이 됩니다
# * 100을 하면 소수가 아닌 퍼센트 숫자로 사용 가능
A재접속률 = A그룹['재접속여부'].mean() * 100
B재접속률 = B그룹['재접속여부'].mean() * 100
A갱신률 = A그룹['구독갱신여부'].mean() * 100
B갱신률 = B그룹['구독갱신여부'].mean() * 100
print(f"재접속률 A:{A재접속률:.1f}%, B:{B재접속률:.1f}%")
print(f"갱신률 A:{A갱신률:.1f}%, B:{B갱신률:.1f}%")
이진형 컬럼(0, 1)의 .mean() = 비율입니다. 예를 들어 80명 중 23명이 1이면 23/80 = 0.288, 즉 28.8%가 됩니다.
여기서는 * 100을 해서 퍼센트 숫자로 변환 후 저장했기 때문에, 출력할 때
여기서는 * 100을 해서 퍼센트 숫자로 변환 후 저장했기 때문에, 출력할 때
:.1f로 포맷팅합니다.3단계: t-test 통계 검정
Python t-test로 두 그룹 차이 유의성 검정
from scipy import stats
# ttest_ind: 독립된 두 그룹의 평균 차이 검정
# t통계량 = 평균차이 / 데이터의 변동성 → 절댓값이 클수록 차이 뚜렷
# p값 = 두 그룹 차이가 없다고 가정했을 때 지금 결과가 우연히 나올 확률
t통계량, p값 = stats.ttest_ind(A그룹['재접속여부'], B그룹['재접속여부'])
print(f"t-통계량: {t통계량:.3f}")
print(f"p값: {p값:.3f}")
if p값 < 0.05:
print("판정: p < 0.05 → 귀무가설 기각")
print(" 쿠폰(B그룹) 효과는 통계적으로 유의미합니다.")
print(" → B그룹 전략(30% 할인 쿠폰)을 전체 이탈위험군에 적용 결정!")
else:
print("판정: p >= 0.05 → 귀무가설 채택 → 재실험 필요")
A/B 테스트 결과
======================================================= 2단계 A/B 테스트 결과 요약 ======================================================= A그룹 재접속률 : 28.7% (리마인드 이메일) B그룹 재접속률 : 47.5% (30% 할인 쿠폰) 재접속률 향상 : +18.8%p 구독 갱신율 향상: +12.5%p p값 : 0.0145 판정 : 쿠폰 효과 유의미 (p < 0.05) =======================================================
마케팅 인사이트
- 재접속률뿐 아니라 구독 갱신율도 B그룹이 더 높습니다. 쿠폰이 단순히 앱을 다시 열게 하는 것을 넘어 실제 구독 유지까지 이어졌다는 의미입니다.
- p = 0.0145 < 0.05 → 우연일 확률 약 1.5%. 통계적으로 유의미한 차이입니다.
- 이 결과를 바탕으로 매달 이탈 위험 고객에게 쿠폰을 보내는 캠페인을 정기적으로 진행합니다.
p값 오해 바로잡기:
"p < 0.05면 효과가 크다" → 틀림. p값은 효과 크기가 아닌 우연일 확률입니다. 효과 크기는 재접속률 차이(+18.8%p)로 판단합니다.
"p ≥ 0.05면 효과가 없다" → 틀림. 이번 데이터로는 효과를 확신할 수 없다는 의미입니다.
p값과 효과 크기는 함께 봐야 합니다.
"p < 0.05면 효과가 크다" → 틀림. p값은 효과 크기가 아닌 우연일 확률입니다. 효과 크기는 재접속률 차이(+18.8%p)로 판단합니다.
"p ≥ 0.05면 효과가 없다" → 틀림. 이번 데이터로는 효과를 확신할 수 없다는 의미입니다.
p값과 효과 크기는 함께 봐야 합니다.
3단계 — ARIMA 시계열 예측
3단계: ARIMA 시계열 예측
시계열 데이터란?
시간 순서대로 기록된 데이터입니다. 순서가 의미 있기 때문에 일반 데이터와 다르게 다뤄야 합니다.
일반 데이터: 고객 200명 → 순서 바꿔도 됨
시계열 데이터: 1월 → 2월 → 3월 → ... → 순서가 곧 정보
일반 데이터: 고객 200명 → 순서 바꿔도 됨
시계열 데이터: 1월 → 2월 → 3월 → ... → 순서가 곧 정보
ARIMA 파라미터 개념
교안에 나온 인포그래픽을 위쪽에서 이미 확인했습니다. 핵심만 다시 정리하면:
| 파라미터 | 이름 | 이 실습에서의 의미 |
|---|---|---|
| p = 1 | 자기회귀 (AR) | 지난달 구독 유지율이 이번 달 예측에 영향을 준다 |
| d = 1 | 차분 (I) | 우상향 추세를 제거해서 패턴(변화량)만 남긴다 |
| q = 1 | 이동평균 (MA) | 지난달 예측 실수(오차)를 이번 달 예측에 반영한다 |
train / test 분리 + MAPE
train/test 분리: 모델이 얼마나 정확한지 확인하기 위해 데이터를 나눕니다.
train (학습): 2022-01 ~ 2023-06 (18개월) → 모델이 패턴 학습
test (검증): 2023-07 ~ 2023-12 (6개월) → 예측 후 실제값과 비교
신뢰할 수 있다고 판단되면 → 전체 데이터로 재학습 후 미래 예측
MAPE(평균 절대 오차율): 예측이 실제값에서 평균적으로 몇 % 벗어났는지. 10% 미만이면 실무 사용 가능.
train (학습): 2022-01 ~ 2023-06 (18개월) → 모델이 패턴 학습
test (검증): 2023-07 ~ 2023-12 (6개월) → 예측 후 실제값과 비교
신뢰할 수 있다고 판단되면 → 전체 데이터로 재학습 후 미래 예측
MAPE(평균 절대 오차율): 예측이 실제값에서 평균적으로 몇 % 벗어났는지. 10% 미만이면 실무 사용 가능.
실습 코드
1단계: 시계열 데이터 준비
Python 시계열 데이터 생성 + 인덱스 설정
날짜 = [
'2022-01', '2022-02', ..., '2023-12' # 24개월
]
구독유지율 = [
84.2, 84.5, 84.9, ..., 91.0 # 84~91% 우상향
]
df_ts = pd.DataFrame({'날짜': 날짜, '구독유지율': 구독유지율})
df_ts['날짜'] = pd.to_datetime(df_ts['날짜'])
# ARIMA가 시계열 순서를 올바르게 인식하려면 날짜가 인덱스여야 합니다
df_ts = df_ts.set_index('날짜')
set_index('날짜') — 날짜 컬럼을 인덱스로 설정합니다. ARIMA 모델이 시계열 순서를 올바르게 인식하려면 날짜가 인덱스여야 합니다.
2단계: train/test 분리 + ARIMA 학습
Python train/test 분리 + ARIMA(1,1,1) 학습
# 앞 18개월: 모델 학습용 / 뒤 6개월: 예측 결과 검증용
train = df_ts.iloc[:18]
test = df_ts.iloc[18:]
# ARIMA(p=1, d=1, q=1) 모델 생성 및 학습
model = ARIMA(train['구독유지율'], order=(1, 1, 1))
학습된모델 = model.fit()
iloc[:18] — 위치 기반 인덱싱으로 0~17번째 행(18개)을 슬라이싱합니다.
ARIMA(order=(1,1,1)) — 파라미터 p, d, q를 순서대로 지정합니다. 튜닝이 필요하면 이 숫자를 바꿔봅니다.
ARIMA(order=(1,1,1)) — 파라미터 p, d, q를 순서대로 지정합니다. 튜닝이 필요하면 이 숫자를 바꿔봅니다.
3단계: 테스트 기간 예측 + MAPE 검증
Python 테스트 기간 예측 + MAPE 계산
# 테스트 기간(6개월) 예측
예측결과 = 학습된모델.get_forecast(steps=6)
예측구독유지율 = 예측결과.predicted_mean.values
예측범위 = 예측결과.conf_int(alpha=0.05) # 95% 신뢰구간
예측하한 = 예측범위.iloc[:, 0].values
예측상한 = 예측범위.iloc[:, 1].values
# MAPE: 평균 오차율 — 실제값과 몇 퍼센트 차이 나는지
오차목록, 오차율목록 = [], []
for i in range(len(test)):
실제 = test['구독유지율'].values[i]
예측 = round(예측구독유지율[i], 1)
오차율 = round((abs(실제 - 예측) / 실제) * 100, 1)
오차율목록.append(오차율)
mape = sum(오차율목록) / len(오차율목록)
print(f"MAPE: {mape:.2f}%") # 10% 이내면 신뢰도 있음
get_forecast(steps=6) — 학습된 모델로 미래 6단계를 예측합니다.
conf_int(alpha=0.05) — 95% 신뢰구간을 반환합니다. 미래로 갈수록 구간이 넓어지는 것은 정상입니다 — 불확실성이 커지기 때문이며 모델의 문제가 아닙니다.
conf_int(alpha=0.05) — 95% 신뢰구간을 반환합니다. 미래로 갈수록 구간이 넓어지는 것은 정상입니다 — 불확실성이 커지기 때문이며 모델의 문제가 아닙니다.
4단계: 미래 6개월 예측
Python 전체 데이터로 재학습 후 2024년 상반기 예측
# 신뢰성 확인 완료 → 전체 데이터(24개월)로 재학습
최종모델 = ARIMA(df_ts['구독유지율'], order=(1, 1, 1)).fit()
미래예측결과 = 최종모델.get_forecast(steps=6)
미래예측구독유지율 = 미래예측결과.predicted_mean.values
# 2024년 1월부터 매월 1일 기준 날짜 6개 생성
# freq='MS' = Month Start (매월 1일)
미래날짜 = pd.date_range(start='2024-01', periods=6, freq='MS')
왜 전체 데이터로 재학습하나요? test 기간 예측은 모델 검증 용도입니다. 실제 미래를 예측할 때는 가진 모든 데이터를 학습에 사용해야 더 정확한 예측이 가능합니다.
ARIMA 예측 결과
======================================================= 3단계 ARIMA 예측 결과 요약 ======================================================= 예측 정확도 (MAPE) : 1.0% ← 10% 미만 → 실무 사용 가능 2024년 1월 예측 : 91.3% (95% 범위: 90.5% ~ 92.1%) 2024년 2월 예측 : 91.7% (95% 범위: 90.2% ~ 93.2%) 2024년 3월 예측 : 92.0% (95% 범위: 89.9% ~ 94.1%) 2024년 4월 예측 : 92.3% (95% 범위: 89.7% ~ 94.9%) 2024년 5월 예측 : 92.6% (95% 범위: 89.5% ~ 95.7%) 2024년 6월 예측 : 92.9% (95% 범위: 89.3% ~ 96.5%) 6개월 변화 예측 : +1.6%p =======================================================
마케팅 인사이트
- MAPE 1.0%로 예측 정확도가 매우 높습니다. 데이터가 이벤트 충격 없이 완만하게 상승하는 구조라 ARIMA가 흐름을 잘 따라갔습니다.
- 쿠폰 전략이 유지된다면 2024년 상반기까지 구독 유지율 상승세가 지속될 것으로 예측됩니다.
- 실무 활용법: 매월 실제 구독 유지율을 모니터링하면서 예측값과 비교하고, 차이가 크면 쿠폰 할인율이나 발송 채널을 재조정합니다.
오늘의 핵심 정리
K=3
K-Means 군집
엘보우 방법으로 탐색
엘보우 방법으로 탐색
+18.8%p
쿠폰 전략으로
재접속률 향상
재접속률 향상
p=0.014
t-test 결과
통계적으로 유의미
통계적으로 유의미
MAPE 1%
ARIMA 예측 정확도
실무 사용 가능
실무 사용 가능
92.9%
2024년 6월
구독 유지율 예측
구독 유지율 예측
3단계
군집분석 → A/B테스트
→ ARIMA 파이프라인
→ ARIMA 파이프라인
오늘의 핵심은 분석 툴 하나하나가 아니라 세 단계가 연결되는 구조입니다. 군집분석으로 대상을 좁히고 → A/B 테스트로 효과를 검증하고 → ARIMA로 미래를 예측하는 것. 그로스 마케터가 데이터로 의사결정하는 실제 파이프라인을 실습한 날이었습니다.
'부트캠프' 카테고리의 다른 글
| 1차 프로젝트_260519_강사님 피드백(1) (0) | 2026.05.19 |
|---|---|
| SQLD 자격증 공부 4월 마지막주 (0) | 2026.05.03 |
| 멋쟁이사자처럼 부트캠프 그로스마케팅 4기 34일차_260427 (0) | 2026.04.27 |
| 멋쟁이사자처럼 부트캠프 그로스마케팅 4기 32일차_260423 (0) | 2026.04.23 |
| 멋쟁이사자처럼 부트캠프 그로스마케팅 4기 31일차_260422 (1) | 2026.04.22 |