Ruby を使ってランダムサンプリングをする

# はじめに

言語学の研究などをしていて「コーパス分析をするぞ！」となった時，コーパスインターフェイスからデータを .csv などの形式でダウンロードすると大量のデータが出てきて途方に暮れる時があります．そしてそのようなデータを「EXCEL などで乱数を生成して…」とするのも大変なので，Ruby を使ってコードを書きました．誰かの役に立てれば嬉しいです．なお「自分を含めた文系の方にも分かりやすく」というモットーで書くので冗長な箇所が見当るかと思いますがご容赦ください．

# タスク

[BCCWJ](https://pj.ninjal.ac.jp/corpus_center/bccwj/)などで任意の言語表現を検索し，その結果をローカルに保存しようとすると場合によっては10万件くらいのデータがでてくる場合があるので困る[^1]．EXCELなどの表計算ソフト上ので複雑な操作を避けながら，このデータからできるだけ簡単に手頃なサイズのデータをランダムサンプリングしたい．

[^1]: ただ全てのコーパスインターフェイスがこのような仕様になっているわけではなく，[Sketch Engine](https://app.sketchengine.eu/) などではダウンロードする用例の数を指定することができます．

# 実装例


解決案としては，Ruby のような（プログラミング）言語を使ってダウンロードしたデータから無作為に標本を抽出すれば良い．実装の手順としては以下のような流れ進めれば良い．

1. CSV ファイルを読み込む
2. 読み込んだファイルの総行数から任意のサンプル数を指定し，そのサンプルの数だけ乱数を生成する．
3. 2. で生成した乱数の行のデータを抽出し，これを新たに別の csv として書き出す．

以上の流れを一つづつ Ruby で実装します．

## 前準備

上に挙げた処理を実行する前に簡単な下準備をします．まずは Ruby の [CSV クラス](https://docs.ruby-lang.org/ja/latest/class/CSV.html)を利用するために，'csv' を require します．その上でコーパスインターフェイスからダウンロードした csv を置いてあるパスの文字列を input_path に，最終的な出力をしたいパスの文字列を output_path に代入します[^2]．とりあえず Users 以降の名前は user_name と適当な名前をつけているので適宜修正してください．

[^2]: （念のため）この output_path で指定する csv ファイルは実際に用意しなくても問題ありません．ちゃんと実在するディレクトリを指定してあげるだけで自動でファイルを生成してくれます．

```random_extractor.rb
require 'csv'

input_path = '/Users/user_name/bccwj_output.csv'
output_path = '/Users/user_name/extracted.csv'
```

また，以降では input_path と output_path で読み込んだ csv ファイルのエンコーディングは UTF-8 であるという仮定のもとで進めます．中納言なんかでダウンロードしたデータは UTF-16 の場合があるので，そういったデータは一度テキストエディタか何かで文字コードを UTF-8 に変換してから進めてください．

## CSV ファイルを読み込む

ここまで来ると CSV を読み込むことができます．ですが，言語データをインポートするさいには必ずクオーテーション等を無視する liberal_parsing オプションを true にしてください．このオプションを指定しないとせっかくダウンロードしたデータを読み込むことが出来ません．

```random_extractor.rb
# ファイルの読み込み（クオテーション等を無視する）
parsed = CSV.read(input_path, liberal_parsing: true)

```

## 乱数を生成してデータを抽出する

今回はとりあえず 500件のデータを無作為抽出することにします．CSV で parse したデータにはヘッダーが含まれている可能性があります．とりあえずは2行目から用例があると仮定しますが，以降のスクリプトの実行前に　`p parsed[1]` などのコマンドで内容を確認すると良いでしょう[^3]．

[^3]: 言うまでもないかもしれませんが，この `parsed` はローカル変数なのでそのままでは動きません．上の CSV クラスを使って読み込んだデータが入っているものとして議論をしています．

乱数の生成には二段階が必要です．まずは (i) ヘッダーを除いた行数 (i.e., 2) から読み込んだ CSV の総行数の整数からなる整数の配列を生成し，(ii) その配列の中から任意の数の要素を抽出します．これはメソッドチェーンを利用して1行で済ませられます．

`(2..parsed.size)`は 2 から総行数 (i.e., `parsed.size`) の Range を生成しており，このクラスを`to_a`で配列に変換することで [2, 3, 4, ...] という形のデータを得ることができます．その配列から 500件を `sample()` というメソッドを使って**行の番号**を抽出しています．

```random_extractor.rb
# ランダムな整数の生成（ヘッダーがあるので 2 から）
indices = (2..parsed.size).to_a.sample(500)
```

今の段階では `indices` というローカル変数に無作為に抽出された整数の配列が入っているだけです．これをもとに読み込んだデータから用例を抽出する必要があります．このために `map` メソッドを使って抽出していきます．


```random_extractor.rb
# 無作為抽出された表現
sample = indices.map {|i| parsed[i] }

```

まず，`indices.map {|i| ... }` という表現全体は「indice という配列の中身を一つづつ取り出して，その取り出した中身を i という変数に入れてください」ということを指示しています[^4]．

[^4]: Rubyで繰り返し処理を行うメソッドとしては他に each がありますが，map は繰り返し処理の結果を配列として返してくれるという点が each と違います．詳しくは[この記事](https://qiita.com/massaaaaan/items/d90d10cb023bedc74fb2)などをご参照ください．

次に `indices.map {|i| parsed[i] }` という表現の中の `parsed[i]` の箇所についてみます．先ほども行ったように，indices は [7, 34, 57, ...] のような無作為に抽出された整数の配列です．ですので，「一回目の処理では i には 7 が，二回目の処理では 34 が，…」といった具合に進んでいきます．ここで CSV を読み込んだ変数の `parsed` を呼び出すことで，「`parsed` の 7行目のデータを `sample` にいれて，次に34行目のデータを `sample` にいれて，……」という処理を実現しています．

これで読み込んだデータから無作為に 500 件のデータを抽出することができました．

## データを書き出す

データの書き出しには File クラスの open というメソッドを利用します．このメソッドの引数にはファイルのパス (前準備で output_path として定義しました) と書き込み ('w') オプションを指定します．今回はオリジナルのデータのヘッダーを再利用することにしたので，`parsed[0]` を指定しています．その上で抽出したデータを保存している変数 sample に対して繰り返し処理を適用して，データを書き込んでいます[^5]．

[^5]: 今回の繰り返し処理は `sample` という配列に含まれる要素を書き出すために行っているため，map ではなく each を使っています．

```random_extractor.rb
# 出力（1行目だけは header なので保存する．）
File.open(output_path, "w") do |f|
	f.puts(parsed[0])
	sample.each { |line| f.puts(line) }
end

```

# おわりに

以上，Ruby を使って無作為抽出をする手法を書いてきました．一応今回のコードをつなげると次のようになります．冗長な箇所などがあれば是非教えていただけると幸いです．

```random_extractor.rb
require 'csv'

# input_path は生のデータのパスを，output_path は整形後のデータのパスをいれる
input_path = "/Users/user_name/bccwj_output.csv"
output_path = "/Users/user_name/extracted.csv"

# ファイルの読み込み（クオテーション等を無視する）
parsed = CSV.read(input_path, liberal_parsing: true)

# ランダムな整数の生成（ヘッダーがあるので 2 から）
indices = (2..parsed.size).to_a.sample(500)

# 無作為抽出された表現
sample = indices.map {|i| parsed[i] }

# 出力（1行目だけは header なので保存する．）
File.open(output_path, "w") do |f|
	f.puts(parsed[0])
	sample.each { |line| f.puts(line) }
end

# 処理が終わったらコンソールに "done!" と書く
p "done!"

```