Wikipediaに偶然現れたポケモンの名前を検出する

# 概要
Wikipediaに偶然現れたポケモンの名前を検出します。
「姉**さん、ど**こへ行くの」（じめんポケモンの「サンド」が偶然登場している）みたいなやつです。

# 方法
## 方針
完璧な検出は無理なので、ルールベースで候補を絞り込んだ後、目視で面白そうな文章を見つけることにします。とはいえ、あまりに偽陽性が多いと、目視で探すのも大変なのである程度は絞り込みたいです。

「偶然現れた」をどう検出するかが重要です。
今回は「単語をまたいで音韻が一致する並びが登場した」ときに偶然現れた候補とみなすことにします。
例えば「アローラ地方でのサンドの姿」のように「サンド」という単語がそのままの意味で登場している文章は「偶然現れた」とは言えません。それを排除するために、単語をまたいで「サンド」という音韻が登場している場合だけを取得するようにします。

## 単語リスト
以下でまとめていただいているデータをお借りします（今回は研究用途であり、商標権の侵害にはならないという判断のもと、利用します）
https://wonderhorn.net/material/pokelist.html

## wikipedia文章
huggingfaceに上がっている以下のデータを使わせていただきます。
https://huggingface.co/datasets/izumi-lab/wikipedia-ja-20230720

# 結果
詳細な実装は最後に述べることにしてとりあえず結果を眺めてみます。
wikipediaデータセットの冒頭1000記事を対象とし、結果105件のポケモンが検出されました。
興味深かったものをいくつか載せます。

|ポケモン|記事|該当文|
|---|---|---|
|デスマス|日本語|たとえば、**「です」「ます」** はのように発音されるし|
|ゴース|日本語|「軟」「薄」などの成分と結**合す**ることにより|
|キモリ|日本語|「**防人**歌」には当時の東国方言による|
|ロコン|パリ|パリ市内では道**路混**雑を避けるため|
|モココ|生物学|それ**も個々**の分野名にこの名を被せる例が|
|イトマル|トルコ|北は黒**海とマル**マラ海|
|ギアル|合理哲学主義|「（自己）完結主**義」、ある**いは|
|ユニラン|高橋留美子|1996年**冬に『らん**ま1/2』を終了し|
|ダイケンキ|赤塚不二夫|全日本満足問**題研究**会|

めちゃくちゃ面白いかというと正直微妙ですが、なるほどなと思えるものは取得できた気がします。
「ダイケンキ」などある程度長い単語が偶然現れたときのほうが面白みが強い気もします。

処理時間がかかるのでやっていませんが、１００００記事など、もっと大量の記事から検索することで、より完成度の高い偶然ポケモンの検出ができるかもしれません。

# 実装
## ライブラリのインストール
必要なライブラリをインストールします。

```sh
pip install pandas datasets mecab-pytyon3 unidic-lite
```

## ライブラリのインポート
必要なライブラリをインプポートします。

```Python
import pandas as pd
import MeCab
import copy
import tqdm
from typing import Dict, List, Any, Tuple, Optional, Callable
import datasets
```

## 解析用クラス
「偶然現れた」の定義に適う文章を検出するための解析用クラスを作ります。
文章を発音に変換する関数や単語境界をまたぐ有無などを判定する関数などをひとまとめにしています。

