Featured image of post [MovieLens] 1. 데이터 살펴보기

[MovieLens] 1. 데이터 살펴보기

연구용 데이터셋으로 프로젝트 만들기

안녕하세요 :D 서칭하던 중 재미있어보이는 데이터를 발견했습니다.

미네소타 대학의 컴퓨터 과학 연구 그룹인 GroupLens에서 공개한 MovieLens 데이터셋으로, GroupLens의 연구 분야 중 하나인 인공지능 추천 시스템을 위해 만들어진 데이터셋입니다.

86,537편의 영화와 그와 관련된 33,832,162개의 평점2,328,315개의 태그로 구성되어있는데, 볼륨이 꽤 커서 직접 활용해보지 못했던 기술들을 실전에 유사한 환경에서 만들어 볼 수 있을것 같다는 생각이 들었습니다.

프로젝트는 처음 RDBMS 부터 시작하여, 일부 데이터들을 다른 기술들로 전환하여 점진적으로 개선하는 방식으로 진행해보려고 합니다.

오늘은 첫 걸음으로 데이터가 어떤 형식으로 구성되어있는지 간략하게 살펴보고, 어떤 형태로 설계할지 고민해보겠습니다.

Python3Pandas를 활용하여 간단히 확인를 했는데, 환경 구성과 같은 내용들은 Movielens 데이터셋 구조 확인 레포지토리를 참고해주시면 되겠습니다.

데이터셋에 대한 정보는 README.html에 상세히 나와있습니다.

데이터셋 구성

데이터셋은 userId, movieId를 공통으로 활용하고 있고, 앞서 설명드렸던 영화, 평점, 태그 포함 총 6개의 .csv 파일로 구성되어 있습니다.

userId

movieId

  • MovieLens에 등록된 영화로, 등급이나 태그가 하나 이상 등록된 영화

포함된 데이터 중 영화에 대한 태그 관련성 점수를 포함하는 데이터 셋인 태그 게놈(genome-scores.csv, genome-tags.csv)와 영화 데이터의 다른 소스에 연결하기 위한 식별자 정보가 담겨있는 links.csv 데이터는 활용하지 않겠습니다.

movies

영화 데이터는 movieId, title, genres 로 구성되어 있습니다.

영화의 제목들은 직접 입력되었거나, https://www.themoviedb.org/에서 가져온 데이터로, 괄호 안에 개봉년이 포함되어 있으나 정확하지 않을 수 있다고 합니다.

영화 장르들을 모두 포함하고있는 genres 컬럼은 아래의 19개 장르중 일부를 | 문자로 합친 형태로 구성된다고 합니다.

  • Action
  • Adventure
  • Animation
  • Children’s
  • Comedy
  • Crime
  • Documentary
  • Drama
  • Fantasy
  • Film-Noir
  • Horror
  • Musical
  • Mystery
  • Romance
  • Sci-Fi
  • Thriller
  • War
  • Western
  • (no genres listed)

장르가 없는 경우는 (no genres listed)가 입력되는 것으로 보아 NULL을 허용하지는 않는 것으로 보이네요.

1
2
3
4
5
import pandas as pd

df = pd.read_csv("dataset/movies.csv")

len(df) # 86537

파일을 pandas DataFrame으로 열고 건수를 확인해보니, 처음 언급한대로 86,537건이었습니다.

1
df.head()

최상단 5건을 확인해보니, README.html에 언급된 형식으로 데이터들이 저장되어 있는 것으로 보입니다.

movieIdtitlegenres
1Toy Stroy (1995)Adventure|Animation|Children|Comedy|Fantasy
2Jumanji (1995)Adventure|Children|Fantasy
3Grumpier Old Men (1995)Comedy|Romance
4Waiting to Exhale (1995)Comedy|Drama|Romance
5Father of the Bride Part II (1995)Comedy

genres 컬럼 같은 경우 공식 설명과는 달리 총 20개로 IMAX가 추가되있습니다. 누락되었나보네요.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
genres_list = df["genres"].map(lambda x: x.split("|"))
genres = set()

for l in genres_list:
    genres.update(l)

print(len(genres)) # 20
print(genres) 
'''
{
    'Adventure', 
    'War', 
    '(no genres listed)', 
    'Action', 
    'Mystery', 
    'Fantasy', 
    'Film-Noir', 
    'Comedy', 
    'Romance', 
    'IMAX', 
    'Crime', 
    'Musical', 
    'Animation', 
    'Drama', 
    'Horror', 
    'Children', 
    'Sci-Fi', 
    'Thriller', 
    'Western', 
    'Documentary'
}
'''
1
len(df[df['genres'].str.contains('IMAX')]) # 195

IMAX가 포함된 장르를 가지는 레코드들은 총 195건으로 많지는 않지만 데이터 분포 같은 것들이 중요한 요소는 아니기때문에 그냥 활용해도 괜찮을 것 같습니다.


눈에 띄는점은 genres 컬럼인데, 각 장르가 | 문자로 구분되어 여러개 항목이 들어있습니다. 장르들이 영어 오름차순으로 정렬되어 저장되어있는 것으로 보이네요.

이러한 경우 일반적인 RDBMS에서 genres를 조건으로 이용하여 SELECT하게 될 경우 %{keyword}%로 처리해야하므로 인덱스를 활용할 수 없고, 이로 인해 성능에서 문제가 발생할 수 있습니다.

