이미지 로딩 중...

상품 추천 지표 개발 완벽 가이드 - 슬라이드 1/10
A

AI Generated

2025. 11. 16. · 4 Views

상품 추천 지표 개발 완벽 가이드

실무에서 바로 활용할 수 있는 상품 추천 시스템의 핵심 지표들을 배워봅니다. Python과 Polars를 사용하여 클릭률, 전환율, 개인화 점수 등 주요 추천 지표를 계산하고 분석하는 방법을 단계별로 알아봅니다.


목차

  1. 클릭률(CTR) 계산 - 추천 성과의 기본 지표
  2. 전환율(CVR) 측정 - 실제 매출로 이어지는 추천
  3. 개인화 점수 계산 - 사용자 맞춤 추천의 핵심
  4. 다양성 지표(Diversity) - 추천의 폭을 넓히기
  5. 신선도 지표(Novelty) - 새로운 발견의 기회
  6. 커버리지 지표(Coverage) - 전체 상품의 공정한 노출
  7. 세렌디피티 점수(Serendipity) - 의외의 만족스러운 발견
  8. 재추천률(Re-recommendation Rate) - 중복 추천 관리
  9. 응답 시간(Latency) - 실시간 추천의 성능

1. 클릭률(CTR) 계산 - 추천 성과의 기본 지표

시작하며

여러분이 이커머스 플랫폼에서 "이 상품은 어때요?"라는 추천을 했는데, 정작 사용자들이 클릭조차 하지 않는 상황을 겪어본 적 있나요? 수백 개의 상품을 추천했지만 실제로 얼마나 효과적인지 알 수 없어 답답했던 경험 말입니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 추천 알고리즘은 작동하지만, 그 성과를 정량적으로 측정하지 못하면 개선할 방법도 찾을 수 없습니다.

특히 A/B 테스트나 모델 성능 비교를 할 때 명확한 지표가 없으면 의사결정이 불가능합니다. 바로 이럴 때 필요한 것이 클릭률(CTR, Click-Through Rate)입니다.

추천된 상품 중 실제로 사용자가 클릭한 비율을 계산하여 추천 시스템의 효과를 즉시 파악할 수 있습니다.

개요

간단히 말해서, CTR은 추천된 상품을 사용자가 실제로 클릭한 비율을 나타내는 가장 기본적인 성과 지표입니다. 왜 이 지표가 필요한지 실무 관점에서 설명하면, 추천 시스템의 즉각적인 효과를 측정할 수 있기 때문입니다.

100개의 상품을 추천했는데 5개만 클릭됐다면 CTR은 5%입니다. 이를 통해 알고리즘의 개선 방향을 빠르게 파악할 수 있죠.

예를 들어, 개인화 추천과 인기 상품 추천의 CTR을 비교하여 어떤 전략이 더 효과적인지 즉시 알 수 있습니다. 전통적인 방법에서는 SQL로 복잡한 조인과 집계를 수행했다면, 이제는 Polars의 빠른 데이터프레임 연산으로 수백만 건의 로그를 몇 초 만에 처리할 수 있습니다.

이 지표의 핵심 특징은 첫째, 실시간에 가까운 피드백을 제공하고, 둘째, 사용자 그룹별, 카테고리별, 시간대별 세분화 분석이 가능하며, 셋째, 추천 알고리즘의 개선 효과를 즉시 검증할 수 있다는 점입니다. 이러한 특징들이 데이터 기반 의사결정을 가능하게 만듭니다.

코드 예제

import polars as pl
from datetime import datetime

# 추천 로그 데이터 로드
recommendation_logs = pl.read_parquet("recommendation_logs.parquet")

# CTR 계산: 사용자별, 날짜별 집계
ctr_by_date = (
    recommendation_logs
    .group_by(["date", "user_id"])
    .agg([
        pl.col("product_id").count().alias("total_recommendations"),  # 총 추천 수
        pl.col("clicked").sum().alias("total_clicks"),  # 실제 클릭 수
    ])
    .with_columns([
        (pl.col("total_clicks") / pl.col("total_recommendations") * 100)
        .round(2)
        .alias("ctr_percentage")  # CTR을 퍼센트로 계산
    ])
    .sort("date", descending=True)
)

print(ctr_by_date.head())

설명

이것이 하는 일: 추천 로그 데이터에서 날짜와 사용자별로 총 추천 수와 클릭 수를 집계한 후, 클릭률을 퍼센트로 계산하여 추천 시스템의 효과를 정량화합니다. 첫 번째로, pl.read_parquet()로 추천 로그 데이터를 로드합니다.

Parquet 형식은 컬럼 기반 저장 방식으로 대용량 데이터를 빠르게 읽을 수 있어 실무에서 선호됩니다. 수백만 건의 로그도 몇 초 안에 메모리에 적재됩니다.

그 다음으로, group_by(["date", "user_id"])로 날짜와 사용자 조합별로 그룹화한 후 agg()로 집계를 수행합니다. pl.col("product_id").count()는 해당 그룹의 총 추천 수를 세고, pl.col("clicked").sum()은 clicked 컬럼(0 또는 1)의 합계로 실제 클릭 수를 계산합니다.

이 단계에서 Polars는 병렬 처리를 자동으로 수행하여 속도가 매우 빠릅니다. 마지막으로, with_columns()로 새로운 CTR 컬럼을 추가합니다.

클릭 수를 추천 수로 나누고 100을 곱해 퍼센트로 변환하며, round(2)로 소수점 둘째 자리까지만 표시합니다. sort("date", descending=True)로 최신 날짜부터 정렬하여 최근 성과를 먼저 확인할 수 있습니다.

여러분이 이 코드를 사용하면 일별 CTR 추이를 즉시 파악할 수 있고, 특정 사용자 그룹의 반응도 분석할 수 있습니다. 또한 A/B 테스트 시 실험군과 대조군의 CTR을 비교하여 새 알고리즘의 효과를 검증할 수 있으며, 카테고리별로 그룹화를 변경하면 어떤 상품군의 추천이 효과적인지도 알 수 있습니다.

실전 팁

💡 CTR만으로는 부족합니다. 클릭은 많지만 구매로 이어지지 않는다면 의미가 없으므로, 항상 전환율(CVR)과 함께 분석하세요.

💡 시간대별 CTR 차이가 클 수 있습니다. 점심시간(12-1시)과 퇴근 후(7-10시)의 CTR을 비교하면 추천 타이밍 최적화에 활용할 수 있습니다.

💡 신규 사용자와 기존 사용자의 CTR을 분리해서 측정하세요. 신규 사용자는 탐색 성향이 강해 CTR이 높지만 전환율은 낮을 수 있습니다.

💡 position bias를 고려하세요. 상단에 노출된 상품의 CTR이 높은 것은 당연하므로, 노출 위치별로 CTR을 정규화하는 것이 공정한 평가입니다.

💡 Polars의 lazy evaluation을 활용하면 메모리 효율을 높일 수 있습니다. pl.scan_parquet()로 시작하고 마지막에 .collect()를 호출하면 전체 파이프라인이 최적화됩니다.


2. 전환율(CVR) 측정 - 실제 매출로 이어지는 추천

시작하며

여러분이 추천 시스템을 개선해서 클릭률이 20%나 증가했는데, 정작 실제 구매는 오히려 줄어든 경험을 해보셨나요? 사용자들이 클릭은 많이 하지만 구매로 이어지지 않아서 매출에는 아무 도움이 안 되는 상황 말입니다.

이런 문제는 클릭률만 보고 추천 시스템을 평가할 때 자주 발생합니다. 클릭은 관심의 표현이지만, 실제 구매는 만족과 신뢰의 표현입니다.