```Python

class SerendipitousWordDetector:
    def __init__(self, tokenize_func: Optional[Callable[[str], Tuple[List[str], List[str]]]] = None):
        """
        SerendipitousWordDetectorクラスのコンストラクタです。
        形態素解析を行うためのトークナイズ関数を受け取り、インスタンス変数に設定します。
        もしトークナイズ関数が指定されていない場合は、デフォルトのトークナイズ関数を使用します。

        Args:
            tokenize_func (Optional[Callable[[str], Tuple[List[str], List[str]]]], optional): 
                形態素解析を行うための関数。
                この関数はテキストを引数に取り、表層形と発音のトークンのリストをタプルで返す必要があります。
                もしNoneが指定された場合は、MeCabを使用したデフォルトのトークナイズ関数が使用されます。
        """
        if callable(tokenize_func):
            self.tokenize = tokenize_func
        else:
            self.tokenize = self.get_default_tokenize_func()
                            
    def get_pronunciation(self, text: str) -> str:
        """
        与えられたテキストを形態素解析し、その発音を連結した文字列を返します。

        Args:
            text (str): 発音を取得したい日本語のテキスト。

        Returns:
            str: テキストの発音を表す文字列。
        """
        _, pronunciation_tokens = self.tokenize(text)
        return "".join(pronunciation_tokens)    
    def get_default_tokenize_func(self):

        mecab = MeCab.Tagger()

        def _tokenize(text: str) -> Tuple[List[str], List[str]]:
            """
            与えられたテキストを形態素解析し、表層形と発音のトークンのリストを返します。

            Args:
                text (str): 形態素解析を行いたい日本語のテキスト。

            Returns:
                Tuple[List[str], List[str]]: 
                    - 最初のリストはテキストの表層形のトークンを含みます。
                    - 二番目のリストは対応する発音のトークンを含みます。
                    これらのリストは同じ長さで、各表層形のトークンは対応する発音のトークンと位置を合わせています。
            """
            lines = mecab.parse(text).splitlines()[:-1]  # EOSを除外するために最後の行を除く
            surface_tokens = [line.split("\t")[0] for line in lines]  # 各行の表層形を抽出
            pronunciation_tokens = [line.split("\t")[1] for line in lines]  # 各行の発音を抽出

            return surface_tokens, pronunciation_tokens

        return _tokenize
    
    def is_word_used_in_original_context(
            self,
            word_surface: str,
            word_pronunciation: str,
            passage_surface: str,
            passage_pronunciation_tokens: List[str]
    ) -> bool:
        """
        単語が元の文脈（意味）で使用されているかどうかを判定します。

        この関数は、単語の表層形が文章中に存在するか、または単語の発音が文章の発音トークンリストに含まれているかどうかをチェックします。
        どちらか一方が真であれば、その単語は元の文脈で使用されていると判定されます。

        Args:
            word_surface (str): チェックしたい単語の表層形。
            word_pronunciation (str): チェックしたい単語の発音。
            passage_surface (str): チェック対象の文章。
            passage_pronunciation_tokens (List[str]): 文章の発音をトークン化したリスト。

        Returns:
            bool: 単語が元の文脈で使用されている場合はTrue、そうでない場合はFalse。
        """
        return (
            word_surface in passage_surface or
            word_pronunciation in passage_pronunciation_tokens
        )    

    def is_crossing_word_boundary(
            self,
            word_surface: str,
            word_pronunciation: str,
            passage_surface: str,
            passage_pronunciation_tokens: List[str]
    ) -> bool:
        """
        単語が文章の単語境界をまたいでいるかどうかを判定します。

        この関数は、単語の発音が文章の発音トークン列に含まれているか（contains_word）、
        単語が元の文脈で使用されていないか（is_word_used_in_original_context）、
        そして単語の発音が単語境界をまたがない形で文章の発音トークン列に含まれているか（is_not_crossing_word_boundary）
        をチェックします。

        Args:
            word_surface (str): チェックしたい単語の表層形。
            word_pronunciation (str): チェックしたい単語の発音。
            passage_surface (str): チェック対象の文章の表層形。
            passage_pronunciation_tokens (List[str]): 文章の発音をトークン化したリスト。

        Returns:
            bool: 単語が単語境界をまたいでいる場合はTrue、そうでない場合はFalse。
        """
        # 単語の発音が文章の発音トークン列に含まれているかどうか
        contains_word = word_pronunciation in "".join(passage_pronunciation_tokens)
        
        # 単語が元の文脈で使用されているかどうか
        is_word_used_in_original_context = self.is_word_used_in_original_context(
            word_surface, word_pronunciation, passage_surface, passage_pronunciation_tokens
        )
        
        # 単語の発音が単語境界をまたがない形で文章の発音トークン列に含まれているかどうか
        is_not_crossing_word_boundary = word_pronunciation in " ".join(passage_pronunciation_tokens)

        # すべての条件を満たす場合、単語は単語境界をまたいでいると判定
        return contains_word and not is_word_used_in_original_context and not is_not_crossing_word_boundary
    
    def get_word_context(
            self,
            word_pronunciation: str,
            passage_surface_tokens: List[str],
            passage_pronunciation_tokens: List[str],
            context_range: Tuple[int] = [10, 10]
    )->str:
        """
        指定された単語の発音に基づいて、その単語が含まれる文章の一部分を抽出します。
        文章は表層形のトークンのリストと発音のトークンのリストで表され、
        単語の発音が文章中で最初に現れる位置を基準に前後のトークンを含めた範囲を返します。

        Args:
            word_pronunciation (str): 抽出したい単語の発音。
            passage_surface_tokens (List[str]): 文章の表層形のトークンのリスト。
            passage_pronunciation_tokens (List[str]): 文章の発音のトークンのリスト。
            context_range (int, optional): 抽出する範囲のトークン数。デフォルトは20。

        Returns:
            str: 指定された単語を含む文章の一部分。
        """

        # 単語の発音が文章の発音トークン列で最初に現れる位置を見つける
        pronunciation_pos = "".join(passage_pronunciation_tokens).index(word_pronunciation)

        # 単語が見つかったトークンのインデックスを特定する
        hit_index_start, hit_index_end = -1, -1
        current_pos = 0
        for i, p in enumerate(passage_pronunciation_tokens):
            current_pos += len(p)
            # 現在のトークンの終わりが単語の発音の開始位置を超えたらループを終了
            if current_pos > pronunciation_pos and hit_index_start < 0:
                hit_index_start = i
            if current_pos >= pronunciation_pos + len(word_pronunciation):
                hit_index_end = i + 1
                break

        # 抽出する範囲の開始インデックスを計算する
        start_index = max(0, hit_index_start-context_range[0])
        # 抽出する範囲の終了インデックスを計算する
        end_index = min(hit_index_end+context_range[1], len(passage_surface_tokens))

        # 指定された範囲の表層形トークンを結合して返す
        return "".join(passage_surface_tokens[start_index: end_index])
    
    def check_hit(self, word_surface: str, word_pronunciation: str
                  , passage_surface_tokens: List[str], passage_pronunciation_tokens: List[str]
                  )->Dict[str, Any]:
        """
        単語が文章中にヒットするかどうかをチェックし、ヒットした場合にはその単語の情報を含む辞書を返します。

        Args:
            word_surface (str): チェックしたい単語の表層形。
            word_pronunciation (str): チェックしたい単語の発音。
            passage_surface_tokens (List[str]): 文章の表層形のトークンのリスト。
            passage_pronunciation_tokens (List[str]): 文章の発音のトークンのリスト。

        Returns:
            Dict[str, Any]: 単語のヒット情報を含む辞書。ヒットしたかどうか、単語の表層形、発音、長さ、
                             単語境界をまたいでいるかどうか、コンテキスト、マッチした文章の表層形を含む。
        """
        # 単語が文章の発音トークン列に含まれているかどうか、および元のコンテキストで使用されていないかどうかをチェック
        hit = word_pronunciation in "".join(passage_pronunciation_tokens) and not self.is_word_used_in_original_context(word_surface, word_pronunciation, "".join(passage_surface_tokens), passage_pronunciation_tokens)
        # 結果を格納する辞書を初期化
        result = {
            "hit": hit,
            "surface": word_surface,
            "pronunciation": word_pronunciation,
            "length": len(word_pronunciation)
        }
        # ヒットした場合、追加の情報を辞書に追加
        if hit:
            # 単語が単語境界をまたいでいるかどうかをチェック
            is_crossing_boundary = self.is_crossing_word_boundary(word_surface, word_pronunciation, "".join(passage_pronunciation_tokens), passage_pronunciation_tokens)
            # 単語のコンテキストを取得
            context = self.get_word_context(word_pronunciation, passage_surface_tokens, passage_pronunciation_tokens)
            if is_crossing_boundary:
                matched_passage_surface = self.get_word_context(word_pronunciation, passage_surface_tokens, passage_pronunciation_tokens, [0,0])
            else:
                matched_passage_surface = self.get_word_context(word_pronunciation, passage_surface_tokens, passage_pronunciation_tokens, [0,0])
            result.update({
                "is_crossing_word_boundary": is_crossing_boundary,
                "context": context,
                "matched_passage_surface": matched_passage_surface
            })
        
        return result


        
    def check_hits(self, word_df: pd.DataFrame, passage_df: pd.DataFrame
                   )->pd.DataFrame:
        """
        単語リストと文章リストを受け取り、各文章に含まれる単語のヒット情報をチェックします。

        Args:
            word_df (pd.DataFrame): チェックしたい単語のデータフレーム。'surface'と'pronunciation'の列が必要です。
            passage_df (pd.DataFrame): チェック対象の文章のデータフレーム。'surface'列が必要です。

        Returns:
            pd.DataFrame: 各文章にヒットした単語の情報を含む新しいデータフレーム。
        """
        # データフレームのディープコピーを作成
        word_df = copy.deepcopy(word_df)
        passage_df = copy.deepcopy(passage_df)

        # 単語データフレームに発音列がない場合は生成
        if "pronunciation" not in word_df.columns:
            word_df["pronunciation"] = word_df["surface"].map(self.get_pronunciation)
        # 文章データフレームに発音トークン列がない場合は生成
        if "pronunciation_tokens" not in passage_df.columns:
            passage_df["surface_tokens"] = passage_df["surface"].map(lambda x: self.tokenize(x)[0])
            passage_df["pronunciation_tokens"] = passage_df["surface"].map(lambda x: self.tokenize(x)[1])
            passage_df["pronunciation"] = passage_df["pronunciation_tokens"].map(lambda x: "".join(x))

        # ヒットした単語を格納するリストを初期化
        hitwords_list = []
        # 各文章に対してヒットした単語をチェック
        for passage_surface_tokens, passage_pronunciation_tokens in tqdm.tqdm(zip(passage_df["surface_tokens"], passage_df["pronunciation_tokens"])):
            hitwords = []
            # 各単語に対してヒットチェックを行い、ヒットしたものをリストに追加
            for word_surface, word_pronunciation in zip(word_df["surface"], word_df["pronunciation"]):
                result = self.check_hit(word_surface, word_pronunciation
                                   , passage_surface_tokens, passage_pronunciation_tokens)
                if result["hit"]:
                    hitwords.append(result)
            
            # ヒットした単語の情報を含むオブジェクトを作成
            hitwords_obj = {"hitwords": hitwords, "hit": False}
            if hitwords:
                hitwords_obj.update({
                    "hit": True
                    , "sum_length": sum([obj["length"] for obj in hitwords])
                    , "max_length": max([obj["length"] for obj in hitwords])
                    , "num": len(hitwords)
                })
            hitwords_list.append(hitwords_obj)
        
        # ヒット情報を文章データフレームに追加
        passage_df["hitword"] = hitwords_list
        # ヒットした単語がある文章のみをフィルタリング
        passage_df = passage_df[passage_df["hitword"].map(lambda x: x["hit"])]
        return passage_df

    def convert_table(self, passage_df: pd.DataFrame) -> pd.DataFrame:
        """
        passage_dfから新しいデータフレームを作成し、各行にヒットした単語の情報を含む表を生成します。

        Args:
            passage_df (pd.DataFrame): 'curid', 'title', および 'hitword' の列を含むデータフレーム。
                'hitword' 列は、ヒットした単語の情報を含む辞書のリストを持つ必要があります。

        Returns:
            pd.DataFrame: ヒットした単語の情報を含む新しいデータフレーム。
        """
        new_rows = []
        for idx, row in tqdm.tqdm(passage_df.iterrows()):
            curid, title = row["curid"], row["title"]
            hitwords_obj = row["hitword"]
            hitwords = hitwords_obj["hitwords"]
            sum_length = hitwords_obj["sum_length"]
            max_length = hitwords_obj["max_length"]
            num = hitwords_obj["num"]
            for hitword in hitwords:
                word_surface, word_pronunciation = hitword["surface"], hitword["pronunciation"]
                word_length = hitword["length"]
                is_crossing_word_boundary = hitword["is_crossing_word_boundary"]
                context = hitword["context"]
                matched_passage_surface = hitword["matched_passage_surface"]

                new_rows.append({
                    "curid": curid,
                    "title": title,
                    "word_surface": word_surface,
                    "word_pronunciation": word_pronunciation,
                    "word_length": word_length,
                    "is_crossing_word_boundary": is_crossing_word_boundary,
                    "context": context,
                    "matched_passage_surface": matched_passage_surface,
                    "sum_length": sum_length,
                    "max_length": max_length,
                    "num": num
                })
        # 新しいデータフレームを作成
        df = pd.DataFrame(new_rows)
        # 重複する行を削除
        df = df.drop_duplicates(subset=['word_surface', 'matched_passage_surface'], keep='first')
        return df
```

