LoginSignup
8
3

More than 3 years have passed since last update.

ポケモン図鑑説明文の謎変更をレーベンシュタイン距離によって見つけ出す

Posted at

はじめに

古くからポケモンを嗜むトレーナーの皆様にとっては常識だが、ポケモン図鑑の説明文は、ゲームのタイトルによって異なっている。

 ピカチュウ

ピカチュウ:赤・緑
ほっぺたの りょうがわに ちいさい でんきぶくろを もつ。ピンチのときに ほうでんする。

ピカチュウ:青
なんびきかが あつまっていると そこに もうれつな でんきが たまり いなずまが おちることがあるという。

このように、たとえ同じポケモンでも、別のゲームタイトルでプレイすることにより、新たな発見が得られるのがポケモンの魅力の一つである


一方、タイトル毎でこういった図鑑説明文の差異もある。

フシギダネ

フシギダネ:赤・緑
うまれたときから せなかに しょくぶつの タネが あって すこしずつ おおきく そだつ。

フシギダネ:青
うまれたときから せなかに ふしぎな タネが うえてあって からだと ともに そだつという。

文章が少し変更されているが、言い方が異なるだけで、ほとんど同じ内容が書かれている。ありがたみが薄い。


こうした、書いてある内容はほとんど変わらないにもかからわす、文章に微妙な変更があるものを謎変更と呼ぶこととし、これを効率よく発見していく。

レーベンシュタイン距離(編集距離)

ポケモンは2020年5月現在890種類。この時点で多いが、更に33タイトル(多分)もある。

'赤・緑','青','ピカチュウ',
'金', '銀', 'クリスタル',
'ルビー','サファイア','ファイアレッド', 'リーフグリーン','エメラルド',
'ダイヤモンド', 'パール', 'プラチナ',
'ハートゴールド', 'ソウルシルバー',
'ブラック', 'ホワイト','ブラック2・ホワイト2',
'X', 'Y', 'オメガルビー', 'アルファサファイア',
'サン', 'ムーン','ウルトラサン', 'ウルトラムーン',
"Let'sGo!ピカチュウ・Let'sGo!イーブイ",
'ソード','シールド'

新しいポケモンは図鑑説明文の種類が2種類程度で済むが、古くからいるポケモンほど多くの種類の図鑑説明文が存在し、累計約15000文となる。

これらを全ポケモンに対してタイトルごとにチェックするとなると、
たとえ1体のポケモンでも20タイトルにもなると、20*19/2=190通りにもなる。
これを全ポケモンについて全てチェックするのは辛いので、
レーベンシュタイン距離という、2つの文字列がどの程度異なっているかについての指標を使う。

この指標は、
1.文字の削除
2.文字の追加
3.他の文字への変更
のいずれかを何回行えば、もう一方の文字列に変換出来るか。
を距離で表す。というシンプルなものである。

例えば、
1. ピカチュウ
2. ライチュウ

の場合、

ピカ チュウ
ライ チュウ

と2文字置き換えればいいのでレーベンシュタイン距離は2である。

  1. サイホーン
  2. サイドン

の場合は

サイ ホー ン
サイ   ン

サイホーンの間の「ホ」を「ド」に変えて「ー」を削除すれば「サイドン」になるので、これもレーベンシュタイン距離は2となる。


実装

レーベンシュタイン距離のアルゴリズムを参考に実装。
また、独自に最短の手順を逆行する処理も追加した。
参考:
【技術解説】似ている文字列がわかる!レーベンシュタイン距離とジャロ・ウィンクラー距離の計算方法とは https://mieruca-ai.com/ai/levenshtein_jaro-winkler_distance/

import pandas as pd
import numpy as np