예를 들어, 자극적인 제목으로 클릭을 유도하는 낚시성 추천은 CTR은 높지만 CVR은 낮을 수밖에 없습니다. 바로 이럴 때 필요한 것이 전환율(CVR, Conversion Rate)입니다.

추천된 상품이 실제 구매로 이어진 비율을 측정하여 진짜 비즈니스 가치를 창출하는 추천인지 판단할 수 있습니다.

개요

간단히 말해서, CVR은 추천 또는 클릭 후 실제 구매로 전환된 비율을 나타내는 핵심 비즈니스 지표입니다. 왜 이 지표가 필요한지 실무 관점에서 설명하면, 최종적으로 회사의 매출과 직결되기 때문입니다.

1000명에게 상품을 추천해서 100명이 클릭하고 그 중 10명이 구매했다면, 클릭 기준 CVR은 10%, 노출 기준 CVR은 1%입니다. 이를 통해 추천의 실질적인 ROI를 계산할 수 있죠.

예를 들어, 프리미엄 상품 추천은 CTR은 낮지만 CVR이 높아서 실제로는 더 수익성이 좋을 수 있습니다. 기존에는 마케팅 팀과 개발 팀이 각자 다른 도구로 전환율을 계산했다면, 이제는 통합된 데이터 파이프라인으로 실시간에 가까운 CVR 모니터링이 가능합니다.

이 지표의 핵심 특징은 첫째, 비즈니스 임팩트를 직접 측정하고, 둘째, 사용자 여정의 각 단계별 전환율을 추적할 수 있으며, 셋째, 추천의 품질과 적합성을 가장 정확하게 반영한다는 점입니다. 이러한 특징들이 데이터 과학자와 비즈니스 리더 모두가 공감할 수 있는 지표를 만듭니다.

코드 예제

import polars as pl

# 추천, 클릭, 구매 데이터 조인
recommendations = pl.read_parquet("recommendations.parquet")
clicks = pl.read_parquet("clicks.parquet")
purchases = pl.read_parquet("purchases.parquet")

# 전환 퍼널 분석
conversion_funnel = (
    recommendations
    .join(clicks, on=["user_id", "product_id"], how="left")  # 클릭 여부 조인
    .join(purchases, on=["user_id", "product_id"], how="left")  # 구매 여부 조인
    .with_columns([
        pl.col("click_timestamp").is_not_null().alias("is_clicked"),  # 클릭 여부
        pl.col("purchase_timestamp").is_not_null().alias("is_purchased"),  # 구매 여부
    ])
    .group_by("recommendation_type")  # 추천 타입별 집계
    .agg([
        pl.count().alias("total_recommendations"),
        pl.col("is_clicked").sum().alias("total_clicks"),
        pl.col("is_purchased").sum().alias("total_purchases"),
        (pl.col("is_purchased").sum() / pl.col("is_clicked").sum() * 100).round(2).alias("cvr_from_click"),  # 클릭 후 전환율
        (pl.col("is_purchased").sum() / pl.count() * 100).round(2).alias("cvr_from_impression"),  # 노출 후 전환율
    ])
)

print(conversion_funnel)

설명

이것이 하는 일: 추천, 클릭, 구매 데이터를 결합하여 전환 퍼널을 구성하고, 추천 타입별로 클릭 전환율과 구매 전환율을 계산하여 어떤 추천 전략이 실제 매출에 기여하는지 분석합니다. 첫 번째로, 세 개의 데이터 소스를 읽어옵니다.

recommendations는 모든 추천 이벤트, clicks는 실제 클릭 이벤트, purchases는 구매 완료 이벤트를 담고 있습니다. 각 테이블은 user_id와 product_id를 키로 가지고 있어 조인이 가능합니다.

그 다음으로, join(..., how="left")로 추천을 기준으로 클릭과 구매 데이터를 왼쪽 조인합니다. 이렇게 하면 클릭되지 않거나 구매되지 않은 추천도 결과에 포함되어 정확한 비율 계산이 가능합니다.

is_not_null()로 타임스탬프의 존재 여부를 확인하여 불리언 컬럼을 생성하는 것이 핵심입니다. 세 번째로, group_by("recommendation_type")로 개인화 추천, 인기 상품, 유사 상품 등 추천 타입별로 그룹화합니다.

agg()에서 총 추천 수, 클릭 수, 구매 수를 집계하고, 두 가지 CVR을 계산합니다. cvr_from_click은 클릭한 사람 중 구매한 비율로 추천의 품질을 나타내고, cvr_from_impression은 전체 노출 대비 구매 비율로 전체적인 효율을 나타냅니다.

여러분이 이 코드를 사용하면 각 추천 전략의 실제 비즈니스 기여도를 명확히 비교할 수 있습니다. 예를 들어, AI 기반 개인화 추천의 CVR이 5%이고 인기 상품 추천의 CVR이 2%라면, 개인화에 더 많은 리소스를 투자해야 한다는 의사결정을 데이터로 뒷받침할 수 있습니다.

또한 카테고리별, 가격대별로 그룹화를 변경하면 세분화된 인사이트를 얻을 수 있습니다.

실전 팁

💡 구매 전환은 시간이 걸립니다. 클릭 후 24시간, 7일, 30일 단위로 CVR을 측정하면 상품 특성에 따른 구매 패턴을 파악할 수 있습니다. 고가 상품은 긴 고려 시간이 필요합니다.

💡 장바구니 추가율도 함께 측정하세요. 클릭 → 장바구니 → 구매의 각 단계 전환율을 보면 어디서 이탈이 발생하는지 알 수 있습니다.

💡 CVR이 비정상적으로 높다면 샘플 편향을 의심하세요. 이미 구매 의도가 높은 사용자에게만 추천했다면 추천 시스템의 기여도가 과대평가될 수 있습니다.

💡 취소/환불률도 함께 추적하세요. CVR은 높지만 환불이 많다면 잘못된 추천일 가능성이 높습니다. 순전환율 = (구매 - 환불) / 노출로 계산하는 것이 더 정확합니다.

💡 Polars의 윈도우 함수를 활용하면 사용자별 누적 CVR을 계산할 수 있습니다. over("user_id") 표현식으로 각 사용자의 추천 반응 패턴을 시계열로 분석하세요.


3. 개인화 점수 계산 - 사용자 맞춤 추천의 핵심

시작하며

여러분이 모든 사용자에게 똑같은 베스트셀러 상품을 추천했는데, 20대 여성과 50대 남성의 반응이 천차만별인 경험을 해보셨나요? 인기 상품이라고 해서 모두에게 적합한 건 아니라는 걸 깨달았을 때의 당혹감 말입니다.

이런 문제는 개인화 없이 일괄 추천을 할 때 자주 발생합니다. 사용자의 과거 행동, 선호도, 컨텍스트를 고려하지 않으면 추천의 적합성이 떨어집니다.

예를 들어, 최근 스포츠 용품을 여러 번 구매한 사용자에게 주방용품을 추천하는 것은 리소스 낭비입니다. 바로 이럴 때 필요한 것이 개인화 점수입니다.

사용자의 프로필, 행동 이력, 컨텍스트를 종합하여 각 상품이 해당 사용자에게 얼마나 적합한지 수치화할 수 있습니다.

개요

간단히 말해서, 개인화 점수는 특정 사용자와 특정 상품 간의 적합도를 0-1 사이의 값으로 표현한 지표입니다. 왜 이 지표가 필요한지 실무 관점에서 설명하면, 수백만 개의 상품 중에서 각 사용자에게 가장 관련성 높은 몇 개를 선택해야 하기 때문입니다.

개인화 점수가 높을수록 사용자의 클릭과 구매 가능성이 높아집니다. 예를 들어, 사용자의 카테고리 선호도, 최근 검색어, 가격대 선호, 브랜드 충성도 등을 종합하여 점수를 계산하면 훨씬 정교한 추천이 가능합니다.