関数名やコメントはGPT4に書いてもらいました。関数名、おしゃれですね。

## テスト
テスト用のデータで出力、挙動を確認します。

```Python
# SerendipitousWordDetectorクラスの定義は省略（上記で提供されたクラス定義を使用）

# テスト用のテキストと単語リストを作成
test_text = "これはテストの文章です。形態素解析を行います。Pythonはプログラミング言語の一つです。"
test_words = ['テスト', '解析', 'Python', 'グゲ']

# SerendipitousWordDetectorクラスのインスタンスを作成
detector = SerendipitousWordDetector()

# tokenizeメソッドのテスト
print("tokenizeメソッドの出力:")
print(detector.tokenize(test_text))

# get_pronunciationメソッドのテスト
print("\nget_pronunciationメソッドの出力:")
print(detector.get_pronunciation(test_text))

# is_word_used_in_original_contextメソッドのテスト
print("\nis_word_used_in_original_contextメソッドの出力:")
surface_tokens, pronunciation_tokens = detector.tokenize(test_text)
for word in test_words:
    print(f"単語 '{word}' が元の文脈で使用されているか: {detector.is_word_used_in_original_context(word, detector.get_pronunciation(word), test_text, pronunciation_tokens)}")

# is_crossing_word_boundaryメソッドのテスト
print("\nis_crossing_word_boundaryメソッドの出力:")
for word in test_words:
    print(f"単語 '{word}' が単語境界をまたいでいるか: {detector.is_crossing_word_boundary(word, detector.get_pronunciation(word), test_text, pronunciation_tokens)}")

# get_word_contextメソッドのテスト
print("\nget_word_contextメソッドの出力:")
for word in test_words:
    print(f"単語 '{word}' のコンテキスト: {detector.get_word_context(detector.get_pronunciation(word), surface_tokens, pronunciation_tokens)}")

# check_hitメソッドのテスト
print("\ncheck_hitメソッドの出力:")
for word in test_words:
    result = detector.check_hit(word, detector.get_pronunciation(word), surface_tokens, pronunciation_tokens)
    print(f"単語 '{word}' のヒット情報: {result}")

# check_hitsメソッドのテスト
print("\ncheck_hitsメソッドの出力:")
word_df = pd.DataFrame({'surface': test_words})
passage_df = pd.DataFrame({'surface': [test_text]})
hitwords_df = detector.check_hits(word_df, passage_df)
print(hitwords_df)
```

