一日のうちに一回も発さない平仮名は存在するのか? (青空文庫編)

# はじめに
http://www.youtube.com/@onsei_housou

ネットラジオを聴いていたらこんな話題が出ていた。
> 一日のうちに一回も発さない平仮名は存在するのか?

確かにコンディション次第ではありそうな気がするので調べてみる。(予想では「ち」か「ぬ」)
# 対象
| 対象 | 詳細 |
| - | - |
| 調査対象 | 青空文庫の著作権切れの文学作品における会話文 |
| 集計対象 | 清音、濁音、半濁音 |
| 除外対象 | 平仮名という日本語依存の音を対象とする為、日本語以外の会話は除外する |

- 発話がはっきりとした日常会話の音声コーパスに心当たりが無い
- 匿名化してもラジオ音声を使っちゃうのは怖い

上記理由で今回は青空文庫中の会話文を抽出し、それを対象に集計していく。
青空文庫のテキストデータは以下から取得が可能。

https://github.com/aozorahack/aozorabunko_text

# 1. 青空文庫から会話文を抽出
まずは青空文庫から会話文を抽出する。
## 1.1 データ形式
青空文庫からテキストデータは以下のようなディレクトリ構造。
```bash:aozorabunko_text-master
# .txt でテキストデータが存在している
aozorabunko_text-master
└── cards
    ├── 000005
    │   └── files
    │       ├── 5_ruby_21311
    │       │   └── 5_ruby_21311.txt
    │       └── 53194_ruby_44732
    │           └── 53194_ruby_44732.txt
    └── 000006
        └── files
            ├── 382_ruby_22429
            │   └── 382_ruby_22429.txt
            └── 383_ruby_22434
                └── 383_ruby_22434.txt
...
```
それぞれの文学作品は以下のように格納されている。
```text: 55215_ruby_49076.txt
露西亞の言葉
トゥルゲニエフ　Ivan Tourguenieff
上田敏訳

-------------------------------------------------------
【テキスト中に現れる記号について】

《》：ルビ
（例）惑《まど》ふ
-------------------------------------------------------

　疑ひ惑《まど》ふけふこのごろ、國運を思ひて、心病みぬるけふこのごろ、なれこそは、杖なれ、より木なれ、噫《あゝ》、大なるかな、忠なるかな、自由なるかな、露西亞の言葉よ。汝なかりせば、今の故國のさまをみて、たれか望《のぞみ》を絶たざらむ。しかも、大なる國民にあらずして、かゝる言葉をもたむこと、夢にも思ひえせざるなり。



底本：「上田敏全訳詩集」岩波文庫、岩波書店
　　　1962（昭和37）年12月16日第1刷発行
　　　2010（平成22）年4月21日第38刷改版発行
初出：「明星　三ノ二」
　　　1902（明治35）年8月
入力：川山隆
校正：岡村和彦
2013年1月9日作成
青空文庫作成ファイル：
このファイルは、インターネットの図書館、青空文庫（http://www.aozora.gr.jp/）で作られました。入力、校正、制作にあたったのは、ボランティアの皆さんです。
```
ここから分かる通り、メタデータやルビなど今回の調査に不要な情報も格納されているため、これらを除外し、その上で`「」`に囲まれた文字列を会話文として抽出する。

## 1.2 会話文を抽出
https://qiita.com/dzbt_dzbt/items/593dbd698a07c12a771c