genres 컬럼들의 각 장르들을 카테고리 테이블로 분리하고 movieId와 장르 간 1:N 테이블을 추가하는 방식으로 바꾼다면 장르를 이용한 검색 조건으로 인덱스를 사용할 수 없는 문제는 해결할 수 있습니다만…

genres가 여러개의 장르를 포함할 수 있다는 특성으로 인해 쿼리 작성할 때 조인을 사용하면 movies의 컬럼들이 중복됩니다.

이로 인해 서브 쿼리나 CONCAT과 같은 처리를 필요로 하거나, movieId로 장르를 조회하는 방식으로 처리해야 하므로 조회 성능이 떨어질 수 있음은 물론, 테이블 구조가 불필요하게 복잡해집니다.

genres의 데이터 형태와 한번 등록되면 변하지 않는 장르의 특성으로 볼 때, 단순 조회를 위한 컬럼이지 않을까 추측되고, 검색 기능은 검색 엔진을 별도로 구성하는 방식으로 처리되고 있을 것 같다는 예상을 해봅니다.

일단 RDBMS를 이용한 genres 컬럼 조건 검색은 배제하고 이후 더 좋은 방법을 고려하는 것이 좋겠습니다.

tags

태그 데이터는 userId, movieId, tag, timestamp로 구성되어 있습니다.

각 행은 한명의 사용자가 한 영화에 적용한 하나의 태그를 의미합니다.

tag는 단일 단어나 짧은 문구로 구성되며, 의미, 가치, 목적은 각 사용자의 목적에 의해 결정됩니다.

데이터는 총 2,328,315건으로 확인됩니다.

1
2
df = pd.read_csv("dataset/tags.csv")
df.head()
userIdmovieIdtagtimestamp
10260good vs evil1430666558
10260Harrison Ford1430666505
10260sci-fi1430666538
141221Al Pacino1311600756
141221mafia1311600746

tags.csv 파일을 읽어 최상단 5건을 확인해보면 1명의 사용자가 여러개의 영화에 여러개의 태그를 남길 수 있다는 것을 예상해볼 수 있습니다.

1
2
unique_tags = df["tag"].drop_duplicates()
len(unique_tags) # 153950

유니크한 tag 값은 153,950건 입니다.


tags.csv는 사용자가 한 영화에 적용한 태그로 1명의 유저가 여러 영화에 여러 종류의 태그를 적용할 수 있습니다.

전체 항목은 2,328,315건 이지만, tag 컬럼의 유니크한 값의 개수는 153,950개 인 것을 확인할 수 있습니다.

이러한 특성을 지닌 tag.csv의 데이터를 RDBMS에 저장하는 것이 좋은 방법인가 의구심이 들긴 하지만, 일반적인 서비스 정책으로 예상해 볼 때(특정 영화에 적용된 태그들 조회, 사용자가 활용한 태그들 조회, 사용자가 특정 영화에 남긴 태그 조회 등) 인덱스 설정만 잘 해준다면 화면에 노출 될 데이터 조회 성능에는 큰 이슈는 없어 보입니다.

처음엔 RDBMS로 처리하더라도 이후 새로운 요구가 있다면, 다른 방법으로 변경을 고려할 수 있겠습니다.

ratings

1
2
df = pd.read_csv("dataset/ratings.csv")
df.head()
userIdmovieIdratingtimestamp
114.01225734739
11104.01225865086
11584.01225733503
12604.51225735204
13565.01225735119

평가 데이터는 userId, movieId, rating, timestamp로 구성되어 있습니다.

각 행은 한 사용자가 한 영화에 남긴 점수를 의미합니다.

1
2
print(len(df)) # 33832162
print(min(df["rating"]), max(df["rating"])) # 0.5 5.0

총 데이터 수는 33,832,162건, 최대, 최소값은 각각 0.5, 5.0 입니다.


ratings.csv는 영화에 대한 사용자의 평점으로 영화 하나에 하나만 만들 수 있습니다.

영화 목록을 검색할 때 일반적으로 평균 평점이 포함되는데, 현재 상태로 테이블을 생성할 시 조회 처리에서 GROUP BY나 서브 쿼리를 통해 평균을 계산한다던가, 활용된 값들을 조회하는 처리가 필요하게 되고, 이 때문에 조회 성능에 문제가 발생할 수 있습니다.

이를 방지 위해 movies 테이블에 통계 정보에 활용될 컬럼을 만들어 두는 방식으로 설계하는 것도 고려해볼 수 있겠습니다.

rating 컬럼도 꼭 소수로 넣을 필요는 없어보이네요.

끝으로

간단하게 MovieLens 데이터셋의 주요 데이터들을 확인해봤습니다.

영화들에 대한 메타데이터가 없어 다채로운 기능들은 구현할 수 없을 것 같다는 점이 조금 아쉬운 마음에 찾아보니 MovieLens 데이터를 기반으로 TBMS API이용하여 만든 The Movies Dataset 데이터가 Kaggle에 공개되어 있었습니다.

하지만 데이터가 최신화가 안되어 사용할 수 없는 데이터가 많아 사용은 보류했습니다.

일단 생각하고 있는 기능들은 MovieLens 데이터으로 충분해서 필요는 없지만, 여유가 된다면 제가 최신화를 해봐도 괜찮을 것 같네요.

다음은 각 데이터의 특성을 고려하여 테이블을 설계하고, DB에 적재해볼예정입니다.

부족한 글 끝까지 읽어주셔서 감사합니다 :D