```
tokenizeメソッドの出力:
(['これ', 'は', 'テスト', 'の', '文章', 'です', '。', '形態', '素', '解析', 'を', '行い', 'ます', '。', 'Python', 'は', 'プログラミング', '言語', 'の', '一', 'つ', 'です', '。'], ['コレ', 'ワ', 'テスト', 'ノ', 'ブンショー', 'デス', '', 'ケータイ', 'ソ', 'カイセキ', 'オ', 'オコナイ', 'マス', '', 'Python', 'ワ', 'プログラミング', 'ゲンゴ', 'ノ', 'ヒト', 'ツ', 'デス', ''])

get_pronunciationメソッドの出力:
コレワテストノブンショーデスケータイソカイセキオオコナイマスPythonワプログラミングゲンゴノヒトツデス

is_word_used_in_original_contextメソッドの出力:
単語 'テスト' が元の文脈で使用されているか: True
単語 '解析' が元の文脈で使用されているか: True
単語 'Python' が元の文脈で使用されているか: True
単語 'グゲ' が元の文脈で使用されているか: False

is_crossing_word_boundaryメソッドの出力:
単語 'テスト' が単語境界をまたいでいるか: False
単語 '解析' が単語境界をまたいでいるか: False
単語 'Python' が単語境界をまたいでいるか: False
単語 'グゲ' が単語境界をまたいでいるか: True

get_word_contextメソッドの出力:
単語 'テスト' のコンテキスト: これはテストの文章です。形態素解析を行います
単語 '解析' のコンテキスト: これはテストの文章です。形態素解析を行います。Pythonはプログラミング言語の一
単語 'Python' のコンテキスト: 文章です。形態素解析を行います。Pythonはプログラミング言語の一つです。
単語 'グゲ' のコンテキスト: 。形態素解析を行います。Pythonはプログラミング言語の一つです。

check_hitメソッドの出力:
...
単語 'Python' のヒット情報: {'hit': False, 'surface': 'Python', 'pronunciation': 'Python', 'length': 6}
単語 'グゲ' のヒット情報: {'hit': True, 'surface': 'グゲ', 'pronunciation': 'グゲ', 'length': 2, 'is_crossing_word_boundary': False, 'context': '。形態素解析を行います。Pythonはプログラミング言語の一つです。', 'matched_passage_surface': 'プログラミング言語'}

check_hitsメソッドの出力:
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
1it [00:00, 3908.95it/s]
                                         surface  \
0  これはテストの文章です。形態素解析を行います。Pythonはプログラミング言語の一つです。   

                                      surface_tokens  \
0  [これ, は, テスト, の, 文章, です, 。, 形態, 素, 解析, を, 行い, ま...   

                                pronunciation_tokens  \
0  [コレ, ワ, テスト, ノ, ブンショー, デス, , ケータイ, ソ, カイセキ, オ,...   

                                       pronunciation  \
0  コレワテストノブンショーデスケータイソカイセキオオコナイマスPythonワプログラミングゲン...   

                                             hitword  
0  {'hitwords': [{'hit': True, 'surface': 'グゲ', '...  
```
## check_hits
check_hits
```Python
# passage_dfの作成
passage_data = {
    'surface': [
        'これはテストの文章です。',
        '形態素解析を行います。',
        'Pythonはプログラミング言語の一つです。'
    ]
}
passage_df = pd.DataFrame(passage_data)

# word_dfの作成
word_data = {
    'surface': ['テスト', '解析', 'Python', 'グゲ']
}
word_df = pd.DataFrame(word_data)

# Parserクラスのインスタンスを作成
parser = SerendipitousWordDetector()
# check_hitsメソッドをテスト
hitwords_df = parser.check_hits(word_df, passage_df)

print(hitwords_df)
```