기존에는 룰 기반으로 "이 카테고리를 본 사람에게는 이 상품"처럼 단순한 매핑을 했다면, 이제는 여러 신호를 가중합하여 연속적인 점수로 우선순위를 매길 수 있습니다. 이 지표의 핵심 특징은 첫째, 다양한 신호를 통합하여 종합적인 적합도를 표현하고, 둘째, 실시간으로 사용자 컨텍스트에 따라 동적으로 계산할 수 있으며, 셋째, 머신러닝 모델의 입력 피처나 출력 점수로 직접 활용할 수 있다는 점입니다.

이러한 특징들이 진정한 개인화 추천을 가능하게 만듭니다.

코드 예제

import polars as pl
import numpy as np

# 사용자 행동 데이터와 상품 메타데이터
user_history = pl.read_parquet("user_history.parquet")
product_catalog = pl.read_parquet("product_catalog.parquet")

# 개인화 점수 계산
personalization_scores = (
    user_history
    .join(product_catalog, on="category_id", how="left")  # 카테고리 기준 조인
    .with_columns([
        # 카테고리 선호도: 해당 카테고리 조회 빈도 정규화
        (pl.col("category_views") / pl.col("total_views")).alias("category_affinity"),
        # 가격 적합도: 사용자 평균 구매가와의 유사도
        (1 - (pl.col("product_price") - pl.col("avg_user_price")).abs() / pl.col("avg_user_price")).clip(0, 1).alias("price_affinity"),
        # 최신성: 최근 본 상품일수록 높은 점수
        (1 / (pl.col("days_since_last_view") + 1)).alias("recency_score"),
    ])
    .with_columns([
        # 가중 합산으로 최종 개인화 점수 계산
        (pl.col("category_affinity") * 0.4 +
         pl.col("price_affinity") * 0.3 +
         pl.col("recency_score") * 0.3).alias("personalization_score")
    ])
    .select(["user_id", "product_id", "personalization_score"])
    .sort("personalization_score", descending=True)
)

print(personalization_scores.head(20))

설명

이것이 하는 일: 사용자의 과거 행동 데이터(카테고리 조회, 구매 가격대, 최근 활동)와 상품 정보를 결합하여 여러 차원의 적합도를 계산하고, 가중치를 적용해 최종 개인화 점수를 산출합니다. 첫 번째로, 사용자 행동 이력과 상품 카탈로그를 카테고리 ID 기준으로 조인합니다.

이를 통해 각 사용자가 관심 있는 카테고리의 상품들을 매칭할 수 있습니다. how="left"를 사용하여 사용자가 본 적 없는 카테고리도 포함시켜 탐색 추천도 가능하게 합니다.

그 다음으로, 세 가지 하위 점수를 계산합니다. category_affinity는 사용자가 해당 카테고리를 전체 조회 중 얼마나 자주 봤는지 비율로 계산합니다.

price_affinity는 상품 가격과 사용자의 평균 구매 가격대의 차이를 계산하고, 이를 정규화하여 1에 가까울수록 사용자의 가격 선호와 일치함을 나타냅니다. .clip(0, 1)로 음수나 1을 초과하는 값을 제한합니다.

recency_score는 마지막 조회 이후 경과 일수의 역수로, 최근에 본 상품일수록 높은 점수를 받습니다. 마지막으로, 세 가지 점수를 가중 합산하여 최종 개인화 점수를 만듭니다.

여기서는 카테고리 선호도에 40%, 가격 적합도에 30%, 최신성에 30%의 가중치를 부여했습니다. 이 가중치는 비즈니스 목표에 따라 조정할 수 있으며, A/B 테스트로 최적값을 찾을 수 있습니다.

sort()로 점수가 높은 순으로 정렬하여 상위 N개를 추천 후보로 선택합니다. 여러분이 이 코드를 사용하면 각 사용자에게 맞춤화된 상품 랭킹을 즉시 생성할 수 있습니다.

예를 들어, 운동화를 자주 보고 3-5만원대를 선호하며 어제도 활동한 사용자에게는 최신 운동화 신상품이 높은 점수를 받을 것입니다. 또한 브랜드 선호도, 색상 선호, 사이즈 등 추가 신호를 계속 통합하여 점수의 정교함을 높일 수 있습니다.

실전 팁

💡 가중치는 하드코딩하지 말고 설정 파일이나 데이터베이스에서 읽어오세요. A/B 테스트로 최적 가중치를 찾을 때 코드 수정 없이 실험할 수 있습니다.

💡 음수 신호도 활용하세요. 사용자가 상품을 봤지만 클릭하지 않았거나, 장바구니에 넣었다가 삭제한 경우는 선호하지 않는다는 강력한 신호입니다.

💡 콜드 스타트 문제를 대비하세요. 신규 사용자는 행동 이력이 없으므로, 인구통계 정보나 첫 온보딩 설문 응답을 활용한 기본 점수를 제공하세요.

💡 점수의 분포를 모니터링하세요. 모든 상품의 점수가 0.3-0.4 사이에 몰려있다면 변별력이 부족한 것이므로, 정규화 방법을 조정하거나 신호를 추가해야 합니다.

💡 Polars의 when().then().otherwise() 표현식으로 조건부 가중치를 적용할 수 있습니다. 예를 들어, 프리미엄 회원에게는 가격 가중치를 낮추고 품질 점수 가중치를 높이는 식으로 세그먼트별 개인화가 가능합니다.


4. 다양성 지표(Diversity) - 추천의 폭을 넓히기

시작하며

여러분이 추천 시스템을 열심히 개선했는데, 사용자들이 "항상 똑같은 상품만 추천해요"라고 불평하는 경험을 해보셨나요? 추천 정확도는 높지만 모든 추천이 비슷비슷해서 사용자가 지루함을 느끼는 상황 말입니다.

이런 문제는 정확도만 최적화할 때 자주 발생합니다. 협업 필터링이나 딥러닝 모델은 사용자의 과거 선호와 가장 유사한 상품만 반복 추천하는 경향이 있습니다.

예를 들어, 운동화를 좋아하는 사용자에게 10개의 추천이 모두 나이키 운동화라면 정확하지만 다양성이 부족합니다. 바로 이럴 때 필요한 것이 다양성 지표입니다.

추천 목록 내에서 상품들이 얼마나 다양한 카테고리, 브랜드, 가격대를 포함하는지 측정하여 사용자에게 풍부한 선택지를 제공할 수 있습니다.

개요

간단히 말해서, 다양성은 추천 목록에 포함된 상품들이 서로 얼마나 다른지를 나타내는 지표입니다. 왜 이 지표가 필요한지 실무 관점에서 설명하면, 사용자 만족도와 탐색 기회를 높이기 위해서입니다.

연구에 따르면 적절한 다양성은 사용자의 장기적인 참여도를 높입니다. 10개의 추천 중 8개가 같은 카테고리라면 다양성이 낮고, 5-6개 카테고리가 고르게 분포한다면 다양성이 높습니다.

예를 들어, 운동화 추천 외에 운동복, 스포츠 용품, 건강식품 등을 함께 추천하면 사용자가 생각지 못한 니즈를 발견할 수 있습니다. 기존에는 "직관적으로 다양해 보이는지" 수작업으로 확인했다면, 이제는 카테고리 엔트로피, 평균 상품 간 거리 등으로 정량화할 수 있습니다.

이 지표의 핵심 특징은 첫째, 추천의 정확도와 트레이드오프 관계에 있어 균형 조정이 필요하고, 둘째, 사용자의 탐색 욕구를 충족시켜 세렌디피티를 제공하며, 셋째, 플랫폼 전체의 상품 노출을 고르게 하여 롱테일 상품의 기회를 늘린다는 점입니다. 이러한 특징들이 단기 성과와 장기 참여도 모두를 고려한 추천을 가능하게 합니다.