前述の格納例から分かる通り、メタデータやルビなど今回の調査に不要な情報も格納されている。
今回は上記記事を参考に、それらを除外した上で`「」`に囲まれた文字列の抽出を行った。
```python: 
import os
import pandas as pd
import re
import chardet
import subprocess

def count_txt_files(directory):
    result = subprocess.run(f'find {directory} -type f -name "*.txt" | wc -l', 
                            stdout=subprocess.PIPE, 
                            stderr=subprocess.PIPE, 
                            text=True, 
                            shell=True)
    if result.returncode == 0:
        return int(result.stdout.strip())
    else:
        print(f"Error: {result.stderr}")
        return 0
      
count_files = count_txt_files('cards')

txt_files = []
for root, dirs, files in os.walk('cards'):
    for file in files:
        if file.endswith('.txt'):
            txt_files.append(os.path.join(root, file))

processed_dataframes = []
count = 0
error_count = 0

for txt_file in txt_files:
    
    count += 1
    print(f'{count}/{count_files}')
    
    # エンコードを検出
    with open(txt_file, 'rb') as f:
        result = chardet.detect(f.read())
    encoding = result['encoding']
    
    try:
        with open(txt_file, 'r', encoding=encoding) as f:
            lines = f.readlines()
        
        lines = [line.strip('\n') for line in lines]
        df = pd.DataFrame(lines, columns=['text'])
        # 作者名を抽出(ここではテキストの2行目を作者名と仮定
        if len(df) >= 2:
            author_name = df.iloc[1]['text']
        else:
            author_name = ''
        head_tx = list(df[df['text'].str.contains('-------------------------------------------------------')].index)
        # '底本：'の行番号を取得
        atx = list(df[df['text'].str.contains('底本：')].index)
        # 本文の開始位置を決定
        if head_tx == []:
            head_tx = list(df[df['text'].str.contains(author_name)].index)
            if head_tx:
                head_tx_num = head_tx[0] + 1
            else:
                head_tx_num = 0
        elif len(head_tx) >= 2:
            head_tx_num = head_tx[1] + 1
        else:
            head_tx_num = head_tx[0] + 1
        # 本文の終了位置を決定
        if atx:
            end_index = atx[0]
        else:
            end_index = len(df)
        # 本文を抽出
        df_e = df.iloc[head_tx_num:end_index].copy()
        # 青空文庫の書式削除
        df_e['text'] = df_e['text'].str.replace('《.*?》', '', regex=True)
        df_e['text'] = df_e['text'].str.replace('［.*?］', '', regex=True)
        df_e['text'] = df_e['text'].str.replace('｜', '', regex=True)
        # 字下げ(行頭の全角スペース)を削除
        df_e['text'] = df_e['text'].str.replace('^　+', '', regex=True)
        # 節区切りを削除
        df_e['text'] = df_e['text'].str.replace('^.$', '', regex=True)
        df_e['text'] = df_e['text'].str.replace('^―――.*$', '', regex=True)
        df_e['text'] = df_e['text'].str.replace('^＊＊＊.*$', '', regex=True)
        df_e['text'] = df_e['text'].str.replace('^×××.*$', '', regex=True)
        # 記号、および記号削除によって残ったカッコを削除
        df_e['text'] = df_e['text'].str.replace('―', '', regex=True)
        df_e['text'] = df_e['text'].str.replace('…', '', regex=True)
        df_e['text'] = df_e['text'].str.replace('※', '', regex=True)
        df_e['text'] = df_e['text'].str.replace('「」', '', regex=True)
        # 一文字以下で構成されている行を削除
        df_e['length'] = df_e['text'].map(lambda x: len(x))
        df_e = df_e[df_e['length'] > 1]
        # インデックスをリセット
        df_e = df_e.reset_index(drop=True)
        # 鉤括弧が囲まれた文字列の行以外を削除し、鉤括弧のなかに含まれる文字列を抽出
        df_e['text'] = df_e['text'].apply(lambda x: ' '.join(re.findall('「(.*?)」', x)))
        # 空白行を削除
        df_e = df_e[df_e['text'] != '']
        
        df_e = df_e.reset_index(drop=True)
        df_e = df_e.drop(columns=['length'])
        
        processed_dataframes.append(df_e)
    except:
        error_count += 1
        continue

print(f'used file: {count_files - error_count}')

final_df = pd.concat(processed_dataframes, ignore_index=True)

final_df.to_csv('processed_texts.csv', index=False, encoding='utf-8')
```
## 1.3 実行結果
- 使用した文学作品数: 17435

