catalinaの備忘録

ソフトウェアやハードウェアの備忘録。後で逆引きできるように。

MTGのカード情報の傾向分析を行う(準備編)

GWが終わって、しばらく祝日なしの日が続きます。 私は毎年この時期を「6月砂漠」と呼ぶことにしています。かたりぃなです。

今回はMTGのカードのテキストデータを分析してみようと思います。 画像処理ばかりやってると数式相手になることが多いので、時には気分転換も必要です。

目的とか

MTGのカードは時代によって流行り廃れがあります。 たとえば、去年はすごく流行っててカードショップでも高額で取引されていたカードが、今は全然見かけないとか。

これを分析することができれば、何かデータが見えてこないかなというのが今回の目的です。

テキストマイニングは初めてなので、できるだけ手軽に既存のツールやライブラリを組み合わせて実現していこうと思います。

まず今回は環境を作り上げて軽く動かすところまでやってみます。

環境

いつものです

  • ホストOS : Windows10 Pro
  • 仮想化 : Docker desktop for windows
  • ゲストOS : Linux
  • 分析基盤 : Elasticsearch
  • GUIフロントエンド : Kibana

コンテナ一式を立てる

Elasticsearchを使うためのコンテナ一式を立てます。

今回のElasticsearchの用途は次のとおりです

  • 他ホストからRESTでデータを投げ込んでもらう
  • 日本語解析にはkuromojiを使う
  • Kibanaで確認する

早速コンテナを立てるところからやってみます。

docker-compose

docker-compose.ymlはこうなりました。

version: "3"
services:
  elasticsearch:
    build: 
      context: .
      dockerfile: ./kuromoji.dockerfile
    image: elastic
    ports: 
      - "9200:9200"
      - "9300:9300"
    environment:
      - discovery.type=single-node
    networks: 
      - elastic-stack

  kibana:
    image: docker.elastic.co/kibana/kibana:6.5.4
    ports:
      - "5601:5601"
    networks: 
      - elastic-stack

networks:
  elastic-stack:

kuromoji.dockerfile

elasticsearchの日本語解析プラグインとしてkuromojiを使いたいので、それを組み込むためのdockerfileを書きます。

上記docker-compose.ymlが参照するkuromoji.dockerfileはこうなりました。

インストールするだけでした。

FROM docker.elastic.co/elasticsearch/elasticsearch:6.5.4

# 公式マニュアルにあるkuromojiをインストールする。
# https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html
RUN elasticsearch-plugin install analysis-kuromoji

これでelasticsearchの用意ができました。

起動確認

docker-compose up -dして起動確認します。うまくいかないときはdocker psで生存確認したり、docker-compose logsでログを見たりします。

うまく起動したら、kuromojiが有効になっているか確認します。

curl -X GET 'http://localhost:9200/_nodes/plugins?pretty' 

このあたりの基本的な操作はこちらのサイトを参考にさせていただきました。 http://pppurple.hatenablog.com/entry/2017/05/28/141143

Elasticsearchに投入するデータを用意する(MTGのカードデータ)

以下の手順でデータを注入します。

  1. MTGのカードデータをMTG公式のRESTから取得し、ファイルに保存
  2. 保存したファイルを整形して、ElasticsearchのRESTに渡す

データのダウンロードとElasticSearchへの注入を分けた理由は、MTG-APIが外部サーバだからというのと、個人的に空いた時間にちまちまとコードを書きたかったからです。

ネットが繋がらない状態でもローカルにファイルを持っていれば作業できるので。

MTG公式からのカードデータ取得と保存

python3でいきます。こうなりました。 以前DeepLearningのときに試したコードの引用ですね。

# -*- coding: utf-8 -*-

from mtgsdk import Card
from mtgsdk import Set
from mtgsdk import Type
from mtgsdk import Supertype
from mtgsdk import Subtype
from mtgsdk import Changelog

import json
import argparse
import pickle
import urllib
import os
import codecs

def print_allsets():
    print("================ cardset list ================")
    allsets = Set.all()
    for cardset in allsets:
        print('{}  ({})'.format(cardset.code, cardset.name) )
    print("================ cardset list ================")

def main():
    parser = argparse.ArgumentParser(description='mtg image download')
    parser.add_argument('--setcode',
        action='store', type=str, nargs=None, const=None, 
        default=None, choices=None,
        help='download card images from set code')
    args = parser.parse_args()    

    if args.setcode == None:
        print_allsets()
        parser.print_help()
        exit()

    cardlist = Card.where(set=args.setcode).where(language='japanese').all()
    workdir = os.path.join( '.', 'card-texts', args.setcode)

    print("{} card num = {}".format(args.setcode, len(cardlist) ) )
    print("image download to {}".format(workdir) )

    if not os.path.exists(workdir):
        os.makedirs(workdir)

    for index, card in enumerate(cardlist):
        # 有用な情報は多々とれそうだが、一旦日本語の部分だけ抽出してみる
        for foreign_names in card.foreign_names:
            if foreign_names['language'] == 'Japanese':
                # ファイル名にはユニークなイテレーションの数値を使う
                out_file_name = '%s/%d%s' % (workdir, index, '.txt')
                print("{} ( {} )".format(out_file_name, card.name) )
                out_file = codecs.open(out_file_name, "w", 'utf-8')
                out_file.writelines( json.dumps(foreign_names, ensure_ascii=False) )
                out_file.close()

    print("download complete")