最後のcheck_hitsのテストでは、word_dfのうち単語をまたいで登場する音韻は「グゲ」だけであるため、それだけがヒットすることを期待されます。実際、そのようにヒットしておりよさそうです。

convert_tableのみ専用のデータを作ってテストします。

```Python
# テストデータの作成
test_data = {
    'curid': [1, 2],
    'title': ['テストタイトル1', 'テストタイトル2'],
    'hitword': [
        {
            "hitwords": [
                {'hit': True, 'surface': 'テスト', 'pronunciation': 'テスト', 'length': 3, 'is_crossing_word_boundary': False, 'context': 'これはテストの文章です。', 'matched_passage_surface': 'テスト'},
                {'hit': True, 'surface': '解析', 'pronunciation': 'カイセキ', 'length': 4, 'is_crossing_word_boundary': True, 'context': '形態素解析を行います。', 'matched_passage_surface': '解析'}
            ],
            "sum_length": 7,
            "max_length": 4,
            "num": 2
        },
        {
            "hitwords": [
                {'hit': True, 'surface': 'Python', 'pronunciation': 'パイソン', 'length': 5, 'is_crossing_word_boundary': False, 'context': 'Pythonはプログラミング言語の一つです。', 'matched_passage_surface': 'Python'}
            ],
            "sum_length": 5,
            "max_length": 5,
            "num": 1
        }
    ]
}

# データフレームの作成
passage_df = pd.DataFrame(test_data)

# SerendipitousWordDetectorクラスのインスタンスを作成
detector = SerendipitousWordDetector()

# convert_tableメソッドをテスト
converted_df = detector.convert_table(passage_df)

# 結果の表示
print(converted_df)
```

```
2it [00:00, 3300.00it/s]
   curid     title word_surface word_pronunciation  word_length  \
0      1  テストタイトル1          テスト                テスト            3   
1      1  テストタイトル1           解析               カイセキ            4   
2      2  テストタイトル2       Python               パイソン            5   

   is_crossing_word_boundary                 context matched_passage_surface  \
0                      False            これはテストの文章です。                     テスト   
1                       True             形態素解析を行います。                      解析   
2                      False  Pythonはプログラミング言語の一つです。                  Python   

   sum_length  max_length  num  
0           7           4    2  
1           7           4    2  
2           5           5    1  
```

## 単語リストの取得と整形
実際のデータで作業をしていきます。
ポケモン名のtxtをダウンロードし、surfaceという列名をつけて再保存します。
元データがshiftjisのため、扱いやすいようにutf8に変換して保存します。

単語リストをダウンロードし、data/poke.csvというパスに保存します。

```sh
# macの場合
curl -o data/poke.txt https://wonderhorn.net/material/poke.txt
```

poke.csvの中身は以下のような感じです。shiftjisでencodeされていることに注意してください。

```data/poke.txt
フシギダネ
フシギソウ
フシギバナ
ヒトカゲ
...
```

surfaceという列名をもつcsvとして、utf8で保存し直します。もしくは手動で書き換え、再エンコードを行ってもよいです。

```Python
def format_poketext(input_path: str, output_path: str
                    , *
                    , input_encoding = "shiftjis"
                    , output_encoding = "utf8")->None:
    df = pd.read_csv(input_path, header=None, encoding=input_encoding)
    df.columns=["surface"]
    df.to_csv(output_path, encoding=output_encoding, index=False)
  
RAW_FILE_PATH = "data/poke.txt"
WORDLIST_PATH = "data/poke_formatted.csv"

format_poketext(RAW_FILE_PATH, WORDLIST_PATH)    
```

```csv:data/poketext_formatted.csv
surface
フシギダネ
フシギソウ
フシギバナ
ヒトカゲ
...
```

## wikipediaデータのダウンロードと整形

huggingfaceからwikipediaのデータをダウンロードします。

```Python
from datasets import load_dataset

# URL of the dataset
dataset_url = "izumi-lab/wikipedia-ja-20230720"

# Load the dataset
dataset = load_dataset(dataset_url)

# To view basic information or manipulate the dataset, you can use:
print(dataset)
```

```
DatasetDict({
    train: Dataset({
        features: ['curid', 'title', 'text'],
        num_rows: 1362415
    })
})
```

trainスプリットから要素を一部取り出し、中身を確認します。ついでに「text」列をsurface列に変換しています。

```Python
subset = dataset["train"][:3]
passage_df = pd.DataFrame(subset)
passage_df.rename(columns={"text": "surface"}, inplace=True)
print(passage_df.head())
```

```
  curid   title                                            surface
0     5  アンパサンド  アンパサンド（&amp;, ）は、並立助詞「…と…」を意味する記号である。ラテン語で「…と…...
1    10      言語  言語（げんご、language）は、狭義には「声による記号の体系」をいう。\n広辞苑や大辞泉...
2    11     日本語  日本語（にほんご、にっぽんご、）は、日本国内や、かつての日本領だった国、そして国外移民や移住...
```

surfaceが記事全体だと長すぎるので、句点で区切って１行1文に変換します。