def LevenshteinDistance(s, t, verbose = False):

    def preprocessing(text):
        re_html = r'(<.+?>)'
        text = text.replace(" ","_").replace("!","!")
        text = re.sub(re_html,"",text)
        return text

    #空白の半角・全角を統一
    s = preprocessing(s)
    t = preprocessing(t)
    #最短距離を逆算できるようにする
    back_trace_dict={0:"UP",
                    1:"LEFT",
                    2:"DIAG"}

    #文字列の長さ+1の2次元配列を作成
    dis_array = np.zeros((len(s)+1,len(t)+1),dtype="int")
    dis_array[:,0] = np.arange(len(s)+1)
    dis_array[0,:] = np.arange(len(t)+1)

    #逆算用
    back_trace = dis_array.copy().astype("str")
    back_trace[:,0] = "DIAG"
    back_trace[0,:] = "DIAG"


    for i in range(1,len(s)+1):
        for j in range(1,len(t)+1):
            d1 = dis_array[i-1,j] + 1 #削除
            d2 = dis_array[i,j-1] + 1 #挿入

            #文字が異なれば置換。同じならそのまま
            d3 = dis_array[i-1,j-1] + (0 if s[i-1] == t[j-1] else 1)

            #削除・挿入・置換のうち、最も距離が小さくなるものを採用
            dis_array[i,j] = min(d1,d2,d3)
            back_trace[i,j] = back_trace_dict[np.argmin([d1,d2,d3])]


    #最小編集距離の過程を逆行する処理
    if verbose:
        def getchar(text,idx):
            return "_" if idx <0 else text[idx]

        #最小編集距離の過程を逆行する処理
        s = " " + s
        t = " " + t      
        s_,t_ = len(s)-1,len(t)-1
        s_list = []
        t_list = []

        trace_list = []
        while s_ >= 0 and t_ >= 0:
            trace = back_trace[s_,t_]
            trace_list.append(trace)
            if trace == "DIAG":
                if s[s_] != t[t_]:
                    wrapper = "x"
                else:
                    wrapper = ""
                s_list.append(getchar(s,s_) + wrapper)
                t_list.append(getchar(t,t_) + wrapper)
                s_ -= 1
                t_ -= 1

            elif trace == "LEFT":
                s_list.append("X")
                t_list.append(getchar(t,t_))
                t_ -= 1

            else:
                s_list.append(getchar(s,s_))
                t_list.append("X")
                s_ -= 1            

        return "".join(s_list[::-1]) + "\n" + "".join(t_list[::-1])

    return dis_array[-1,-1]

計算結果

str1 = "うまれてから しばらくの あいだは せなかの たねから えいようを もらって おおきく そだつ。"
str2 = "うまれて しばらくの あいだ せなかの タネに つまった えいようをとって そだつ。"

lv_dist = LevenshteinDistance(str1,str2,verbose=True)
print(f"レーベンシュタイン距離:{lv_dist}")

'''
output:
うまれてから_しばらくの_あいだは_せなかの_たxねxかxらxXXXX_えいようを_xもらって_おおきく_そだつ。
うまれてXX_しばらくの_あいだX_せなかの_タxネxにx_xつまった_えいようをとxXXって_XXXXXそだつ。

レーベンシュタイン距離:21

'''

全ポケモンの全図鑑文について計算していく

各ポケモンの図鑑文どうしのレーベンシュタイン距離を計算し、比較していく。

以下のようにゲームタイトルを列名。行をポケモンの名前としたファイルを用意した。

まず1種類のポケモンについての図鑑説明文を比較する。

実装

import matplotlib.pyplot as plt
import japanize_matplotlib
import seaborn as sns
from itertools import combinations,product
import os

df = pd.read_csv("図鑑本文.csv",encoding="utf-8-sig").applymap(lambda x:np.nan if x == "__" else x)

def compare_pokedex(poke_num,threshold=30):

    row = df.query("num == @poke_num").iloc[0].dropna()

    text_array=row.values[2:].copy()  #num,name列は除外
    indices = range(len(text_array))

    #全タイトル同士の組み合わせを作成
    comb = list(product(indices,indices))
    col1_list =[]
    col2_list = []
    lev_list = []

    for (col1,col2) in comb:
        if col1 > col2:
            lev_dist=LevenshteinDistance(text_array[col1],text_array[col2])
            col1_list.append(col1)
            col2_list.append(col2)
            lev_list.append(lev_dist)

            col1_list.append(col2)
            col2_list.append(col1)
            lev_list.append(lev_dist)

        elif col1 == col2:
            col1_list.append(col1)
            col2_list.append(col2)
            lev_list.append(0)

    lev_df = pd.DataFrame({"col1":col1_list,
                          "col2":col2_list,
                          "lev":lev_list})
    lev_pivot=pd.pivot_table(index="col1",columns="col2",values="lev",data=lev_df)

    lev_pivot.index=row[2:].index
    lev_pivot.columns=row[2:].index

    sns.clustermap(lev_pivot, method='ward', metric='euclidean',annot=True)

    lev_stack=pd.DataFrame(lev_pivot.stack().sort_values())
    lev_stack.reset_index(inplace=True)
    lev_stack.columns = ["col1","col2","lev"]

    lev_stack["col1_text"] = lev_stack["col1"].map(lambda x:row[x])
    lev_stack["col2_text"] = lev_stack["col2"].map(lambda x:row[x])
    lev_stack = lev_stack.query("col1_text != col2_text")
    lev_stack.drop_duplicates(subset=["col1_text","col2_text"],inplace=True)

    #threshold以下の文字列どうしのみに限定して表示
    similar_df = lev_stack.query("lev > 0 and lev < @threshold & col1 > col2")
    for i,sim in similar_df.iterrows():
        dist = sim["lev"]
        idx1 = sim["col1"]
        idx2 = sim["col2"]

        print("レーベンシュタイン距離:{}".format(dist))
        print(",".join(list(row[row == row[idx1]].index)))
        print(sim["col1_text"])
        print(",".join(list(row[row == row[idx2]].index)))
        print(sim["col2_text"])
        print("-----------")

