BigGorillaを試してみよう

A Hands on Tutorial

このチュートリアルではBigGorillaが提供するエンティティマッチングのためのツールの使い方を学習します。ここでは異なるデータソースから取得した二つの映画に関するデータセットを統合します。このステップはチュートリアルの最後 (Part 4) で説明していますが、事前のデータ準備のためにPart1~3において既存のPythonパッケージの利用方法も解説しています。

Part 1: データ収集

Part1ではurllibを使ってこのチュートリアルに必要なデータセットをダウンロードします。urllibはウェブからデータを取り込むためによく使われているPythonパッケージです。

ステップ1: “Kaggle 5000 Movie Dataset”のダウンロード

ダウンロードするデータセットは下記のコードで指定されているurlを含むcsvファイルです。

In [1]:
# Importing urlib
import urllib
import os

# Creating the data folder
if not os.path.exists('./data'):
	os.makedirs('./data')

# Obtaining the dataset using the url that hosts it
kaggle_url = 'https://github.com/sundeepblue/movie_rating_prediction/raw/master/movie_metadata.csv'
if not os.path.exists('./data/kaggle_dataset.csv'):     # avoid downloading if the file exists
	response = urllib.urlretrieve(kaggle_url, './data/kaggle_dataset.csv')

ステップ2: “IMDB Plain Text Data”のダウンロード

IMDB Plain Text Data (こちらを参照)はそれぞれ映画の1つまたは複数の属性を記述するファイルの集まりです。ここでは映画属性の1つのサブセットに焦点を当てるので、関係するのは下記のファイルの一部のみです:

  • genres.list.gz
  • ratings.list.gz

** 注: 上記のファイルの合計サイズは約30Mです。 下記のコードの実行には数分かかります。

In [2]:
import gzip

# Obtaining IMDB's text files
imdb_url_prefix = 'ftp://ftp.funet.fi/pub/mirrors/ftp.imdb.com/pub/'
imdb_files_list = ['genres.list.gz', 'ratings.list.gz']
for name in imdb_files_list:
	if not os.path.exists('./data/' + name):
		response = urllib.urlretrieve(imdb_url_prefix + name, './data/' + name)
		urllib.urlcleanup()   # urllib fails to download two files from a ftp source. This fixes the bug!
		with gzip.open('./data/' + name) as comp_file, open('./data/' + name[:-3], 'w') as reg_file:
			file_content = comp_file.read()
			reg_file.write(file_content)

ステップ3: “IMDB Prepared Data”のダウンロード

このチュートリアルではgenres.list.gzファイルとratings.list.gzファイルのコンテンツを統合する方法を学びます。しかし、このチュートリアルをわかりやすくするために、”IMDB Plain Text Data”の中のすべてのファイルについて同じプロセスを含めることは避けています。”IMDB Prepared Data”はこのチュートリアルの後の段階で使用する”IMDB Plain Text Data”からのいくつかのファイルを統合することによって収集したデータセットです。下記のコードはこのデータセットをダウンロードします。

In [3]:
imdb_url = 'https://anaconda.org/BigGorilla/datasets/1/download/imdb_dataset.csv'
if not os.path.exists('./data/imdb_dataset.csv'):     # avoid downloading if the file exists
	response = urllib.urlretrieve(kaggle_url, './data/imdb_dataset.csv')

Part 2: データ抽出

“Kaggle 5000 Movie Dataset”は.csvファイルに格納されており、このファイルはすぐに使用できるように構造化されています。これに対して”IMDB Plain Text Data”は半構造化テキストファイルの集まりであり、データを抽出するための処理が必要です。各ファイルの最初の数行を見れば、各ファイルの形式が異なっており、別々に処理しなければならないことがわかります。

“ratings.list”データファイルの内容

In [4]:
with open("./data/ratings.list") as myfile:
	head = [next(myfile) for x in range(38)]
print (''.join(head[28:38]))   # skipping the first 28 lines as they are descriptive headers
      0000000125  1728818   9.2  The Shawshank Redemption (1994)
	  0000000125  1181412   9.2  The Godfather (1972)
	  0000000124  810055   9.0  The Godfather: Part II (1974)
	  0000000124  1714042   8.9  The Dark Knight (2008)
	  0000000133  461310   8.9  12 Angry Men (1957)
	  0000000133  885509   8.9  Schindler's List (1993)
	  0000000123  1354135   8.9  Pulp Fiction (1994)
	  0000000124  1241908   8.9  The Lord of the Rings: The Return of the King (2003)
	  0000000123  514540   8.9  Il buono, il brutto, il cattivo (1966)
	  0000000133  1380148   8.8  Fight Club (1999)