```Python
def split_surface_to_sentences(df: pd.DataFrame) -> pd.DataFrame:
    """
    DataFrame内の'surface'列に含まれるテキストを文に分割し、新しいDataFrameを作成する関数。
    各行は'surface'列のテキストを句点「。」で分割し、それぞれの文を新しい行として追加する。
    分割された文は'surface'列に格納される。
    df: 分割する文が含まれるDataFrame。'surface'列が必要。
    戻り値: 'surface'列に分割された文を含む新しいDataFrame。
    """
    def split_sentences(row):
        # 'surface'列のテキストを行ごとに分割し、さらに句点「。」で文に分割する
        sentences = [sentence for line in row['surface'].splitlines() for sentence in line.split('。') if sentence]
        # 分割された文を新しいDataFrameに格納し、他の列は元の値を繰り返す
        return pd.DataFrame({col: [row[col]]*len(sentences) if col != 'surface' else sentences for col in df.columns})

    # 全ての行に対してsplit_sentences関数を適用し、結果を結合する
    new_df = pd.concat(df.apply(split_sentences, axis=1).tolist(), ignore_index=True)
    return new_df

subset = dataset["train"][:3]
article_df = pd.DataFrame(subset)
article_df.rename(columns={"text": "surface"}, inplace=True)
passage_df = split_surface_to_sentences(article_df)
print(len(article_df))
print(article_df.head())
```

```
1589
  curid   title                                            surface
0     5  アンパサンド               アンパサンド（&amp;, ）は、並立助詞「…と…」を意味する記号である
1     5  アンパサンド                    ラテン語で「…と…」を表す接続詞 "et" の合字を起源とする
2     5  アンパサンド  現代のフォントでも、Trebuchet MS など一部のフォントでは、"et" の合字である...
3     5  アンパサンド                                                語源.
4     5  アンパサンド  英語で教育を行う学校でアルファベットを復唱する場合、その文字自体が単語となる文字（"A", ...
```

3記事が1589行になりました。

## 記事の検出
冒頭３記事分のpassage_dfをそのまま使用して、記事の検出を行います。

```Python
detector = SerendipitousWordDetector()
word_df = pd.read_csv(WORDLIST_PATH)
# check_hitsメソッドをテスト
hitwords_df = detector.check_hits(df, passage_df)

converted_df = detector.convert_table(hitwords_df)

hitwords_df.to_csv(f"data/poke_hitwords_{len(article_df)}.csv", index=False)
converted_df.to_csv(f"data/poke_hitwords_{len(article_df)}_converted.csv", index=False)
```

生成されたファイルの中身を確認してみます。