combinationによって全てのタイトル同士のレーベンシュタイン距離を計算したところで、
seabornのclustermapによって近い文章のクラスタを確認する。
黒く塗りつぶされ、0で表示されている集まりは全く同じ文章が使われていることを意味する。

どのくらいの距離が怪しい?

・近距離の文章(レーベンシュタイン距離:19)

うまれてから_しばらくの_あいだは_せなかの_たxねxかxらxXXXX_えいようを_xもらって_おおきく_そだつ。
うまれてXX_しばらくの_あいだX_せなかの_タxネxにx_xつまった_えいようをとxXXって_XXXXXそだつ。

「から」「は」などの助詞違い
「たね」「タネ」の表記違い
「とって」「もらって」 動詞違い

でレーベンシュタイン距離が19にもなっているが、内容的には言い方が違うだけでほぼ同じ情報が書かれている。

・中距離の文章例(レーベンシュタイン距離:30)

うまれてxXXから_しxばxらxくxの_あxいxだxはx_xせなかの_たxねxかxら_えxいxよxうxをx_xもxらxっxて_おおきく_そだつ。
うまれたxときから_せxなxかxにxX_しxょxくxぶxつxXXXの_タxネxがxX_あxっxてx_xすxこxしxずxつxX_おおきく_そだつ。

近距離同様にほぼ同じ情報に見えるが、
上の文章は「背中にタネがあって育つ」と書かれているのに対し
下の文章は「タネから栄養をもらって」という情報が追加されている

・遠距離の文章例(レーベンシュタイン距離:56)

ひxなたxでx_xひxるxねxを_すxるxX_すがたをx_xみxかxけxるx。xたxいxよxうxのx_xひかりxをx_xいxっぱい_あxびxるxこxとで_せxなxかxのx_タネが_おxおxきxくx_そxだxつxのだ。x
 xなんxにxちxだxっxてxX_なxにxも_XXたべxなxくxてxもx_xげxんxきx!x_xせxなxXかのxタxネxにxXXX_たxくxさxんxXX_えxいxよxうxXXXが_あxるxかxらx_へxいxきxXだ!x

レーベンシュタイン距離56にもなると、ほとんど違う内容の文章となった。
名詞部分と助詞部分や空白が重なっている部分以外は別物。


いろいろ見てみると、レーベンシュタイン距離20以下くらいで探すと、図鑑説明文の謎変更が見つかりそう。ということがわかった。

レーベンシュタイン距離集計結果の確認

今回、図鑑説明文はポケモンwikiから取得させていただいた。
https://wiki.xn--rckteqa2e.com/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8

世代ごと平均値

世代ごとにレーベンシュタイン距離の平均値は以下のようになっている。

3世代のポケモン図鑑説明文のレーベンシュタイン距離が特に大きいようだが、これは単純に各世代ごとの文章の長さそのものであった。

「3世代初出のポケモン図鑑の文章は長い」というのは地味に発見ではないだろうか?検索してもこれについて述べてる記事やSNSアカウントなどが見つからなかった。

全図鑑説明文(の組み合わせ)によるレーベンシュタイン距離を集計してみると、謎変更の候補である20以下の組み合わせは全体のうち 0.47% 程度であった。

謎変更のあった図鑑説明文を一部紹介

0.47%といっても110組もあるので、ごく一部を紹介する。

また、中でも以下の空白の有無で差異が生まれているものもいくつかあった。

サイドン
レーベンシュタイン距離:1
ぜんしんを よろいのような ひふで まもっている。2000どの マグマの なかでも いきられる。(青,リーフグリーン,X)
ぜんしんを よろいのような ひふで まもっている。 2000どの マグマの なかでも いきられる。(シールド)

わかりにくいが、空白の有無で距離が1発生している。

私の手元に全ポケモンタイトルがあるわけではないので、空白の違いがゲーム上で実際に存在するものなのか、ポケモンwikiでのミスなのか調査するのが難しい。(どなたかポケモン全タイトル持っていて全ポケモン集めている方!!検証をお願いします!!)


ゴルバット
距離20
するどいキバ かみついて いちどに 300シーシーの を すいとってしまう。(赤・緑,ファイアレッド)
えものに キバ さしこむと いっしゅんで 300シーシーの けつえきを すいとってしまう。(クリスタル)


パラセクト
距離19
キノコの カサから どくほうしを まきちらす。しかし ちゅうごくでは このほうしを かんぽうやくに る。(赤・緑,ファイアレッド)
キノコの カサから どくのほうしを まきちらす。 ほうしを あつめて せんじると かんぽうやくに る。(ムーン)