코드 예제

import polars as pl
import numpy as np

# 사용자별 추천 목록
recommendations = pl.read_parquet("user_recommendations.parquet")

# 카테고리 다양성 계산 (Shannon Entropy)
diversity_scores = (
    recommendations
    .group_by("user_id")
    .agg([
        pl.col("category_id").n_unique().alias("unique_categories"),  # 고유 카테고리 수
        pl.col("brand_id").n_unique().alias("unique_brands"),  # 고유 브랜드 수
        pl.col("product_id").count().alias("total_recommendations"),
    ])
    .with_columns([
        # 카테고리 비율 기반 엔트로피 계산을 위한 준비
        (pl.col("unique_categories") / pl.col("total_recommendations")).alias("category_ratio"),
        (pl.col("unique_brands") / pl.col("total_recommendations")).alias("brand_ratio"),
    ])
    .with_columns([
        # 다양성 점수: 1에 가까울수록 다양함
        (1 - (1 / pl.col("unique_categories"))).alias("category_diversity"),
        (1 - (1 / pl.col("unique_brands"))).alias("brand_diversity"),
    ])
)

# 추가: 상품 간 평균 유사도 계산 (낮을수록 다양함)
# product_embeddings와 조인하여 코사인 유사도 계산 가능

print(diversity_scores)

설명

이것이 하는 일: 사용자별 추천 목록에서 고유한 카테고리와 브랜드의 수를 세고, 이를 전체 추천 수와 비교하여 다양성 점수를 계산합니다. 점수가 높을수록 추천이 다양한 선택지를 제공합니다.

첫 번째로, group_by("user_id")로 각 사용자의 추천 목록을 그룹화합니다. n_unique()로 고유 카테고리 수와 고유 브랜드 수를 계산하는데, 이는 중복을 제거한 고유값 개수를 세는 효율적인 방법입니다.

예를 들어, 10개 추천 중 5개 카테고리가 있다면 unique_categories는 5입니다. 그 다음으로, 고유값 수를 전체 추천 수로 나누어 비율을 계산합니다.

만약 10개 추천에 10개 카테고리가 모두 다르다면 비율은 1.0이고, 10개 추천이 모두 같은 카테고리라면 비율은 0.1입니다. 이 비율이 다양성의 기본 지표가 됩니다.

마지막으로, 다양성 점수를 0-1 범위로 정규화합니다. 1 - (1 / unique_categories) 공식은 카테고리 수가 1일 때 0(전혀 다양하지 않음), 무한대로 갈수록 1(매우 다양함)에 가까워집니다.

실제로는 10개 추천에 5개 카테고리면 점수는 0.8, 2개 카테고리면 0.5가 됩니다. 이를 통해 다양성을 직관적인 점수로 표현할 수 있습니다.

여러분이 이 코드를 사용하면 사용자별로 추천의 다양성을 모니터링할 수 있습니다. 만약 대부분의 사용자가 다양성 점수 0.3 이하를 받는다면 추천 알고리즘에 다양성 제약을 추가해야 한다는 신호입니다.

또한 개인화 추천과 탐색 추천의 비율을 조정하여 정확도와 다양성의 균형을 맞출 수 있습니다. 예를 들어, 상위 5개는 정확도 우선, 하위 5개는 다양성 우선으로 선택하는 하이브리드 전략이 가능합니다.

실전 팁

💡 다양성과 정확도는 트레이드오프 관계입니다. 두 지표를 동시에 모니터링하며 비즈니스 목표에 맞는 균형점을 찾으세요. 보통 정확도 5% 희생으로 다양성 30% 향상이 가능합니다.

💡 카테고리 계층을 고려하세요. "나이키 운동화"와 "아디다스 운동화"는 브랜드는 다르지만 대분류는 같습니다. 대/중/소분류 각각의 다양성을 측정하면 더 정교합니다.

💡 사용자 세그먼트별로 다른 다양성 전략을 적용하세요. 신규 사용자는 탐색을 위해 높은 다양성이 필요하지만, 충성 고객은 선호가 명확해서 낮은 다양성도 괜찮습니다.

💡 시간에 따른 다양성도 중요합니다. 오늘 추천과 어제 추천이 80% 중복된다면 사용자는 새로움을 느끼지 못합니다. 일별 추천 간 중복률도 함께 측정하세요.

💡 임베딩 기반 다양성을 추가하세요. 상품 임베딩 벡터의 코사인 유사도를 계산하면 카테고리는 다르지만 의미적으로 유사한 상품들도 감지할 수 있습니다.


5. 신선도 지표(Novelty) - 새로운 발견의 기회

시작하며

여러분이 사용자에게 계속 같은 상품만 추천하고 있다는 걸 나중에야 깨달은 경험 있나요? 사용자가 이미 여러 번 봤거나 심지어 구매한 상품을 또 추천해서 "이미 샀는데요?"라는 피드백을 받는 상황 말입니다.

이런 문제는 추천 시스템이 사용자의 과거 상호작용을 추적하지 않을 때 발생합니다. 이미 본 상품, 구매한 상품, 거부한 상품을 계속 추천하면 사용자는 추천을 무시하게 됩니다.

예를 들어, 지난주에 10번이나 본 상품을 또 추천하는 것은 리소스 낭비이자 사용자 경험 저하입니다. 바로 이럴 때 필요한 것이 신선도 지표입니다.

사용자가 아직 보지 못한 새로운 상품의 비율을 측정하고, 이미 익숙한 상품을 필터링하여 진짜 발견의 기회를 제공할 수 있습니다.

개요

간단히 말해서, 신선도는 추천 목록에서 사용자가 이전에 접하지 않은 새로운 상품의 비율을 나타냅니다. 왜 이 지표가 필요한지 실무 관점에서 설명하면, 사용자에게 계속 새로운 가치를 제공하기 위해서입니다.

연구에 따르면 신선한 추천은 사용자의 참여도를 30% 이상 높입니다. 10개 추천 중 2개만 새 상품이라면 신선도는 20%이고, 8개가 새 상품이라면 80%입니다.

예를 들어, 사용자가 이미 구매한 상품을 제외하고, 최근 입고된 신상품이나 사용자가 아직 탐색하지 않은 카테고리의 상품을 추천하면 신선도가 높아집니다. 기존에는 "최신 상품을 우선 추천"하는 단순한 규칙을 사용했다면, 이제는 각 사용자별 상호작용 이력과 비교하여 개인화된 신선도를 계산할 수 있습니다.

이 지표의 핵심 특징은 첫째, 사용자별로 다르게 측정되어 진정한 개인화 지표이고, 둘째, 플랫폼의 상품 회전율을 높여 재고 관리에 도움이 되며, 셋째, 사용자의 지루함을 방지하여 장기 이탈을 줄인다는 점입니다. 이러한 특징들이 추천 시스템을 지속적으로 가치 있게 만듭니다.

코드 예제

import polars as pl
from datetime import datetime, timedelta

# 사용자별 과거 상호작용 이력
user_interactions = pl.read_parquet("user_interactions.parquet")
# 현재 추천 목록
current_recommendations = pl.read_parquet("current_recommendations.parquet")

# 지난 30일간 본 상품 집합
seen_products = (
    user_interactions
    .filter(pl.col("interaction_date") >= datetime.now() - timedelta(days=30))
    .select(["user_id", "product_id"])
    .unique()
)

# 신선도 계산
novelty_analysis = (
    current_recommendations
    .join(seen_products, on=["user_id", "product_id"], how="left")  # 과거 이력과 조인
    .with_columns([
        pl.when(pl.col("product_id_right").is_null())  # 조인 안 된 = 새 상품
        .then(1)
        .otherwise(0)
        .alias("is_novel")
    ])
    .group_by("user_id")
    .agg([
        pl.col("product_id").count().alias("total_recommendations"),
        pl.col("is_novel").sum().alias("novel_recommendations"),
        (pl.col("is_novel").sum() / pl.col("product_id").count() * 100).round(2).alias("novelty_percentage")
    ])
    .sort("novelty_percentage", descending=True)
)

