Skip to Content

윈도우 함수를 활용한 순위 및 이동 분석

중급고급

학습 목표

이 레시피를 완료하면 다음을 할 수 있습니다:

  • 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_NUMBERRANKDENSE_RANK
100111
90222
90322
80443
70554

실습: 카테고리별 매출 상위 제품

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

실행 결과:

categorynamebrandtotal_revenuerank_in_category
AccessoriesRay-Ban SunglassesRay-Ban2,520.001
AccessoriesPersol SunglassesPersol2,506.002
AccessoriesTom Ford SunglassesTom Ford2,371.603
ActiveJordan ShortJordan6,321.001
💡
QUALIFY 절

BigQuery의 QUALIFY는 윈도우 함수 결과를 필터링할 때 사용합니다. WHEREHAVING 대신 사용하면 서브쿼리 없이 깔끔하게 필터링할 수 있습니다.


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

실행 결과:

monthrevenueprev_month_revenuerevenue_changegrowth_rate_pct
2024-01-01156,234.50142,567.2513,667.259.58
2024-02-01148,923.75156,234.50-7,310.75-4.68
2024-03-01172,456.30148,923.7523,532.5515.80
2024-04-01165,890.20172,456.30-6,566.10-3.81

4. 누적 합계와 이동 평균

이론

윈도우 프레임을 지정하여 계산 범위를 조정할 수 있습니다.

SUM(col) OVER ( ORDER BY date ROWS BETWEEN start AND end )
프레임설명
UNBOUNDED PRECEDING첫 번째 행부터
CURRENT ROW현재 행
n PRECEDINGn행 이전
n FOLLOWINGn행 이후
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

실행 결과:

daterevenuecumulative_revenuemoving_avg_7d
2024-10-015,234.502,345,678.254,892.50
2024-10-024,891.302,350,569.554,956.33
2024-10-035,123.752,355,693.305,012.75
2024-10-044,980.202,360,673.505,057.44
2024-10-055,345.602,366,019.105,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_idorder_idcreated_atsale_pricepurchase_ordercumulative_purchasedays_since_last_purchase
110012023-01-15125.501125.50NULL
112452023-03-2289.992215.4966
220312023-02-10234.001234.00NULL
221562023-05-18156.752390.7597
330892023-01-2878.50178.50NULL

퀴즈

문제 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

🤖AI 모의면접실전처럼 연습하기