import itertools
from surprise import accuracy
from collections import defaultdict

class RecommenderMetrics:

    # ─────────────────────────────
    # MAE: 평균 절대 오차
    # 예측 평점과 실제 평점의 절댓값 평균
    # ─────────────────────────────
    def MAE(predictions):
        return accuracy.mae(predictions, verbose=False)

    # ─────────────────────────────
    # RMSE: 평균 제곱근 오차
    # 예측 평점과 실제 평점의 제곱 평균의 루트
    # ─────────────────────────────
    def RMSE(predictions):
        return accuracy.rmse(predictions, verbose=False)

    # ─────────────────────────────
    # GetTopN: 사용자별 상위 N개 추천 목록 생성
    # n: 추천 개수 (기본 10개)
    # minimumRating: 최소 예측 평점 기준 (기본 4.0)
    # ─────────────────────────────
    def GetTopN(predictions, n=10, minimumRating=4.0):
        topN = defaultdict(list)

        for userID, movieID, actualRating, estimatedRating, _ in predictions:
            # 예측 평점이 최소 기준 이상인 것만 추가
            if (estimatedRating >= minimumRating):
                topN[int(userID)].append((int(movieID), estimatedRating))

        for userID, ratings in topN.items():
            # 예측 평점 높은 순으로 정렬
            ratings.sort(key=lambda x: x[1], reverse=True)
            # 상위 N개만 유지
            topN[int(userID)] = ratings[:n]

        return topN

    # ─────────────────────────────
    # HitRate: 적중률
    # Leave-One-Out 방식으로 숨긴 항목이
    # 추천 목록에 있으면 적중
    # ─────────────────────────────
    def HitRate(topNPredicted, leftOutPredictions):
        hits = 0
        total = 0

        for leftOut in leftOutPredictions:
            userID = leftOut[0]
            leftOutMovieID = leftOut[1]

            # 숨긴 영화가 추천 목록에 있는지 확인
            hit = False
            for movieID, predictedRating in topNPredicted[int(userID)]:
                if (int(leftOutMovieID) == int(movieID)):
                    hit = True
                    break

            if (hit):
                hits += 1
            total += 1

        # 적중률 = 적중 수 / 전체 사용자 수
        return hits / total

    # ─────────────────────────────
    # CumulativeHitRate: 누적 적중률
    # 실제 평점이 기준(ratingCutoff) 이상인 것만 적중으로 인정
    # 사용자가 실제로 좋아하는 영화를 추천했는지 측정
    # ─────────────────────────────
    def CumulativeHitRate(topNPredicted, leftOutPredictions, ratingCutoff=0):
        hits = 0
        total = 0

        for userID, leftOutMovieID, actualRating, estimatedRating, _ in leftOutPredictions:
            # 실제 평점이 기준 이상인 것만 측정
            if (actualRating >= ratingCutoff):
                hit = False
                for movieID, predictedRating in topNPredicted[int(userID)]:
                    if (int(leftOutMovieID) == movieID):
                        hit = True
                        break
                if (hit):
                    hits += 1
                total += 1

        return hits / total

    # ─────────────────────────────
    # RatingHitRate: 평점별 적중률
    # 평점 구간별로 적중률을 나눠서 출력
    # 어떤 평점대의 영화를 잘 추천하는지 분석
    # ─────────────────────────────
    def RatingHitRate(topNPredicted, leftOutPredictions):
        hits = defaultdict(float)
        total = defaultdict(float)

        for userID, leftOutMovieID, actualRating, estimatedRating, _ in leftOutPredictions:
            hit = False
            for movieID, predictedRating in topNPredicted[int(userID)]:
                if (int(leftOutMovieID) == movieID):
                    hit = True
                    break
            if (hit):
                # 실제 평점별로 적중 수 카운트
                hits[actualRating] += 1
            total[actualRating] += 1

        # 평점별 적중률 출력
        # 이상적: 5점짜리 적중률이 가장 높아야 함
        for rating in sorted(hits.keys()):
            print(rating, hits[rating] / total[rating])

    # ─────────────────────────────
    # AverageReciprocalHitRank: ARHR
    # 적중 위치가 상위일수록 높은 점수
    # 공식: Σ(1/rank) / 전체 사용자 수
    # ─────────────────────────────
    def AverageReciprocalHitRank(topNPredicted, leftOutPredictions):
        summation = 0
        total = 0

        for userID, leftOutMovieID, actualRating, estimatedRating, _ in leftOutPredictions:
            hitRank = 0
            rank = 0
            for movieID, predictedRating in topNPredicted[int(userID)]:
                rank = rank + 1
                if (int(leftOutMovieID) == movieID):
                    hitRank = rank  # 적중한 순위 저장
                    break

            if (hitRank > 0):
                # 1번 적중 → 1/1 = 1.0
                # 5번 적중 → 1/5 = 0.2
                summation += 1.0 / hitRank
            total += 1

        # ARHR = 전체 1/rank 합계 / 전체 사용자 수
        return summation / total

    # ─────────────────────────────
    # UserCoverage: 제공률
    # 최소 하나 이상의 좋은 추천을 받은 사용자 비율
    # ratingThreshold: 좋은 추천 기준 평점
    # ─────────────────────────────
    def UserCoverage(topNPredicted, numUsers, ratingThreshold=0):
        hits = 0
        for userID in topNPredicted.keys():
            hit = False
            for movieID, predictedRating in topNPredicted[userID]:
                # 기준 평점 이상인 추천이 하나라도 있으면 적중
                if (predictedRating >= ratingThreshold):
                    hit = True
                    break
            if (hit):
                hits += 1

        # 제공률 = 좋은 추천 받은 사용자 수 / 전체 사용자 수
        return hits / numUsers

    # ─────────────────────────────
    # Diversity: 다양성
    # 추천 목록 내 항목들의 평균 유사도를 1에서 뺀 값
    # 공식: Diversity = 1 - S (S = 평균 유사도)
    # ─────────────────────────────
    def Diversity(topNPredicted, simsAlgo):
        n = 0
        total = 0
        # 유사도 행렬 계산
        simsMatrix = simsAlgo.compute_similarities()

        for userID in topNPredicted.keys():
            # 추천 목록 내 모든 영화 쌍 조합
            pairs = itertools.combinations(topNPredicted[userID], 2)
            for pair in pairs:
                movie1 = pair[0][0]
                movie2 = pair[1][0]
                # 내부 ID로 변환
                innerID1 = simsAlgo.trainset.to_inner_iid(str(movie1))
                innerID2 = simsAlgo.trainset.to_inner_iid(str(movie2))
                # 두 영화 간 유사도
                similarity = simsMatrix[innerID1][innerID2]
                total += similarity
                n += 1

        # 평균 유사도
        S = total / n
        # 다양성 = 1 - 평균 유사도
        return (1 - S)

    # ─────────────────────────────
    # Novelty: 참신성
    # 추천 항목의 평균 인기 순위
    # 순위가 높을수록(비인기) 참신성 높음
    # ─────────────────────────────
    def Novelty(topNPredicted, rankings):
        n = 0
        total = 0
        for userID in topNPredicted.keys():
            for rating in topNPredicted[userID]:
                movieID = rating[0]
                # 영화의 인기 순위 가져오기
                # 순위가 높은 숫자 = 비인기 영화 = 참신성 높음
                rank = rankings[movieID]
                total += rank
                n += 1

        # 참신성 = 추천 항목들의 평균 인기 순위
        return total / n

+ Recent posts