if __name__ == '__main__':
    main()

使い方はこんな感じです

python download_cardtext.py --setcode=DOM

こうするとカレントディレクトリに./card-texts/DOMディレクトリが作られ、そこにドミナリアのカード情報一覧が保存されます。

たとえば./card-texts/DOM/0.txtはこんな内容になります。json形式ですね。

{"name": "アカデミーのドレイク", "text": "キッカー{4}(あなたはこの呪文を唱えるに際し、追加で{4}を支払ってもよい。)\n飛行\nアカデミーのドレイクがキッカーされていたなら、これは+1/+1カウンターが2個置かれた状態で戦場に出る。", "flavor": null, "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=444273&type=card", "language": "Japanese", "multiverseid": 444273}

elasticsearchにデータを入れていく

ElasticSearchの前準備として次の2つを行います。

  • kuromoji日本語解析器を設定します
  • MTGのカードを解析するためのマッピング設定を行います。

簡単に説明します、前者の日本語解析器はいわゆる形態素解析してくれるモジュールです。

形態素解析 is 何?

私はその分野の専門家ではないので、今回の用途だけに絞って簡単に説明します。

興味ない人は読み飛ばしてください。

上記jsonのままでは、たとえばtextフィールドの長い文字列からの検索になってしまい、単語単位での分析がうまくできません。

おおざっぱに解釈すると、単語単位に分割する方法の一つが形態素解析らしいです。

そんな面倒な処理必要なの?って思ったので軽く調べたところ、

  • 英語やドイツ語などであれば、空白で区切れば単語単位になる
  • 日本語では空白で区切るという概念はない

あたりが要因のようです。

「日本語は助詞で区切ればいいのでは?」と思いますが、境界判別という問題があって、助詞がどこかわからない文字列だと解釈に困ります。

そのあたりを頑張ってくれるモジュールということなので、便利に使わせてもらいましょう。

設定はこうなりました。これをmapping.jsonとして保存しておいて

{
  "settings": {
    "index": {
      "analysis": {
        "tokenizer": {
          "kuromoji_user_dict": {
            "type": "kuromoji_tokenizer",
            "mode": "extended"
          }
        },
        "analyzer" : {
          "my_analyzer" : {
            "type" : "custom",
            "tokenizer" : "kuromoji_tokenizer"
          }
        }
      }
    }
  },
  "mappings": {
    "cards" : {
      "properties": {
        "name": {
          "type": "text",
          "fielddata": true,
          "analyzer": "kuromoji",
          "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
          }
        },
        "text": {
          "type": "text",
          "fielddata": true,
          "analyzer": "kuromoji",
          "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
          }
        },
        "flavor": {
          "type": "text",
          "fielddata": true,
          "analyzer": "kuromoji",
          "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
          }
        }
      }
    }
  }
}

こうやってelastic-searchに投げます。

curl -X PUT 'localhost:9200/mtg?&pretty=true' -d @mapping.json -H 'Content-Type: application/json'

うまくいけばこんなjsonレスポンスが返ってきます。

{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "mtg"
}

下準備ができたのでデータ注入します。

Elasticsearchに入力するデータは少々加工する必要があるので、スクリプト書きました。

スクリプトの概要はこんな感じです。

  • ローカル環境に保存してある特定のカードセットすべてのファイルを読み込んで、結合
  • ElasticSearchのために各カード情報にヘッダ(のようなもの)を付与
  • 動作確認用にall_<カードセット名>.txtとして保存
  • elasticSearchに入れる

思ったより簡単にできたのでブログに直接乗せてしまいます。

# -*- coding: utf-8 -*-

import json
import argparse
import urllib
import os
import codecs

from mtgsdk import Set

def print_allsets():
    print("================ cardset list ================")
    allsets = Set.all()
    for cardset in allsets:
        print('{}  ({})'.format(cardset.code, cardset.name) )
    print("================ cardset list ================")

def listup_carddata(workdir):
    for filename in os.listdir(workdir):
        fullpath = os.path.join(workdir, filename)
        if os.path.isfile(fullpath):
            yield fullpath

# 1件のエントリをelasticsearchに転送できるよう加工する
def deform_carddata(data):
    return '{ "index": {"_index":"mtg", "_type":"cards" } }\n' + data + '\n'


