ソフトウェア開発職でよく求められるスキルは、MongoDB を含む NoSQL データベースの経験です。このチュートリアルでは、API を使ってデータを収集し、MongoDB データベースに保存し、データの分析を行います。
しかし、コードに入る前に MongoDB と API について少し説明し、収集したデータをどのように扱うか理解できるようにします。
MongoDBとNoSQL
MongoDB は NoSQL データベースの一種であり、非リレーショナルな形式でデータを保存することができます。NoSQLデータベースは、その祖先でありライバルであるSQLデータベースと比較することで最もよく理解できる。
SQLとはStructure Query Languageの略で、リレーショナルデータベース管理ツールの一種です。リレーショナルデータベースは、データを一連のキーと値として保存するデータベースであり、データテーブルの各行にはそれぞれ固有のキーがあります。データベース内の値は、対応するキーを調べることで取得することができます。これがSQLデータベースのデータ格納方法ですが、NoSQLデータベースはリレーショナルではない方法でデータを格納することができます。
NoSQLは「Not Only SQL」の略で、SQL的な問い合わせはNoSQLシステムでできるが、SQLデータベースが苦手とすることもできることを指している。NoSQLデータベースは、扱うデータの保存方法の選択肢が広く、データの関連性が低いため、より多くの方法でデータを取り出すことができ、ある種の操作をより迅速に行うことができる。NoSQLデータベースは、SQLデータベースと比較して、ノードやフィールドの追加をより簡単に行うことができる。
NoSQLのフレームワークには、MongoDB、OrientDB、InfinityDB、Aerospike、CosmosDBなど、多くの有名なものがある。MongoDBは、特定のNoSQLフレームワークの一つで、データをドキュメントの形で保存し、ドキュメント指向のデータベースとして機能する。
MongoDBは、汎用性が高く、クラウドとの統合が容易で、さまざまなタスクに使用できることから人気があります。MongoDBはJSON形式を使用してデータを保存する。MongoDBのデータベースへの問い合わせもJSON形式で行われ、保存も取り出しもJSON形式が基本なので、覚えやすくコマンドを組みやすい。
APIとは?
APIとはApplication Programming Interfacesの略で、クライアントとサーバーの間の通信を容易にするための機能です。APIは、アプリケーションの開発者が使用する言語について経験の浅い人が情報を収集しやすくするために作成されることが多い。
また、APIは、サーバーからの情報の流れを制御する方法としても有効で、サーバーの情報にアクセスしようとする人は、ウェブスクレイパーを構築するのではなく、正式なチャンネルを使ってアクセスするように促すことができる。Webサイトで最も一般的なAPIは、REST(Representational State Transfer)APIで、標準的なHTTPリクエストとレスポンスを使用して、データの送受信、削除、変更を行います。このチュートリアルでは、REST API にアクセスし、HTTP 形式でリクエストを作成します。
どのようなAPIを使用するのですか?
私たちが使用するAPIはGameSpotのAPIです。GameSpotはウェブ上で最大のビデオゲームレビューサイトの一つであり、そのAPIはここからアクセスできる。
セットアップ
始める前に、GameSpotのAPIキーを取得しておく必要があります。また、MongoDBとそのPythonライブラリがインストールされていることも確認する必要があります。Mongoのインストール方法はこちらで確認できます。
PyMongo ライブラリは次のコマンドを実行するだけでインストールできます。
$ pip install pymongo
MongoDB Compass もインストールしておくといいでしょう。MongoDB データベースを GUI で簡単に可視化したり編集したりできます。
MongoDBデータベースの作成
さて、MongoDB データベースを作成してプロジェクトを開始します。まず、インポートを処理します。PyMongoからMongoClient
を、requests
とpandas
をインポートします。
from pymongo import MongoClient
import requests
import pandas as pd
MongoDB でデータベースを作成するときは、まずクライアントに接続し、クライアントを使って希望のデータベースを作成します。
client = MongoClient('127.0.0.1', 27017)
db_name = 'gamespot_reviews'
# connect to the database
db = client[db_name]
MongoDB はひとつのデータベース内に複数のデータコレクションを格納できるので、 使用するコレクションの名前も定義する必要があります。
# open the specific collection
reviews = db.reviews
これで完了です。データベースとコレクションが作成され、データの挿入を開始する準備ができました。かなり簡単だったでしょう?
APIを使用する
これでGameSpot APIを利用してデータを収集する準備が整いました。ここでAPIのドキュメントを見ることで、リクエストに必要な形式を決定できる。
APIキーを含むベースURLに対してリクエストを行う必要があります。GameSpotのAPIには、データを引き出すことができる複数のリソースがある。例えば、リリース日やコンソールなど、ゲームに関するデータをリストアップするリソースがある。
しかし、我々はゲームレビューのリソースに興味があり、APIリソースからいくつかの特定のフィールドを取得することになる。また、GameSpotはリクエストを行う際に、ユニークなユーザーエージェント識別子を指定するよう求めている。これはヘッダーを作成し、requests
関数に渡すことで実現する。
headers = {
"user_agent": "[YOUR IDENTIFIER] API Access"
}
games_base = "http://www.gamespot.com/api/reviews/?api_key=[YOUR API KEY HERE]&format=json"
以下のデータフィールドが必要です。id,
title,
score,
deck,
body,
good,
bad`:
review_fields = "id,title,score,deck,body,good,bad"
GameSpotは一度に100件の結果しか返すことができません。このため、分析するための適切な数のレビューを取得するためには、数値の範囲を作成し、それをループして一度に100件の結果を取得する必要があります。
好きな数を選択することができます。私は、すべてのレビューを取得することを選択し、その上限は14,900である。
pages = list(range(0, 14900))
pages_list = pages[0:14900:100]
ベースURL、返したいフィールドのリスト、ソートスキーム(昇順または降順)、クエリのオフセットを結合した関数を作成することになります。
ループスルーしたいページ数を受け取り、100エントリごとに新しいURLを作成し、データを要求します。
def get_games(url_base, num_pages, fields, collection):
field_list = "&field_list=" + fields + "&sort=score:desc" + "&offset="
for page in num_pages:
url = url_base + field_list + str(page)
print(url)
response = requests.get(url, headers=headers).json()
print(response)
video_games = response['results']
for i in video_games:
collection.insert_one(i)
print("Data Inserted")
MongoDBはデータをJSONとして保存することを思い出してください。そのため、json()
メソッドを使ってレスポンスデータを JSON 形式に変換する必要があります。
データがJSONに変換されたら、レスポンスから “results” プロパティを取得します。次に、100種類の結果を調べて、PyMongoの insert_one()
コマンドを使ってコレクションにそれぞれ挿入します。代わりにリストにまとめて insert_many()
を使うこともできます。
それでは、この関数を呼び出してデータを収集してみましょう。
get_games(review_base, pages_list, review_fields, reviews)
データが期待通りにデータベースに挿入されたかどうか、確認してみましょう。Compass プログラムでデータベースとその内容を直接見ることができます。
データが正しく挿入されたことを確認することができます。
また、データベースを検索し、印刷することもできます。そのためには、エントリーを格納する空のリストを作成し、 .find()
コマンドを “reviews” コレクションに使用します。
PyMongoの find
関数を使用する場合、検索結果はJSONとしてフォーマットする必要があります。find` 関数に与えるパラメータには、フィールドと値を指定します。
デフォルトでは、MongoDBは常に _id
フィールド (GameSpotから取得したIDではなく、独自のIDフィールド) を返しますが、0
を指定することでこれを抑制することができます。返したいフィールド、たとえば今回の場合は score
フィールドには 1
という値を指定します。
scores = []
for score in list(reviews.find({}, {"_id":0, "score": 1})):
scores.append(score)
print(scores[:900])
以下は、正常に取り出され、印刷されたものです。
[{'score': '10.0'}, {'score': '10.0'}, {'score': '10.0'}, {'score': '10.0'}, {'score': '10.0'}, {'score': '10.0'}, {'score': '10.0'}, {'score': '10.0'} ...
Pandasを使えば、クエリの結果を簡単にデータフレームに変換することもできます。
scores_data = pd.DataFrame(scores, index=None)
print(scores_data.head(20))
以下は、返された結果です。
score
0 10.0
1 10.0
2 10.0
3 10.0
4 10.0
5 10.0
6 10.0
7 10.0
8 10.0
9 10.0
10 10.0
11 10.0
12 10.0
13 10.0
14 10.0
15 10.0
16 10.0
17 9.9
18 9.9
19 9.9
データの分析を始める前に、2つのコレクションを結合する方法について少し考えてみましょう。前述のとおり、GameSpot にはデータを取得するための複数のリソースがあり、Games データベースのような 2 つ目のデータベースから値を取得したい場合があります。
MongoDBはNoSQLデータベースなので、SQLと違ってデータベース間のリレーションを扱ったり、データフィールドを結合したりすることは想定していません。しかし、データベースの結合を近似的に行う関数があります – lookup()
です。
このパイプラインには、要素を結合したいデータベースと、入力ドキュメント (localField
) と “from” ドキュメント (foreignField
) の両方から取得したいフィールドを指定することができます。
最後に、外部ドキュメントを変換するための名前を選択すると、クエリの応答テーブルにこの新しい名前で表示されるようになります。もし、games
という名前の2つ目のデータベースがあって、それらをクエリで結合したい場合は、このようにすることができます。
pipeline = [{
'$lookup': {
'from': 'reviews',
'localField': 'id',
'foreignField': 'score',
'as': 'score'
}
},]
for doc in (games.aggregate(pipeline)):
print(doc)
データの分析
さて、新しく作成したデータベース内のデータを分析し、可視化することができます。分析に必要なすべての関数が揃っていることを確認しましょう。
from pymongo import MongoClient
import pymongo
import pandas as pd
from bs4 import BeautifulSoup
import re
from nltk.corpus import stopwords
from wordcloud import WordCloud
import matplotlib.pyplot as plt
from collections import Counter
import string
import en_core_web_sm
import seaborn as sns
例えば、GameSpot のゲームレビューに含まれる単語を分析したいとします。データベースにはその情報があるので、あとはそれを取得するだけです。
しかし、今回は score
変数でソートすることと、降順にソートすることを指定します。
d_name = 'gamespot_reviews'
collection_name = 'gamespot'
client = MongoClient('127.0.0.1', 27017)
db = client[d_name]
reviews = db.reviews
review_bodies = []
for body in list(reviews.find({}, {"_id":0, "body": 1}).sort("score", pymongo.DESCENDING).limit(40)):
review_bodies.append(body)
そのレスポンスを Pandas のデータフレームに変換して、文字列に変換します。次に、レビューテキストを含む <p
HTML タグ内のすべての値を抽出します。これは BeautifulSoup で行います。
reviews_data = pd.DataFrame(review_bodies, index=None)
def extract_comments(input):
soup = BeautifulSoup(str(input), "html.parser")
comments = soup.find_all('p')
return comments
review_entries = extract_comments(str(review_bodies))
print(review_entries[:500])
レビューテキストが収集されたことを確認するには、print
ステートメントを参照してください。
[<pFor anyone who hasn't actually seen the game on a TV right in front of them, the screenshots look too good to be true. In fact, when you see NFL 2K for the first time right in front of you...]
さて、レビューテキストのデータを手に入れたので、いくつかの異なる方法でそれを分析したいと思います。上位40のレビューでどのような種類の単語がよく使われているか、直感的にわかるようにしてみましょう。いくつかの方法でこれを行うことができます。
- ワードクラウドを作成する
- すべての単語を数え、その出現数で並べ替えることができます。
- 名前付きエンティティ認識
しかし、データを分析する前に、前処理をする必要があります。
データを前処理するために、エントリーをフィルターする関数を作りたい。テキストデータには様々なタグや非標準文字が残っているので、レビューコメントの生のテキストを取得することで、それらを除去したいと思います。正規表現を使って、非標準文字を空白に置き換えることにします。
また、NTLKのストップワード(テキストにほとんど意味を与えない非常に一般的な単語)を使用し、すべての単語を保持するリストを作成し、ストップワードのリストにない単語だけをそのリストに追加して、テキストからそれらを削除します。
ワードクラウド
コーパスとして可視化するために、レビューワードのサブセットを取得してみましょう。生成時に大きすぎるとワードクラウドに問題が発生する可能性があります。
例えば、最初の5000語をフィルタリングしてみました。
stop_words = set(stopwords.words('english'))
def filter_entries(entries, stopwords):
text_entries = BeautifulSoup(str(entries), "lxml").text
subbed_entries = re.sub('[^A-Za-z0-9]+', ' ', text_entries)
split_entries = subbed_entries.split()
stop_words = stopwords
entries_words = []
for word in split_entries:
if word not in stop_words:
entries_words.append(word)
return entries_words
review_words = filter_entries(review_entries, stop_words)
review_words = review_words[5000:]
ここで紹介するWordCloudライブラリを使えば、とても簡単にワードクラウドを作ることができる。
このワードクラウドでは、上位のレビューでどのような単語がよく使われているかという情報を得ることができます。
残念ながらまだ一般的な単語でいっぱいなので、tf-idfフィルタリングスキームでレビュー単語をフィルタリングするのが良いアイデアですが、この簡単なデモの目的ではこれで十分です。
実際、ゲームレビューでは、ゲームプレイ、ストーリー、キャラクター、世界観、アクション、ロケーションなど、どのようなコンセプトが語られているか、いくつかの情報を持っています。
これらの言葉がゲームレビューでよく使われていることは、私たちが選んだ上位40本のレビューのひとつを見れば、自分で確認することができます。Mike Mahardy氏による「Uncharted 4」のレビューです。
確かに、このレビューでは、アクション、ゲームプレイ、キャラクター、ストーリーについて述べられています。
単語の大きさから、これらのレビューで単語がどれだけよく登場するかを直感的に理解することができますが、特定の単語がどれだけ頻繁に登場するかをカウントすることもできます。
カウンター
単語を分割して、個々のカウントとともに単語辞書に追加することで、最も一般的な単語のリストを取得することができます。このカウントは、同じ単語が現れるたびにインクリメントされます。
あとは、 Counter
と most_common()
関数を使うだけです。
def get_word_counts(words_list):
word_count = {}
for word in words_list:
word = word.translate(translator).lower()
if word not in stop_words:
if word not in word_count:
word_count[word] = 1
else:
word_count[word] += 1
return word_count
review_word_count = get_word_counts(review_words)
review_word_count = Counter(review_word_count)
review_list = review_word_count.most_common()
print(review_list)
以下は、最も一般的な単語のカウントです。
[('game', 1231), ('one', 405), ('also', 308), ('time', 293), ('games', 289), ('like', 285), ('get', 278), ('even', 271), ('well', 224), ('much', 212), ('new', 200), ('play', 199), ('level', 195), ('different', 195), ('players', 193) ...]
名前付き固有表現(NEM)認識
spaCyに含まれる言語モデルである en_core_web_sm
を用いて、名前付き実体の認識を行うこともできます。検出できる様々な概念と言語的特徴はここにリストアップされています。
検出された名前付き実体と概念のリスト(単語のリスト)を文書から取得する必要があります。
doc = nlp(str(review_words))
labels = [x.label_ for x in doc.ents]
items = [x.text for x in doc.ents]
検出された実体と実体の数を印刷することができます。
# Example of named entities and their categories
print([(X.text, X.label_) for X in doc.ents])
# All categories and their counts
print(Counter(labels))
# Most common named entities
print(Counter(items).most_common(20))
以下は、印刷された内容です。
[('Nintendo', 'ORG'), ('NES', 'ORG'), ('Super', 'WORK_OF_ART'), ('Mario', 'PERSON'), ('15', 'CARDINAL'), ('Super', 'WORK_OF_ART'), ('Mario', 'PERSON'), ('Super', 'WORK_OF_ART') ...]
Counter({'PERSON': 1227, 'CARDINAL': 496, 'ORG': 478, 'WORK_OF_ART': 204, 'ORDINAL': 200, 'NORP': 110, 'PRODUCT': 88, 'GPE': 63, 'TIME': 12, 'DATE': 12, 'LOC': 12, 'QUANTITY': 4 ...]
[('first', 147), ('two', 110), ('Metal', 85), ('Solid', 82), ('GTAIII', 78), ('Warcraft', 72), ('2', 59), ('Mario', 56), ('four', 54), ('three', 42), ('NBA', 41) ...]
たとえば、人物や組織など、さまざまなカテゴリで最も一般的に認識される用語をプロットしたいとします。異なるクラスのエンティティのカウントを取得する関数を作成し、それを使用して希望するエンティティを取得するだけです。
名前付きエンティティ/人、組織、GPE(場所)のリストを得ることができます。
def word_counter(doc, ent_name, col_name):
ent_list = []
for ent in doc.ents:
if ent.label_ == ent_name:
ent_list.append(ent.text)
df = pd.DataFrame(data=ent_list, columns=[col_name])
return df
review_persons = word_counter(doc, 'PERSON', 'Named Entities')
review_org = word_counter(doc, 'ORG', 'Organizations')
review_gpe = word_counter(doc, 'GPE', 'GPEs')
あとは、カウントを関数でプロットするだけです。
def plot_categories(column, df, num):
sns.countplot(x=column, data=df,
order=df.value_counts().iloc[0:num].index)
plt.xticks(rotation=-45)
plt.show()
plot_categories("Named Entities", review_persons, 30)
plot_categories("Organizations", review_org, 30)
plot_categories("GPEs", review_gpe, 30)
生成されたプロットを見てみましょう。
名前付き実体に期待されるように、返される結果のほとんどはビデオゲームのキャラクターの名前です。これは完璧ではありません。”Xbox “のような用語を組織ではなく名前付きエンティティとして誤って分類してしまうからです。しかし、上位のレビューで議論されているキャラクターをある程度知ることができます。
組織図では、PlaystationやNintendoといったゲーム開発会社やパブリッシャーが表示されていますが、”480p “のようなものも組織としてタグ付けされています。
上はGPE、つまり地理的な位置のプロットです。ゲームのレビューで「ハリウッド」「マイアミ」などがよく出てくるようです。(ゲームの設定?それとも、ゲーム内の何かをハリウッド風と表現しているのでしょうか?)
このように、固有表現認識や概念認識を行うことは完璧ではありませんが、あるテキストでどのようなトピックが議論されているかを直感的に理解することは可能です。
数値のプロット
最後に、データベースから数値をプロットしてみましょう。レビューコレクションからスコア値を取得し、それらをカウントアップし、そしてプロットしてみましょう。
scores = []
for score in list(reviews.find({}, {"_id":0, "score": 1})):
scores.append(score)
scores = pd.DataFrame(scores, index=None).reset_index()
counts = scores['score'].value_counts()
sns.countplot(x="score", data=scores)
plt.xticks(rotation=-90)
plt.show()
上のグラフは、0点から9.9点までのレビュースコアの総数のグラフです。最も多いのは7点と8点のようですが、これは直感的に理解できます。7点は、10点満点で平均的と思われます。
結論
データの収集、保存、検索、分析は、今日の世界で非常に需要の高いスキルであり、MongoDBは最も一般的に使用されるNoSQLデータベースプラットフォームの1つです。
NoSQLデータベースの使い方とデータの解釈方法を知っていれば、多くの一般的なデータ分析タスクを実行できるようになります。
<a href = “/stock/stock_detail.html?