이미지 로딩 중...
AI Generated
2025. 11. 15. · 4 Views
리텐션 및 이탈 분석 시스템 완벽 가이드
사용자의 재방문율과 이탈 패턴을 분석하여 서비스 개선점을 찾는 데이터 분석 시스템입니다. Python과 Polars를 활용하여 대규모 로그 데이터를 빠르게 처리하고, 실무에서 바로 활용할 수 있는 분석 인사이트를 도출하는 방법을 배웁니다.
목차
- 리텐션 분석 기초 - 사용자 재방문 패턴 이해하기
- 이탈 분석 기초 - 사용자가 떠나는 이유 찾기
- 코호트 리텐션 매트릭스 - 시각화로 패턴 발견하기
- 이탈 예측 모델링 - 머신러닝으로 사전 감지하기
- 세그먼트별 리텐션 비교 - 사용자 그룹별 차이 분석
- 윈백 캠페인 효과 분석 - 이탈 사용자 복귀 측정
- RFM 분석과 리텐션 - 고객 가치 기반 세분화
- 시계열 이탈률 추이 - 트렌드와 계절성 파악
- 활성도 기반 세그먼트 - 참여 레벨별 분류
- 코호트 라이프사이클 분석 - 가입 시점별 수명 비교
1. 리텐션 분석 기초 - 사용자 재방문 패턴 이해하기
시작하며
여러분이 모바일 앱이나 웹 서비스를 운영할 때 이런 의문을 가져본 적 있나요? "우리 서비스에 가입한 사용자들이 과연 다시 돌아올까?" 또는 "첫 주에는 열심히 사용하다가 왜 갑자기 사라질까?" 이런 질문은 모든 서비스 운영자가 직면하는 핵심 고민입니다.
신규 사용자를 유치하는 것도 중요하지만, 기존 사용자가 지속적으로 서비스를 이용하도록 만드는 것이 훨씬 더 중요합니다. 실제로 신규 고객 확보 비용은 기존 고객 유지 비용의 5배에 달한다는 연구 결과도 있습니다.
바로 이럴 때 필요한 것이 리텐션 분석입니다. 리텐션(Retention)은 사용자가 특정 기간 동안 서비스에 얼마나 재방문하는지를 측정하는 핵심 지표로, 여러분의 서비스가 진짜 가치를 제공하고 있는지 객관적으로 보여줍니다.
개요
간단히 말해서, 리텐션 분석은 사용자가 서비스에 얼마나 자주 돌아오는지를 측정하고 분석하는 과정입니다. 왜 이 분석이 필요할까요?
높은 리텐션은 사용자가 서비스에서 지속적인 가치를 느낀다는 의미입니다. 예를 들어, 여러분이 운영하는 학습 플랫폼에서 Day 1 리텐션이 40%, Day 7 리텐션이 20%라면, 가입자 중 40%는 다음 날 다시 방문하고, 20%는 일주일 후에도 계속 사용한다는 뜻입니다.
전통적으로는 SQL로 복잡한 쿼리를 작성하거나 엑셀로 수작업 집계를 했다면, 이제는 Polars 같은 고성능 데이터프레임 라이브러리로 수백만 건의 로그를 몇 초 만에 분석할 수 있습니다. 리텐션 분석의 핵심은 코호트(Cohort) 개념입니다.
같은 시기에 가입한 사용자 그룹을 추적하면서 시간에 따른 재방문 패턴을 관찰합니다. 이를 통해 어떤 기능이 사용자를 붙잡는지, 어느 시점에서 이탈이 급증하는지 명확히 파악할 수 있습니다.
코드 예제
import polars as pl
from datetime import datetime, timedelta
# 사용자 활동 로그 데이터 로드
user_logs = pl.read_csv("user_activity.csv")
# 각 사용자의 첫 방문일 계산 (코호트 기준일)
first_visit = user_logs.group_by("user_id").agg(
pl.col("visit_date").min().alias("cohort_date")
)
# 원본 데이터와 조인하여 가입 후 경과일 계산
retention_data = user_logs.join(first_visit, on="user_id").with_columns(
# 가입일 대비 며칠 후 방문인지 계산
((pl.col("visit_date") - pl.col("cohort_date")).dt.total_days()).alias("days_since_signup")
)
# 코호트별 리텐션 계산
retention_matrix = retention_data.group_by(["cohort_date", "days_since_signup"]).agg(
pl.col("user_id").n_unique().alias("active_users")
)
print(retention_matrix.head(10))
설명
이것이 하는 일: 위 코드는 사용자 활동 로그에서 각 사용자의 첫 방문일을 기준으로 코호트를 생성하고, 가입 후 경과일별로 얼마나 많은 사용자가 재방문했는지 집계합니다. 첫 번째로, group_by("user_id")와 min() 함수를 사용하여 각 사용자의 첫 방문일을 찾습니다.
이 날짜가 바로 그 사용자의 코호트 기준일이 됩니다. 예를 들어 2024년 1월 1일에 가입한 모든 사용자는 "2024-01-01 코호트"에 속하게 됩니다.
그 다음으로, join() 함수로 원본 로그와 첫 방문일 정보를 결합한 후, with_columns()로 "가입 후 며칠째"인지 계산합니다. Polars의 날짜 연산 기능(dt.total_days())을 활용하면 복잡한 날짜 계산도 한 줄로 처리할 수 있습니다.
이 과정에서 Polars는 lazy evaluation을 통해 메모리 효율적으로 대용량 데이터를 처리합니다. 마지막으로, 코호트별, 경과일별로 그룹핑하여 활성 사용자 수를 집계합니다.
이렇게 생성된 리텐션 매트릭스를 통해 "1월 1일 코호트의 Day 7 리텐션은 250명"과 같은 구체적인 인사이트를 얻을 수 있습니다. 여러분이 이 코드를 사용하면 엑셀로 수작업하던 리텐션 분석을 자동화할 수 있고, 수백만 건의 로그도 몇 초 만에 처리할 수 있습니다.
또한 코호트별 비교를 통해 어떤 시기에 가입한 사용자가 더 충성도가 높은지, 특정 마케팅 캠페인의 효과는 어떤지 명확히 파악할 수 있습니다.
실전 팁
💡 첫 방문일 정의를 명확히 하세요. "회원가입일"과 "첫 활동일"은 다를 수 있습니다. 실제 서비스 사용을 기준으로 해야 정확한 리텐션을 측정할 수 있습니다.
💡 주간 단위 리텐션도 함께 분석하세요. 일간 리텐션은 변동이 크지만, 주간 리텐션(Week 1, Week 2...)은 더 안정적인 패턴을 보여줍니다.
💡 Polars의 scan_csv() 함수를 사용하면 lazy loading으로 메모리 부담 없이 대용량 파일을 처리할 수 있습니다. 실제 계산은 collect()를 호출할 때만 실행됩니다.
💡 코호트 크기가 너무 작으면 통계적으로 의미가 없습니다. 최소 100명 이상의 사용자가 포함된 코호트만 분석 대상으로 삼으세요.
💡 리텐션 커브의 모양이 중요합니다. 급격히 떨어지다가 평평해지는 지점이 "코어 유저"가 되는 시점입니다.
2. 이탈 분석 기초 - 사용자가 떠나는 이유 찾기
시작하며
여러분의 서비스에서 사용자가 갑자기 활동을 멈췄다면, 그 이유가 무엇일까요? 단순히 바빠서일까요, 아니면 서비스에 불만이 있어서일까요?
이탈(Churn) 분석은 리텐션의 반대편에 있는 개념으로, 사용자가 서비스를 떠나는 패턴과 이유를 파악하는 과정입니다. 실제로 많은 기업들이 이탈한 사용자를 다시 데려오는 데 엄청난 비용을 쓰지만, 애초에 왜 떠났는지 모르면 같은 문제가 반복됩니다.
바로 이럴 때 필요한 것이 체계적인 이탈 분석입니다. 단순히 "몇 명이 이탈했다"가 아니라 "어떤 특성을 가진 사용자가, 어느 시점에, 어떤 행동 후에 이탈했는지"를 데이터로 증명할 수 있어야 합니다.
개요
간단히 말해서, 이탈 분석은 사용자가 서비스 사용을 중단하는 패턴을 찾아내고, 이탈 위험이 높은 사용자를 조기에 발견하는 분석 기법입니다. 왜 이 분석이 필요할까요?
이탈은 매출 감소로 직결됩니다. 특히 구독 서비스의 경우 월간 이탈률(Monthly Churn Rate)이 5%만 되어도 1년이면 사용자의 절반 이상이 사라집니다.
예를 들어, 여러분이 운영하는 SaaS 서비스에서 최근 30일간 활동이 없는 사용자를 "이탈 위험군"으로 정의하고 사전에 개입한다면, 실제 이탈을 크게 줄일 수 있습니다. 전통적으로는 단순히 "N일간 미접속 = 이탈"로 정의했다면, 이제는 사용 빈도 감소, 핵심 기능 미사용, 고객 지원 문의 이력 등 다양한 시그널을 종합적으로 분석합니다.
이탈 분석의 핵심은 타이밍입니다. 사용자가 완전히 떠난 후에는 되돌리기 어렵지만, "이탈 조짐"을 보일 때 개입하면 효과가 큽니다.
또한 이탈한 사용자의 공통점을 찾아내면 서비스 개선점을 명확히 알 수 있습니다.
코드 예제
import polars as pl
# 사용자별 최근 활동일과 활동 빈도 계산
user_activity_summary = user_logs.group_by("user_id").agg([
pl.col("visit_date").max().alias("last_active_date"),
pl.col("visit_date").count().alias("total_visits"),
pl.col("visit_date").min().alias("first_visit_date")
])
# 현재 날짜 기준으로 비활동 기간 계산
current_date = pl.lit(datetime.now().date())
churn_analysis = user_activity_summary.with_columns([
(current_date - pl.col("last_active_date")).dt.total_days().alias("days_inactive"),
(pl.col("last_active_date") - pl.col("first_visit_date")).dt.total_days().alias("lifetime_days")
]).with_columns([
# 평균 방문 빈도 계산 (생애 기간 동안 며칠에 한 번 방문했는지)
(pl.col("lifetime_days") / pl.col("total_visits")).alias("avg_days_between_visits")
])
# 이탈 위험군 식별 (30일 이상 미접속 + 기존 방문 주기의 2배 초과)
at_risk_users = churn_analysis.filter(
(pl.col("days_inactive") > 30) &
(pl.col("days_inactive") > pl.col("avg_days_between_visits") * 2)
)
print(f"이탈 위험 사용자: {at_risk_users.height}명")
설명
이것이 하는 일: 위 코드는 각 사용자의 활동 패턴을 분석하여 최근 활동일, 총 방문 횟수, 평균 방문 주기를 계산하고, 이를 바탕으로 이탈 위험이 높은 사용자를 식별합니다. 첫 번째로, group_by()와 여러 집계 함수를 사용하여 사용자별 활동 요약 정보를 만듭니다.
최근 활동일(max()), 총 방문 횟수(count()), 첫 방문일(min())을 한 번에 계산합니다. 이렇게 하면 각 사용자의 전체 활동 히스토리를 한눈에 파악할 수 있습니다.
그 다음으로, with_columns()를 체이닝하여 파생 지표들을 계산합니다. 현재 날짜와 마지막 활동일의 차이로 "며칠간 활동이 없었는지"를 계산하고, 전체 생애 기간 동안의 평균 방문 주기도 산출합니다.
이 평균 방문 주기가 중요한 이유는, 원래 주 1회 방문하던 사용자와 매일 방문하던 사용자의 "이탈" 기준이 달라야 하기 때문입니다. 마지막으로, 두 가지 조건을 결합하여 이탈 위험군을 정의합니다.
30일 이상 미접속이면서 동시에 자신의 평균 방문 주기의 2배를 초과한 경우를 "명백한 이탈 조짐"으로 판단합니다. 이렇게 하면 사용자별 특성을 반영한 정교한 이탈 예측이 가능합니다.
여러분이 이 코드를 사용하면 모든 사용자를 일률적으로 판단하지 않고 개인별 활동 패턴을 고려한 맞춤형 이탈 분석을 할 수 있습니다. 또한 이탈 위험군을 조기에 발견하여 이메일 캠페인이나 푸시 알림 등으로 재참여를 유도할 수 있습니다.
실전 팁
💡 이탈 정의는 서비스 특성에 따라 달라져야 합니다. 쇼핑몰은 90일, 뉴스 앱은 7일처럼 업종별로 적절한 기준을 설정하세요.
💡 "일시적 휴면"과 "진짜 이탈"을 구분하세요. 시즌성이 있는 서비스는 특정 시기에만 사용하는 사용자가 많습니다.
💡 이탈 사유를 카테고리화하세요. 가격 불만, 기능 부족, 경쟁사 전환, 더 이상 필요 없음 등으로 분류하면 대응 전략이 명확해집니다.
💡 코호트별 이탈률을 비교하면 서비스 개선의 효과를 측정할 수 있습니다. 신규 기능 출시 전후의 코호트 이탈률을 비교해보세요.
💡 Polars의 filter() 대신 when().then().otherwise() 구문으로 이탈 위험도를 등급화(High/Medium/Low)할 수 있습니다.
3. 코호트 리텐션 매트릭스 - 시각화로 패턴 발견하기
시작하며
여러분이 매달 신규 사용자를 열심히 확보하고 있지만, 정작 얼마나 많은 사용자가 계속 남아있는지 모른다면 어떨까요? 숫자로만 보면 헷갈리지만, 표로 정리하면 한눈에 패턴이 보입니다.
코호트 리텐션 매트릭스는 데이터 분석가들이 가장 애용하는 시각화 도구 중 하나입니다. 행은 가입 시기(코호트), 열은 경과 기간을 나타내며, 각 셀은 해당 시점의 리텐션율을 보여줍니다.
이 매트릭스를 보면 "어느 시기에 가입한 사용자가 더 오래 남았는지", "이탈이 급증하는 시점은 언제인지" 즉시 파악할 수 있습니다. 바로 이럴 때 필요한 것이 Polars와 Pandas를 결합한 매트릭스 생성 기법입니다.
Polars로 빠르게 집계하고, Pandas로 피벗 테이블을 만들어 시각화하면 최고의 성능과 편의성을 동시에 얻을 수 있습니다.
개요
간단히 말해서, 코호트 리텐션 매트릭스는 시간에 따른 사용자 유지율을 2차원 표 형태로 보여주는 분석 도구입니다. 왜 이 분석이 필요할까요?
단순히 "전체 리텐션 70%"라는 숫자보다, "1월 코호트는 Week 4에 60%지만, 2월 코호트는 50%"처럼 구체적인 비교가 훨씬 유용합니다. 예를 들어, 여러분이 3월에 신규 기능을 출시했다면, 3월 이후 코호트의 리텐션이 개선되었는지 매트릭스로 즉시 확인할 수 있습니다.
전통적으로는 엑셀 피벗 테이블로 수작업하거나 SQL로 복잡한 CASE WHEN 문을 작성했다면, 이제는 Polars의 고속 집계와 Pandas의 pivot_table을 결합하여 몇 줄로 완성할 수 있습니다. 매트릭스의 핵심은 "상대적 비교"입니다.
절대 숫자보다 비율(%)로 표현하고, 색상 그라데이션을 적용하면 어느 구간이 문제인지 시각적으로 바로 드러납니다. 또한 대각선 방향으로 읽으면 시간 경과에 따른 자연스러운 이탈 곡선을 볼 수 있고, 세로 방향으로 읽으면 특정 시점에서의 코호트 간 차이를 비교할 수 있습니다.
코드 예제
import polars as pl
import pandas as pd
# 코호트별, 경과일별 사용자 수 집계
retention_counts = retention_data.group_by(["cohort_date", "days_since_signup"]).agg(
pl.col("user_id").n_unique().alias("retained_users")
)
# 각 코호트의 초기 사용자 수 (Day 0)
cohort_sizes = retention_counts.filter(pl.col("days_since_signup") == 0).select([
"cohort_date",
pl.col("retained_users").alias("cohort_size")
])
# 조인하여 리텐션율 계산
retention_rates = retention_counts.join(cohort_sizes, on="cohort_date").with_columns(
(pl.col("retained_users") / pl.col("cohort_size") * 100).alias("retention_pct")
)
# Pandas로 변환하여 피벗 테이블 생성
df = retention_rates.to_pandas()
matrix = df.pivot_table(
index="cohort_date",
columns="days_since_signup",
values="retention_pct"
).round(1)
print(matrix)
설명
이것이 하는 일: 위 코드는 각 코호트(가입 시기 그룹)의 시간 경과에 따른 리텐션율을 계산하여 2차원 매트릭스 형태로 변환합니다. 첫 번째로, 코호트별, 경과일별로 활성 사용자 수를 집계합니다.
이 과정에서 n_unique()를 사용하는 이유는, 한 사용자가 같은 날 여러 번 방문해도 1명으로 카운트하기 위함입니다. 중복 제거가 중요합니다.
그 다음으로, 각 코호트의 초기 크기(Day 0 사용자 수)를 별도로 추출합니다. 이 값이 분모가 되어 리텐션율을 계산하는 기준이 됩니다.
filter()로 Day 0만 선택하고 컬럼명을 cohort_size로 변경하여 의미를 명확히 합니다. 세 번째로, join()으로 두 데이터셋을 결합하여 각 시점의 활성 사용자 수를 코호트 크기로 나눕니다.
이렇게 하면 "1월 1일 코호트의 Day 7 리텐션 = (Day 7 활성자 수 / Day 0 전체 수) × 100"이 계산됩니다. Polars의 컬럼 연산은 벡터화되어 있어 수백만 행도 빠르게 처리됩니다.
마지막으로, Pandas의 pivot_table()로 행=코호트, 열=경과일 형태의 매트릭스를 생성합니다. Polars에는 아직 피벗 기능이 제한적이므로, to_pandas()로 변환 후 Pandas의 강력한 피벗 기능을 활용합니다.
round(1)로 소수점 한 자리까지만 표시하면 가독성이 좋아집니다. 여러분이 이 코드를 사용하면 경영진에게 리텐션 현황을 한 장의 표로 명확하게 보고할 수 있습니다.
또한 히트맵(heatmap)으로 시각화하면 문제 구간이 빨간색으로 강조되어 즉시 개선이 필요한 영역을 파악할 수 있습니다. 예를 들어 seaborn의 sns.heatmap(matrix, annot=True, cmap="RdYlGn")를 추가하면 프로페셔널한 시각화가 완성됩니다.
실전 팁
💡 주간 단위 매트릭스를 만들 때는 days_since_signup // 7로 주차를 계산하면 일간보다 안정적인 패턴을 볼 수 있습니다.
💡 코호트가 너무 많으면 매트릭스가 복잡해집니다. 월 단위로 그룹핑하거나 최근 6개월만 표시하세요.
💡 절대 숫자와 비율을 함께 보면 맥락 파악에 도움됩니다. 100% → 80%로 하락했어도 원래 10명이었다면 통계적 의미가 적습니다.
💡 Polars의 to_pandas() 변환은 메모리 복사가 일어나므로, 집계를 최대한 Polars에서 끝내고 마지막 피벗만 Pandas에서 하세요.
💡 매트릭스에서 "계단식 하락"이 보인다면 정상, "중간에 급락"이 보인다면 해당 시점에 문제가 있다는 신호입니다.
4. 이탈 예측 모델링 - 머신러닝으로 사전 감지하기
시작하며
여러분이 이탈 위험이 높은 사용자를 미리 알 수 있다면 어떨까요? 떠나기 전에 특별 혜택을 제공하거나 개인화된 메시지를 보내 다시 참여시킬 수 있을 것입니다.
이탈 예측 모델링은 과거 이탈 사용자의 행동 패턴을 학습하여, 현재 활동 중인 사용자 중 누가 곧 떠날 것인지 예측하는 기법입니다. 실제로 Netflix, Spotify 같은 서비스들은 정교한 이탈 예측 모델로 수백만 명의 구독자를 유지하고 있습니다.
바로 이럴 때 필요한 것이 Polars로 피처를 생성하고 scikit-learn으로 모델을 학습하는 파이프라인입니다. 데이터 전처리는 빠르게, 모델 학습은 정확하게 분리하면 효율적입니다.
개요
간단히 말해서, 이탈 예측 모델은 사용자의 행동 데이터(피처)를 입력받아 이탈 확률을 출력하는 머신러닝 모델입니다. 왜 이 분석이 필요할까요?
사후 대응보다 사전 예방이 훨씬 효과적이기 때문입니다. 이미 떠난 사용자를 되돌리는 것은 어렵지만, 떠나려는 조짐을 보이는 사용자에게 타겟 개입을 하면 성공률이 30% 이상 높아집니다.
예를 들어, 여러분의 모델이 "이 사용자는 70% 확률로 이탈할 것"이라고 예측하면, 할인 쿠폰이나 프리미엄 기능 무료 체험을 제공하여 이탈을 막을 수 있습니다. 전통적으로는 단순 규칙 기반("30일 미접속 = 위험")으로 판단했다면, 이제는 방문 빈도 변화, 세션 길이 감소, 핵심 기능 미사용 등 수십 가지 시그널을 종합하여 판단합니다.
이탈 예측의 핵심은 피처 엔지니어링입니다. 좋은 피처는 모델 성능을 극적으로 높입니다.
방문 횟수, 평균 세션 시간, 마지막 방문 후 경과일, 방문 빈도 추세(증가/감소), 핵심 기능 사용 여부 등을 조합하여 사용자의 참여도(Engagement)를 정량화합니다.
코드 예제
import polars as pl
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
# 피처 생성: 사용자별 활동 통계
features = user_logs.group_by("user_id").agg([
pl.col("visit_date").count().alias("total_visits"),
(pl.col("visit_date").max() - pl.col("visit_date").min()).dt.total_days().alias("account_age_days"),
pl.col("session_duration").mean().alias("avg_session_minutes"),
pl.col("page_views").sum().alias("total_pageviews"),
# 최근 7일 vs 이전 7일 방문 비교
pl.col("visit_date").filter(pl.col("visit_date") >= pl.lit(datetime.now().date() - timedelta(days=7))).count().alias("recent_visits")
]).with_columns([
(pl.col("total_visits") / pl.col("account_age_days")).alias("visit_frequency"),
(pl.col("recent_visits") == 0).alias("is_churned") # 최근 7일 미방문 = 이탈
])
# 학습 데이터 준비
df = features.to_pandas()
X = df[["total_visits", "account_age_days", "avg_session_minutes", "total_pageviews", "visit_frequency"]]
y = df["is_churned"]
# 모델 학습
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
print(f"모델 정확도: {model.score(X_test, y_test):.2%}")
설명
이것이 하는 일: 위 코드는 사용자 활동 로그에서 이탈 예측에 유용한 피처들을 추출하고, 이를 바탕으로 랜덤 포레스트 분류 모델을 학습하여 이탈 여부를 예측합니다. 첫 번째로, group_by()와 다양한 집계 함수로 사용자별 활동 특성을 추출합니다.
총 방문 횟수는 충성도를, 계정 나이는 라이프사이클 단계를, 평균 세션 시간은 참여 깊이를 나타냅니다. 특히 filter() 내부에서 날짜 조건을 적용하여 "최근 7일 방문 횟수"를 계산하는 부분이 핵심입니다.
이렇게 하면 과거 활동과 최근 활동을 분리하여 트렌드 변화를 포착할 수 있습니다. 그 다음으로, with_columns()로 파생 피처를 생성합니다.
방문 빈도(일평균 방문 횟수)는 단순 방문 횟수보다 정규화된 지표입니다. 가입 후 10일째 10회 방문한 사용자와 100일째 10회 방문한 사용자는 참여도가 전혀 다르기 때문입니다.
또한 recent_visits == 0으로 이탈 레이블을 생성하여 지도 학습의 타겟 변수로 사용합니다. 세 번째로, Pandas로 변환하여 scikit-learn과 호환되는 형태로 만듭니다.
피처 행렬 X와 레이블 벡터 y를 분리하고, train_test_split()으로 학습/테스트 세트를 나눕니다. 80:20 비율이 일반적이며, random_state를 고정하면 재현 가능한 실험이 됩니다.
마지막으로, RandomForestClassifier를 학습합니다. 랜덤 포레스트는 과적합에 강하고 피처 중요도를 제공하여 "어떤 요인이 이탈에 가장 큰 영향을 미치는지" 해석할 수 있습니다.
n_estimators=100은 100개의 결정 트리를 앙상블한다는 의미로, 일반적으로 좋은 성능을 보입니다. 여러분이 이 코드를 사용하면 단순 규칙 기반보다 훨씬 정교한 이탈 예측이 가능합니다.
예를 들어 "방문 횟수는 많지만 최근 감소 추세"인 사용자를 자동으로 감지할 수 있습니다. 또한 model.feature_importances_로 어떤 피처가 가장 중요한지 확인하여 개선 우선순위를 정할 수 있습니다.
실전 팁
💡 클래스 불균형 문제에 주의하세요. 이탈자가 5%뿐이라면 class_weight='balanced' 옵션을 추가하거나 SMOTE로 오버샘플링하세요.
💡 피처에 시간 축을 추가하세요. "최근 7일 vs 이전 7일 방문 횟수 비율" 같은 트렌드 피처가 매우 효과적입니다.
💡 정확도보다 Precision/Recall을 보세요. 이탈 예측에서는 실제 이탈자를 놓치지 않는 것(Recall)이 더 중요할 수 있습니다.
💡 모델 재학습 주기를 정하세요. 사용자 행동은 계속 변하므로 매월 새 데이터로 재학습해야 성능이 유지됩니다.
💡 예측 확률(predict_proba)을 활용하여 위험도를 등급화하세요. 상위 10%만 타겟팅하면 비용 대비 효과가 극대화됩니다.
5. 세그먼트별 리텐션 비교 - 사용자 그룹별 차이 분석
시작하며
여러분의 서비스를 사용하는 모든 사용자가 같은 방식으로 행동할까요? 당연히 아닙니다.
유료 사용자와 무료 사용자, 모바일 사용자와 웹 사용자, 20대와 40대는 전혀 다른 패턴을 보입니다. 세그먼트별 리텐션 비교는 사용자를 의미 있는 그룹으로 나누고 각 그룹의 리텐션을 별도로 분석하는 기법입니다.
이렇게 하면 "전체 리텐션이 낮다"는 막연한 문제가 "무료 사용자의 Day 3 리텐션이 유독 낮다"는 구체적인 문제로 바뀝니다. 바로 이럴 때 필요한 것이 Polars의 그룹 연산과 조건부 집계 기능입니다.
한 번의 쿼리로 여러 세그먼트의 리텐션을 동시에 계산하고 비교할 수 있습니다.
개요
간단히 말해서, 세그먼트별 리텐션 분석은 사용자를 특정 기준으로 그룹화한 후 각 그룹의 리텐션 패턴을 비교하는 분석 방법입니다. 왜 이 분석이 필요할까요?
평균의 함정을 피하기 위함입니다. 전체 리텐션 50%라는 숫자는 "A 세그먼트 80%, B 세그먼트 20%"를 가릴 수 있습니다.
예를 들어, 여러분이 운영하는 학습 앱에서 "유료 구독자의 주간 리텐션 90%, 무료 사용자 30%"라는 사실을 발견하면, 무료 사용자를 유료로 전환시키는 전략에 집중해야 한다는 명확한 방향성이 생깁니다. 전통적으로는 세그먼트마다 별도의 쿼리를 실행하고 결과를 수동으로 합쳤다면, 이제는 partition_by()나 group_by() 여러 컬럼으로 한 번에 처리할 수 있습니다.
세그먼트 선정의 핵심은 "실행 가능성(Actionable)"입니다. 나이, 성별 같은 인구통계학적 세그먼트는 바꿀 수 없지만, 사용 빈도, 기능 사용 여부, 유입 경로 같은 행동 기반 세그먼트는 개선 가능합니다.
"검색 기능을 사용한 사용자의 리텐션이 2배 높다"면, 신규 사용자에게 검색 기능을 적극 안내하는 온보딩을 만들어야 합니다.
코드 예제
import polars as pl
# 사용자 속성 데이터 (세그먼트 정보)
user_segments = pl.read_csv("user_attributes.csv") # user_id, subscription_type, device_type, age_group
# 활동 데이터와 조인
segmented_retention = retention_data.join(user_segments, on="user_id")
# 세그먼트별, 경과일별 리텐션 계산
segment_analysis = segmented_retention.group_by([
"subscription_type",
"device_type",
"days_since_signup"
]).agg([
pl.col("user_id").n_unique().alias("active_users")
])
# 각 세그먼트의 초기 사용자 수 계산
segment_sizes = segment_analysis.filter(pl.col("days_since_signup") == 0).select([
"subscription_type",
"device_type",
pl.col("active_users").alias("segment_size")
])
# 리텐션율 계산
segment_retention_rates = segment_analysis.join(
segment_sizes,
on=["subscription_type", "device_type"]
).with_columns(
(pl.col("active_users") / pl.col("segment_size") * 100).alias("retention_pct")
)
# Day 7 리텐션만 추출하여 세그먼트 비교
day7_comparison = segment_retention_rates.filter(pl.col("days_since_signup") == 7).sort("retention_pct", descending=True)
print(day7_comparison)
설명
이것이 하는 일: 위 코드는 사용자를 구독 유형, 디바이스 종류 등의 속성으로 세그먼트화한 후, 각 세그먼트의 시간 경과에 따른 리텐션율을 계산하고 비교합니다. 첫 번째로, 사용자 속성 데이터(세그먼트 정보)를 활동 로그와 조인합니다.
이렇게 하면 각 활동 레코드에 "이 사용자는 유료 구독자이며 iOS 사용자다" 같은 맥락 정보가 추가됩니다. join()은 inner join이 기본이므로, 속성 정보가 없는 사용자는 자동으로 제외됩니다.
그 다음으로, group_by()에 여러 컬럼을 전달하여 다차원 그룹화를 수행합니다. "유료+iOS", "유료+Android", "무료+iOS", "무료+Android" 같은 조합별로 각 경과일의 활성 사용자 수를 집계합니다.
이것이 바로 Polars의 강점입니다. 수백만 행도 빠르게 처리합니다.
세 번째로, 앞서 배운 방식과 동일하게 각 세그먼트의 초기 크기(Day 0)를 추출하고, 이를 분모로 사용하여 리텐션율을 계산합니다. 여기서 주의할 점은 조인 키에 세그먼트 컬럼들을 모두 포함해야 올바른 매칭이 된다는 것입니다.
마지막으로, 특정 시점(예: Day 7)만 필터링하여 세그먼트 간 비교표를 만듭니다. sort()로 리텐션이 높은 순으로 정렬하면 "어떤 세그먼트가 가장 우수한지" 한눈에 보입니다.
이 결과를 막대 그래프로 시각화하면 경영진 보고 자료로 완벽합니다. 여러분이 이 코드를 사용하면 "모든 사용자"라는 추상적인 개념 대신 "구체적인 사용자 그룹"별로 전략을 수립할 수 있습니다.
예를 들어 "iOS 유료 사용자의 리텐션이 가장 높다"는 사실을 발견하면, 마케팅 예산을 iOS 유료 전환에 집중 투자하는 의사결정을 내릴 수 있습니다. 또한 "무료 사용자 중 특정 기능을 사용한 그룹의 리텐션이 2배 높다"는 인사이트로 무료→유료 전환 전략을 최적화할 수 있습니다.
실전 팁
💡 세그먼트를 너무 잘게 나누면 통계적 유의성이 떨어집니다. 각 세그먼트가 최소 수백 명 이상 되도록 하세요.
💡 행동 기반 세그먼트(핵심 기능 사용 여부)와 속성 기반 세그먼트(유료/무료)를 교차 분석하면 강력한 인사이트가 나옵니다.
💡 시간에 따른 세그먼트 구성 변화도 추적하세요. 유료 비율이 증가하면서 전체 리텐션이 올라가는 것은 좋은 신호입니다.
💡 Polars의 pivot() 함수를 사용하면 세그먼트를 열로 펼쳐서 한눈에 비교할 수 있습니다 (Pandas 변환 없이).
💡 통계적 유의성 검정(t-test)을 추가하면 세그먼트 간 차이가 우연인지 진짜 차이인지 판단할 수 있습니다.
6. 윈백 캠페인 효과 분석 - 이탈 사용자 복귀 측정
시작하며
여러분이 이탈한 사용자에게 이메일 캠페인을 보냈는데, 정말 효과가 있었는지 어떻게 알 수 있을까요? 단순히 "몇 명이 돌아왔다"가 아니라 "캠페인 덕분에 돌아왔다"는 인과관계를 증명해야 합니다.
윈백(Win-back) 캠페인 효과 분석은 이탈 사용자 대상 마케팅 활동의 성과를 정량적으로 측정하는 기법입니다. 실제로 많은 기업이 신규 고객 확보보다 이탈 고객 윈백이 ROI가 3배 이상 높다는 사실을 발견하고 있습니다.
바로 이럴 때 필요한 것이 A/B 테스트 프레임워크와 Polars의 시계열 분석 기능입니다. 캠페인 대상 그룹과 대조 그룹을 비교하여 순수한 효과를 측정할 수 있습니다.
개요
간단히 말해서, 윈백 캠페인 효과 분석은 이탈 사용자를 대상으로 한 재참여 활동이 실제로 복귀율을 높였는지 측정하는 분석입니다. 왜 이 분석이 필요할까요?
마케팅 예산을 효율적으로 배분하기 위함입니다. 윈백 이메일을 보냈는데 5% 복귀했다면 성공일까요?
아닙니다. 캠페인을 안 받은 그룹도 3% 복귀했다면 실제 효과는 2%p뿐입니다.
예를 들어, 여러분이 "30일 무료 프리미엄 체험" 오퍼를 담은 이메일을 10,000명에게 보냈고 500명이 복귀했다면, 이것이 이메일 덕분인지 아니면 원래 돌아올 사람들인지 구분해야 합니다. 전통적으로는 캠페인 전후 비교만 했다면, 이제는 무작위 대조군(Control Group)을 설정하여 인과관계를 증명합니다.
윈백 분석의 핵심은 타이밍과 타겟팅입니다. 이탈 후 너무 빨리 접근하면 부담스럽고, 너무 늦으면 이미 경쟁사로 갔습니다.
또한 모든 이탈자에게 같은 메시지를 보내는 것보다, 이탈 사유별로 맞춤형 오퍼를 제공하는 것이 훨씬 효과적입니다. "가격 때문에 떠난 사용자"에게는 할인을, "기능 부족 때문에 떠난 사용자"에게는 신규 기능 소개를 보내야 합니다.
코드 예제
import polars as pl
from datetime import datetime, timedelta
# 윈백 캠페인 대상자 (이탈 후 30일 경과)
churned_users = churn_analysis.filter(pl.col("days_inactive") > 30)
# 무작위로 절반은 캠페인 발송, 절반은 대조군
import random
random.seed(42)
churned_with_group = churned_users.with_columns(
pl.Series("campaign_group",
["treatment" if random.random() > 0.5 else "control"
for _ in range(churned_users.height)])
)
# 캠페인 발송 14일 후 복귀율 측정
campaign_date = datetime.now().date() - timedelta(days=14)
post_campaign_activity = user_logs.filter(pl.col("visit_date") > campaign_date)
# 복귀 여부 확인
winback_results = churned_with_group.join(
post_campaign_activity.select("user_id").unique(),
on="user_id",
how="left"
).with_columns(
pl.col("visit_date").is_not_null().alias("returned")
)
# 그룹별 복귀율 비교
effectiveness = winback_results.group_by("campaign_group").agg([
pl.col("returned").sum().alias("returned_count"),
pl.col("user_id").count().alias("total_users")
]).with_columns(
(pl.col("returned_count") / pl.col("total_users") * 100).alias("return_rate_pct")
)
print(effectiveness)
설명
이것이 하는 일: 위 코드는 이탈 사용자를 무작위로 캠페인 대상 그룹과 대조 그룹으로 나눈 후, 캠페인 발송 일정 기간 후 복귀율을 비교하여 캠페인의 순효과를 측정합니다. 첫 번째로, 이탈 정의(30일 이상 미접속)에 해당하는 사용자를 필터링합니다.
이들이 윈백 캠페인의 잠재 대상자입니다. 실무에서는 여기에 추가 조건(예: 과거 구매 이력 있음, 이메일 수신 동의)을 붙이는 경우가 많습니다.
그 다음으로, 무작위 배정을 통해 A/B 테스트 그룹을 생성합니다. random.random() > 0.5로 50:50 비율로 나누며, random.seed(42)로 재현 가능성을 보장합니다.
실무에서는 더 정교한 방법(stratified sampling)으로 그룹 간 특성을 균등하게 맞추기도 합니다. Polars에서는 리스트 컴프리헨션으로 생성한 값을 pl.Series()로 컬럼 추가합니다.
세 번째로, 캠페인 발송 후 일정 기간(예: 14일) 동안의 활동 로그를 조회합니다. 이 기간에 활동한 사용자는 "복귀했다"고 판단합니다.
join(..., how="left")을 사용하면 복귀하지 않은 사용자도 결과에 포함되며, is_not_null()로 복귀 여부를 Boolean으로 표현합니다. 마지막으로, 캠페인 그룹과 대조군의 복귀율을 각각 계산하여 비교합니다.
예를 들어 treatment 그룹이 7%, control 그룹이 3%라면, 캠페인의 순효과는 4%p입니다. 이를 통계적으로 검정(카이제곱 검정 등)하면 우연인지 진짜 효과인지 판단할 수 있습니다.
여러분이 이 코드를 사용하면 "감으로" 마케팅 효과를 판단하는 대신 데이터로 증명할 수 있습니다. 예를 들어 "20% 할인 vs 무료 체험" 중 어느 오퍼가 더 효과적인지 A/B 테스트로 비교하여 최적의 전략을 찾을 수 있습니다.
또한 복귀한 사용자의 후속 행동(재이탈 여부, LTV)까지 추적하면 단기 복귀뿐 아니라 장기 가치도 평가할 수 있습니다.
실전 팁
💡 최소 샘플 크기를 계산하세요. 그룹당 최소 수백 명은 되어야 통계적으로 유의미한 결과를 얻을 수 있습니다.
💡 복귀 정의를 명확히 하세요. "단순 재방문"과 "핵심 기능 사용"은 다릅니다. 후자가 진짜 복귀입니다.
💡 이탈 시점별로 세분화하세요. 이탈 후 30일 사용자와 90일 사용자는 다른 메시지가 필요합니다.
💡 다중 터치포인트를 추적하세요. 이메일→SMS→푸시 시퀀스로 여러 번 접촉한 경우 어느 채널이 결정적이었는지 분석하세요.
💡 장기 효과도 측정하세요. 복귀 후 30일 리텐션까지 추적해야 진짜 성공인지 알 수 있습니다.
7. RFM 분석과 리텐션 - 고객 가치 기반 세분화
시작하며
여러분이 100만 명의 사용자를 보유하고 있다면, 모두에게 같은 방식으로 접근해야 할까요? 당연히 아닙니다.
어제 방문한 VIP 고객과 6개월 전 한 번 방문한 사용자는 완전히 다른 전략이 필요합니다. RFM 분석은 Recency(최근성), Frequency(빈도), Monetary(금액) 세 가지 축으로 고객을 세분화하는 고전적이지만 강력한 기법입니다.
리텐션 분석과 결합하면 "어떤 유형의 고객이 오래 남는지" 명확히 보입니다. 바로 이럴 때 필요한 것이 Polars의 다차원 집계와 구간화(binning) 기능입니다.
수백만 사용자를 몇 초 만에 의미 있는 세그먼트로 나눌 수 있습니다.
개요
간단히 말해서, RFM 분석은 사용자를 "얼마나 최근에", "얼마나 자주", "얼마나 많이" 사용했는지 세 가지 기준으로 점수화하고 그룹화하는 방법입니다. 왜 이 분석이 필요할까요?
고객 생애 가치(LTV)를 예측하고 맞춤형 전략을 수립하기 위함입니다. RFM 점수가 높은 "챔피언" 그룹은 충성도가 높아 추천 프로그램 대상으로 적합하고, RFM 점수가 낮은 "이탈 위험" 그룹은 윈백 캠페인 대상입니다.
예를 들어, 여러분의 이커머스 사이트에서 R=5(최근 방문), F=5(자주 방문), M=5(많이 구매)인 고객은 VIP 대우를 받아야 하지만, R=1(오래 전 방문), F=1(한두 번 방문), M=1(적게 구매)인 고객은 재방문 유도 쿠폰을 보내야 합니다. 전통적으로는 엑셀에서 수작업으로 5분위 구간을 나눴다면, 이제는 Polars의 qcut()으로 자동화할 수 있습니다.
RFM의 핵심은 상대적 순위입니다. 절대값보다 "상위 20%", "하위 20%" 같은 백분위가 중요합니다.
또한 업종에 따라 M(금액) 대신 다른 지표를 쓸 수 있습니다. 무료 서비스라면 "세션 시간", "콘텐츠 소비량" 등으로 대체할 수 있습니다.
코드 예제
import polars as pl
import numpy as np
# RFM 지표 계산
current_date = pl.lit(datetime.now().date())
rfm = user_logs.group_by("user_id").agg([
(current_date - pl.col("visit_date").max()).dt.total_days().alias("recency"),
pl.col("visit_date").count().alias("frequency"),
pl.col("revenue").sum().alias("monetary") # 매출 데이터가 있다면
])
# 각 지표를 5분위로 구간화 (1=최하위, 5=최상위)
# Recency는 낮을수록 좋으므로 역순
rfm_scored = rfm.with_columns([
pl.col("recency").qcut(5, labels=["5","4","3","2","1"]).alias("R_score"),
pl.col("frequency").qcut(5, labels=["1","2","3","4","5"]).alias("F_score"),
pl.col("monetary").qcut(5, labels=["1","2","3","4","5"]).alias("M_score")
]).with_columns(
(pl.col("R_score").cast(int) + pl.col("F_score").cast(int) + pl.col("M_score").cast(int)).alias("RFM_total")
)
# RFM 세그먼트 정의
rfm_segments = rfm_scored.with_columns(
pl.when((pl.col("R_score").cast(int) >= 4) & (pl.col("F_score").cast(int) >= 4))
.then(pl.lit("Champions"))
.when((pl.col("R_score").cast(int) >= 3) & (pl.col("F_score").cast(int) >= 3))
.then(pl.lit("Loyal"))
.when((pl.col("R_score").cast(int) >= 4) & (pl.col("F_score").cast(int) <= 2))
.then(pl.lit("Promising"))
.when((pl.col("R_score").cast(int) <= 2))
.then(pl.lit("At Risk"))
.otherwise(pl.lit("Others"))
.alias("segment")
)
# 세그먼트별 통계
segment_summary = rfm_segments.group_by("segment").agg([
pl.col("user_id").count().alias("user_count"),
pl.col("monetary").mean().alias("avg_revenue")
])
print(segment_summary)
설명
이것이 하는 일: 위 코드는 사용자별로 최근 활동일, 방문 빈도, 총 매출을 계산하고, 이를 5점 척도로 점수화한 후 의미 있는 고객 세그먼트로 분류합니다. 첫 번째로, 기본 RFM 지표를 집계합니다.
Recency는 마지막 방문 후 경과일수(작을수록 좋음), Frequency는 총 방문 횟수(많을수록 좋음), Monetary는 총 매출(많을수록 좋음)입니다. 매출 데이터가 없는 서비스라면 세션 시간, 콘텐츠 소비량 등으로 대체할 수 있습니다.
그 다음으로, qcut() 함수로 각 지표를 5분위로 나눕니다. Quantile cut은 데이터를 동일한 개수의 그룹으로 나누는 방법으로, 각 그룹에 약 20%씩 배분됩니다.
중요한 점은 Recency는 "낮을수록 좋으므로" 레이블을 역순(["5","4","3","2","1"])으로 부여한다는 것입니다. 이렇게 하면 모든 점수가 "높을수록 좋음"으로 통일됩니다.
세 번째로, R, F, M 점수를 문자열에서 정수로 변환(cast(int))한 후 합산하여 총점을 계산합니다. 이 총점은 3~15 사이 값을 가지며, 높을수록 가치 있는 고객입니다.
하지만 총점만으로는 부족하므로, 각 점수의 조합 패턴으로 구체적인 세그먼트를 정의합니다. 마지막으로, when().then() 구문으로 비즈니스 로직에 맞는 세그먼트를 생성합니다.
"Champions"는 R과 F 모두 4점 이상인 최고 고객, "At Risk"는 R이 2점 이하인 이탈 위험군입니다. 이러한 세그먼트 정의는 업종과 비즈니스 목표에 따라 커스터마이즈해야 합니다.
여러분이 이 코드를 사용하면 수백만 사용자를 10개 이하의 의미 있는 그룹으로 압축할 수 있습니다. 예를 들어 "Champions" 세그먼트에게는 신제품 얼리 액세스를 제공하고, "At Risk" 세그먼트에게는 개인화된 할인 쿠폰을 보내는 식으로 차별화된 마케팅이 가능합니다.
또한 각 세그먼트의 시간에 따른 변화를 추적하면 전체 고객 베이스의 건강도를 모니터링할 수 있습니다.
실전 팁
💡 5분위가 항상 정답은 아닙니다. 데이터 분포에 따라 3분위나 10분위가 더 적절할 수 있습니다.
💡 RFM 점수의 가중치를 조정하세요. 어떤 비즈니스는 R이 가장 중요하고, 어떤 비즈니스는 M이 가장 중요합니다.
💡 동적 세그먼트를 만드세요. 매월 RFM을 재계산하여 사용자가 세그먼트 간 이동하는 패턴을 추적하세요.
💡 Polars 0.19+ 버전에서는 cut(), qcut() 함수가 개선되었으니 최신 버전을 사용하세요.
💡 RFM 세그먼트별 리텐션 커브를 그려보세요. Champions의 리텐션이 90%인데 At Risk가 10%라면 세그먼트 정의가 잘 된 것입니다.
8. 시계열 이탈률 추이 - 트렌드와 계절성 파악
시작하며
여러분의 서비스 이탈률이 갑자기 증가했다면, 이것이 일시적인 현상일까요 아니면 심각한 문제의 시작일까요? 한 시점의 스냅샷만으로는 판단하기 어렵습니다.
시계열 이탈률 분석은 매일, 매주, 매월 이탈률을 추적하여 장기 트렌드와 계절성 패턴을 발견하는 기법입니다. 실제로 많은 서비스가 특정 시즌(방학, 연말 등)에 이탈률이 급증하거나 급감하는 패턴을 보입니다.
바로 이럴 때 필요한 것이 Polars의 날짜 집계 기능과 이동 평균(Rolling Average) 계산입니다. 일간 변동성을 제거하고 진짜 트렌드를 볼 수 있습니다.
개요
간단히 말해서, 시계열 이탈률 분석은 이탈률을 시간 축에 따라 추적하여 증가/감소 추세, 계절성, 이상치를 발견하는 분석입니다. 왜 이 분석이 필요할까요?
조기 경보 시스템을 구축하기 위함입니다. 이탈률이 서서히 증가하는 추세를 조기에 발견하면 큰 문제가 되기 전에 대응할 수 있습니다.
예를 들어, 여러분의 서비스 이탈률이 지난 3개월간 매월 0.5%p씩 증가하고 있다면, 6개월 후에는 3%p 증가하게 되어 심각한 타격을 입을 수 있습니다. 전통적으로는 엑셀 차트로 수작업 시각화를 했다면, 이제는 Polars로 자동화된 대시보드를 만들 수 있습니다.
시계열 분석의 핵심은 노이즈 제거입니다. 일간 이탈률은 요일 효과(주말 vs 평일) 때문에 변동이 크므로, 7일 이동 평균을 사용하면 부드러운 추세선을 얻을 수 있습니다.
또한 전년 동기 대비(YoY) 비교를 하면 계절성 효과를 제거하고 순수한 성장/하락을 볼 수 있습니다.
코드 예제
import polars as pl
from datetime import datetime, timedelta
# 일별 활성 사용자 집계
daily_active = user_logs.group_by("visit_date").agg(
pl.col("user_id").n_unique().alias("dau")
).sort("visit_date")
# 각 날짜의 전체 등록 사용자 수 (누적)
total_users_by_date = user_logs.group_by("user_id").agg(
pl.col("visit_date").min().alias("signup_date")
).group_by("signup_date").agg(
pl.col("user_id").count().alias("new_signups")
).sort("signup_date").with_columns(
pl.col("new_signups").cum_sum().alias("total_users")
)
# 일별 이탈률 계산 (전체 사용자 - 활성 사용자) / 전체 사용자
churn_timeseries = daily_active.join(total_users_by_date, left_on="visit_date", right_on="signup_date", how="left").with_columns([
((pl.col("total_users") - pl.col("dau")) / pl.col("total_users") * 100).alias("churn_rate_pct")
])
# 7일 이동 평균으로 노이즈 제거
smoothed_churn = churn_timeseries.with_columns(
pl.col("churn_rate_pct").rolling_mean(window_size=7).alias("churn_rate_7d_avg")
)
# 최근 30일 추이 출력
recent_trend = smoothed_churn.tail(30).select(["visit_date", "churn_rate_pct", "churn_rate_7d_avg"])
print(recent_trend)
# 트렌드 방향 판단
recent_avg = smoothed_churn.tail(30)["churn_rate_7d_avg"].mean()
previous_avg = smoothed_churn.tail(60).head(30)["churn_rate_7d_avg"].mean()
trend = "증가" if recent_avg > previous_avg else "감소"
print(f"이탈률 트렌드: {trend} (최근 30일 평균: {recent_avg:.2f}%, 이전 30일 평균: {previous_avg:.2f}%)")
설명
이것이 하는 일: 위 코드는 일별 이탈률을 계산하고 7일 이동 평균을 적용하여 단기 변동성을 제거한 후, 최근 추세를 이전 기간과 비교하여 이탈률이 증가 중인지 감소 중인지 판단합니다. 첫 번째로, 일별 활성 사용자 수(DAU, Daily Active Users)를 집계합니다.
n_unique()로 중복 제거된 순수 사용자 수를 계산하고, sort()로 시간 순으로 정렬합니다. 시계열 분석에서는 정렬이 필수입니다.
그 다음으로, 각 날짜까지의 누적 가입자 수를 계산합니다. 사용자별 첫 방문일을 찾아 "가입일"로 정의하고, 날짜별 신규 가입자를 집계한 후 cum_sum()으로 누적합을 구합니다.
이렇게 하면 특정 날짜에 "전체 가입자가 몇 명이었는지" 알 수 있습니다. 세 번째로, 이탈률을 계산합니다.
정확한 정의는 "(전체 사용자 - 해당일 활성 사용자) / 전체 사용자"입니다. 하지만 이 방법은 누적 가입자를 분모로 쓰므로 과거 사용자까지 포함합니다.
실무에서는 "최근 N일 이내 활동한 사용자"를 분모로 쓰는 것이 더 정확할 수 있습니다. 네 번째로, rolling_mean(window_size=7)로 7일 이동 평균을 계산합니다.
이렇게 하면 특정일의 이상치(공휴일, 장애 등)가 희석되어 전체적인 추세가 부드러운 곡선으로 드러납니다. Polars의 rolling 함수는 메모리 효율적으로 구현되어 있어 대용량 데이터도 빠릅니다.
마지막으로, 최근 30일 평균과 이전 30일 평균을 비교하여 트렌드 방향을 판단합니다. 이것은 간단한 추세 감지 알고리즘으로, 더 정교하게는 선형 회귀의 기울기를 계산하거나 Mann-Kendall 검정 같은 통계 기법을 사용할 수 있습니다.
여러분이 이 코드를 사용하면 매일 아침 이탈률 대시보드를 자동으로 업데이트하여 이상 징후를 즉시 발견할 수 있습니다. 예를 들어 "이탈률이 3일 연속 전일 대비 0.5%p 이상 상승"하면 알림을 보내는 모니터링 시스템을 구축할 수 있습니다.
또한 계절성 패턴을 학습하여 "이번 연말 이탈률 급증은 작년과 비슷한 수준이므로 정상"인지 판단할 수 있습니다.
실전 팁
💡 이동 평균 윈도우 크기는 데이터 특성에 따라 조정하세요. 일간 변동이 크면 14일, 안정적이면 3일로 설정합니다.
💡 다중 시간 단위를 함께 보세요. 일간, 주간, 월간 이탈률을 레이어로 쌓으면 단기/장기 트렌드를 동시에 파악할 수 있습니다.
💡 이상치 탐지 알고리즘을 추가하세요. 표준편차의 3배를 벗어나는 날은 장애나 특별 이벤트가 있었을 가능성이 높습니다.
💡 Polars의 rolling_* 함수는 min_periods 옵션으로 초반 부족한 데이터를 처리할 수 있습니다.
💡 전년 동기 대비(YoY) 비교를 추가하면 성장률을 더 정확히 평가할 수 있습니다. "작년 12월 이탈률 5%, 올해 12월 이탈률 4%"면 개선된 것입니다.
9. 활성도 기반 세그먼트 - 참여 레벨별 분류
시작하며
여러분의 서비스에는 매일 사용하는 열성 팬도 있고, 한 달에 한 번 들르는 가벼운 사용자도 있습니다. 이 두 그룹을 같은 방식으로 대하면 효율적일까요?
활성도 기반 세그먼트는 사용 빈도와 참여 깊이에 따라 사용자를 "슈퍼 유저", "일반 유저", "라이트 유저", "휴면 유저"로 분류하는 기법입니다. 각 세그먼트는 서로 다른 니즈와 행동 패턴을 가지므로 맞춤형 전략이 필요합니다.
바로 이럴 때 필요한 것이 Polars의 조건부 집계와 복합 필터링입니다. 여러 지표를 결합하여 정교한 세그먼트를 정의할 수 있습니다.
개요
간단히 말해서, 활성도 세그먼트는 사용자의 최근 활동 빈도, 세션 길이, 핵심 기능 사용 여부 등을 종합하여 참여 수준을 분류하는 방법입니다. 왜 이 분석이 필요할까요?
각 세그먼트마다 다른 목표와 전략을 가져야 하기 때문입니다. 슈퍼 유저는 이미 충분히 참여하므로 "추천 프로그램"으로 신규 유입에 활용하고, 라이트 유저는 "참여 증대" 캠페인으로 활성화를 유도하며, 휴면 유저는 "윈백" 전략을 적용해야 합니다.
예를 들어, 여러분의 피트니스 앱에서 주 5회 이상 운동 기록하는 사용자는 "챌린지"로 동기를 부여하고, 주 1회 미만 사용자는 "쉬운 목표"로 재참여를 유도해야 합니다. 전통적으로는 단일 지표(방문 횟수)로만 분류했다면, 이제는 다차원 기준(빈도 + 깊이 + 최근성)을 결합합니다.
활성도 세그먼트의 핵심은 "행동 기반"이라는 점입니다. 인구통계나 가입 경로가 아니라 실제로 "무엇을 했는지"로 분류하므로, 세그먼트를 이동시킬 수 있습니다.
라이트 유저를 일반 유저로, 일반 유저를 슈퍼 유저로 만드는 것이 성장 전략의 핵심입니다.
코드 예제
import polars as pl
from datetime import datetime, timedelta
# 최근 30일 활동 기준 세그먼트 정의
recent_window = 30
cutoff_date = datetime.now().date() - timedelta(days=recent_window)
user_engagement = user_logs.filter(pl.col("visit_date") >= cutoff_date).group_by("user_id").agg([
pl.col("visit_date").n_unique().alias("active_days"),
pl.col("session_duration").sum().alias("total_minutes"),
pl.col("core_feature_used").any().alias("used_core_feature") # Boolean 컬럼
])
# 세그먼트 정의 (비즈니스 로직에 맞게 조정)
engagement_segments = user_engagement.with_columns(
pl.when((pl.col("active_days") >= 20) & (pl.col("total_minutes") >= 300) & pl.col("used_core_feature"))
.then(pl.lit("Super User"))
.when((pl.col("active_days") >= 10) & (pl.col("total_minutes") >= 120))
.then(pl.lit("Active User"))
.when((pl.col("active_days") >= 3) & (pl.col("total_minutes") >= 30))
.then(pl.lit("Regular User"))
.when(pl.col("active_days") >= 1)
.then(pl.lit("Light User"))
.otherwise(pl.lit("Dormant"))
.alias("engagement_level")
)
# 세그먼트 분포 확인
segment_distribution = engagement_segments.group_by("engagement_level").agg([
pl.col("user_id").count().alias("user_count")
]).with_columns(
(pl.col("user_count") / pl.col("user_count").sum() * 100).alias("percentage")
).sort("user_count", descending=True)
print(segment_distribution)
# 각 세그먼트의 리텐션 비교 (다음 30일 재방문율)
next_window_start = datetime.now().date()
next_window_end = next_window_start + timedelta(days=30)
future_activity = user_logs.filter(
(pl.col("visit_date") >= next_window_start) & (pl.col("visit_date") < next_window_end)
).select("user_id").unique()
retention_by_segment = engagement_segments.join(
future_activity, on="user_id", how="left"
).with_columns(
pl.col("visit_date").is_not_null().alias("retained")
).group_by("engagement_level").agg([
pl.col("retained").mean().alias("retention_rate")
]).sort("retention_rate", descending=True)
print(retention_by_segment)
설명
이것이 하는 일: 위 코드는 최근 30일간의 활동 데이터를 바탕으로 사용자를 참여 수준별로 분류하고, 각 세그먼트의 크기와 향후 리텐션율을 계산합니다. 첫 번째로, 분석 기간(최근 30일)을 정의하고 해당 기간의 활동만 필터링합니다.
시간 윈도우를 명확히 하는 것이 중요합니다. "활성 사용자"의 정의가 "최근 7일" vs "최근 30일"에 따라 완전히 달라지기 때문입니다.
그 다음으로, 사용자별로 세 가지 핵심 지표를 집계합니다. active_days는 며칠 방문했는지(단순 세션 수가 아닌 고유 날짜 수), total_minutes는 총 사용 시간, used_core_feature는 핵심 기능(예: 콘텐츠 작성, 결제 등)을 사용했는지 여부입니다.
.any()는 Boolean 컬럼에서 "한 번이라도 true가 있으면 true"를 반환합니다. 세 번째로, 복합 조건으로 세그먼트를 정의합니다.
"슈퍼 유저"는 20일 이상 활동 + 300분 이상 사용 + 핵심 기능 사용 모두를 만족해야 합니다. 이렇게 여러 조건을 AND로 결합하면 진짜 파워 유저만 선별됩니다.
조건의 순서가 중요한데, when()은 위에서 아래로 평가되므로 가장 엄격한 조건을 맨 위에 둡니다. 네 번째로, 세그먼트 분포를 계산하여 각 그룹의 크기를 파악합니다.
이상적으로는 피라미드 구조(많은 라이트 유저, 적은 슈퍼 유저)가 자연스럽지만, 건강한 서비스는 슈퍼+일반 유저 비율이 점점 증가합니다. 마지막으로, 각 세그먼트의 향후 리텐션을 측정합니다.
"슈퍼 유저의 리텐션 95%, 라이트 유저의 리텐션 20%"처럼 명확한 차이가 보인다면 세그먼트 정의가 잘 된 것입니다. 이 정보로 "라이트 유저를 일반 유저로 업그레이드"하는 전략의 우선순위를 정할 수 있습니다.
여러분이 이 코드를 사용하면 추상적인 "사용자"가 아닌 구체적인 "슈퍼 유저 1만 명, 라이트 유저 5만 명"처럼 실행 가능한 타겟을 얻을 수 있습니다. 예를 들어 "라이트 유저 중 핵심 기능을 한 번도 안 써본 그룹"을 찾아 온보딩 튜토리얼을 보여주는 식의 세밀한 전략이 가능해집니다.
실전 팁
💡 세그먼트 경계값은 데이터 분포를 보고 정하세요. 상위 10%를 슈퍼 유저로 정의하려면 90 percentile 값을 threshold로 사용합니다.
💡 세그먼트 이동 추적(Transition Matrix)을 만드세요. "이번 달 라이트 유저 중 몇 %가 다음 달 일반 유저가 되었는지" 추적하면 개선 효과를 측정할 수 있습니다.
💡 너무 많은 세그먼트는 오히려 혼란을 줍니다. 3~5개 정도가 실행 가능한 수준입니다.
💡 "핵심 기능"은 서비스마다 다릅니다. 여러분의 서비스에서 리텐션과 가장 상관관계가 높은 행동을 찾아야 합니다.
💡 세그먼트별 LTV(생애 가치)를 계산하면 어느 그룹에 투자해야 ROI가 높은지 명확해집니다.
10. 코호트 라이프사이클 분석 - 가입 시점별 수명 비교
시작하며
여러분이 1월에 가입한 사용자와 6월에 가입한 사용자 중 누가 더 오래 남을까요? 신규 기능을 출시한 후 가입한 사용자의 수명이 길어졌을까요?
코호트 라이프사이클 분석은 가입 시점이 다른 코호트들의 평균 수명(생존 기간)을 비교하여 서비스 개선 효과를 측정하는 기법입니다. 실제로 많은 기업이 제품 업데이트 전후의 코호트 수명을 비교하여 개선 효과를 정량화합니다.
바로 이럴 때 필요한 것이 Polars의 날짜 연산과 생존 분석(Survival Analysis) 개념입니다. 각 코호트가 얼마나 오래 활성 상태를 유지하는지 계산할 수 있습니다.
개요
간단히 말해서, 코호트 라이프사이클 분석은 각 가입 시기 그룹의 평균 활동 기간, 중간 생존 기간, 이탈까지 걸린 시간을 비교하는 분석입니다. 왜 이 분석이 필요할까요?
서비스 개선의 장기 효과를 측정하기 위함입니다. 신규 기능이 리텐션을 높였다면, 그 이후 가입한 코호트의 평균 수명이 길어져야 합니다.
예를 들어, 여러분이 3월에 개인화 추천 기능을 출시했다면, "3월 이후 코호트의 평균 활동 기간 120일 vs 이전 코호트 90일"처럼 비교하여 효과를 증명할 수 있습니다. 전통적으로는 단순히 "이탈률"만 봤다면, 이제는 "이탈까지 걸린 시간"이라는 더 정교한 지표를 봅니다.
라이프사이클 분석의 핵심은 "생존 함수"입니다. 가입 후 N일째에 아직 활동 중인 사용자 비율을 그래프로 그리면, 어느 시점에서 급격한 이탈이 발생하는지 시각적으로 드러납니다.
또한 "중간 생존 기간(Median Lifetime)"은 평균보다 이상치에 강건한 지표로, "사용자의 절반이 이탈하는 시점"을 나타냅니다.
코드 예제
import polars as pl
from datetime import datetime, timedelta
# 각 사용자의 가입일과 마지막 활동일 계산
user_lifecycle = user_logs.group_by("user_id").agg([
pl.col("visit_date").min().alias("first_visit"),
pl.col("visit_date").max().alias("last_visit")
]).with_columns([
(pl.col("last_visit") - pl.col("first_visit")).dt.total_days().alias("lifetime_days"),
pl.col("first_visit").dt.strftime("%Y-%m").alias("cohort_month")
])
# 코호트별 라이프사이클 통계
cohort_lifecycle = user_lifecycle.group_by("cohort_month").agg([
pl.col("lifetime_days").mean().alias("avg_lifetime"),
pl.col("lifetime_days").median().alias("median_lifetime"),
pl.col("lifetime_days").max().alias("max_lifetime"),
pl.col("user_id").count().alias("cohort_size")
]).sort("cohort_month")
print(cohort_lifecycle)
# 생존 곡선 데이터 생성 (특정 코호트)
target_cohort = "2024-01"
cohort_users = user_lifecycle.filter(pl.col("cohort_month") == target_cohort)
# 각 날짜별 생존율 계산
max_days = int(cohort_users["lifetime_days"].max())
survival_curve = pl.DataFrame({
"days": range(0, max_days + 1, 7) # 주 단위
}).with_columns([
pl.col("days").map_elements(
lambda d: (cohort_users.filter(pl.col("lifetime_days") >= d).height / cohort_users.height * 100),
return_dtype=pl.Float64
).alias("survival_pct")
])
print(f"\n{target_cohort} 코호트 생존 곡선:")
print(survival_curve)
# 50% 생존 시점 찾기 (중간 수명)
median_day = survival_curve.filter(pl.col("survival_pct") <= 50).select("days").min()
print(f"\n중간 생존 기간: {median_day} 일")
설명
이것이 하는 일: 위 코드는 각 사용자의 첫 방문일부터 마지막 방문일까지의 기간을 계산하고, 가입 월별 코호트로 그룹화하여 평균 수명과 중간 수명을 비교합니다. 또한 특정 코호트의 생존 곡선을 생성하여 시간에 따른 잔존율을 보여줍니다.
첫 번째로, 사용자별로 첫 방문일(가입일)과 마지막 방문일을 추출합니다. 이 두 날짜의 차이가 바로 "활동 기간(Lifetime)"입니다.
dt.total_days()로 일 단위로 변환하고, 월별 코호트를 만들기 위해 strftime("%Y-%m")으로 "2024-01" 형식으로 변환합니다. 그 다음으로, 코호트별로 평균, 중간값, 최대값을 집계합니다.
평균은 전체적인 경향을, 중간값은 이상치에 강건한 대표값을, 최대값은 가장 충성도 높은 사용자의 수명을 나타냅니다. 코호트 크기도 함께 보여주면 "소수 코호트의 높은 평균"을 걸러낼 수 있습니다.
세 번째로, 특정 코호트(예: 2024년 1월)의 생존 곡선을 생성합니다. 각 시점(주 단위)마다 "아직 활동 중인 사용자 비율"을 계산합니다.
map_elements()로 각 일자에 대해 "lifetime_days >= d"인 사용자 수를 세고 전체 코호트 크기로 나눕니다. 이것이 바로 생존율입니다.
마지막으로, 생존율이 50% 이하로 떨어지는 첫 시점을 찾아 "중간 생존 기간"을 계산합니다. 이 지표는 "절반의 사용자가 이탈하는 시점"을 나타내며, 코호트 간 비교에 매우 유용합니다.
예를 들어 "1월 코호트 중간 수명 60일, 6월 코호트 중간 수명 90일"이면 서비스가 개선되었다는 증거입니다. 여러분이 이 코드를 사용하면 "우리 서비스는 사용자를 얼마나 오래 붙잡을 수 있는가"라는 근본적인 질문에 답할 수 있습니다.
예를 들어 생존 곡선이 "처음 7일간 급락 → 이후 완만"한 패턴이면 온보딩이 중요하다는 의미이고, "점진적으로 하락"하면 지속적인 참여 유도가 필요하다는 의미입니다. 또한 제품 개선 전후의 코호트를 비교하여 투자 대비 효과를 정량화할 수 있습니다.
실전 팁
💡 "마지막 활동일"을 수명으로 쓰면 아직 활동 중인 사용자는 과소평가됩니다. 이를 "중도절단(Censored Data)"이라 하며, Kaplan-Meier 추정법 같은 생존 분석 기법으로 보정해야 정확합니다.
💡 코호트 간 비교는 동일 조건에서 해야 공정합니다. 최근 코호트는 관찰 기간이 짧으므로 최소 3개월 이상 경과한 코호트만 비교하세요.
💡 생존 곡선을 여러 코호트 겹쳐 그리면 개선 추세가 한눈에 보입니다. Matplotlib으로 시각화하세요.
💡 50% 생존 시점뿐 아니라 25%, 75% 분위도 함께 보면 분포를 더 잘 이해할 수 있습니다.
💡 Python의 lifelines 라이브러리를 사용하면 전문적인 생존 분석(Log-rank test, Cox regression 등)을 수행할 수 있습니다.