```data/poke_hitwords_3.csv
curid,title,surface,surface_tokens,pronunciation_tokens,pronunciation,hitword
10,言語,言語と非言語の境界が問題になるが、文字を使う方法と文字を用いない方法の区別のみで、言語表現を非言語表現から区別することはできない,"['言語', 'と', '非', '言語', 'の', '境界', 'が', '問題', 'に', 'なる', 'が', '、', '文字', 'を', '使う', '方法', 'と', '文字', 'を', '用い', 'ない', '方法', 'の', '区別', 'のみ', 'で', '、', '言語', '表現', 'を', '非', '言語', '表現', 'から', '区別', 'する', 'こと', 'は', 'でき', 'ない']","['ゲンゴ', 'ト', 'ヒ', 'ゲンゴ', 'ノ', 'キョーカイ', 'ガ', 'モンダイ', 'ニ', 'ナル', 'ガ', '', 'モジ', 'オ', 'ツカウ', 'ホーホー', 'ト', 'モジ', 'オ', 'モチー', 'ナイ', 'ホーホー', 'ノ', 'クベツ', 'ノミ', 'デ', '', 'ゲンゴ', 'ヒョーゲン', 'オ', 'ヒ', 'ゲンゴ', 'ヒョーゲン', 'カラ', 'クベツ', 'スル', 'コト', 'ワ', 'デキ', 'ナイ']",ゲンゴトヒゲンゴノキョーカイガモンダイニナルガモジオツカウホーホートモジオモチーナイホーホーノクベツノミデゲンゴヒョーゲンオヒゲンゴヒョーゲンカラクベツスルコトワデキナイ,"{'hitwords': [{'hit': True, 'surface': 'ホーホー', 'pronunciation': 'ホーホ', 'length': 3, 'is_crossing_word_boundary': False, 'context': '境界が問題になるが、文字を使う方法と文字を用いない方法の区別のみで', 'matched_passage_surface': '方法'}], 'hit': True, 'sum_length': 3, 'max_length': 3, 'num': 1}"
10,言語,同一語族に属する言語群の場合、共通語彙から言語の分化した年代を割り出す方法も考案されている,"['同一', '語', '族', 'に', '属する', '言語', '群', 'の', '場合', '、', '共通', '語彙', 'から', '言語', 'の', '分化', 'し', 'た', '年代', 'を', '割り出す', '方法', 'も', '考案', 'さ', 'れ', 'て', 'いる']","['ドーイツ', 'ゴ', 'ゾク', 'ニ', 'ゾクスル', 'ゲンゴ', 'グン', 'ノ', 'バアイ', '', 'キョーツー', 'ゴイ', 'カラ', 'ゲンゴ', 'ノ', 'ブンカ', 'シ', 'タ', 'ネンダイ', 'オ', 'ワリダス', 'ホーホー', 'モ', 'コーアン', 'サ', 'レ', 'テ', 'イル']",ドーイツゴゾクニゾクスルゲンゴグンノバアイキョーツーゴイカラゲンゴノブンカシタネンダイオワリダスホーホーモコーアンサレテイル,"{'hitwords': [{'hit': True, 'surface': 'ホーホー', 'pronunciation': 'ホーホ', 'length': 3, 'is_crossing_word_boundary': False, 'context': '語彙から言語の分化した年代を割り出す方法も考案されている', 'matched_passage_surface': '方法'}], 'hit': True, 'sum_length': 3, 'max_length': 3, 'num': 1}"
10,言語,学校教育もこの言語で行われるが、民族語とかけ離れた存在であることもあり国民の中で使用できる層はさほど多くない,"['学校', '教育', 'も', 'この', '言語', 'で', '行わ', 'れる', 'が', '、', '民族', '語', 'と', 'かけ離れ', 'た', '存在', 'で', 'ある', 'こと', 'も', 'あり', '国民', 'の', '中', 'で', '使用', 'できる', '層', 'は', 'さほど', '多く', 'ない']","['ガッコー', 'キョーイク', 'モ', 'コノ', 'ゲンゴ', 'デ', 'オコナワ', 'レル', 'ガ', '', 'ミンゾク', 'ゴ', 'ト', 'カケハナレ', 'タ', 'ソンザイ', 'デ', 'アル', 'コト', 'モ', 'アリ', 'コクミン', 'ノ', 'ナカ', 'デ', 'シヨー', 'デキル', 'ソー', 'ワ', 'サホド', 'オーク', 'ナイ']",ガッコーキョーイクモコノゲンゴデオコナワレルガミンゾクゴトカケハナレタソンザイデアルコトモアリコクミンノナカデシヨーデキルソーワサホドオークナイ,"{'hitwords': [{'hit': True, 'surface': 'ドオー', 'pronunciation': 'ドオー', 'length': 3, 'is_crossing_word_boundary': False, 'context': 'もあり国民の中で使用できる層はさほど多くない', 'matched_passage_surface': 'さほど多く'}], 'hit': True, 'sum_length': 3, 'max_length': 3, 'num': 1}"
11,日本語,大野晋は日本語が語彙・文法などの点でタミル語と共通点を持つとの説を唱えるが、比較言語学の方法上の問題から批判が多い,"['大野', '晋', 'は', '日本', '語', 'が', '語彙', '・', '文法', 'など', 'の', '点', 'で', 'タミル', '語', 'と', '共通', '点', 'を', '持つ', 'と', 'の', '説', 'を', '唱える', 'が', '、', '比較', '言語', '学', 'の', '方法', '上', 'の', '問題', 'から', '批判', 'が', '多い']","['オーノ', 'ススム', 'ワ', 'ニッポン', 'ゴ', 'ガ', 'ゴイ', '', 'ブンポー', 'ナド', 'ノ', 'テン', 'デ', 'タミル', 'ゴ', 'ト', 'キョーツー', 'テン', 'オ', 'モツ', 'ト', 'ノ', 'セツ', 'オ', 'トナエル', 'ガ', '', 'ヒカク', 'ゲンゴ', 'ガク', 'ノ', 'ホーホー', 'ジョー', 'ノ', 'モンダイ', 'カラ', 'ヒハン', 'ガ', 'オーイ']",オーノススムワニッポンゴガゴイブンポーナドノテンデタミルゴトキョーツーテンオモツトノセツオトナエルガヒカクゲンゴガクノホーホージョーノモンダイカラヒハンガオーイ,"{'hitwords': [{'hit': True, 'surface': 'ホーホー', 'pronunciation': 'ホーホ', 'length': 3, 'is_crossing_word_boundary': False, 'context': 'の説を唱えるが、比較言語学の方法上の問題から批判が多い', 'matched_passage_surface': '方法'}], 'hit': True, 'sum_length': 3, 'max_length': 3, 'num': 1}"
11,日本語,これは、「直前の母音を1モーラ分引く」という方法で発音される独立した特殊モーラである,"['これ', 'は', '、', '「', '直前', 'の', '母音', 'を', '1', 'モーラ', '分', '引く', '」', 'と', 'いう', '方法', 'で', '発音', 'さ', 'れる', '独立', 'し', 'た', '特殊', 'モーラ', 'で', 'ある']","['コレ', 'ワ', '', '', 'チョクゼン', 'ノ', 'ボイン', 'オ', '1', 'モーラ', 'ブン', 'ヒク', '', 'ト', 'ユー', 'ホーホー', 'デ', 'ハツオン', 'サ', 'レル', 'ドクリツ', 'シ', 'タ', 'トクシュ', 'モーラ', 'デ', 'アル']",コレワチョクゼンノボインオ1モーラブンヒクトユーホーホーデハツオンサレルドクリツシタトクシュモーラデアル,"{'hitwords': [{'hit': True, 'surface': 'ホーホー', 'pronunciation': 'ホーホ', 'length': 3, 'is_crossing_word_boundary': False, 'context': 'の母音を1モーラ分引く」という方法で発音される独立した特殊モーラで', 'matched_passage_surface': '方法'}], 'hit': True, 'sum_length': 3, 'max_length': 3, 'num': 1}"
11,日本語,たとえば、「です」「ます」は のように発音されるし、「菊」「力」「深い」「放つ」「秋」などはそれぞれ と発音されることがある,"['たとえば', '、', '「', 'です', '」', '「', 'ます', '」', 'は', 'の', 'よう', 'に', '発音', 'さ', 'れる', 'し', '、', '「', '菊', '」', '「', '力', '」', '「', '深い', '」', '「', '放つ', '」', '「', '秋', '」', 'など', 'は', 'それぞれ', 'と', '発音', 'さ', 'れる', 'こと', 'が', 'ある']","['タトエバ', '', '', 'デス', '', '', 'マス', '', 'ワ', 'ノ', 'ヨー', 'ニ', 'ハツオン', 'サ', 'レル', 'シ', '', '', 'キク', '', '', 'チカラ', '', '', 'フカイ', '', '', 'ハナツ', '', '', 'アキ', '', 'ナド', 'ワ', 'ソレゾレ', 'ト', 'ハツオン', 'サ', 'レル', 'コト', 'ガ', 'アル']",タトエバデスマスワノヨーニハツオンサレルシキクチカラフカイハナツアキナドワソレゾレトハツオンサレルコトガアル,"{'hitwords': [{'hit': True, 'surface': 'デスマス', 'pronunciation': 'デスマス', 'length': 4, 'is_crossing_word_boundary': False, 'context': 'たとえば、「です」「ます」はのように発音されるし、', 'matched_passage_surface': 'です」「ます'}], 'hit': True, 'sum_length': 4, 'max_length': 4, 'num': 1}"
11,日本語,なお、「だ行」の「ぢ」「づ」は、一部方言を除いて「ざ行」の「じ」「ず」と同音に帰しており、発音方法は同じである,"['なお', '、', '「', 'だ行', '」', 'の', '「', 'ぢ', '」', '「', 'づ', '」', 'は', '、', '一部', '方言', 'を', '除い', 'て', '「', 'ざ行', '」', 'の', '「', 'じ', '」', '「', 'ず', '」', 'と', '同音', 'に', '帰し', 'て', 'おり', '、', '発音', '方法', 'は', '同じ', 'で', 'ある']","['ナオ', '', '', 'ダギョー', '', 'ノ', '', 'ジ', '', '', 'ズ', '', 'ワ', '', 'イチブ', 'ホーゲン', 'オ', 'ノゾイ', 'テ', '', 'ザギョー', '', 'ノ', '', 'ジ', '', '', 'ズ', '', 'ト', 'ドーオン', 'ニ', 'カエシ', 'テ', 'オリ', '', 'ハツオン', 'ホーホー', 'ワ', 'オナジ', 'デ', 'アル']",ナオダギョーノジズワイチブホーゲンオノゾイテザギョーノジズトドーオンニカエシテオリハツオンホーホーワオナジデアル,"{'hitwords': [{'hit': True, 'surface': 'ホーホー', 'pronunciation': 'ホーホ', 'length': 3, 'is_crossing_word_boundary': False, 'context': 'ず」と同音に帰しており、発音方法は同じである', 'matched_passage_surface': '方法'}], 'hit': True, 'sum_length': 3, 'max_length': 3, 'num': 1}"
...
```