```: processed_texts.csv
text
困るのは小作だけでない
学校
お前等にもわかる時が来る
戦争反対
いまにわかる時が来る
何たる矛盾ぞ
何たる矛盾ぞ！
ひとっ走りに行って来るよ
ん、大胆に細心に
オーライ
神様を粗末にするから罰が当ったのだ
三ちゃあー
俺の不注意から
・・・
```
良い感じに抽出出来てそうなものの、「学校」など強調目的で`「」`を付けられているものも持ってきている感じがする。

# 2. 平仮名化
会話文は抽出出来たものの、漢字の読みは現状取得出来ない為、会話文を平仮名へ変換する。
## 2.1 実行内容
平仮名化を行うとともに、1ですり抜けた特殊文字や日本語以外の言語も除外をする。
```python: 
import pandas as pd
import re
import MeCab
import jaconv
import ipadic

df = pd.read_csv('processed_texts.csv')

tagger = MeCab.Tagger(ipadic.MECAB_ARGS)

def process_text(text):
    # 平仮名、カタカナ、漢字以外の文字を除外
    text = re.sub(r'[^\u3040-\u30FF\u4E00-\u9FFF]+', '', text)
    
    # カタカナを平仮名に変換
    text = jaconv.kata2hira(text)
    
    # MeCabを用いて文脈に従った読み仮名を取得
    node = tagger.parseToNode(text)
    result = ''
    while node:
        if node.surface:
            features = node.feature.split(',')
            if len(features) >= 8 and features[7] != '*':
                reading = features[7]
                reading = jaconv.kata2hira(reading)
                result += reading
            else:
                result += node.surface
        node = node.next
    return result

df['processed_text'] = df['text'].apply(process_text)

df[['processed_text']].to_csv('refined_texts.csv', index=False, encoding='utf-8')
```
## 2.2 実行結果
「おまえとう」や「ひとっはしり」など、若干文脈から外れてしまっていそうな気もするが、全体的には悪くなさそう。
```text: refined_texts.csv
processed_text
こまるのはこさくだけでない
がっこう
おまえとうにもわかるときがくる
せんそうはんたい
いまにわかるときがくる
なんたるむじゅんぞ
なんたるむじゅんぞ
ひとっはしりにいってくるよ
んだいたんにさいしんに
おーらい
かみさまをそまつにするからばちがあたったのだ
さんちゃあー
おれのふちゅういから
・・・
```

# 3. 集計
今までの前処理をもとに清音、濁音、半濁音のカウントと全体に対する割合を集計する。
## 3.1 実行内容
今回は小文字系を除外して集計。
```python:
import pandas as pd
from collections import Counter

df = pd.read_csv('refined_texts.csv')

seion_chars = set('あいうえお'
                  'かきくけこ'
                  'さしすせそ'
                  'たちつてと'
                  'はひふへほ'
                  'まみむめも'
                  'やゆよ'
                  'らりるれろ'
                  'わをん')  # 小文字は除外

dakuon_chars = set('がぎぐげご'
                   'ざじずぜぞ'
                   'だぢづでど'
                   'ばびぶべぼ'
                   'ゔ')

handakuon_chars = set('ぱぴぷぺぽ')

seion_counter = Counter()
dakuon_counter = Counter()
handakuon_counter = Counter()
total_chars = 0

for text in df['processed_text']:
    try:
      for ch in text:
          # 平仮名以外の文字は無視する
          if not ('\u3040' <= ch <= '\u309F'):
              continue
          total_chars += 1
          if ch in seion_chars:
              seion_counter[ch] += 1
          elif ch in dakuon_chars:
              dakuon_counter[ch] += 1
          elif ch in handakuon_chars:
              handakuon_counter[ch] += 1
    except:
        continue

total_hiragana = seion_counter.total() + dakuon_counter.total() + handakuon_counter.total()

def create_dataframe(counter, total_hiragana):
    df = pd.DataFrame(counter.items(), columns=['character', 'count'])
    df['proportion'] = df['count'] / total_hiragana * 100 
    df['proportion'] = df['proportion'].round(2)
    return df

df_seion = create_dataframe(seion_counter, total_hiragana)
df_dakuon = create_dataframe(dakuon_counter, total_hiragana)
df_handakuon = create_dataframe(handakuon_counter, total_hiragana)

df_seion_counts = df_seion.sort_values('count', ascending=False)
df_seion_counts.to_csv('seion_counts.csv', index=False, encoding='utf-8')

df_dakuon_counts = df_dakuon.sort_values('count', ascending=False)
df_dakuon_counts.to_csv('dakuon_counts.csv', index=False, encoding='utf-8')

df_handakuon_counts = df_handakuon.sort_values('count', ascending=False)
df_handakuon_counts.to_csv('handakuon_counts.csv', index=False, encoding='utf-8')
```

