윈도우 함수를 활용한 순위 및 이동 분석
중급고급
학습 목표
이 레시피를 완료하면 다음을 할 수 있습니다:
- Window Functions의 개념과 GROUP BY와의 차이점 이해
- ROW_NUMBER, RANK, DENSE_RANK로 순위 매기기
- LAG/LEAD로 이전/다음 행 참조하여 증감 분석
- SUM OVER로 누적 합계 계산
- Moving Average(이동평균)로 트렌드 분석
2.1 BigQuery 실행 환경 설정
BigQuery 쿼리 실행을 위한 환경을 설정합니다.
from google.cloud import bigquery
import pandas as pd
# 인증 설정 (서비스 계정 키 사용 시)
# import os
# os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "path/to/your-key.json"
# BigQuery 클라이언트 생성
client = bigquery.Client(project='your-project-id')
# 쿼리 실행 함수
def run_query(query):
return client.query(query).to_dataframe()1. 윈도우 함수란?
이론
**윈도우 함수(Window Function)**는 행 집합에 대해 계산을 수행하지만, GROUP BY처럼 행을 축소하지 않습니다. 각 행에 대해 관련된 행들의 집합(윈도우)을 기준으로 계산 결과를 반환합니다.
GROUP BY vs 윈도우 함수
| 특성 | GROUP BY | 윈도우 함수 |
|---|---|---|
| 결과 행 수 | 그룹 수만큼 축소 | 원본 행 수 유지 |
| 개별 행 정보 | 손실됨 | 유지됨 |
| 사용 예시 | 카테고리별 합계 | 순위, 누적합, 이동평균 |
기본 구문
함수명() OVER (
[PARTITION BY 그룹_컬럼] -- 그룹 단위 지정 (선택)
[ORDER BY 정렬_컬럼] -- 정렬 기준 (함수에 따라 필수)
[ROWS/RANGE 프레임_지정] -- 윈도우 범위 (선택)
)ℹ️
PARTITION BY vs GROUP BY
PARTITION BY: 윈도우 함수 내에서만 그룹화, 행 수 유지GROUP BY: 전체 쿼리에서 그룹화, 행 수 축소
2. 순위 함수: ROW_NUMBER, RANK, DENSE_RANK
이론
| 함수 | 설명 | 동점 처리 |
|---|---|---|
ROW_NUMBER() | 고유한 순번 | 동점이어도 다른 번호 |
RANK() | 순위 (동점 시 건너뜀) | 1, 2, 2, 4, 5 |
DENSE_RANK() | 순위 (동점 시 연속) | 1, 2, 2, 3, 4 |
비교 예시
| 매출 | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|
| 100 | 1 | 1 | 1 |
| 90 | 2 | 2 | 2 |
| 90 | 3 | 2 | 2 |
| 80 | 4 | 4 | 3 |
| 70 | 5 | 5 | 4 |
실습: 카테고리별 매출 상위 제품
WITH product_revenue AS (
SELECT
p.product_id,
p.name,
p.category,
p.brand,
SUM(oi.sale_price) AS total_revenue
FROM `your-project-id.thelook_ecommerce.order_items` oi
JOIN `your-project-id.thelook_ecommerce.products` p ON oi.product_id = p.product_id
GROUP BY p.product_id, p.name, p.category, p.brand
)
SELECT
category,
name,
brand,
total_revenue,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY total_revenue DESC) AS rank_in_category
FROM product_revenue
QUALIFY rank_in_category <= 3
ORDER BY category, rank_in_category실행 결과:
| category | name | brand | total_revenue | rank_in_category |
|---|---|---|---|---|
| Accessories | Ray-Ban Sunglasses | Ray-Ban | 2,520.00 | 1 |
| Accessories | Persol Sunglasses | Persol | 2,506.00 | 2 |
| Accessories | Tom Ford Sunglasses | Tom Ford | 2,371.60 | 3 |
| Active | Jordan Short | Jordan | 6,321.00 | 1 |
| … | … | … | … | … |
💡
QUALIFY 절
BigQuery의 QUALIFY는 윈도우 함수 결과를 필터링할 때 사용합니다. WHERE나 HAVING 대신 사용하면 서브쿼리 없이 깔끔하게 필터링할 수 있습니다.
3. LAG/LEAD - 이전/다음 행 참조
이론
| 함수 | 설명 | 용도 |
|---|---|---|
LAG(col, n) | n행 이전 값 | 전월 대비, 전일 대비 |
LEAD(col, n) | n행 다음 값 | 다음 구매까지 기간 |
LAG(column, offset, default) OVER (PARTITION BY ... ORDER BY ...)offset: 몇 행 이전/이후 (기본값: 1)default: NULL 대신 반환할 값
실습: 월별 매출 증감 분석
WITH monthly_revenue AS (
SELECT
DATE_TRUNC(CAST(o.created_at AS DATE), MONTH) AS month,
SUM(oi.sale_price) AS revenue
FROM `your-project-id.thelook_ecommerce.orders` o
JOIN `your-project-id.thelook_ecommerce.order_items` oi ON o.order_id = oi.order_id
GROUP BY month
)
SELECT
month,
revenue,
LAG(revenue, 1) OVER (ORDER BY month) AS prev_month_revenue,
revenue - LAG(revenue, 1) OVER (ORDER BY month) AS revenue_change,
ROUND(
(revenue - LAG(revenue, 1) OVER (ORDER BY month)) /
LAG(revenue, 1) OVER (ORDER BY month) * 100,
2
) AS growth_rate_pct
FROM monthly_revenue
WHERE month >= '2024-01-01'
ORDER BY month실행 결과:
| month | revenue | prev_month_revenue | revenue_change | growth_rate_pct |
|---|---|---|---|---|
| 2024-01-01 | 156,234.50 | 142,567.25 | 13,667.25 | 9.58 |
| 2024-02-01 | 148,923.75 | 156,234.50 | -7,310.75 | -4.68 |
| 2024-03-01 | 172,456.30 | 148,923.75 | 23,532.55 | 15.80 |
| 2024-04-01 | 165,890.20 | 172,456.30 | -6,566.10 | -3.81 |
4. 누적 합계와 이동 평균
이론
윈도우 프레임을 지정하여 계산 범위를 조정할 수 있습니다.
SUM(col) OVER (
ORDER BY date
ROWS BETWEEN start AND end
)| 프레임 | 설명 |
|---|---|
UNBOUNDED PRECEDING | 첫 번째 행부터 |
CURRENT ROW | 현재 행 |
n PRECEDING | n행 이전 |
n FOLLOWING | n행 이후 |
UNBOUNDED FOLLOWING | 마지막 행까지 |
실습: 누적 매출 및 7일 이동평균
WITH daily_revenue AS (
SELECT
CAST(o.created_at AS DATE) AS date,
SUM(oi.sale_price) AS revenue
FROM `your-project-id.thelook_ecommerce.orders` o
JOIN `your-project-id.thelook_ecommerce.order_items` oi ON o.order_id = oi.order_id
GROUP BY date
)
SELECT
date,
revenue,
-- 누적 합계
SUM(revenue) OVER (ORDER BY date ROWS UNBOUNDED PRECEDING) AS cumulative_revenue,
-- 7일 이동평균
ROUND(
AVG(revenue) OVER (
ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
),
2
) AS moving_avg_7d
FROM daily_revenue
WHERE date >= '2024-10-01'
ORDER BY date실행 결과:
| date | revenue | cumulative_revenue | moving_avg_7d |
|---|---|---|---|
| 2024-10-01 | 5,234.50 | 2,345,678.25 | 4,892.50 |
| 2024-10-02 | 4,891.30 | 2,350,569.55 | 4,956.33 |
| 2024-10-03 | 5,123.75 | 2,355,693.30 | 5,012.75 |
| 2024-10-04 | 4,980.20 | 2,360,673.50 | 5,057.44 |
| 2024-10-05 | 5,345.60 | 2,366,019.10 | 5,115.07 |
ℹ️
이동평균 활용
- 7일 이동평균: 일별 변동을 완화하여 주간 트렌드 파악
- 30일 이동평균: 계절적 패턴 확인
- 주식/금융: 골든크로스(단기 > 장기), 데드크로스 시그널
5. PARTITION BY 활용
실습: 고객별 구매 순서 및 누적 구매액
SELECT
o.user_id,
o.order_id,
o.created_at,
oi.sale_price,
-- 고객별 구매 순서
ROW_NUMBER() OVER (PARTITION BY o.user_id ORDER BY o.created_at) AS purchase_order,
-- 고객별 누적 구매액
SUM(oi.sale_price) OVER (
PARTITION BY o.user_id
ORDER BY o.created_at
ROWS UNBOUNDED PRECEDING
) AS cumulative_purchase,
-- 이전 구매와의 간격 (일)
DATE_DIFF(
CAST(o.created_at AS DATE),
CAST(LAG(o.created_at) OVER (PARTITION BY o.user_id ORDER BY o.created_at) AS DATE),
DAY
) AS days_since_last_purchase
FROM `your-project-id.thelook_ecommerce.orders` o
JOIN `your-project-id.thelook_ecommerce.order_items` oi ON o.order_id = oi.order_id
WHERE o.user_id IN (1, 2, 3) -- 샘플 고객
ORDER BY o.user_id, o.created_at실행 결과:
| user_id | order_id | created_at | sale_price | purchase_order | cumulative_purchase | days_since_last_purchase |
|---|---|---|---|---|---|---|
| 1 | 1001 | 2023-01-15 | 125.50 | 1 | 125.50 | NULL |
| 1 | 1245 | 2023-03-22 | 89.99 | 2 | 215.49 | 66 |
| 2 | 2031 | 2023-02-10 | 234.00 | 1 | 234.00 | NULL |
| 2 | 2156 | 2023-05-18 | 156.75 | 2 | 390.75 | 97 |
| 3 | 3089 | 2023-01-28 | 78.50 | 1 | 78.50 | NULL |
퀴즈
문제 1: 카테고리별 브랜드 매출 순위
각 카테고리 내에서 브랜드별 매출 순위를 구하고, 상위 3개 브랜드만 조회하세요.
정답 보기
WITH category_brand_revenue AS (
SELECT
p.category,
p.brand,
SUM(oi.sale_price) AS total_revenue
FROM `your-project-id.thelook_ecommerce.order_items` oi
JOIN `your-project-id.thelook_ecommerce.products` p ON oi.product_id = p.product_id
GROUP BY p.category, p.brand
)
SELECT
category,
brand,
total_revenue,
RANK() OVER (PARTITION BY category ORDER BY total_revenue DESC) AS brand_rank
FROM category_brand_revenue
QUALIFY brand_rank <= 3
ORDER BY category, brand_rank해설:
PARTITION BY category로 카테고리별 그룹 생성RANK()는 동점 시 같은 순위 부여 (매출이 같은 브랜드 처리)QUALIFY로 서브쿼리 없이 필터링
문제 2: 고객 재구매 간격 분석
각 고객의 평균 재구매 간격(일)을 계산하세요. (2회 이상 구매 고객만)
정답 보기
WITH purchase_gaps AS (
SELECT
user_id,
created_at,
LAG(created_at) OVER (PARTITION BY user_id ORDER BY created_at) AS prev_purchase,
DATE_DIFF(
CAST(created_at AS DATE),
CAST(LAG(created_at) OVER (PARTITION BY user_id ORDER BY created_at) AS DATE),
DAY
) AS days_gap
FROM `your-project-id.thelook_ecommerce.orders`
)
SELECT
user_id,
COUNT(*) + 1 AS total_purchases,
ROUND(AVG(days_gap), 1) AS avg_repurchase_days
FROM purchase_gaps
WHERE days_gap IS NOT NULL
GROUP BY user_id
HAVING COUNT(*) >= 1 -- 2회 이상 구매 (gap이 1개 이상)
ORDER BY avg_repurchase_days
LIMIT 20해설:
LAG로 이전 구매일 참조DATE_DIFF로 간격 계산- 첫 구매는
days_gap이 NULL이므로 제외 - 평균 재구매 간격이 짧은 충성 고객 식별 가능
정리
| 함수 | 용도 | 예시 |
|---|---|---|
ROW_NUMBER() | 고유 순번 | 고객별 최근 주문 1건 |
RANK() | 순위 (동점 건너뜀) | 매출 순위 |
DENSE_RANK() | 순위 (동점 연속) | 등급 분류 |
LAG() | 이전 행 값 | 전월 대비 증감 |
LEAD() | 다음 행 값 | 다음 구매까지 기간 |
SUM() OVER | 누적/이동 합계 | 누적 매출, 이동평균 |
AVG() OVER | 이동 평균 | 7일/30일 평균 |
다음 단계
윈도우 함수를 마스터했습니다! 다음으로 CTE와 코호트 분석에서 복잡한 쿼리 구조화와 고급 분석 기법을 배워보세요.
Last updated on