import re, html
from math import comb
import numpy as np
import pandas as pd
import scipy.sparse as sp
from scipy.stats import binom
import matplotlib.pyplot as plt
from matplotlib import font_manager
from matplotlib.patches import Patch
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
SEED = 42
pd.set_option("display.width", 120)
_installed = {f.name for f in font_manager.fontManager.ttflist} # 그래프의 한글 표시: 설치된 첫 한글 폰트 (Windows → macOS → Linux)
plt.rcParams["font.family"] = next((f for f in ["Malgun Gothic", "AppleGothic", "NanumGothic"] if f in _installed), "DejaVu Sans")
plt.rcParams["axes.unicode_minus"] = False
DATA_DIR = "ml-1m"
ratings = pd.read_csv(f"{DATA_DIR}/ratings.dat", sep="::", engine="python",
names=["user", "item", "rating", "ts"])
users = pd.read_csv(f"{DATA_DIR}/users.dat", sep="::", engine="python",
names=["user", "gender", "age", "occupation", "zip"])
movies = pd.read_csv(f"{DATA_DIR}/movies.dat", sep="::", engine="python",
names=["item", "title", "genres"], encoding="latin-1")
assert len(ratings) == 1_000_209 and len(users) == 6_040 and len(movies) == 3_883
print(ratings.shape, users.shape, movies.shape)
Part A. 데이터의 전체 지도와 이벤트 로그
DATA_MAP = {
"ratings.user": "① 상호작용", # 누가 (이벤트의 주체)
"ratings.item": "① 상호작용", # 무엇에
"ratings.rating": "① 상호작용", # 얼마나 (explicit 값)
"ratings.ts": "④ 컨텍스트", # 언제 → 시간대·요일 등 '지금 이 순간'의 정보
"movies.item": "② 아이템 메타데이터",
"movies.title": "② 아이템 메타데이터", # 비구조 텍스트 (+ 개봉연도 포함)
"movies.genres": "② 아이템 메타데이터", # 구조적 다중 범주
"users.user": "③ 유저 정보",
"users.gender": "③ 유저 정보",
"users.age": "③ 유저 정보",
"users.occupation": "③ 유저 정보",
"users.zip": "③ 유저 정보",
}
① 상호작용 : user, item, rating
(rating.ts는 context를 위한 정보이기 때문에 아님)
② 아이템 메타데이터 : item, title, genres
(item 자체의 data이므로 영화의 설명과 관련된 것 존재)
③ 유저 정보 : user, gender, age, occupation, zip
(user 자체의 data이므로 개인의 취향과 관련된 것 존재)
④ 컨텍스트 : ts
(MovieLens에는 기기·위치 정보 X, 시간(ts)만 존재 → ts에서 파생한 요일·시간대만 사용)

이벤트 로그 :
def to_event_log(ratings):
t = pd.to_datetime(ratings["ts"], unit="s")
log = pd.DataFrame({
"유저": ratings["user"],
"아이템": ratings["item"],
"행동": "별점", # MovieLens 1M의 행동 종류는 '별점'
"값": ratings["rating"],
"시각": t,
"컨텍스트": WEEKDAY_KO[t.dt.dayofweek] + " · " + time_of_day(t.dt.hour).astype(str),
})

Part B. 상호작용 데이터
: Explicit feedback + Implicit feedback


★4–5가 57.52%, ★1–2는 16.37%
평균(3.58)이 척도 중간(3)보다 높고 중앙값은 4
→ 분포가 높은 점수 쪽에 쏠려 있다 (★4–5에 쏠린 J자형)
이것은 MNAR로 설명 가능하다.
유저는 좋아할 것 같은 영화를 골라 보고 평가하므로, 관측된 평점의 평균이 '모든 영화에 대한 취향'을 과대평가한 상황일 수 있다.
하지만 평가하지 않은 칸의 평점은 로그에 없으므로, 이 설명을 관측 로그만으로 직접 검증할 수는 없다.


같은 데이터 안에 평균 ≥ 4.2인 후한 평가자 658명(10.89%)과 평균 ≤ 3.0인 짠 평가자 379명(6.27%)이 섞여 있다.
같은 ★4라도 후한 유저에게는 '보통', 짠 유저에게는 '극찬'이므로 원점수를 그대로 비교할 수 없다.
→ 유저별 평균을 빼는 중심화가 필요해보임
평가 수 중앙값은 짠 평가자가 142건, 후한 평가자가 62건이다(전체 96건).
그래서 유저 평균의 평균(3.70)이 평점 전체 평균(3.58)보다 높다.
헤비 유저는 평가 건수가 많아 전체 평균에서 비중이 큰데,
평가를 많이 하는 유저일수록 짠 경향이 있는 것을 확인할 수 있다.
→ '평균 평점'을 평가 단위로 내느냐 유저 단위로 내느냐 선택할 필요 있음



평가를 '평가함 = 1'로 바꿔 6,040 × 3,706 CSR(Compressed Sparse Row) 행렬을 만들었다.
대부분의 칸이 비어 있는 희소 행렬을 효율적으로 저장하는 방식이다.

하지만 implicit 이진 행렬을 쓰면 칸 수는 그대로인데 ★1의 "싫다"까지 양성이 된다.
즉, 좋아해서 평가한 영화와 싫어서 평가한 영화를 구분할 수 없게 되는 상황이 된다.
(= 평점 대신 1을 넣는 단순 대체를 경계해야 하는 이유)