## 3.2 実行結果
「ぬ」最下位！
#### 清音
|character|count|proportion|
|:----|:----|:----|
|い|3062554|6.08|
|ん|2325221|4.62|
|う|2293769|4.55|
|し|2163863|4.3|
|の|1910571|3.79|
|か|1845513|3.66|
|な|1794257|3.56|
|た|1666292|3.31|
|と|1534117|3.05|
|に|1371809|2.72|
|て|1346338|2.67|
|は|1312313|2.61|
|ま|1284596|2.55|
|こ|1203612|2.39|
|も|1129907|2.24|
|お|1068815|2.12|
|く|1047231|2.08|
|あ|989729|1.96|
|ら|942140|1.87|
|き|923110|1.83|
|る|908342|1.8|
|す|852654|1.69|
|れ|812520|1.61|
|さ|776871|1.54|
|り|734383|1.46|
|つ|728509|1.45|
|を|673940|1.34|
|ち|663004|1.32|
|そ|648368|1.29|
|よ|637147|1.26|
|え|586937|1.17|
|わ|569768|1.13|
|や|531819|1.06|
|せ|470352|0.93|
|み|461048|0.92|
|け|458212|0.91|
|ろ|419268|0.83|
|ね|338920|0.67|
|め|293132|0.58|
|ひ|292300|0.58|
|へ|224798|0.45|
|ほ|224001|0.44|
|ふ|215010|0.43|
|む|169493|0.34|
|ゆ|113394|0.23|
|ぬ|77913|0.15|

#### 濁音
|character|count|proportion|
|:----|:----|:----|
|で|1050027|2.34|
|が|1026398|2.29|
|だ|947744|2.11|
|じ|666931|1.49|
|ど|460272|1.03|
|ご|321846|0.72|
|ば|289059|0.64|
|ぶ|187692|0.42|
|げ|159564|0.36|
|ず|147735|0.33|
|ざ|140327|0.31|
|ぼ|135546|0.3|
|ぎ|121097|0.27|
|ぞ|101783|0.23|
|び|95773|0.21|
|ぜ|92980|0.21|
|ぐ|83188|0.19|
|べ|81525|0.18|
|づ|25816|0.06|
|ぢ|24326|0.05|
|ゔ|5743|0.01|

#### 半濁音
|character|count|proportion|
|:----|:----|:----|
|ぱ|45758|0.1|
|ぽ|29564|0.07|
|ぷ|12861|0.03|
|ぴ|10634|0.02|
|ぺ|7895|0.02|

# 最後に
やってる内にいつの間にか一日に一回も発さないひらがなではなく、使用頻度調査になってしまった。
ちなみに調べていたら会話における文字・音節の使用頻度を調査した論文を見つけた。

