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

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

####　ピカチュウ

>ピカチュウ：赤・緑
ほっぺたの りょうがわに ちいさい でんきぶくろを もつ。ピンチのときに ほうでんする。
>![](https://i.imgur.com/TIEQRLn.png)
>ピカチュウ：青
なんびきかが あつまっていると そこに もうれつな でんきが たまり いなずまが おちることがあるという。
>![](https://i.imgur.com/WNg6jwe.png)


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

--- 

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

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

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

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

---

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


# レーベンシュタイン距離（編集距離）

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

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

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

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

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


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

の場合、

**ピカ**　チュウ
**ライ**　チュウ

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

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

の場合は

サイ　**ホー**　ン
サイ　**ド**　　ン

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

--- 

# 実装


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

```python
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("Ｘ")
                t_list.append(getchar(t,t_))
                t_ -= 1
                
            else:
                s_list.append(getchar(s,s_))
                t_list.append("Ｘ")
                s_ -= 1            
        
        return "".join(s_list[::-1]) + "\n" + "".join(t_list[::-1])
    
    return dis_array[-1,-1]
```

## 計算結果

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

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

'''
output:
うまれてから＿しばらくの＿あいだは＿せなかの＿たxねxかxらxＸＸＸＸ＿えいようを＿xもらって＿おおきく＿そだつ。
うまれてＸＸ＿しばらくの＿あいだＸ＿せなかの＿タxネxにx＿xつまった＿えいようをとxＸＸって＿ＸＸＸＸＸそだつ。

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

'''
```


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

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

以下のようにゲームタイトルを列名。行をポケモンの名前としたファイルを用意した。
![](https://i.imgur.com/kzOil9P.png)

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


## 実装

```python
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で表示されている集まりは全く同じ文章が使われていることを意味する。
![](https://i.imgur.com/FR6tw7T.png)


# どのくらいの距離が怪しい？

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

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

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

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

#### ・中距離の文章例(レーベンシュタイン距離:30)
```
うまれて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すxこxしxずxつxＸ＿おおきく＿そだつ。
```

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


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

```
ひ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だ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かxらx＿へxいxきxＸだ！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


## 世代ごと平均値

世代ごとにレーベンシュタイン距離の平均値は以下のようになっている。
![](https://i.imgur.com/5NS2f0b.png)

3世代のポケモン図鑑説明文のレーベンシュタイン距離が特に大きいようだが、これは単純に各世代ごとの文章の長さそのものであった。
![](https://i.imgur.com/nHAe50F.png)

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

全図鑑説明文（の組み合わせ）によるレーベンシュタイン距離を集計してみると、**謎変更**の候補である20以下の組み合わせは全体のうち **0.47%** 程度であった。
![](https://i.imgur.com/kuUGuIR.png)


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

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文字
<img src="https://i.imgur.com/XvEstWd.png" width="300">
<img src="https://i.imgur.com/Ru98uOa.png" width="300">

- 1行24文字
<img src="https://i.imgur.com/qdVqxpu.png" width="300">


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

| 世代     | 文字数（全角）      |最大文字数<br>(改行含む)|
|----------|-------------|-------|
| 赤緑青ピ | 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行の文字数が同じタイトルどうしでのレーベンシュタインが小さい組み合わせだったため、改行位置を調整する必要がない。

![](https://i.imgur.com/nHAe50F.png)

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

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

>なお、７世代のサン・ムーン以降は、各タイトルで手に入るポケモン以外のポケモンの全国図鑑での説明文は存在しない。
>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以降の漢字が導入されたのが影響。
- 海外展開への配慮。（ポケモン図鑑は英語などに翻訳されても情報が一切増えないように配慮がされている）
- **担当者の気分で読みやすく変更した。**