print(novelty_analysis)

설명

이것이 하는 일: 사용자의 과거 30일간 상호작용 이력과 현재 추천 목록을 비교하여, 사용자가 이전에 접하지 않은 새로운 상품의 비율을 계산합니다. 첫 번째로, user_interactions에서 지난 30일간의 상호작용만 필터링합니다.

filter()에서 날짜 조건을 사용하여 최근 이력만 추출하는데, 30일이라는 기준은 비즈니스 특성에 따라 조정할 수 있습니다. 패션은 7-14일, 가전은 90일 정도가 적절할 수 있습니다.

unique()로 중복을 제거하여 사용자-상품 쌍의 집합을 만듭니다. 그 다음으로, 현재 추천 목록과 과거 이력을 how="left"로 조인합니다.

왼쪽 조인의 핵심은 추천된 상품이 과거에 없었다면 product_id_right가 null이 된다는 점입니다. when().then().otherwise() 표현식으로 null인 경우(새 상품) 1, 아닌 경우(이미 본 상품) 0으로 불리언 플래그를 생성합니다.

마지막으로, 사용자별로 그룹화하여 전체 추천 수 대비 새 상품 수의 비율을 계산합니다. 신선도가 100%라면 모든 추천이 새 상품이고, 0%라면 모두 이미 본 상품입니다.

보통 70-80% 이상이 바람직하며, 20-30%는 친숙한 상품(예: 재구매 유도)으로 채우는 것이 균형 잡힌 전략입니다. 여러분이 이 코드를 사용하면 각 사용자의 추천 신선도를 실시간으로 모니터링할 수 있습니다.

신선도가 낮은 사용자 세그먼트를 찾아내면 추천 알고리즘에 "이미 본 상품 필터링" 로직을 추가할 수 있습니다. 또한 신상품 입고 시 이를 우선 추천하는 부스팅 로직과 결합하면 재고 회전율도 높일 수 있습니다.

A/B 테스트로 신선도 비율을 조정하며 최적 비율을 찾는 것이 실무적인 접근입니다.

실전 팁

💡 상호작용 유형별로 가중치를 다르게 두세요. 구매한 상품은 절대 재추천하지 말아야 하지만, 단순 조회만 한 상품은 일정 시간 후 다시 추천해도 괜찮습니다.

💡 음성 피드백을 활용하세요. 사용자가 "관심 없음" 버튼을 눌렀거나 빠르게 스크롤로 넘긴 상품은 본 것보다 더 강한 부정 신호로 영구 필터링하세요.

💡 신선도와 개인화는 때로 충돌합니다. 사용자 선호와 정확히 맞는 상품을 이미 다 추천했다면, 신선도를 높이려면 덜 정확한 상품을 추천해야 합니다. 이 트레이드오프를 인식하고 균형을 맞추세요.

💡 시즈널리티를 고려하세요. 겨울 코트를 작년 겨울에 봤다고 해서 올해도 새롭지 않다고 판단하면 안 됩니다. 1년 주기로 신선도를 리셋하는 로직이 필요할 수 있습니다.

💡 Polars의 set 연산을 활용하면 더 효율적입니다. is_in() 메서드로 추천 상품 ID가 과거 이력 집합에 포함되는지 빠르게 체크할 수 있습니다.


6. 커버리지 지표(Coverage) - 전체 상품의 공정한 노출

시작하며

여러분이 1만 개의 상품을 보유한 플랫폼을 운영하는데, 추천 시스템이 항상 같은 100개 상품만 반복적으로 추천한다는 걸 발견한 경험 있나요? 나머지 9,900개 상품은 아무리 좋아도 사용자에게 노출조차 안 되는 상황 말입니다.

이런 문제는 추천 알고리즘이 인기 상품에 편향될 때 자주 발생합니다. 협업 필터링은 많이 팔린 상품을 더 추천하고, 그 상품이 또 많이 팔리는 부익부 빈익빈 현상을 만듭니다.

예를 들어, 소규모 판매자의 우수한 상품이나 신규 입고 상품은 기회조차 얻지 못하고 묻힙니다. 바로 이럴 때 필요한 것이 커버리지 지표입니다.

전체 상품 중 실제로 추천되는 상품의 비율을 측정하여 롱테일 상품에도 공정한 기회를 주고, 플랫폼 생태계의 건강성을 확보할 수 있습니다.

개요

간단히 말해서, 커버리지는 전체 상품 카탈로그 중 실제로 추천된 상품의 비율을 나타내는 지표입니다. 왜 이 지표가 필요한지 실무 관점에서 설명하면, 플랫폼의 장기적인 건강성과 공정성을 위해서입니다.

카탈로그 커버리지가 5%라면 100개 중 5개만 추천되고, 80%라면 대부분의 상품이 기회를 얻습니다. 낮은 커버리지는 판매자 불만, 재고 적체, 플랫폼 다양성 저하로 이어집니다.

예를 들어, 마켓플레이스 모델에서는 모든 판매자에게 공정한 노출 기회를 제공하는 것이 생태계 유지의 핵심입니다. 기존에는 "베스트셀러만 추천하면 안전하다"는 보수적 전략을 썼다면, 이제는 커버리지를 모니터링하며 탐색과 활용의 균형을 데이터로 관리할 수 있습니다.

이 지표의 핵심 특징은 첫째, 플랫폼 전체의 상품 생태계를 건강하게 유지하고, 둘째, 롱테일 상품의 기회를 늘려 매출 다각화에 기여하며, 셋째, 알고리즘 편향을 감지하는 조기 경보 역할을 한다는 점입니다. 이러한 특징들이 단기 최적화와 장기 지속가능성을 모두 고려한 추천을 가능하게 합니다.

코드 예제

import polars as pl

# 전체 상품 카탈로그
product_catalog = pl.read_parquet("product_catalog.parquet")
# 추천 로그 (지난 30일)
recommendation_logs = pl.read_parquet("recommendation_logs_30d.parquet")

# 추천된 고유 상품 수
recommended_products = (
    recommendation_logs
    .select("product_id")
    .unique()
)

# 전체 커버리지 계산
total_products = product_catalog.select("product_id").n_unique()
recommended_count = recommended_products.select("product_id").n_unique()
overall_coverage = (recommended_count / total_products * 100).round(2)

print(f"Overall Coverage: {overall_coverage}% ({recommended_count}/{total_products})")

# 카테고리별 커버리지
category_coverage = (
    product_catalog
    .join(recommended_products, on="product_id", how="left")
    .with_columns([
        pl.col("product_id_right").is_not_null().alias("is_recommended")
    ])
    .group_by("category_name")
    .agg([
        pl.col("product_id").count().alias("total_products_in_category"),
        pl.col("is_recommended").sum().alias("recommended_products_in_category"),
        (pl.col("is_recommended").sum() / pl.col("product_id").count() * 100).round(2).alias("category_coverage_percentage")
    ])
    .sort("category_coverage_percentage")
)

print(category_coverage)

설명

이것이 하는 일: 전체 상품 카탈로그와 실제 추천 로그를 비교하여 전체 커버리지와 카테고리별 커버리지를 계산하고, 어떤 상품군이 충분히 노출되지 못하는지 파악합니다. 첫 번째로, 지난 30일간의 추천 로그에서 select("product_id").unique()로 실제 추천된 고유 상품 집합을 추출합니다.

예를 들어, 100만 건의 추천 로그가 있어도 실제로는 5,000개 상품만 반복 추천됐을 수 있습니다. 이 집합이 커버리지 계산의 분자가 됩니다.

