Captain's Log
← Observatory
Observatory·

문화 콘텐츠를 데이터로 — 수집부터 추천까지 파이프라인 만들기

영화 데이터를 모아 정리하고, 그 위에 추천·트렌드·시대별·인물별 분석을 올렸다. 핵심은 화려한 모델이 아니라 '같은 인물을 하나로 합치는' 지루한 전처리와, 분야를 넘어 재사용되는 데이터 모델이었다.

ProjectDataRecommendation

왜 만들었나

넷플릭스에서 영화를 고르고, 멜론에서 음악을 듣고, 서점에서 책을 살 때 — 그 뒤에는 늘 데이터가 있다. 제목·장르·줄거리·감독·배우·평점·인기. 이 데이터를 잘 모아 정리하면 네 가지 질문에 답할 수 있다.

  • 이걸 좋아했다면 다음엔 뭘 볼까? (추천)
  • 요즘 어떤 장르가 뜨고 있나? (트렌드)
  • 2000년대와 2020년대는 뭐가 달라졌나? (시대별)
  • 이 감독은 누구와 자주 작업했나? (인물별)

그래서 영화를 첫 대상으로, 데이터를 가져오는 것부터 분석 결과를 보여주는 화면까지 전 과정을 직접 만들어 보기로 했다.

파이프라인의 전체 그림

물이 정수장을 거쳐 깨끗해지듯, 데이터도 단계를 거쳐 분석 가능한 형태가 된다.

수집 → 원본저장 → 전처리(정규화·인물병합) → 정리된 표 → 분석 → 대시보드
TMDB   raw_payload      normalize/entity        work/person…   SQL+계산   Flask

원본은 손대지 않고 그대로 보관(raw_payload)하고, 다듬는 건 그다음 단계에서 한다. 이렇게 하면 전처리 로직을 바꿔도 다시 수집할 필요 없이 원본에서 재처리할 수 있다.

가장 중요한 건 모델이 아니라 데이터 구조였다

작품·인물·관계를 각각 다른 표로 분리한 게 이 프로젝트의 척추다.

work        -- 작품 공통 정보 (영화든 음악이든 책이든)
person      -- 인물 (병합된 표준 인물)
credit      -- 작품 ↔ 인물 관계 (감독/배우…)
metric      -- 시간별 인기·평점 (트렌드의 원천)

한 표에 다 넣으면 "봉준호가 만든 영화"를 셀 때 이름을 매번 반복해서 적어야 하고, 표기가 조금만 달라도 못 센다. 인물을 person에 한 번만 저장하고 credit으로 잇는 순간, 분야가 영화든 음악이든 같은 구조로 담긴다. 나중에 Spotify를 붙여도 domain만 바꾸면 된다.

지루하지만 결정적인 단계 — 엔티티 해소

전처리에서 제일 오래 붙잡은 건 화려한 알고리즘이 아니라 이거였다. 같은 사람이 데이터마다 다르게 적혀 있다.

"봉준호" / "Bong Joon-ho" / "Joon-ho Bong" — 전부 같은 사람.

이걸 하나로 합치지 못하면 "봉준호의 필모그래피"가 세 조각으로 쪼개진다. 그래서 3단계로 병합했다.

# 1) 같은 출처의 같은 ID면 무조건 동일인 (가장 신뢰도 높음)
# 2) 이름 정규화 키가 정확히 일치하면 재사용
# 3) 그래도 없으면 문자열 유사도(rapidfuzz)로 후보 비교
score = fuzz.token_sort_ratio(key, existing_key)
if score >= 92:   # 임계값 이상이면 동일인
    return existing_id

결과를 검증했더니 송강호는 5편에 나오지만 person 표엔 정확히 한 명으로 저장됐다. 이 한 줄이 뒤의 "인물별 분석"을 통째로 떠받친다.

추천 — 줄거리를 숫자로 바꾸기

컴퓨터는 글자를 모르니 영화를 숫자 벡터로 바꿔야 한다(임베딩). 처음엔 장르·태그만 썼는데, 줄거리까지 반영하니 결과가 눈에 띄게 자연스러워졌다.

한국어라 형태소 분석기 없이 문자 단위 n-gram TF-IDF를 썼다. 장르·태그(단어 단위)와 줄거리(문자 단위)를 각각 정규화한 뒤 가중 결합했다.

tag_mat = normalize(TfidfVectorizer().fit_transform(tag_docs))
syn_mat = normalize(TfidfVectorizer(analyzer="char_wb",
                                    ngram_range=(2, 3)).fit_transform(syn_docs))
combined = hstack([tag_mat * (1 - w), syn_mat * w])   # w = 줄거리 비중

그리고 코사인 유사도로 비슷한 작품을 골랐다. "기생충" 기준으로, 줄거리를 반영하자 가족 서사인 "미나리"가 상위로 올라왔다.

기생충 →  0.143 미나리 · 0.123 설국열차 · 0.119 부산행 · 0.098 괴물

나머지 세 시스템, 그리고 화면

  • 트렌드: metric을 시점별로 쌓아 인기 급상승과 연도별 장르 추이를 봤다.
  • 시대별: 개봉연도를 10년 단위로 묶어 작품 수·평균 평점·러닝타임을 비교했다.
  • 인물별: 같은 작품에 함께 오른 사람들을 세어 협업 네트워크를 만들었다. (봉준호–송강호 3회)

마지막으로 이 네 결과를 matplotlib 차트와 Flask 대시보드/API로 묶어, 브라우저에서 영화를 고르면 추천이 막대그래프로 바로 나오게 했다.

회고

  • 모델보다 데이터 구조. 추천 알고리즘 자체는 몇 줄이었다. 진짜 일은 인물을 하나로 합치고, 분야를 넘어 재사용되는 표를 설계하는 데 있었다.
  • 원본은 원본대로 남겨라. raw를 그대로 보관한 덕에 전처리를 몇 번이고 다시 돌릴 수 있었다.
  • 한계도 솔직하게. 지금은 샘플 12편이라 통계적 의미는 제한적이고, 추천도 콘텐츠 기반뿐이다. 실데이터를 붙이고 협업 필터링을 얹는 게 다음 항해다.