ウィンディ
距離5
ちゅうごくで でんせつの ポケモン。かろやかに はしる そのすがたに とりこに される ものも おおい。(ピカチュウ)
とうようで でんせつの ポケモン。 かろやかに はしる そのすがたに とりこに される ものも おおい。(ピカブイ)

近年発売のタイトルになると、「ちゅうごく」表記から変更されている。中国展開のための配慮か。(なお、現実世界の地名のインドだのエベレストも登場しているが今後変更されるのだろうか)


ジュペッタ
距離19
すてられた ぬいぐるみおんねんやどり ポケモンになった。じぶんを すてた こどもを さがしている。(ダイヤモンド,パール,プラチナ,ブラック,ホワイト,X)
すてられた ヌイグルミうらみたまって ポケモンに なった。じぶんをすてた こどもを さがすぞ。(ブラック2・ホワイト2)

ぬいぐるみ→ヌイグルミ。 おんねん→うらみ。 
やどり→たまって。 さがしている→さがすぞ
距離は20もあるが、お手本のような謎変更


サイドン
距離7
しんかして うしろあし だけで たつようになった。ツノで つかれると がんせきにも あながあいてしまう。(赤・緑,ファイアレッド,Y)
しんかして うしろあし だけで たつようになった。 ツノで つくと がんせきにも あなを あけてしまう。(ソード)

Yまでは受け身形だったのがソードで能動態になった?なんで???


パールル
距離1
いっしょうに 1かい しんかの とき サイコパワーを ぞうふくする ふしぎな しんじゅを つくるのだ。(ハートゴールド,ソウルシルバー)
いっしょうに 1かい しんかの とき サイコパワーを ぞうふくする ふしぎな しんじゅを つくるのだ。(X)

「1かいの」→「1かい」


「ちゅうごく」など現実の地名を使っていたものを改変する。というのならわかるが、謎変更の多くは「前回と同じパスワードを使用しないでください」と言われたから仕方がなく・・・程度の変更ばかりで、意図が不明なものばかりだった。

(多分)間違いだった仮説:きれいに改行するため??

各ポケモンタイトルごとに図鑑の横幅が異なっており、
1行あたりの文字数が違うものもあるようだ。

  • 1行18文字

  • 1行24文字

ほとんどのタイトルは実家にあるため、残念ながら検証できていないが、ネット上で、文字をなるべく端から端まで使用しているものを探して数えてみた。HGSSやBWなど、空白が半角のタイトルもあった。

世代 文字数(全角) 最大文字数
(改行含む)
赤緑青ピ 18 56
金銀 18 56
RSE 24 74
FRLG 18 56
DP 18 56
HGSS 21(可変幅?) 65?
BW 18(可変幅?) 56?
XY,ORAS 24 74
SM,USUM 24 74
剣盾 18 56

XY,ORAS、SM,USUMは24文字となっているが、1行24文字フルに使用されているのは3世代のRSEの図鑑説明文と、リメイク版ORASで改変のあった文章のみであった。

おかしい。どのタイトルもほとんど同じじゃないか・・・・。

先程計算した謎変更は、どれも1行の文字数が同じタイトルどうしでのレーベンシュタインが小さい組み合わせだったため、改行位置を調整する必要がない。

ちなみに、同じ図鑑説明文が使われがちなタイトルについて図示してみると、以下のようになっている。
(タイトルの横の数字。
「18_ピカチュウ」はピカチュウ版は1行18文字ですよ。という意味)

例外的に1行24文字をフルに使用している、
ルビー・サファイア・エメラルド
オメガルビー・アルファサファイア
の5タイトルが固まってるのがわかる。

なお、7世代のサン・ムーン以降は、各タイトルで手に入るポケモン以外のポケモンの全国図鑑での説明文は存在しない。
https://wiki.xn--rckteqa2e.com/wiki/%E3%83%9D%E3%82%B1%E3%83%A2%E3%83%B3%E3%81%9A%E3%81%8B%E3%82%93


まとめ

かなり消化不良だが、まとめさせていただく。

  • レーベンシュタイン距離を用いることで謎変更を効率良く見つけることができた。
  • ルビー・サファイア・エメラルドでの初出図鑑説明文の文章は他のタイトルのものより長い。
  • 各タイトルの1行の文字数の幅は18文字と24文字が多いが、24文字フルに使用されているのはRSE(ルビー・サファイア・エメラルド)のみ。
  • 「ちゅうごく」など実在の地名を使ったものから、別の表現へ変更したものが確認された。
  • 結論としては、よくわからない。
  • よくわからない。

謎変更の理由について、検証していない仮説

  • 5世代BW以降の漢字が導入されたのが影響。
  • 海外展開への配慮。(ポケモン図鑑は英語などに翻訳されても情報が一切増えないように配慮がされている)
  • 担当者の気分で読みやすく変更した。
8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3