“genres.list”データファイルの内容

In [5]:
with open("./data/genres.list") as myfile:
	head = [next(myfile) for x in range(392)]
print (''.join(head[382:392]))   # skipping the first 382 lines as they are descriptive header
"!Next?" (1994)						Documentary
"#1 Single" (2006)					Reality-TV
"#15SecondScare" (2015)					Horror
"#15SecondScare" (2015)					Short
"#15SecondScare" (2015)					Thriller
"#15SecondScare" (2015) {Who Wants to Play with the Rabbit? (#1.2)}	Drama
"#15SecondScare" (2015) {Who Wants to Play with the Rabbit? (#1.2)}	Horror
"#15SecondScare" (2015) {Who Wants to Play with the Rabbit? (#1.2)}	Short
"#15SecondScare" (2015) {Who Wants to Play with the Rabbit? (#1.2)}	Thriller
"#1MinuteNightmare" (2014)				Horror

ステップ1: “genres.list”からの情報の抽出

このステップの目標は”movies.list”から映画のタイトルとその作成年を抽出し、抽出されたデータをDataframeへ格納することです。Dataframe (Pythonパッケージpandasに入っています)はBigGorillaで推奨される主要なデータプロファイリングおよびクリーニングの1つです。テキストから希望する情報を抽出するために、Pythonパッケージ”re“に実装されている正規表現を使用します。

In [6]:
import re
import pandas as pd

with open("./data/genres.list") as genres_file:
	raw_content = genres_file.readlines()
	genres_list = []
	content = raw_content[382:]
	for line in content:
		m = re.match(r'"?(.*[^"])"? \(((?:\d|\?){4})(?:/\w*)?\).*\s((?:\w|-)+)', line.strip())
		genres_list.append([m.group(1), m.group(2), m.group(3)])
	genres_data = pd.DataFrame(genres_list, columns=['movie', 'year', 'genre'])

ステップ2: “ratings.list”からの情報の抽出

In [7]:
with open("./data/ratings.list") as ratings_file:
	raw_content = ratings_file.readlines()
	ratings_list = []
	content = raw_content[28:]
	for line in content:
		m = re.match(r'(?:\d|\.|\*){10}\s+\d+\s+(1?\d\.\d)\s"?(.*[^"])"? \(((?:\d|\?){4})(?:/\w*)?\)', line.strip())
		if m is None: continue
		ratings_list.append([m.group(2), m.group(3), m.group(1)])
	ratings_data = pd.DataFrame(ratings_list, columns=['movie', 'year', 'rating'])

他のデータファイルの情報にも関心がある場合、そのデータファイルにも情報抽出手順を繰り返す必要があります。ここでは(チュートリアルを複雑にしないために)、映画のジャンルと評価にだけ関心があると仮定します。上記のコードは、これらの2つの属性に関する抽出データを2つのデータフレーム(genres_list と ratings_list)に格納します。

Part 3: データプロファイリングとクリーニング

データ準備のこの段階におけるハイレベルの目標は、これまでの作業で収集および抽出してきたデータを詳しく調べることです。これによってデータに習熟し、データをクリーニングまたは変換する方法を理解し、その結果、データ統合タスクの次のステップのためにデータを準備することができるようになります。

ステップ1: “Kaggle 5000 Movie Dataset”のローディング

データフレーム(Pythonパッケージpandasに入っています)はデータ探索およびデータプロファイリングに適しています。このチュートリアルのPart2で”IMDB Plain Text Data”からの抽出データをデータフレームに格納しました。”Kaggle 5000 Movies Dataset” もデータフレームにロードして、すべてのデータセットに同じデータプロファイリング手順を実行するとよいでしょう。

In [8]:
import pandas as pd

# Loading the Kaggle dataset from the .csv file (kaggle_dataset.csv)
kaggle_data = pd.read_csv('./data/kaggle_dataset.csv')

ステップ2: いくつかの基本的な統計の計算(プロファイリング)

各データフレームに何本の映画がリストされているかを調べましょう。

In [9]:
print ('Number of movies in kaggle_data: {}'.format(kaggle_data.shape[0]))
print ('Number of movies in genres_data: {}'.format(genres_data.shape[0]))
print ('Number of movies in ratings_data: {}'.format(ratings_data.shape[0]))
Number of movies in kaggle_data: 5043
Number of movies in genres_data: 2384400
Number of movies in ratings_data: 691621

また、データに重複(2回以上現れる映画)があるか調べることもできます。同じ映画タイトルと作成年の別のエントリが見つかった場合、そのエントリを重複とみなします。

In [10]:
print ('Number of duplicates in kaggle_data: {}'.format(
	sum(kaggle_data.duplicated(subset=['movie_title', 'title_year'], keep=False))))
print ('Number of duplicates in genres_data: {}'.format(
	sum(genres_data.duplicated(subset=['movie', 'year'], keep=False))))
print ('Number of duplicates in ratings_data: {}'.format(
	sum(ratings_data.duplicated(subset=['movie', 'year'], keep=False))))
Number of duplicates in kaggle_data: 241
Number of duplicates in genres_data: 1807712
Number of duplicates in ratings_data: 286515

ステップ3: 重複の処理(クリーニング)

重複を処理する多くの戦略があります。ここでは重複を処理する単純な方法を利用します。つまり、重複するエントリのうち最初のものだけを残し、それ以外を削除します。

In [11]:
kaggle_data = kaggle_data.drop_duplicates(subset=['movie_title', 'title_year'], keep='first').copy()
genres_data = genres_data.drop_duplicates(subset=['movie', 'year'], keep='first').copy()
ratings_data = ratings_data.drop_duplicates(subset=['movie', 'year'], keep='first').copy()

ステップ4: テキストのノーマライズ(クリーニング)

映画データベースを統合するための主要な属性は映画タイトルです。したがってこれらのタイトルをノーマライズしておくことが重要です。下記のコードはすべての映画タイトルを小文字に変換し、”‘”、”?”などの特殊文字を除去し、一部の特殊文字を置換します(たとえば”&”を”and”に)。

In [12]:
def preprocess_title(title):
	title = title.lower()
	title = title.replace(',', ' ')
	title = title.replace("'", '')    
	title = title.replace('&', 'and')
	title = title.replace('?', '')
	title = title.decode('utf-8', 'ignore')
	return title.strip()

kaggle_data['norm_movie_title'] = kaggle_data['movie_title'].map(preprocess_title)
genres_data['norm_movie'] = genres_data['movie'].map(preprocess_title)
ratings_data['norm_movie'] = ratings_data['movie'].map(preprocess_title)

ステップ5: いくつかの例を見てみましょう

ここでの目標は簡易サニティチェックのために各データセットについていくつかのサンプルエントリを調べることです。わかりやすくするために、このステップは”Kaggle 5000 Movies Dataset”についてのみ示します。このデータセットはkaggle_dataデータフレームに格納されています。

In [13]:
kaggle_data.sample(3, random_state=0)
Out[13]:
color director_name num_critic_for_reviews duration director_facebook_likes actor_3_facebook_likes actor_2_name actor_1_facebook_likes gross genres language country content_rating budget title_year actor_2_facebook_likes imdb_score aspect_ratio movie_facebook_likes norm_movie_title
4422 Color Simeon Rice 6.0 93.0 6.0 56.0 Lisa Brave 393.0 NaN Action|Horror|Thriller English USA R 1500000.0 2014.0 191.0 5.5 2.35 307 unsullied
1022 Color Doug Liman 214.0 108.0 218.0 405.0 Ty Burrell 6000.0 9528092.0 Biography|Drama|Thriller English USA PG-13 22000000.0 2010.0 3000.0 6.8 2.35 9000 fair game
3631 Color Jonathan Levine 147.0 99.0 129.0 362.0 Aaron Yoo 976.0 2077046.0 Comedy|Drama|Romance English USA R 6000000.0 2008.0 617.0 7.0 2.35 0 the wackness

3 rows × 29 columns

データを調べることによってデータをクリーニングするのにどの方法が適切かを判断することができます。たとえば上記の小さなサンプルデータによって、title_year属性が浮動小数点数(有理数)として格納されていることがわかります。title_yearを文字列に変換し、欠落しているタイトル、作成年を“?”に置換するためのクリーニングステップを追加することができます。

In [14]:
def preprocess_year(year):
	if pd.isnull(year):
		return '?'
	else:
		return str(int(year))

kaggle_data['norm_title_year'] = kaggle_data['title_year'].map(preprocess_year)
kaggle_data.head()
Out[14]:
color director_name num_critic_for_reviews duration director_facebook_likes actor_3_facebook_likes actor_2_name actor_1_facebook_likes gross genres country content_rating budget title_year actor_2_facebook_likes imdb_score aspect_ratio movie_facebook_likes norm_movie_title norm_title_year
0 Color James Cameron 723.0 178.0 0.0 855.0 Joel David Moore 1000.0 760505847.0 Action|Adventure|Fantasy|Sci-Fi USA PG-13 237000000.0 2009.0 936.0 7.9 1.78 33000 avatar 2009
1 Color Gore Verbinski 302.0 169.0 563.0 1000.0 Orlando Bloom 40000.0 309404152.0 Action|Adventure|Fantasy USA PG-13 300000000.0 2007.0 5000.0 7.1 2.35 0 pirates of the caribbean: at worlds end 2007
2 Color Sam Mendes 602.0 148.0 0.0 161.0 Rory Kinnear 11000.0 200074175.0 Action|Adventure|Thriller UK PG-13 245000000.0 2015.0 393.0 6.8 2.35 85000 spectre 2015
3 Color Christopher Nolan 813.0 164.0 22000.0 23000.0 Christian Bale 27000.0 448130642.0 Action|Thriller USA PG-13 250000000.0 2012.0 23000.0 8.5 2.35 164000 the dark knight rises 2012
4 NaN Doug Walker NaN NaN 131.0 NaN Rob Walker 131.0 NaN Documentary NaN NaN NaN NaN 12.0 7.1 NaN 0 star wars: episode vii – the force awakens ?

5 rows × 30 columns

Part 4: データマッチングとマージング

ここでの主要な目標は種々のソースから取得したデータをマッチングして、1つの包括的なデータセットを作成することです。Part3ではすべてのデータセットを1つのデータフレームに変換し、それを使ってデータをクリーニングしました。Part4ではここまでで作成してきたデータに対して同じデータフレームを引き続き使用します。

ステップ1: “IMDB Plain Text Data”ファイルの統合

ratings_datagenres_dataの両方のデータフレームが同じソース(“the IMDB Plain Text data”)からのデータを含んでいることに注意してください。したがってこれらのデータフレームに格納されているデータの間に不一致はないと想定しており、それらを結合するために必要なことは同じタイトルと作成年を共有するエントリをマッチングすることだけです。この単純な”完全一致”はデータフレームを使用することによって簡単に処理できます。

In [15]:
brief_imdb_data = pd.merge(ratings_data, genres_data, how='inner', on=['norm_movie', 'year'])
brief_imdb_data.head()
Out[15]:
movie_x year rating norm_movie movie_y genre
0 The Shawshank Redemption 1994 9.2 the shawshank redemption The Shawshank Redemption Crime
1 The Godfather 1972 9.2 the godfather The Godfather Crime
2 The Godfather: Part II 1974 9.0 the godfather: part ii The Godfather: Part II Crime
3 The Dark Knight 2008 8.9 the dark knight The Dark Knight Action
4 12 Angry Men 1957 8.9 12 angry men 12 Angry Men Crime

上記で作成したデータセットをbrief_imdb_dataと呼びます。なぜならこれは2つの属性(genreとrating)だけを含むからです。これ以降は、”IMDB Plain Text Data”からのいくつかのファイルを統合することによって作成したより包括的なIMDBデータセットのバージョンを使用します。すでにこのチュートリアルのPart1を完了している場合、このデータセットはすでにダウンロードされており“data”フォルダ内の“imdb_dataset.csv”に格納されています。下記のコードはこのデータセットをロードし、映画のタイトルと作成年の前処理を実行し、重複を除去し、データセットのサイズを表示します。

In [16]:
# reading the new IMDB dataset
imdb_data = pd.read_csv('./data/imdb_dataset.csv')
# let's normlize the title as we did in Part 3 of the tutorial
imdb_data['norm_title'] = imdb_data['title'].map(preprocess_title)
imdb_data['norm_year'] = imdb_data['year'].map(preprocess_year)
imdb_data = imdb_data.drop_duplicates(subset=['norm_title', 'norm_year'], keep='first').copy()
imdb_data.shape
Out[16]:
(869178, 27)

ステップ2: KaggleおよびIMDBデータセットの統合

2つのデータセットを統合する単純なアプローチでは、単に同じ映画タイトルと作成年を共有するエントリを結合します。下記のコードではこの単純なアプローチで4,248件のマッチが見つかることがわかります。

In [17]:
data_attempt1 = pd.merge(imdb_data, kaggle_data, how='inner', left_on=['norm_title', 'norm_year'],
						 right_on=['norm_movie_title', 'norm_title_year'])
data_attempt1.shape
Out[17]:
(4248, 57)

しかし、IMDBおよびKaggleデータセットは異なるソースから収集されたものであり、2つのデータセットの中で映画のタイトルが少し異なることが考えられます(“Wall.E”と”WallE”)。そのような一致を見つけるには、映画のタイトルの類似性を見つけ、非常に類似しているタイトルを同じエンティティとみなすことができます。BigGorillaでは、2つのデータセット間の類似エンティティを結合するツールとして、Pythonパッケージpy_stringsimjoinを推奨しています。下記のコードではpy_stringsimjoinを使って、違いが1文字以内(2つのタイトルを同じにするために必要な変更/追加/削除が1文字以内)であるすべてのタイトルを一致とみなします。類似エンティティの結合が完了した後、同じ年に作成されたタイトルのペアだけを選択します。

In [18]:
import py_stringsimjoin as ssj
import py_stringmatching as sm

imdb_data['id'] = range(imdb_data.shape[0])
kaggle_data['id'] = range(kaggle_data.shape[0])
similar_titles = ssj.edit_distance_join(imdb_data, kaggle_data, 'id', 'id', 'norm_title',
										'norm_movie_title', l_out_attrs=['norm_title', 'norm_year'],
										 r_out_attrs=['norm_movie_title', 'norm_title_year'], threshold=1)
# selecting the entries that have the same production year
data_attempt2 = similar_titles[similar_titles.r_norm_title_year == similar_titles.l_norm_year]
data_attempt2.shape
0%                          100%
[##############################] | ETA: 00:00:00
Total time elapsed: 00:02:46
Out[18]:
(4689, 8)

類似エンティティの結合を使って4,689のタイトルのマッチが見つかりました。類似エンティティの結合によってマッチングされたタイトルで、完全に同じではないものを調べてみましょう。

In [19]:
data_attempt2[data_attempt2.l_norm_title != data_attempt2.r_norm_movie_title].head()
Out[19]:
_id l_id r_id l_norm_title l_norm_year r_norm_movie_title r_norm_title_year _sim_score
144 144 852736 46 world war v 2013 world war z 2013 1.0
162 162 281649 56 grave 2012 brave 2012 1.0
180 180 831490 58 walle 2008 wall·e 2008 1.0
236 236 816188 67 upe 2009 up 2009 1.0
243 243 817366 67 ut 2009 up 2009 1.0

“walle”と”wall.e”のようなインスタンスは正しくマッチングされていますが、この技法では若干の誤りが発生します(たとえば”grave”と”brave”)。ここから次のような問題が出てきます:「データマッチングにどのようなメソッドを使うべきか?」、マッチングの適切性をどのように判断できるのか?」。この問題に対処するために、BigGorillaではPythongパッケージpy_entitymatchingの使用を推奨しています。これはMagellan プロジェクトの一環として開発されました。

次のステップでは、py_entitymatchingが機械学習技術を使ってデータマッチングを処理している方法、およびそれによって生成されたマッチングの適切さを評価できることを示します。

ステップ3: Magellanによるデータマッチング

サブステップA: 候補セットの検索 (ブロッキング)

このステップの目標は簡単な発見的方法を使ってマッチとみなす可能性があるペアの数を減らすことです。この作業のために、重要な属性値を1つの文字列に結合する新しい列を各データセットに作成することができます(mixtureと呼びます)。それによって前に行ったのと同じように文字列が類似するエンティティの結合を使って、重要な列の値に重複がある一連のエンティティを見つけることができます。その前に、文字列のmixtureに含まれる列を変換する必要があります。この処理はpy_stringsimjoinパッケージを使うと簡単です。

In [20]:
# transforming the "budget" column into string and creating a new **mixture** column
ssj.utils.converter.dataframe_column_to_str(imdb_data, 'budget', inplace=True)
imdb_data['mixture'] = imdb_data['norm_title'] + ' ' + imdb_data['norm_year'] + ' ' + imdb_data['budget']

# repeating the same thing for the Kaggle dataset
ssj.utils.converter.dataframe_column_to_str(kaggle_data, 'budget', inplace=True)
kaggle_data['mixture'] = kaggle_data['norm_movie_title'] + ' ' + kaggle_data['norm_title_year'] + \
						 ' ' + kaggle_data['budget']

次にmixture列を使って希望する候補セット(Cと呼びます)を作成することができます。

In [21]:
C = ssj.overlap_coefficient_join(kaggle_data, imdb_data, 'id', 'id', 'mixture', 'mixture', sm.WhitespaceTokenizer(), 
								 l_out_attrs=['norm_movie_title', 'norm_title_year', 'duration',
											  'budget', 'content_rating'],
								 r_out_attrs=['norm_title', 'norm_year', 'length', 'budget', 'mpaa'],
								 threshold=0.65)
C.shape
0%                          100%
[##############################] | ETA: 00:00:00
Total time elapsed: 00:01:08
Out[21]:
(18317, 14)

類似するエンティティの結合を行うことによって候補セットの数が18,317ペアに減りました。

サブステップB: キーの指定

次のステップはpy_entitymatchingパッケージに、各データフレームのキーにどの列が対応するかを指定することです。また、候補セットの2つのデータフレームの外部キーにどの列が対応するかを指定する必要があります。

In [22]:
import py_entitymatching as em
em.set_key(kaggle_data, 'id')   # specifying the key column in the kaggle dataset
em.set_key(imdb_data, 'id')     # specifying the key column in the imdb dataset
em.set_key(C, '_id')            # specifying the key in the candidate set
em.set_ltable(C, kaggle_data)   # specifying the left table 
em.set_rtable(C, imdb_data)     # specifying the right table
em.set_fk_rtable(C, 'r_id')     # specifying the column that matches the key in the right table 
em.set_fk_ltable(C, 'l_id')     # specifying the column that matches the key in the left table 
Out[22]:
True

サブステップC: ブロッカのデバッグr

ここで候補セットがそれほど類似していない映画ペアを含めることができるような柔軟性を備えていることを確認しておく必要があります。そうでないと、マッチしていた可能性があるペアを除去していた可能性があります。候補セットの中のいくつかのペアを調べることによって、ブロッキングのステップが厳格すぎないかどうかを判断できます。

注: py_entitymatchingパッケージはブロッカをデバッグするためのいくつかのツールも提供しています。

In [23]:
C[['l_norm_movie_title', 'r_norm_title', 'l_norm_title_year', 'r_norm_year',
   'l_budget', 'r_budget', 'l_content_rating', 'r_mpaa']].head()
Out[23]:
l_norm_movie_title r_norm_title l_norm_title_year r_norm_year l_budget r_budget l_content_rating r_mpaa
0 dude wheres my dog! #hacked 2014 2014 20000 20000 PG NaN
1 road hard #horror 2015 2015 1500000 1500000 NaN NaN
2 me you and five bucks #horror 2015 2015 1500000 1500000 NaN NaN
3 checkmate #horror 2015 2015 1500000 1500000 NaN NaN
4 #horror #horror 2015 2015 1500000 1500000 Not Rated NaN

上記のサンプリングによってブロッキングが合理的に行われていることがわかりました。

サブステップD: 候補セットからのサンプリング

このステップの目標は候補セットからサンプルを取得し、サンプリングされた候補に手動でラベルを付ける、つまり候補ペアが正しいマッチかどうかを指定することです。

In [24]:
# Sampling 500 pairs and writing this sample into a .csv file
sampled = C.sample(500, random_state=0)
sampled.to_csv('./data/sampled.csv', encoding='utf-8')

サンプリングされたデータにラベルを付けるためにcsvファイルに新しい列を作成し(labelと名付けます)、その列にペアが正しいペアであれば値1、そうでなければ値0を入力します。ファイルの重複を避けるために、新しいファイルにはlabeled.csvという名前を付けます。

In [25]:
# If you would like to avoid labeling the pairs for now, you can download the labled.csv file from
# BigGorilla using the following command (if you prefer to do it yourself, command the next line)
response = urllib.urlretrieve('https://anaconda.org/BigGorilla/datasets/1/download/labeled.csv',
							  './data/labeled.csv')
labeled = em.read_csv_metadata('data/labeled.csv', ltable=kaggle_data, rtable=imdb_data,
							   fk_ltable='l_id', fk_rtable='r_id', key='_id')
labeled.head()
No handlers could be found for logger "py_entitymatching.io.parsers"
Out[25]:
Unnamed: 0 _id l_id r_id l_norm_movie_title l_norm_title_year l_duration l_budget l_content_rating r_norm_title r_norm_year r_length r_budget r_mpaa _sim_score label
0 4771 4771 2639 235925 eye of the beholder 1999 109.0 15000000 R eye of the beholder 1999 109.0 35000000 R 0.833333 1
1 11478 11478 2001 600301 rocky balboa 2006 139.0 24000000 PG rocky balboa 2006 139.0 24000000 PG 1.000000 1
2 13630 13630 4160 691766 from russia with love 1963 115.0 2000000 Approved the aeolians: from russia with love 2012 NaN 20000 NaN 0.666667 0
3 1972 1972 1248 101029 sex tape 2014 94.0 40000000 R blended 2014 117.0 40000000 PG-13 0.666667 0
4 15903 15903 722 758133 the scorch trials 2015 132.0 61000000 PG-13 the scorch trials 2015 132.0 61000000 PG-13 1.000000 1

サブステップE: 機械学習アルゴリズムのトレーニング

ここからは、サンプリングしたデータセットを使って、予測タスクのための種々の機械学習アルゴリズムのトレーニングを行います。そのためにはデータセットをトレーニングセットとテストセットに分割して、予測タスクのために希望する機械学習アルゴリズムを選択する必要があります。

In [26]:
split = em.split_train_test(labeled, train_proportion=0.5, random_state=0)
train_data = split['train']
test_data = split['test']

dt = em.DTMatcher(name='DecisionTree', random_state=0)
svm = em.SVMMatcher(name='SVM', random_state=0)
rf = em.RFMatcher(name='RF', random_state=0)
lg = em.LogRegMatcher(name='LogReg', random_state=0)
ln = em.LinRegMatcher(name='LinReg')
nb = em.NBMatcher(name='NaiveBayes')

機械学習アルゴリズムを適用するには、その前に一連のフィーチャーを抽出する必要があります。好都合なことに、py_entitymatchingパッケージは、2つのデータセットのどの列が相互に対応するかを指定しておけば自動的に一連のフィーチャーを抽出できます。下記のコードは最初に2つのデータセットの列の対応関係を指定します。次に、py_entitymatchingパッケージを使って各列のタイプを判別します。各データセットの列のタイプ(変数l_attr_types and r_attr_typesに格納されています)を検討し、パッケージによって推奨されているtokenizersおよび類似性関数を使って、フィーチャーを抽出するための一連の命令を抽出することができます。変数Fは抽出されたフィーチャーのセットではなく、フィーチャーを計算するための命令をエンコードするために使用します。

In [27]:
attr_corres = em.get_attr_corres(kaggle_data, imdb_data)
attr_corres['corres'] = [('norm_movie_title', 'norm_title'), 
						 ('norm_title_year', 'norm_year'),
						('content_rating', 'mpaa'),
						 ('budget', 'budget'),
]

l_attr_types = em.get_attr_types(kaggle_data)
r_attr_types = em.get_attr_types(imdb_data)

tok = em.get_tokenizers_for_matching()
sim = em.get_sim_funs_for_matching()

F = em.get_features(kaggle_data, imdb_data, l_attr_types, r_attr_types, attr_corres, tok, sim)

希望するフィーチャーのセットFが抽出されたので、ここでトレーニングデータのフィーチャー値を計算し、データの中の欠落している値を補定することができます。ここでは欠落している値を列の平均値で置換します。

In [28]:
train_features = em.extract_feature_vecs(train_data, feature_table=F, attrs_after='label', show_progress=False) 
train_features = em.impute_table(train_features,  exclude_attrs=['_id', 'l_id', 'r_id', 'label'], strategy='mean')

計算したフィーチャーを使って種々の機械学習アルゴリズムの性能を評価して、このマッチングタスクに最も適したアルゴリズムを選択することができます。

In [29]:
result = em.select_matcher([dt, rf, svm, ln, lg, nb], table=train_features, 
						   exclude_attrs=['_id', 'l_id', 'r_id', 'label'], k=5,
						   target_attr='label', metric='f1', random_state=0)
result['cv_stats']
Out[29]:
Name Matcher Num folds Fold 1 Fold 2 Fold 3 Fold 4 Fold 5 Mean score
0 DecisionTree <py_entitymatching.matcher.dtmatcher.DTMatcher object at 0x15d828090> 5 1.000000 0.967742 1.0 1.000000 1.000 0.993548
1 RF <py_entitymatching.matcher.rfmatcher.RFMatcher object at 0x15d828550>gt; 5 1.000000 0.967742 1.0 1.000000 1.000 0.993548
2 SVM <py_entitymatching.matcher.svmmatcher.SVMMatcher object at 0x15d8284d0> 5 0.956522 0.967742 1.0 1.000000 0.875 0.959853
3 LinReg <py_entitymatching.matcher.linregmatcher.LinRegMatcher object at 0x15d8560d0> 5 1.000000 0.967742 1.0 1.000000 1.000 0.993548
4 LogReg <py_entitymatching.matcher.logregmatcher.LogRegMatcher object at 0x15d8281d0> 5 1.000000 0.967742 1.0 0.956522 1.000 0.984853
5 NaiveBayes <py_entitymatching.matcher.nbmatcher.NBMatcher object at 0x111b2c290> 5 1.000000 0.967742 1.0 1.000000 1.000 0.993548

報告された種々の技法の正確さから、”random forest (RF)”アルゴリズムが最高の性能を示すことがわかります。したがってマッチングにはこの技法を使うのが最も適切です。

サブステップF: マッチングの品質の評価

マッチングの品質の評価は重要です。ここでこの目的のためにトレーニングセットを使用して、ランダムフォレストがどの程度適切にマッチを予想できるかを評価します。高い精度が得られていることを確認でき、テストセットをrecallすることができます。

In [30]:
best_model = result['selected_matcher']
best_model.fit(table=train_features, exclude_attrs=['_id', 'l_id', 'r_id', 'label'], target_attr='label')

test_features = em.extract_feature_vecs(test_data, feature_table=F, attrs_after='label', show_progress=False)
test_features = em.impute_table(test_features, exclude_attrs=['_id', 'l_id', 'r_id', 'label'], strategy='mean')

# Predict on the test data
predictions = best_model.predict(table=test_features, exclude_attrs=['_id', 'l_id', 'r_id', 'label'], 
								 append=True, target_attr='predicted', inplace=False)

# Evaluate the predictions
eval_result = em.eval_matches(predictions, 'label', 'predicted')
em.print_eval_summary(eval_result)
Precision : 94.44% (51/54)
Recall : 100.0% (51/51)
F1 : 97.14%
False positives : 3 (out of 54 positive predictions)
False negatives : 0 (out of 196 negative predictions)

サブステップG: トレーニング済みのモデルによるデータセットのマッチング

下記のようにトレーニング済みのモデルを使って2つのテーブルをマッチングすることができます:

In [31]:
candset_features = em.extract_feature_vecs(C, feature_table=F, show_progress=True)
candset_features = em.impute_table(candset_features, exclude_attrs=['_id', 'l_id', 'r_id'], strategy='mean')
predictions = best_model.predict(table=candset_features, exclude_attrs=['_id', 'l_id', 'r_id'],
								 append=True, target_attr='predicted', inplace=False)
matches = predictions[predictions.predicted == 1]
0%                          100%
[##############################] | ETA: 00:00:00
Total time elapsed: 00:01:05

matchesデータフレームは両方のデータセットのために抽出されたフィーチャーを格納するための多くの列を含んでいます。下記のコードはすべての不要な列を削除し、適切な形式のデータフレームを作成し、その結果として統合的なデータセットが生成されます。

In [32]:
from py_entitymatching.catalog import catalog_manager as cm
matches = matches[['_id', 'l_id', 'r_id', 'predicted']]
matches.reset_index(drop=True, inplace=True)
cm.set_candset_properties(matches, '_id', 'l_id', 'r_id', kaggle_data, imdb_data)
matches = em.add_output_attributes(matches, l_output_attrs=['norm_movie_title', 'norm_title_year', 'budget', 'content_rating'],
								   r_output_attrs=['norm_title', 'norm_year', 'budget', 'mpaa'],
								   l_output_prefix='l_', r_output_prefix='r_',
								   delete_from_catalog=False)
matches.drop('predicted', axis=1, inplace=True)
matches.head()
Out[32]:
_id l_id r_id l_norm_movie_title l_norm_title_year l_budget l_content_rating r_norm_title r_norm_year r_budget r_mpaa
0 4 4352 106 #horror 2015 1500000 Not Rated #horror 2015 1500000 NaN
1 8 2726 450 crocodile dundee ii 1988 15800000 PG crocodile dundee ii 1988 14000000 NaN
2 11 3406 838 500 days of summer 2009 7500000 PG-13 (500) days of summer 2009 7500000 PG-13
3 24 3631 1872 10 cloverfield lane 2016 15000000 PG-13 10 cloverfield lane 2016 15000000 PG-13
4 26 2965 1881 10 days in a madhouse 2015 12000000 R 10 days in delaware 2015 0 NaN
:)