그 다음으로, 전체 상품 카탈로그의 상품 수와 추천된 상품 수를 비교하여 전체 커버리지를 계산합니다. n_unique()는 고유값 개수를 효율적으로 세는 Polars의 메서드입니다.

만약 10,000개 상품 중 2,000개가 추천됐다면 커버리지는 20%입니다. 이는 80%의 상품이 아예 노출되지 않았다는 의미로, 개선이 필요한 신호입니다.

마지막으로, 카테고리별 세분화 분석을 수행합니다. 전체 카탈로그와 추천 상품을 왼쪽 조인하여 각 상품이 추천됐는지 플래그를 만들고, 카테고리별로 그룹화하여 비율을 계산합니다.

이를 통해 특정 카테고리(예: 소형 가전)는 커버리지가 60%인데 다른 카테고리(예: 명품 가방)는 5%에 불과한 불균형을 발견할 수 있습니다. 여러분이 이 코드를 사용하면 추천 시스템의 편향을 즉시 발견할 수 있습니다.

커버리지가 10% 미만이라면 알고리즘이 과도하게 보수적이므로, 탐색 비율을 높이거나 신상품 부스팅을 강화해야 합니다. 반대로 90% 이상이라면 너무 무작위 추천에 가까워 정확도가 희생됐을 수 있습니다.

보통 40-60%가 건강한 범위이며, A/B 테스트로 비즈니스 목표에 맞는 최적값을 찾아야 합니다.

실전 팁

💡 카테고리 크기를 고려한 가중 커버리지를 계산하세요. 100개 상품 카테고리의 50% 커버리지와 10,000개 상품 카테고리의 50%는 임팩트가 다릅니다.

💡 시간 윈도우를 다양하게 측정하세요. 일별 커버리지는 낮아도 월별 커버리지는 높을 수 있습니다. 단기와 장기를 모두 모니터링하세요.

💡 판매자별 커버리지도 추적하세요. 특정 판매자의 상품만 추천된다면 플랫폼 공정성 문제가 발생할 수 있습니다. 특히 마켓플레이스 모델에서는 필수입니다.

💡 신상품의 커버리지 진입 속도를 측정하세요. 새 상품이 등록 후 며칠 만에 추천에 포함되는지가 플랫폼 역동성의 지표입니다. 14일 이상 걸린다면 콜드 스타트 전략이 필요합니다.

💡 커버리지를 높이는 간단한 방법은 추천 목록의 마지막 1-2개를 무작위 샘플링하는 것입니다. 이는 정확도에 큰 영향 없이 커버리지를 크게 개선할 수 있습니다.


7. 세렌디피티 점수(Serendipity) - 의외의 만족스러운 발견

시작하며

여러분이 사용자의 과거 행동을 완벽히 분석해서 100% 예측 가능한 상품만 추천했는데, 사용자들이 "재미없다", "뻔하다"고 반응한 경험 있나요? 모든 추천이 사용자가 이미 알고 있거나 당연히 좋아할 만한 것들뿐이어서 놀라움이 없는 상황 말입니다.

이런 문제는 정확도만 극대화할 때 발생합니다. 사용자가 좋아할 확률이 높은 상품만 추천하면 안전하지만 지루합니다.

예를 들어, 운동화만 계속 사는 사용자에게 운동화만 추천하면 맞지만, 가끔은 스포츠 스마트워치 같은 예상 밖의 추천이 더 큰 만족을 줄 수 있습니다. 바로 이럴 때 필요한 것이 세렌디피티 점수입니다.

사용자가 예상하지 못했지만 실제로는 만족하는 추천의 비율을 측정하여, 즐거운 발견의 경험을 제공할 수 있습니다.

개요

간단히 말해서, 세렌디피티는 사용자에게 예상 밖이지만 만족스러운 추천의 정도를 나타내는 지표입니다. 왜 이 지표가 필요한지 실무 관점에서 설명하면, 추천 시스템의 진정한 가치는 사용자 스스로 찾지 못했을 것을 발견시켜주는 데 있기 때문입니다.

연구에 따르면 세렌디피티가 높은 추천은 사용자 만족도와 브랜드 충성도를 크게 높입니다. 예를 들어, 요리 책을 자주 사는 사람에게 고급 주방 도구를 추천했는데 실제로 구매하고 만족한다면 이는 높은 세렌디피티입니다.

기존에는 "예상 밖"과 "만족"을 측정할 방법이 없어 직관에 의존했다면, 이제는 예측 모델과 실제 피드백을 결합하여 정량화할 수 있습니다. 이 지표의 핵심 특징은 첫째, 예상 밖(unexpected)과 만족(relevant) 두 조건을 모두 충족해야 하고, 둘째, 사용자의 잠재 니즈를 발견하게 해주며, 셋째, 플랫폼에 대한 신뢰와 흥미를 높인다는 점입니다.

이러한 특징들이 추천 시스템을 단순 필터링 도구에서 발견 도구로 진화시킵니다.

코드 예제

import polars as pl

# 추천 상품과 사용자 프로필
recommendations = pl.read_parquet("recommendations_with_scores.parquet")
user_profiles = pl.read_parquet("user_profiles.parquet")
feedback = pl.read_parquet("user_feedback.parquet")

# 세렌디피티 계산
serendipity_analysis = (
    recommendations
    .join(feedback, on=["user_id", "product_id"], how="inner")  # 피드백이 있는 것만
    .join(user_profiles, on="user_id", how="left")
    .with_columns([
        # 예상 밖: 사용자 프로필과 카테고리가 다름
        (pl.col("product_category") != pl.col("preferred_category")).alias("is_unexpected"),
        # 만족: 클릭하고 긍정 피드백
        (pl.col("clicked") & (pl.col("rating") >= 4)).alias("is_satisfying"),
    ])
    .with_columns([
        # 세렌디피티: 예상 밖이면서 만족스러운 경우
        (pl.col("is_unexpected") & pl.col("is_satisfying")).alias("is_serendipitous")
    ])
    .group_by("recommendation_algorithm")
    .agg([
        pl.col("product_id").count().alias("total_recommendations"),
        pl.col("is_serendipitous").sum().alias("serendipitous_recommendations"),
        (pl.col("is_serendipitous").sum() / pl.col("product_id").count() * 100).round(2).alias("serendipity_percentage")
    ])
    .sort("serendipity_percentage", descending=True)
)

print(serendipity_analysis)

설명

이것이 하는 일: 추천된 상품이 사용자의 기존 선호와 다르지만 실제로는 긍정적인 피드백을 받았는지 분석하여, 각 추천 알고리즘의 세렌디피티 수준을 계산합니다. 첫 번째로, 추천 데이터와 사용자 피드백을 내부 조인합니다.

how="inner"를 사용하여 실제로 피드백이 있는 추천만 분석 대상으로 삼습니다. 피드백이 없으면 만족 여부를 알 수 없기 때문입니다.

그리고 사용자 프로필을 조인하여 기존 선호 카테고리 정보를 가져옵니다. 그 다음으로, 두 가지 조건을 평가합니다.

is_unexpected는 추천 상품의 카테고리가 사용자의 선호 카테고리와 다른지 확인합니다. 예를 들어, 주로 의류를 사는 사람에게 전자제품을 추천했다면 예상 밖입니다.

is_satisfying은 사용자가 실제로 클릭했고 4점 이상의 평점을 줬는지 확인합니다. 이 두 조건이 모두 참일 때만 진정한 세렌디피티입니다.

마지막으로, 추천 알고리즘별로 그룹화하여 세렌디피티 비율을 계산합니다. 협업 필터링, 콘텐츠 기반, 딥러닝 등 각 알고리즘의 세렌디피티 성과를 비교할 수 있습니다.