def collect_data(workdir):
    allcard = ''
    for filepath in listup_carddata(workdir):
        f = codecs.open(filepath, 'r', 'utf-8')
        data = f.read()
        allcard += deform_carddata(data)
    return allcard

def register_es(elasticsearch_host, indexname, json_data):
    headers = {'Content-Type': 'application/json',}
    api = '/_bulk/?pretty'
    url = urllib.parse.urljoin(elasticsearch_host, indexname) + api
    print("request url = {}".format(url) )
    req = urllib.request.Request(url=url, data=json_data.encode(), headers=headers )
    result = urllib.request.urlopen(req)
    print(result)

def main():
    parser = argparse.ArgumentParser(description='mtg image download')
    parser.add_argument('--setcode',
        action='store', type=str, nargs=None, const=None, 
        default=None, choices=None,
        help='download card images from set code')
    args = parser.parse_args()

    if args.setcode == None:
        print_allsets()
        parser.print_help()
        exit()

    workdir = os.path.join( '.', './card-texts', args.setcode)
    if not os.path.exists(workdir):
        exit()

    # データを集めて
    allcard = collect_data(workdir)
    allcard += '\n\n'  # esとしては末尾に改行が必要なので

    # データ形式があってるか目視確認用のファイルを保存
    f = codecs.open('all_'+args.setcode+'.txt', 'w', 'utf-8')
    f.write(allcard)
    f.close()

    # ElasticSearchに入れる
    elasticsearch_host = 'http://localhost:9200'
    es_index = 'mtg'
    register_es(elasticsearch_host, es_index, allcard)


if __name__ == '__main__':
    main()

動かします。

python register_cardlist.py --setcode=DOM
request url = http://localhost:9200/mtg/_bulk/?pretty
<http.client.HTTPResponse object at 0x000001FCA430A048>

エラーチェックがすっぽ抜けてますね。。。 HTTPResponseオブジェクトが返ってきてとりあえずいけました。

API叩いていても寂しいので、テキストマイニングっぽくワードクラウドを作ってみます。

これでkibanaから日本語で検索したり色々できるようになります。

ElasticSearchでワードクラウドを作る用意

kibanaで操作していきます。

インデックスパターンを作る

  1. kibanaの画面左のManagementを選択
  2. ElasticSearch の Index Pattern を選択
  3. インデックス名に"MTG"を入力
  4. あとは適当にOKおしていく

こんだけです。

このときの注意点で、フィールド名一覧の管理画面でtext,name,flavorの各フィールドの「Aggregatable」が有効になっていることを確認しておきます。

詳しいことはわからないのですが、どうやらRAM上に載せておいて色々な処理に使える(たとえば今回のワードクラウド)ようです。

データが入っているか確認する

  1. kibanaの画面左のDiscoverを選択
  2. 目視確認

それっぽいデータが入っていればOKです。

ワードクラウドを作る用意

ElasticSearchではタグクラウドというらしいです。

タグクラウドを作るための基本設定をしていきます。

  1. kibanaの画面左のVisualizeを選択
  2. "+ Create a visualization"を選択
  3. ビジュアライズ手法の最下部にある'Tag Cloud'を選択
  4. From a New Search, Select IndexのNameから'MTG'を選択(これはkibanaで最初に作ったインデックスの名前です)

これでタグクラウドの設定画面まできました。

タグクラウドを出してみる

タグクラウドの設定画面で色々設定してみます。

テキストマイニングは初めてなので、まずは雰囲気を掴んで面白いと思うところが大事だと思います。

設定値はこんな感じにしました。

  • Buckets
    • Aggregation = Terms
    • Field = text
    • Order By = metric:count
    • order = desenc
    • size = 40

こんな感じのできました。 f:id:Catalina1344:20190528133913p:plain

なかなか楽しいですね。

ぱっと見たところ、MTG固有の用語がうまく分析できてなさそうです。

  • プレインズウォーカー = (プレイン / ズウォーカー)
  • トークン = (トー / クン)
  • トレイリア(地名) = (トレイ / リア)

ひとまずテキストマイニングの土台ができたと思うので一旦良しとします。

感想と今後の展望

テキストの分析がうまくいっていないのは形態素解析の都合で、未知の単語をうまく解釈できてないことによるものです。

今回の用途(MTG専用の分析をやりたい)であれば、kuromojiの辞書に追加してあげるだけでよさそうです。

また単語の出現傾向を分析するのも色々楽しそうです。

今回のタグクラウドをいじっていると歴史的アーティファクトがセットで現れることが多いです。

ゲームが分かる人からみれば「ドミナリアでテストしたから当然でしょ」って内容ではありますが、こういう「当たり前だね」っていうのをデータで示しておくことは分析では重要なことだと思います。

それでは今回はこれくらいで。