- [堀田 修．"文字・音節の使用頻度による平仮名の文字習得要因に関する研究"，1984](https://www.jstage.jst.go.jp/article/jjep1953/32/1/32_68/_pdf)

清音のみで今回の結果と比較してみると

| 文字   | 成人 (%) | 5歳 (%) | 3歳 (%) | proportion |
|--------|----------|---------|---------|------------|
| ア     | 38.33    | 46.43   | 55.94   | 1.96       |
| イ     | 79.98    | 73.14   | 72.63   | 6.08       |
| ウ     | 58.76    | 49.46   | 51.21   | 4.55       |
| エ     | 13.79    | 21.60   | 24.78   | 1.17       |
| オ     | 16.89    | 18.37   | 21.77   | 2.12       |
| カ     | 42.83    | 30.68   | 26.14   | 3.66       |
| キ     | 14.24    | 12.99   | 11.82   | 1.83       |
| ク     | 20.09    | 17.70   | 16.40   | 2.08       |
| ケ     | 11.93    | 9.29    | 6.52    | 0.91       |
| コ     | 20.43    | 33.24   | 30.80   | 2.39       |
| サ     | 9.74     | 11.37   | 10.46   | 1.54       |
| シ     | 22.96    | 26.92   | 23.78   | 4.3        |
| ス     | 13.65    | 9.76    | 8.52    | 1.69       |
| セ     | 6.59     | 2.89    | 4.37    | 0.93       |
| ソ     | 16.77    | 10.50   | 8.31    | 1.29       |
| タ     | 25.33    | 28.60   | 33.59   | 3.31       |
| チ     | 12.55    | 22.27   | 26.79   | 1.32       |
| ツ     | 47.17    | 52.69   | 51.14   | 1.45       |
| テ     | 30.90    | 32.43   | 27.36   | 2.67          |
| ト     | 30.11    | 21.47   | 19.27   | 3.05       |
| ナ     | 39.51    | 38.42   | 36.03   | 3.56       |
| ニ     | 17.84    | 15.07   | 11.53   | 2.72       |
| ヌ     | 0.11     | 0.47    | 0.29    | 0.15       |
| ネ     | 12.83    | 13.73   | 12.89   | 0.67       |
| ノ     | 29.66    | 22.27   | 21.70   | 3.79       |
| ハ     | 13.79    | 12.31   | 15.61   | 2.61          |
| ヒ     | 4.00     | 5.05    | 3.58    | 0.58       |
| フ     | 4.11     | 3.16    | 1.65    | 0.43       |
| ヘ     | 1.63     | 1.21    | 1.00    | 0.45       |
| ホ     | 6.02     | 5.05    | 6.02    | 0.44       |
| マ     | 14.63    | 15.01   | 16.47   | 2.55       |
| ミ     | 5.18     | 8.68    | 7.02    | 0.92       |
| ム     | 1.86     | 2.02    | 1.50    | 0.34       |
| メ     | 2.70     | 4.64    | 7.95    | 0.58       |
| モ     | 16.94    | 13.12   | 18.19   | 2.24          |
| ヤ     | 23.25    | 30.48   | 34.52   | 1.06       |
| ユ     | 7.49     | 5.05    | 2.44    | 0.23       |
| ヨ     | 25.72    | 33.44   | 28.29   | 1.26       |
| ラ     | 14.80    | 16.08   | 15.69   | 1.87       |
| リ     | 8.27     | 6.59    | 10.67   | 1.46       |
| ル     | 17.22    | 19.38   | 16.40   | 1.8        |
| レ     | 13.23    | 20.66   | 19.70   | 1.61       |
| ロ     | 5.57     | 7.47    | 5.58    | 0.83       |
| ワ     | 9.68     | 10.63   | 9.96    | 1.13       |
| ン     | 60.51    | 53.23   | 60.45   | 4.62       |

割と成人の結果に近づけている気がする。
総合的な結果としては
- 清音: 「ぬ」、「ゆ」の使用率が低い
- 濁音: 「ゔ」の使用率が低い
- 半濁音: 「ぺ」の使用率が低い

今回やった事は音声でも流用出来るので、どこかに使える日常会話音声コーパスとか無いかしら。