일반적으로 협업 필터링은 세렌디피티가 높고, 콘텐츠 기반은 낮은 경향이 있습니다. 5-10% 정도면 좋은 수준이며, 너무 높으면 정확도가 희생됐을 수 있습니다.

여러분이 이 코드를 사용하면 어떤 알고리즘이 사용자에게 의미 있는 발견을 제공하는지 평가할 수 있습니다. 정확도는 낮지만 세렌디피티가 높은 알고리즘을 전체 추천의 10-20%에 할당하는 하이브리드 전략을 설계할 수 있습니다.

또한 세렌디피티가 높은 추천을 특별한 UI(예: "당신을 위한 특별한 발견")로 강조하면 사용자 경험이 더 풍부해집니다.

실전 팁

💡 예상 밖을 측정하는 방법은 다양합니다. 카테고리 차이 외에도 가격대 차이, 브랜드 차이, 사용자의 과거 검색 키워드와의 거리 등을 활용할 수 있습니다.

💡 세렌디피티는 측정이 어렵습니다. 사용자 설문("이 추천이 예상 밖이었나요?")을 주기적으로 실시하여 알고리즘 기반 측정을 보정하세요.

💡 신규 사용자에게는 세렌디피티가 덜 의미 있습니다. 선호가 불분명한 상태에서는 모든 추천이 예상 밖이기 때문입니다. 충분한 이력이 쌓인 사용자에게만 적용하세요.

💡 너무 예상 밖이면 실패합니다. 남성 사용자에게 여성 화장품을 추천하는 것은 예상 밖이지만 만족스럽지 않습니다. "예상 밖의 정도"를 조절하는 파라미터가 필요합니다.

💡 Polars의 when().then().otherwise() 체인을 활용하면 복잡한 세렌디피티 기준을 유연하게 정의할 수 있습니다. 예를 들어, 카테고리는 다르지만 대분류는 같은 경우를 중간 수준 세렌디피티로 분류할 수 있습니다.


8. 재추천률(Re-recommendation Rate) - 중복 추천 관리

시작하며

여러분이 매일 추천 목록을 갱신하는데, 사용자가 "어제 본 상품이 또 나왔어요"라고 불평하는 경험 있나요? 추천 알고리즘은 바뀌지 않았는데 데이터가 변하지 않아서 똑같은 상품만 반복 추천되는 상황 말입니다.

이런 문제는 추천 시스템이 시간 축을 고려하지 않을 때 발생합니다. 오늘의 최적 추천이 어제와 동일하다면 사용자는 새로움을 느끼지 못합니다.

예를 들어, 협업 필터링은 사용자 행동이 급격히 변하지 않는 한 비슷한 상품을 계속 추천하는 경향이 있습니다. 바로 이럴 때 필요한 것이 재추천률입니다.

이전 추천 목록과 현재 추천 목록의 중복 비율을 측정하여, 추천의 신선도를 시간 축에서 관리할 수 있습니다.

개요

간단히 말해서, 재추천률은 특정 기간 내에 동일한 상품이 반복적으로 추천되는 비율을 나타냅니다. 왜 이 지표가 필요한지 실무 관점에서 설명하면, 사용자에게 매번 새로운 가치를 제공하기 위해서입니다.

오늘 추천과 어제 추천이 80% 중복된다면 재추천률이 너무 높은 것이고, 0% 중복이라면 일관성이 부족한 것입니다. 보통 30-50% 정도의 중복이 적절합니다.

예를 들어, 주요 추천 상품은 유지하되 하위 추천은 매번 바꾸는 전략으로 안정성과 신선도를 균형 있게 제공할 수 있습니다. 기존에는 "추천 목록을 자주 갱신하면 되겠지"라는 막연한 접근을 했다면, 이제는 재추천률을 정량적으로 측정하고 목표 범위 내로 관리할 수 있습니다.

이 지표의 핵심 특징은 첫째, 추천의 시간적 다양성을 측정하고, 둘째, 사용자의 피로도를 방지하며, 셋째, 추천 알고리즘의 갱신 빈도 최적화에 활용할 수 있다는 점입니다. 이러한 특징들이 지속적으로 흥미로운 추천 경험을 만듭니다.

코드 예제

import polars as pl
from datetime import datetime, timedelta

# 오늘과 어제의 추천 로그
today = datetime.now().date()
yesterday = today - timedelta(days=1)

today_recommendations = (
    pl.read_parquet("recommendation_logs.parquet")
    .filter(pl.col("recommendation_date") == today)
    .select(["user_id", "product_id"])
)

yesterday_recommendations = (
    pl.read_parquet("recommendation_logs.parquet")
    .filter(pl.col("recommendation_date") == yesterday)
    .select(["user_id", "product_id"])
)

# 재추천률 계산
re_recommendation_analysis = (
    today_recommendations
    .join(yesterday_recommendations, on=["user_id", "product_id"], how="inner")  # 중복 찾기
    .group_by("user_id")
    .agg([
        pl.col("product_id").count().alias("re_recommended_count")
    ])
    .join(
        today_recommendations.group_by("user_id").agg(pl.col("product_id").count().alias("total_today_count")),
        on="user_id"
    )
    .with_columns([
        (pl.col("re_recommended_count") / pl.col("total_today_count") * 100).round(2).alias("re_recommendation_rate")
    ])
    .sort("re_recommendation_rate", descending=True)
)

print(re_recommendation_analysis)
print(f"Average Re-recommendation Rate: {re_recommendation_analysis['re_recommendation_rate'].mean():.2f}%")

설명

이것이 하는 일: 오늘과 어제의 추천 목록을 비교하여 사용자별로 동일한 상품이 반복 추천된 비율을 계산하고, 추천의 시간적 다양성을 평가합니다. 첫 번째로, 오늘과 어제의 추천 로그를 각각 필터링하여 분리합니다.

filter(pl.col("recommendation_date") == today)로 특정 날짜의 추천만 추출하고, select(["user_id", "product_id"])로 필요한 컬럼만 선택하여 메모리 효율을 높입니다. 두 데이터셋이 동일한 스키마를 가지고 있어야 조인이 가능합니다.

그 다음으로, 두 추천 목록을 내부 조인하여 중복된 user_id-product_id 쌍만 추출합니다. how="inner"를 사용하면 양쪽에 모두 존재하는 것만 결과에 포함되므로, 이것이 바로 재추천된 상품들입니다.

사용자별로 그룹화하여 재추천된 상품 수를 세는 것이 중요합니다. 마지막으로, 오늘의 전체 추천 수와 재추천 수를 비교하여 비율을 계산합니다.

또 다른 조인으로 각 사용자의 오늘 총 추천 수를 가져와서 나눗셈을 수행합니다. 예를 들어, 오늘 10개를 추천받았는데 그중 4개가 어제와 동일하다면 재추천률은 40%입니다.

전체 사용자의 평균 재추천률을 계산하면 시스템 레벨의 지표를 얻을 수 있습니다. 여러분이 이 코드를 사용하면 추천의 갱신 전략을 데이터로 검증할 수 있습니다.

재추천률이 70% 이상이라면 알고리즘이 정체되었거나 데이터 갱신이 부족한 신호입니다. 반대로 10% 미만이라면 일관성이 부족해서 사용자가 혼란스러울 수 있습니다.

상품 타입에 따라 다른 목표를 설정할 수 있습니다. 예를 들어, 뉴스 추천은 5% 미만, 의류 추천은 40% 정도가 적절할 수 있습니다.

실전 팁

💡 기간을 다양하게 측정하세요. 일별뿐 아니라 주별, 월별 재추천률도 추적하면 장기 패턴을 파악할 수 있습니다.

💡 포지션별로 분석하세요. 상위 3개 추천은 안정성을 위해 재추천률이 높아도 되지만, 하위 추천은 낮아야 합니다. 슬라이딩 윈도우 전략을 고려하세요.