```data/poke_hitwords_3_converted.csv
curid,title,word_surface,word_pronunciation,word_length,is_crossing_word_boundary,context,matched_passage_surface,sum_length,max_length,num
10,言語,ホーホー,ホーホ,3,False,境界が問題になるが、文字を使う方法と文字を用いない方法の区別のみで,方法,3,3,1
10,言語,ドオー,ドオー,3,False,もあり国民の中で使用できる層はさほど多くない,さほど多く,3,3,1
11,日本語,デスマス,デスマス,4,False,たとえば、「です」「ます」はのように発音されるし、,です」「ます,4,4,1
11,日本語,サンド,サンド,3,False,」「姉さん、どこへ行くの,さん、どこ,3,3,1
11,日本語,ヤクデ,ヤクデ,3,False,"英訳で""Wehold…""（われらは信ずる）",英訳で,3,3,1
11,日本語,ラッタ,ラッタ,3,False,」「私は泣かない」「花が笑った」「さあ、出かけよう」「今日は来,笑った,3,3,1
11,日本語,ドードー,ドードー,4,False,点を表さない「に」（例、受動動作の主体「先生にほめられた」、,受動動作,4,4,1
11,日本語,ラッタ,ラッタ,3,False,の手塚治虫は、漫画を英訳してもらったところ、「ドギューン」「シーン」などの,もらった,3,3,1
11,日本語,ゴース,ゴース,3,False,「軟」「薄」などの成分と結合することにより、「脆弱」「貧弱」,結合する,3,3,1
11,日本語,ドオー,ドオー,3,False,」「挙式」「即答」「熱演」など多くの和製漢語が用いられている,など多く,3,3,1
11,日本語,フシデ,フシデ,3,False,「語種」の節で触れた混種語、すなわち、「プロ野球,節で,3,3,1
11,日本語,ホーホー,ホーホ,3,True,わ」などと読まれる国字で、中国地方ほかで定着しているという,地方ほか,3,3,1
11,日本語,デスマス,デスマス,4,False,このことは、「ですます体」「でございます体」「だ,ですます,4,4,1
11,日本語,メテノ,メテノ,3,False,は「文体」に「話体」も含めて述べる,含めて述べる,3,3,1
11,日本語,ホーホー,ホーホ,3,True,なく、東京式アクセントは概ね北海道、東北地方北部、関東地方西部、甲信越地方、東海地方,地方北部,3,3,1
11,日本語,ホーホー,ホーホ,3,True,の方言に対し、東北地方南部から関東地方北東部にかけての地域や、九州の,地方北東,3,3,1
11,日本語,ドオー,ドオー,3,False,を借用することは、古代にはそれほど多くなかった,ほど多く,3,3,1
11,日本語,キモリ,キモリ,3,False,の巻14「東歌」や巻20「防人歌」には当時の東国方言による,防人,3,3,1
```

# おわりに
以上です。単語リストをポケモン以外の者にするなどして楽しんでいただければと思います。

# 参考
- [ウーマンコミュニケーション](https://store.steampowered.com/agecheck/app/2095090/): アイデアの着想
- [偶然短歌bot](https://twitter.com/g57577?lang=ja): 「偶然」という用語のリスペクト元