데이터분석/PostgreSQL

데이터를 무기로 삼기 위한 분석 기술

환성 2023. 7. 30. 23:07
728x90
목차

1. 검색 기능 평가하기

 

2. 검색 결과의 포괄성을 지표화하기

 

3. 검색 결과 순위와 관련된 지표 계산하기

 

4. 데이터 마이닝

 

5. 추천 시스템 구성하기

 

6. 점수 계산하기

 

7. 거대한 숫자 지표 쉽게 가공하기

 

 

실습에 필요한 테이블 및 자료

access_log 테이블
purchase_detail_log 테이블
action_counts_with_date 테이블

검색 기능 평가하기

  • 검색하는 사용자의 행동
    • 사용자가 무엇을 검색하고 검색 결과에 대해 어떤 행동을 취하는지 구분할 수 있으면, 엔지니어에게 기능 개선등을 요청 가능하다.
  • 검색 기능 개선 방법
    • 검색 키워드의 흔들림을 흡수할 수 있게 동의어 사전 추가하는 방법이 있다.
    • 검색 키워드를 검색 엔진이 이해할 수 있게 사용자 사전 추가하는 방법이 있다.
      • Ex.) 위스키 검색 시 위, 스키 단어로 분해 → 스키 검색 시 위스키로 결과나오는 경우
      • 이런 경우를 방지하기 위해 사용자 사전을 추가해야 한다.
    • 검색 결과가 사용자가 원하는 순서로 나오게 순서 조정
      • 검색 쿼리와의 연관성을 수치화해서 점수가 높은 순서로 정렬해주는 순위 기능이 필요하다.
      • 문장의 출현 위치 및 빈도, 아이템 갱신 일자, 접속 수 등 다양한 요소와 조합해야 한다.
💡 흔들림 : 검색 엔진의 키워드를 설명할 때 단어가 정확하지 않고 조금 떨리는 현상
  • NoMatch 비율과 키워드 집계하기
    • NoMatch : 검색 총 수 중에서 검색 결과를 0으로 리턴하는 검색 결과 비율
    • NoMatch 비율 = 검색 결과 0인 수 / 검색 총 수
    • NoMatch 비율 집계 쿼리
SELECT
	substring(stamp, 1, 10) AS dt
	, COUNT(1) AS search_count
	, SUM(CASE WHEN result_num = 0 THEN 1 ELSE 0 END) AS no_match_count
	, AVG(CASE WHEN result_num = 0 THEN 1.0 ELSE 0.0 END) AS no_match_rate
FROM
	access_log
WHERE
	action = 'search'
GROUP BY
	dt;
  • NoMatch  비율과 키워드 집계하기
WITH
search_keyword_stat AS (
	SELECT
		keyword
		, result_num
		, COUNT(1) AS search_count
		, 100.0 * COUNT(1) / COUNT(1) OVER() AS search_share
	FROM
		access_log
	WHERE
		action = 'search'
	GROUP BY
		keyword, result_num
)
-- 집계 결과
SELECT
	keyword
	, search_count
	, search_share
	, 100.0 * search_count / SUM(search_count) OVER() AS no_match_share
FROM
	search_keyword_stat
WHERE
	result_num = 0;
  • 재검색 비율과 키워드 집계하기
    • 사용자가 검색 결과의 출력과 관계 없이, 어떤 결과도 클릭하지 않고 새로 검색을 실행한 비율이다.
    • 검색 화면과 상세 화면의 접근 로그에 다음 줄의 액션을 기록하는 쿼리
WITH
access_log_with_next_action AS (
	SELECT
		stamp
		, session
		, action
		, LEAD(action)
			OVER(PARTITION BY session ORDER BY stamp ASC) AS next_action
	FROM
		access_log
)
SELECT *
FROM access_log_with_next_action
ORDER BY
	session, stamp;

 

 

 

검색 결과의 포괄성을 지표화하기

  • 재현율을 사용해 검색의 포괄성 평가하는 방법이 있다.
  • 재현율(recall) : 어떤 키워드의 검색 결과에서 미리 준비한 정답 아이템이 얼마나 나왔는지에 대한 척도
  • 검색 결과와 정답 아이템을 결합하는 쿼리