💡 사용자 활동 수준에 따라 다르게 관리하세요. 매일 방문하는 활성 사용자는 낮은 재추천률이 필요하지만, 주 1회 방문자는 높아도 괜찮습니다.

💡 의도적인 재추천도 있습니다. 장바구니에 넣었지만 구매 안 한 상품을 다시 추천하는 것은 전환 유도 전략이므로, 이를 별도로 분류하여 측정하세요.

💡 Polars의 윈도우 함수로 롤링 재추천률을 계산할 수 있습니다. 지난 7일간의 재추천률 추이를 보면 알고리즘 변경의 효과를 즉시 확인할 수 있습니다.


9. 응답 시간(Latency) - 실시간 추천의 성능

시작하며

여러분이 완벽한 추천 알고리즘을 개발했는데, 상품 페이지 로딩이 5초씩 걸려서 사용자들이 이탈하는 경험 있나요? 추천 품질은 훌륭하지만 계산이 너무 오래 걸려서 실제 서비스에 적용할 수 없는 상황 말입니다.

이런 문제는 복잡한 모델을 실시간으로 실행할 때 자주 발생합니다. 딥러닝 모델이나 복잡한 그래프 연산은 정확도는 높지만 레이턴시가 길어집니다.

예를 들어, 사용자가 상품 페이지를 열 때마다 1초 이상 기다려야 한다면 전환율이 급격히 떨어집니다. 바로 이럴 때 필요한 것이 응답 시간 측정입니다.

추천 요청부터 결과 반환까지의 시간을 모니터링하여 사용자 경험을 보장하는 성능 목표를 설정하고 유지할 수 있습니다.

개요

간단히 말해서, 응답 시간은 추천 시스템이 요청을 받고 결과를 반환하기까지 걸리는 시간을 밀리초 단위로 나타냅니다. 왜 이 지표가 필요한지 실무 관점에서 설명하면, 아무리 정확한 추천도 늦게 제공되면 가치가 없기 때문입니다.

연구에 따르면 페이지 로딩이 1초 늦어질 때마다 전환율이 7% 감소합니다. 추천 API의 P50 레이턴시가 100ms, P99가 500ms라면 대부분 사용자가 빠른 응답을 받지만 일부는 느립니다.

예를 들어, 실시간 개인화 추천은 200ms 이내, 이메일 추천은 수 초도 괜찮습니다. 기존에는 "잘 돌아가는 것 같다"는 주관적 판단을 했다면, 이제는 백분위수별 레이턴시를 측정하고 SLA(Service Level Agreement)를 정의하여 관리할 수 있습니다.

이 지표의 핵심 특징은 첫째, 사용자 경험에 직접적인 영향을 미치고, 둘째, 시스템 성능 병목을 조기에 발견할 수 있으며, 셋째, 정확도와 트레이드오프 관계에 있어 비즈니스 우선순위에 따라 균형을 맞춰야 한다는 점입니다. 이러한 특징들이 실용적인 추천 시스템을 만듭니다.

코드 예제

import polars as pl
import numpy as np

# 추천 API 응답 시간 로그
latency_logs = pl.read_parquet("recommendation_latency_logs.parquet")

# 레이턴시 분석
latency_analysis = (
    latency_logs
    .group_by("endpoint")  # API 엔드포인트별
    .agg([
        pl.col("latency_ms").count().alias("total_requests"),
        pl.col("latency_ms").mean().round(2).alias("avg_latency_ms"),
        pl.col("latency_ms").median().alias("p50_latency_ms"),
        pl.col("latency_ms").quantile(0.95).alias("p95_latency_ms"),
        pl.col("latency_ms").quantile(0.99).alias("p99_latency_ms"),
        pl.col("latency_ms").max().alias("max_latency_ms"),
    ])
    .with_columns([
        # SLA 위반: P95가 300ms 초과
        (pl.col("p95_latency_ms") > 300).alias("sla_violated")
    ])
    .sort("p95_latency_ms", descending=True)
)

print(latency_analysis)

# 시간대별 레이턴시 추이
hourly_latency = (
    latency_logs
    .with_columns([
        pl.col("timestamp").dt.hour().alias("hour")
    ])
    .group_by("hour")
    .agg([
        pl.col("latency_ms").mean().round(2).alias("avg_latency_ms"),
        pl.col("latency_ms").quantile(0.95).alias("p95_latency_ms"),
    ])
    .sort("hour")
)

print(hourly_latency)

설명

이것이 하는 일: 추천 API의 응답 시간 로그를 분석하여 평균, 중앙값, 백분위수별 레이턴시를 계산하고, 엔드포인트별 성능과 시간대별 추이를 파악합니다. 첫 번째로, 레이턴시 로그를 API 엔드포인트별로 그룹화합니다.

홈페이지 추천, 상품 상세 추천, 검색 결과 추천 등 각 엔드포인트는 복잡도가 다르므로 별도로 분석해야 합니다. count()로 총 요청 수를 세어 트래픽 규모도 함께 파악합니다.

그 다음으로, 다양한 통계를 계산합니다. mean()은 평균 레이턴시이지만, 이상치에 민감하므로 median() (P50)이 더 대표적입니다.

quantile(0.95)quantile(0.99)는 상위 5%, 1%의 최악 케이스를 나타냅니다. P95가 300ms라면 95%의 사용자는 300ms 이내 응답을 받지만 5%는 그보다 느립니다.

이 백분위수가 SLA의 기준이 됩니다. 마지막으로, SLA 위반 여부를 플래그로 표시합니다.

예를 들어, "모든 엔드포인트의 P95가 300ms 이내"라는 SLA를 정의했다면, 이를 초과하는 엔드포인트를 즉시 발견할 수 있습니다. 시간대별 분석으로 피크 시간(예: 점심, 저녁)의 성능 저하도 감지합니다.

트래픽이 몰리는 시간에는 캐싱이나 오토스케일링 전략이 필요할 수 있습니다. 여러분이 이 코드를 사용하면 추천 시스템의 성능 병목을 즉시 파악할 수 있습니다.

P99가 1000ms를 초과한다면 1%의 사용자가 매우 느린 경험을 받고 있으므로, 데이터베이스 쿼리 최적화, 캐싱 도입, 모델 경량화 등을 검토해야 합니다. 또한 A/B 테스트로 새 알고리즘을 적용할 때 정확도뿐 아니라 레이턴시도 함께 측정하여 둘 다 만족하는 솔루션을 선택할 수 있습니다.

실전 팁

💡 캐싱을 적극 활용하세요. 동일한 추천 요청이 짧은 시간에 반복된다면 Redis 같은 인메모리 캐시로 레이턴시를 10배 이상 줄일 수 있습니다.

💡 비동기 처리를 고려하세요. 복잡한 추천은 백그라운드에서 사전 계산하고, API는 캐시된 결과만 반환하면 레이턴시가 극도로 낮아집니다.

💡 모델 경량화 기법을 적용하세요. 딥러닝 모델의 양자화(quantization), 프루닝(pruning), 지식 증류(knowledge distillation)로 정확도 1-2% 희생으로 레이턴시를 50% 줄일 수 있습니다.

💡 지역별 레이턴시를 측정하세요. CDN이나 멀티 리전 배포로 물리적 거리를 줄이면 네트워크 레이턴시가 크게 개선됩니다.

💡 Polars는 빠르지만, 실시간 API에서는 사전 집계된 결과를 사용하세요. 요청마다 Polars로 데이터를 처리하는 대신, 배치로 미리 계산하고 API는 조회만 하도록 설계하면 레이턴시가 안정적입니다.


#Python#Polars#추천시스템#데이터분석#지표개발#데이터분석,Python,Polars

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.