WITH
search_result_with_correct_items AS (
	SELECT
		COALESCE(r.keyword, c.keyword) AS keyword
		, r.rank
		, COALESCE(r.item, c.item) AS item
		, CASE WHEN c.item IS NOT NULL THEN 1 ELSE 0 END AS correct
	FROM
		search_result AS r
		FULL OUTER JOIN
			correct_result AS c
			ON r.keyword = c.keyword
			AND r.item = c.item
)
SELECT *
FROM
	search_result_with_correct_items
ORDER BY
	keyword, rank;
  • 재현율의 값을 집약해서 비교하기 쉽게 만드는 쿼리
    • 검색 엔진의 일반적인 인터페이스는 상위 n개 순위 형식으로 출력한다(Ex.) 네이버 실시간 검색어)
    • 따라서 재현율은 사용자에게 출력되는 아이템 개수로 한정해서 구한다.
    • 해당 쿼리에서는 출력 default를 5개로 가정한다.
WITH
search_result_with_correct_items AS (
	SELECT
		COALESCE(r.keyword, c.keyword) AS keyword
		, r.rank
		, COALESCE(r.item, c.item) AS item
		, CASE WHEN c.item IS NOT NULL THEN 1 ELSE 0 END AS correct
	FROM
		search_result AS r
		FULL OUTER JOIN
			correct_result AS c
			ON r.keyword = c.keyword
			AND r.item = c.item
)
, search_result_with_recall AS (
	SELECT
		*
		-- 검색 결과 상위에서 정답 데이터에 포함되는 아이템 수의 누계 구하기
	, SUM(correct)
		-- rank가 NULL이면 정렬 순서의 마지막 위치, 편의상 큰 값으로 변환해서 넣기
		OVER(PARTITION BY keyword ORDER BY COALESCE(rank, 100000) ASC
			ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cum_correct
	, CASE
		-- 검색 결과에 포함되지 않은 아이템은 편의상 적합률을 0으로 다루기
		WHEN rank IS NULL THEN 0.0
		ELSE
			100.0 * SUM(correct) 
			OVER(PARTITION BY keyword ORDER BY COALESCE(rank, 100000) ASC
				ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) / SUM(correct) OVER(PARTITION BY keyword) END AS recall
	FROM
		search_result_with_correct_items
)
, recall_over_rank_5 AS (
	SELECT
		keyword
		, rank
		, recall
		-- 검색 결과 순위가 높은 순서로 번호 붙이기
		-- 검색 결과에 안나오는 아이템은 0으로 다루기
		, ROW_NUMBER()
				OVER(PARTITION BY keyword ORDER BY COALESCE(rank, 0) DESC) AS desc_number
	FROM
		search_result_with_recall
	WHERE
		-- 검색 결과 상위 5개 이하, 없는 아이템만 출력
		COALESCE(rank, 0) <= 5
)
SELECT
	keyword
	, recall AS recall_at_5
FROM
	recall_over_rank_5
WHERE
	desc_number = 1;
  • 검색 결과의 타당성 지표화하기
  • 정밀도(precision) : 검색 결과중 정답 아이템의 비율
  • 검색 결과 상위 n개의 정밀도 계산 쿼리
  • 100000의 경우 임의의 큰 값
WITH
search_result_with_correct_items AS (
	SELECT
		COALESCE(r.keyword, c.keyword) AS keyword
		, r.rank
		, COALESCE(r.item, c.item) AS item
		, CASE WHEN c.item IS NOT NULL THEN 1 ELSE 0 END AS correct
	FROM
		search_result AS r
		FULL OUTER JOIN
			correct_result AS c
			ON r.keyword = c.keyword
			AND r.item = c.item
)
, search_result_with_precision AS (
	SELECT
		*
	-- 검색 결과의 상위에서 정답 데이터에 포함되는 아이템 수 누계
	, SUM(correct)
		OVER(PARTITION BY keyword ORDER BY COALESCE(rank, 100000) ASC
			ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cum_correct
	, CASE
			WHEN rank IS NULL THEN 0.0
			ELSE 100.0 * SUM(correct) 
				OVER(PARTITION BY keyword ORDER BY COALESCE(rank, 100000) ASC
					ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
				-- 재현률과 다르게 분모에 검색 결과 순위까지의 누계 아이템 수
					/ COUNT(1) OVER (PARTITION BY keyword ORDER BY COALESCE(rank, 100000) ASC
						ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
	END AS precision
FROM
	search_result_with_correct_items
)
SELECT *
FROM
	search_result_with_precision
ORDER BY
	keyword, rank;
  • 검색 엔진 전체의 평균 정확률 계산 쿼리
WITH
search_result_with_correct_items AS (
	SELECT
		COALESCE(r.keyword, c.keyword) AS keyword
		, r.rank
		, COALESCE(r.item, c.item) AS item
		, CASE WHEN c.item IS NOT NULL THEN 1 ELSE 0 END AS correct
	FROM
		search_result AS r
		FULL OUTER JOIN
			correct_result AS c
			ON r.keyword = c.keyword
			AND r.item = c.item
)
, search_result_with_precision AS (
	SELECT
		*
	-- 검색 결과의 상위에서 정답 데이터에 포함되는 아이템 수 누계
	, SUM(correct)
		OVER(PARTITION BY keyword ORDER BY COALESCE(rank, 100000) ASC
			ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cum_correct
	, CASE
			WHEN rank IS NULL THEN 0.0
			ELSE 100.0 * SUM(correct) 
				OVER(PARTITION BY keyword ORDER BY COALESCE(rank, 100000) ASC
					ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
				-- 재현률과 다르게 분모에 검색 결과 순위까지의 누계 아이템 수
					/ COUNT(1) OVER (PARTITION BY keyword ORDER BY COALESCE(rank, 100000) ASC
						ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
	END AS precision
FROM
	search_result_with_correct_items
)
, precision_over_rank_5 AS (
	SELECT
		keyword
		, rank
		, precision
		, ROW_NUMBER()
				OVER(PARTITION BY keyword ORDER BY COALESCE(rank, 0) DESC) AS desc_number
	FROM
		search_result_with_precision
	WHERE
		COALESCE(rank, 0) <= 5
)
SELECT
	, AVG(precision) AS average_precision_at_5
FROM
	precision_over_rank_5
WHERE
	desc_number = 1;

 

 

 

검색 결과 순위와 관련된 지표 계산하기

  • 재현률과 정확률의 부족한 부분
    • 검색 결과의 순위 고려 X( P@10)
      • (Ex. 적합률이 40% 일 때 ,  전체 100개중 1,4 번째에 있는거랑 2, 7번째 있는거랑 첫번쨰께 더 좋은 검색 엔진임)
      • 이를 위해 검색 순위를 고려한 지표 MAP(Mean Average Precision), MRR(Mean Reciprocal Rank)가 있다.
    • 0과 1이라는 두가지 값으로만 표현
      • Ex.) 사용자의 별점 리뷰 데이터, 점수를 정답 데이터로 사용시 → 사용자로부터 입력받은 별점 활용 X
      • 단계적인 점수를 고려하는 지표 DCG(Discounted Cumulated Gain)와 NDCG(Normalized DCG)가 있다.
    • 모든 아이템에 대한 정답을 미리 준비 X
      • 검색 대상 아이템 수에서 정답 아이템 수가 한정될 때 BPREF(Binary Preference)를 활용한다.
    • MAP으로 검색 결과의 순위를 고려해 평가하기
      • Ex.) 10개 중 정답 아이템 수가 4개일때 P@10 = 40%, 만약 상위 1~4번째까지 모두 정답 아이템인 경우 MAP = 100 x ((1/1) + (2/2) + (3/3) + (4/4)) / 4 = 100이 된다.
      • 반면, 상위 7~10번째가 정답 아이템이면, MAP = 100 x (1/7) + (2/8) + (3/9) + (4/10)) / 4 = 28.15가 되어 이전보다 낮게 평가된다.
    • 정답 아이템별로 적합률을 추출하는 쿼리
WITH
search_result_with_correct_items AS (
	SELECT
		COALESCE(r.keyword, c.keyword) AS keyword
		, r.rank
		, COALESCE(r.item, c.item) AS item
		, CASE WHEN c.item IS NOT NULL THEN 1 ELSE 0 END AS correct
	FROM
		search_result AS r
		FULL OUTER JOIN
			correct_result AS c
			ON r.keyword = c.keyword
			AND r.item = c.item
)
, search_result_with_precision AS (
	SELECT
		*
	-- 검색 결과의 상위에서 정답 데이터에 포함되는 아이템 수 누계
	, SUM(correct)
		OVER(PARTITION BY keyword ORDER BY COALESCE(rank, 100000) ASC
			ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cum_correct
	, CASE
			WHEN rank IS NULL THEN 0.0
			ELSE 100.0 * SUM(correct) 
				OVER(PARTITION BY keyword ORDER BY COALESCE(rank, 100000) ASC
					ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
				-- 재현률과 다르게 분모에 검색 결과 순위까지의 누계 아이템 수
					/ COUNT(1) OVER (PARTITION BY keyword ORDER BY COALESCE(rank, 100000) ASC
						ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
	END AS precision
FROM
	search_result_with_correct_items
)
SELECT
	keyword
	, rank
	, precision
FROM
	search_result_with_precision
WHERE
	correct = 1;

 

 

 

데이터 마이닝

  • 데이터 마이닝의 대표적 방법 상관 규칙 추출 방법 중 하나인 **어소시에이션 분석(연관분석)**을 다룸
  • 상품 조합(A,B,C..)을 구매한 사람의 n%는 상품 조합(X,Y..)도 구매한다.. 라는 규칙이다.
  • 사용되는 지표 : 지지도(Support), 확신도,신뢰도(Confidence), 리프트(Lift)

지지도, 신뢰도, 향상도

  • 두 상품의 연관성을 연관분석으로 찾기
    • 구매 로그 수와 상품별 구매 수를 세는 쿼리
WITH
purchase_id_count AS (
	-- 구매 상세 로그에서 유니크한 구매 로그 수 계산하기
	SELECT COUNT(DISTINCT purchase_id) AS purchase_count
	FROM purchase_detail_log
)
, purchase_detail_log_with_counts AS (
		SELECT
			d.purchase_id
			, p.purchase_count
			, d.product_id
			-- 상품별 구매 수 계산하기
			, COUNT(1) OVER(PARTITION BY d.product_id) AS product_count
	FROM
		purchase_detail_log AS d
		CROSS JOIN
		-- 구매 로그 수를 모든 레코드와 결합
			purchase_id_count AS p
)
SELECT
	*
FROM
	purchase_detail_log_with_counts
ORDER BY
	product_id, purchase_id;
  • 지지도, 확신도, 향상도를 계산하는 쿼리
WITH
purchase_id_count AS (
	-- 구매 상세 로그에서 유니크한 구매 로그 수 계산하기
	SELECT COUNT(DISTINCT purchase_id) AS purchase_count
	FROM purchase_detail_log
)
, purchase_detail_log_with_counts AS (
		SELECT
			d.purchase_id
			, p.purchase_count
			, d.product_id
			-- 상품별 구매 수 계산하기
			, COUNT(1) OVER(PARTITION BY d.product_id) AS product_count
	FROM
		purchase_detail_log AS d
		CROSS JOIN
		-- 구매 로그 수를 모든 레코드와 결합
			purchase_id_count AS p
)
, product_pair_with_stat AS (
	SELECT
		l1.product_id AS p1
		, l2.product_id AS p2
		, l1.product_count AS p1_count
		, l2.product_count AS p2_count
		, COUNT(1) AS p1_p2_count
		, l1.purchase_count AS purchase_count
	FROM
		purchase_detail_log_with_counts AS l1
		JOIN
			purchase_detail_log_with_counts SA l2
		ON l1.purchase_id = l2.purchase_id
	WHERE
		-- 같은 상품 조합 제외
		l1.product_id <> l2.product_id
	GROUP BY
		l1.product_id
		, l2.product_id
		, l1.product_count
		, l2.product_count
		, l1.purchase_count
)
SELECT
	p1
	, p2
	, 100.0 * p1_p2_count / purchase_count AS support
	, 100.0 * p1_p2_count / p1_count AS confidence
	, (100.0 * p1_p2_count / p1_count) / (100.0 * p2_count / purchase_count) AS lift
FROM
	product_pair_with_stat
ORDER BY
	p1, p2;

 

 

 

추천 시스템 구성하기

  • 추천 시스템은 두 종류
    • 열람/구매한 아이템을 기반으로 다른 아아템을 추천해주는 Item to Item
    • 과거의 행동 또는 데모그래픽 정보를 기반으로 흥미를 유추하고 아이템을 추천하는 User to Item
  • EC 사이트의 모듈의 종류
모듈 설명 예시
리마인드 사용자의 과거 행동을 기반으로 아이템을 다시 제안 최근 보았던 상품, 한 번 더 구매하기
순위 열람 수, 구매 수 등을 기반으로 인기 있는 아이템 제안 인기 순위, 급상승 순위
콘텐츠베이스 아이템의 추가 정보를 기반으로 다른 아이템 추천 해당 배우가 출연한 다른 작품
추천 서비스를 사용하는 사용자의 전체의 행동 이력 기반으로, 다음에 볼만한 아이템 또는 관련된 아이템을 추측해 기반 이 상품을 보았던 사람들은 이러한 상품들도 함께 보았습니다
개별 추천 사용자 개인의 행동 이력을 기반으로 흥미 기호 추측, 아이템 제안 당신만을 위한 추천
  • 추천의 효과
효과 설명 예시
다운셀 가격이 높아 구매를 고민하는 사용자에게 더 저렴한 아이템을 제안해서 구매 수를 올리는 것 이전 모델 가격 할인
크로스셀 관련 상품을 함께 구매하게 해서 구매 단가 올리기 함께 구매되는 상품
업셀 상위 모델 또는 고성능의 아이템을 제안해 구매 단가를 올리는 것 이 상품의 최신 모델
  • 데이터의 명시적 획득과 암묵적 획득
    • 추천 시스템 구축 시 '어떤 데이터를 기반으로 추천 시스템을 구축할 것 인가?'를 중요시 해야 한다.
데이터 기반 설명 예시
암묵적 데이터 획득 사용자 행동 기반으로 기호 추측
데이터 양은 많지만 정확도가 떨어진다
구매 로그, 열람 로그
명시적 데이터 획득 사용자에게 직접 기호를 물어본다 별점 주기
  • 열람 로그, 구매 로그 차이
데이터 소스 내용 데이터 예시
열람 로그 특정 아이템과 유사한 아이템을 추천해서 사용자의 선택지 늘리기 여행용 가방을 구매한다고 할 때 크기, 형태, 색이 다른 것을 추천하는 경우
구매 로그 함께 구매 가능한 상품을 추천해서 구매 단가 끌어 올리기 여행용 가방을 구매한다고 할 때 함께 구매하면 좋은 여권 케이스 등을 추천하는 경우
  • 접근 로그를 사용해 아아템의 상관도 계산하기
    • Item to Item,  사용자 유동성 > 아이템 유동성 이므로 데이터 축적이 쉽다.
    • 열람 수와 구매 수를 조합한 점수를 계산하는 쿼리
WITH
ratings AS (
	SELECT
		user_id
		, product
		-- 상품 열람 수
		, SUM(CASE WHEN action = 'view' THEN 1 ELSE 0 END) AS view_count
		-- 상품 구매 수
		, SUM(CASE WHEN action = 'purchase' THEN 1 ELSE 0 END) AS purchase_count
		-- 열람수, 구매수 3:7 비율의 가중치를 주어 평균 구하기
		, 0.3 * SUM(CASE WHEN action = 'view' THEN 1 ELSE 0 END) 
		+ 0.7 * SUM(CASE WHEN action = 'purchase' THEN 1 ELSE 0 END) AS score
	FROM
		action_log
	GROUP BY
		user_id, product
)
SELECT *
FROM
	ratings
ORDER BY
	user_id, score DESC;
  • 아이템 사이의 유사도를 계산하고 순위 생성하는 쿼리
WITH
ratings AS (
	SELECT
		user_id
		, product
		-- 상품 열람 수
		, SUM(CASE WHEN action = 'view' THEN 1 ELSE 0 END) AS view_count
		-- 상품 구매 수
		, SUM(CASE WHEN action = 'purchase' THEN 1 ELSE 0 END) AS purchase_count
		-- 열람수, 구매수 3:7 비율의 가중치를 주어 평균 구하기
		, 0.3 * SUM(CASE WHEN action = 'view' THEN 1 ELSE 0 END) 
		+ 0.7 * SUM(CASE WHEN action = 'purchase' THEN 1 ELSE 0 END) AS score
	FROM
		action_log
	GROUP BY
		user_id, product
)
SELECT
	r1.product AS target
	, r2.product AS related
	-- 모든 아이템을 열람 / 구매한 사용자 수
	, COUNT(r1.user_id) AS users
	-- 점수들을 곱하고 합계를 구해 유사도 구하기
	, SUM(r1.score * r2.score) AS score
	-- 상품 유사도 순위 구하기
	, ROW_NUMBER() OVER(PARTITION BY r1.product ORDER BY SUM(r1.score * r2.score) DESC) AS rank
FROM
	ratings AS r1
	JOIN
	ratings AS r2
	ON
		r1.user_id = r2.user_id
WHERE
	-- 같은 아이템의 경우 페어 제외
	r1.product <> r2.product
GROUP BY
	r1.product, r2.product
ORDER BY
	target, rank;
  • 이러한 방식을 사용했을 때 문제점이 발생할 수 있다.
    • 첫 번째는, 접근 수가 많은 아이템의 유사도가 상대적으로 높게 나온다
    • 점수의 값이 어느 정도의 유사도를 나타내느지 점수만으로 알기 어렵다
    • 이러한 문제 해결을 위해 터 정규화 사용한다
    • 벡터의 크기를 노름(norm)이라고 부르는데, 노름으로 벡터의 각 수치를 나누면 벡터의 노름을 1로 만들 수 있음
    • 이러한 과정을 L2 정규화라고 한다.
  • 다음 쿼리는 아이템 벡터를 L2 정규화하는 쿼리
WITH
ratings AS (
	SELECT
		user_id
		, product
		-- 상품 열람 수
		, SUM(CASE WHEN action = 'view' THEN 1 ELSE 0 END) AS view_count
		-- 상품 구매 수
		, SUM(CASE WHEN action = 'purchase' THEN 1 ELSE 0 END) AS purchase_count
		-- 열람수, 구매수 3:7 비율의 가중치를 주어 평균 구하기
		, 0.3 * SUM(CASE WHEN action = 'view' THEN 1 ELSE 0 END) 
		+ 0.7 * SUM(CASE WHEN action = 'purchase' THEN 1 ELSE 0 END) AS score
	FROM
		action_log
	GROUP BY
		user_id, product
)
, product_base_normalized_ratings AS (
	-- 아이템 벡터 정규화하기
	SELECT
		user_id
		, product
		, score
		, SQRT(SUM(score * score) OVER(PARTITION BY product)) AS norm
		, score / SQRT(SUM(score * score) OVER(PARTITION BY product)) AS norm_score
	FROM
		ratings
)
SELECT *
FROM
	product_base_normalized_ratings
  • 정규화된 점수로 아이템의 유사도를 계산하는 쿼리(코사인 유사도)
WITH
ratings AS (
	SELECT
		user_id
		, product
		-- 상품 열람 수
		, SUM(CASE WHEN action = 'view' THEN 1 ELSE 0 END) AS view_count
		-- 상품 구매 수
		, SUM(CASE WHEN action = 'purchase' THEN 1 ELSE 0 END) AS purchase_count
		-- 열람수, 구매수 3:7 비율의 가중치를 주어 평균 구하기
		, 0.3 * SUM(CASE WHEN action = 'view' THEN 1 ELSE 0 END) 
		+ 0.7 * SUM(CASE WHEN action = 'purchase' THEN 1 ELSE 0 END) AS score
	FROM
		action_log
	GROUP BY
		user_id, product
)
, product_base_normalized_ratings AS (
	-- 아이템 벡터 정규화하기
	SELECT
		user_id
		, product
		, score
		, SQRT(SUM(score * score) OVER(PARTITION BY product)) AS norm
		, score / SQRT(SUM(score * score) OVER(PARTITION BY product)) AS norm_score
	FROM
		ratings
)
SELECT
	r1.product AS target
	, r2.product AS related
	, COUNT(r1.user_id) AS users
	, SUM(r1.score * r2.score) AS score
	, SUM(r1.norm_score * r2.norm_score) AS norm_score
	, ROW_NUMBER() OVER(PARTITION BY r1.product ORDER BY SUM(r1.norm_score * r2.norm_score) DESC)
		AS rank
FROM
	product_base_normalizd_ratings AS r1
	JOIN
	product_base_normalized_ratings AS r2
	ON r1.user_id = r2.user_id
GROUP BY
	r1.product, r2.product
ORDER BY
	target, rank;
  • 당신을 위한 추천 상품(User to Item)
    • 사용자와 관련된 추천이므로 웹사이트 최상위 페이지, 사용자의 마이 페이지등 다양한 상황에 활용된다.
    • 사용자와 사용자의 유사도 계산후 유사 사용자가 흥미를 가진 아이템을 구해야 한다.
    • 사용자끼리의 유사도를 계산하는 쿼리
WITH
ratings AS (
	SELECT
		user_id
		, product
		-- 상품 열람 수
		, SUM(CASE WHEN action = 'view' THEN 1 ELSE 0 END) AS view_count
		-- 상품 구매 수
		, SUM(CASE WHEN action = 'purchase' THEN 1 ELSE 0 END) AS purchase_count
		-- 열람수, 구매수 3:7 비율의 가중치를 주어 평균 구하기
		, 0.3 * SUM(CASE WHEN action = 'view' THEN 1 ELSE 0 END) 
		+ 0.7 * SUM(CASE WHEN action = 'purchase' THEN 1 ELSE 0 END) AS score
	FROM
		action_log
	GROUP BY
		user_id, product
)
, user_base_normalized_ratings AS (
	-- 사용자 벡터 정규화하기
	SELECT
		user_id
		, product
		, score
		-- PARTITION BY user_id으로 사용자별 벡터 노름 계산하기
		, SQRT(SUM(score * score) OVER(PARTITION BY user_id)) AS norm
		, score / SQRT(SUM(score * score) OVER(PARTITION BY user_id)) AS norm_score
	FROM
		ratings
)
, related_users AS (
	-- 경향이 비슷한 사용자 찾기
	SELECT
		r1.user_id
		, r2.user_id AS related_user
		, COUNT(r1.product) AS products
		, SUM(r1.norm_score * r2.norm_score) AS score
		, ROW_NUMBER() OVER(PARTITION BY r1.user_id ORDER BY SUM(r1.norm_score * r2.norm_score) DESC) AS rank
	FROM
		user_based_normalized_ratings AS r1
		JOIN
		user_based_normalized_ratings AS r2
		ON r1.product = r2.product
	WHERE
		r1.user_id <> r2.user_id
	GROUP BY
		r1.user_id, r2.user_id
)
SELECT *
FROM
	related_users
ORDER BY
	user_id, rank;

 

점수 계산하기

  • 여러 값을 균형있게 조합해서 점수 계산하기
  • 세 종류의 평균(산술 평균, 기하 평균, 조화 평균)
    • 산술 평균 - 일반적인 평균, 각각의 값을 모두 더한 뒤 전체 개수로 나눈 것
    • 기하 평균 - 각각의 값을 곱한뒤 개수만큼 제곱근(성장률, 이율)
    • 조화 평균 - 각 값의 역수의 산술 평균에 다시 역수를 취한 값(평균 속도 계산)
  • 평균 계산하기
    • 세로 데이터의 평균을 구하는 쿼리
SELECT
	id
	, COUNT(*) AS record_num
	-- 데이터를 배열로 집약하고, 쉼표로 구분된 문자열로 변환하기
	, string_agg(name, ',') AS name_list
	, string_agg(stamp, ',') AS stamp_list
FROM
	mst_categories
GROUP BY
	id
HAVING COUNT(*) > 1 -- 중복된 ID 확인
  • 가로 기반 데이터의 평균을 계산하는 쿼리
SELECT
	path
	-- 산술 평균
	, AVG(value) AS arithmetic_mean
	-- 기하 평균(상용 로그로 log 함수 사용)
	, POWER(10, AVG(log(value))) AS geometric_mean
	-- 조화 평균
	, 1.0 / (AVG(1.0 / value)) AS harmonic_mean
FROM
	search_evaluation_by_row
WHERE
	value > 0
GROUP BY
	path
	-- 빠진 데이터가 없게 path로 한정
HAVING
	COUNT(*) = 2
ORDER BY
	path;
  • 가중 평균
  • 검색 결과의 포괄성을 고려하면서 검색 결과 상위에 있는 요소들의 타당성을 중요시하면서 검색 엔진을 평가할 때(재현율과 적합률의 비율을 둘 때)
    • 산술 평균 - 각각의 값에 가중치 곱하고 가중치 합계로 나누기
    • 기하 평균 - 가중치만큼 제곱, 합계만큼 제곱근
    • 조화 평균 - 각각의 값에 역수에 가중치 곱하고 다시 역수
  • 세로 기반 데이터의 가중 평균을 계산하는 쿼리
SELECT
	*
	-- 가중치 산술 평균
	, 0.3 * recall + 0.7 * precision AS weighted_a_mean
	-- 가중치 기하 평균
	, POWER(recall, 0.3) * POWER(recall, 0.7) AS weighted_g_mean
	-- 가중치 조화 평균
	, 1.0 / ((0.3 / recall) + (0.7 / precision)) AS weighted_h_mean
FROM
	searched_evaluation_by_col
WHERE
	recall * precision > 0
ORDER BY
	path;
  • 가로 기반 테이블의 가중 평균을 계산하는 쿼리
WITH
weights AS (
	-- 가중치 마스터 테이블(가중치 합계 1.0)
	SELECT 'recall' AS index, 0.3 AS weight
	UNION ALL SELECT 'precision' AS index, 0.7 AS weight
)
SELECT
	e.path
	-- 가중치 산술 평균
	, SUM(w.weight * e.value) AS weighted_a_mean
	-- 가중치 기하 평균
	, POWER(10, SUM(w.weight * log10(e.value))) AS weighted_g_mean

	-- 가중치 조화 평균
	, 1.0 / SUM(w.weight / e.value)) AS weighted_h_mean
FROM
	search_evaluation_by_row AS e
	JOIN
		weights AS w
		ON e.index = w.index
WHERE
	e.value > 0
GROUP BY
	e.path
HAVING 
	COUNT(*) = 2
ORDER BY
	path;
  • Min-Max 정규화
    • 서로 다른 값의 폭을 가지는 각 지표를 0~1로 스케일로 정규화하는 방법
  • 열람 수와 구매 수에 Min-Max 정규화를 적용하는 쿼리
SELECT
	user_id
	, product
	, view_count AS v_count
	, purchase_count AS p_count
	, 1.0 * (view_count - MIN(view_count) OVER()) / NULLIF((MAX(view_count) OVER() - MIN(view_count) OVER()), 0) AS norm_p_count
FROM
	action_counts
ORDER BY
	user_id, product;

 

 

거대한 숫자 지표 쉽게 가공하기

  • 원래 값이 작은 경우와 원래 값이 큰 경우 있다고 할 때 숫자 차이 절댓값은 1이지만, 사람은 숫자가 작을 때 더 큰 차이라고 느낀다.
  • 사용자 액션 수, 경과일 수처럼 값이 크면 클수록 값이 차이가 큰 영향을 주지 않는 경우, 로그를 사용해 데이터 변환 시 쉽게 점수 가공할 수 있다.
  • 최종 액션일과의 차이가 크면 클수록 가중치를 작게하는 방법 : 날짜 차이에 역수를 취하는 방법이 있다.
  • 날짜 차이에 로그를 취하는 방정식 : y = 1/ log2(ax+2)
  • 다음 쿼리는 사용자들의 최종 접근일과 각 레코드와의 날짜 차이를 계산하는 쿼리
WITH
action_counts_with_diff_date AS (
	SELECT *
	-- 사용자별로 최종 접근일과 각 레코드의 날짜 차이 계산
	, MAX(dt::date) OVER(PARTITION BY user_id) AS last_access
	, MAX(dt::date) OVER(PARTITION BY user_id) - dt::date AS diff_date
	FROM
		actions_count_with_date
)
SELECT *
FROM
	action_counts_with_diff_date;
  • 날짜 차이에 따른 가중치를 계산하는 쿼리
WITH
action_counts_with_diff_date AS (
	SELECT *
	-- 사용자별로 최종 접근일과 각 레코드의 날짜 차이 계산
	, MAX(dt::date) OVER(PARTITION BY user_id) AS last_access
	, MAX(dt::date) OVER(PARTITION BY user_id) - dt::date AS diff_date
	FROM
		action_counts_with_date
)
, action_counts_with_weight AS (
	SELECT *
	-- 날짜 차이에 따른 가중치 계산(매개 변수 a = 0.1)
	, 1.0 / log(2, 0.1 * diff_date + 2) AS weight
	FROM 
		action_counts_with_diff_date
)
SELECT *
FROM
	action_counts_with_weight
ORDER BY
	user_id, product, diff_date DESC;