ナンバーズ予想で学ぶ PowerShell によるデータ分析

PowerShell には CSV ファイルを読み込むコマンドや、集計するためのコマンドがあります。そのため、ちょっとした集計作業であれば実行することが可能です。ここでは、ナンバーズ予想を題材に、PowerShell によるデータ分析（と呼べるほど高級な処理はやりませんが……）の方法についてまとめます[^コマンドレットの記述方針]。

※「ナンバーズ予想で学ぶ ～」と言いつつも、この記事の大半は「予想に至るまでに必要な前処理やデータ俯瞰を PowerShell で行う方法」の説明です。予想の部分だけ読みたい方は「[各当せん番号のストレート口数の平均を求める](#各当せん番号のストレート口数の平均を求める)」へ飛んでください！

[^コマンドレットの記述方針]: コマンドレットの記述方針として、エイリアスは極力利用せず、プロパティ名も省略しないようにしています。コマンドレットの使い方を正確に覚えきれていない身としては、エイリアスの利用やプロパティ名の省略はわかりにくくなるので……ISE 使えばコマンドレットが長くても大した手間ではないし

## 準備

### データのダウンロード

ナンバーズ3の過去データを用意します。
今回は [ナンバーズ34 データダウンロードサービス](http://www.toe.jp/numbers34/download/) からデータをお借りしました。

### PowerShell の基本を習得

この記事では PowerShell の基本的な部分については説明しないので、そもそも PowerShell をよく知らんぞという方は以下のページなどをまずはご覧ください。

* [Windows PowerShell: スクリプト作成の短期集中講座](https://technet.microsoft.com/ja-jp/magazine/hh551144.aspx)

また、各コマンドレットの詳細は

```
Get-Help <コマンドレット名> -Online
```

で確認できます。オンラインヘルプが開きます。

## とりあえずファイルを開いてみる

数行出力して、ファイルの構造を把握します。

### コマンド

```ps1
# 先頭5行を表示する
Get-Content -Path .\Numbers3.csv -Encoding Default -TotalCount 5

# 末尾5行を表示する
Get-Content -Path .\Numbers3.csv -Encoding Default -Tail 5
```

|項目|説明|
|:--|:--|
|`Get-Content`|テキストファイルを1行ずつ読み込む|
|　`-Encoding Defalut`|文字コードを指定する。`Default` は Shift-JIS|
|　`-TotalCount 5`|先頭の5行だけ読み込む|
|　`-Tail 5`|末尾の5行だけ読み込む|

### 実行結果

```text:先頭5行
"ナンバーズ３４","データダウンロードサービス","http://toe.jp/numbers34/"
"回別","抽選日","当せん番号","ストレート口数","ストレート金額","ボックス口数","ボックス金額","セットストレート口数","セットストレート金額","セットボックス口数","セットボックス金額","ミニ口数","ミニ金額"
"第4304回","2015/12/24(木)","187","47","116,000","508","19,300","123","67,600","1,083","9,600","419","11,600"
"第4303回","12/23(水)","447","70","116,400","204","38,800","139","77,600","198","19,400","404","11,600"
"第4302回","12/22(火)","454","83","115,300","198","38,400","133","76,800","263","19,200","423","11,500"
```

```text:末尾5行
"第5回","11/04(金)","592","0","113,400","0","18,900","0","66,100","0","9,400","0","11,300"
"第4回","10/28(金)","105","0","57,200","0","9,500","0","33,300","0","4,700","0","5,700"
"第3回","10/21(金)","194","0","71,700","0","11,900","0","41,800","0","5,900","0","7,100"
"第2回","10/14(金)","988","0","333,500","0","111,100","0","222,300","0","55,500","0","33,300"
"第1回","10/07(金)","191","0","119,800","0","39,900","0","79,800","0","19,900","0","11,900"
```

ここから読み取るべき内容としては、

* 1行目にデータ提供元が記述されている
* 2行目にヘッダが記述されている
* 末尾にコメントなどの記述はなく、最後の行までレコードが入っている
* 文字コードは Shift-JIS
* 区切り文字はカンマ

あたりでしょうか。また、以下は注意したほうがよさそうです。

* 年が一部のレコードにしか明記されていない
* 数値がカンマで桁区切りされている



## ファイルの規模を把握する

数万行にも及ぶような巨大なファイルを扱うには PowerShell は向いていないので、先にファイルの規模を把握して PowerShell でさばけそうか確認します。

### コマンド

```ps1
# 行数や文字数を表示する
Get-Content -Path .\Numbers3.csv -Encoding Default |
Measure-Object -Line

# 1レコードだけ読み込み、属性の一覧を得る
Get-Content -Path .\Numbers3.csv -Encoding Default |
Select-Object -Skip 1 -First 2 | 
ConvertFrom-Csv
```

|項目|説明|
|:--|:--|
|`Measure-Object`|オブジェクトの統計情報を生成する|
|　`-Line`|行数を数える|
|`Select-Object`|オブジェクトを（この場合は行を）選択する|
|　`-Skip 1`|先頭1行を読み飛ばす|
|　`-First 2`|`-Skip` で読み飛ばした行の後に続く先頭2行を読み込む|
|`ConvertFrom-Csv`|CSV 文字列を読み込む|

### 実行結果

```text:行数と文字数
Lines Words Characters Property
----- ----- ---------- --------
 4306                          
```

```text:属性の一覧
回別         : 第4304回
抽選日        : 2015/12/24(木)
当せん番号      : 187
ストレート口数    : 47
ストレート金額    : 116,000
ボックス口数     : 508
ボックス金額     : 19,300
セットストレート口数 : 123
セットストレート金額 : 67,600
セットボックス口数  : 1,083
セットボックス金額  : 9,600
ミニ口数       : 419
ミニ金額       : 11,600
```

約4,300行 × 13列 程度の小さな表なので、PowerShell で処理しても問題なさそうです。

## 不要な行を削除する

1行目のデータ提供元はいらないので削除します。

### コマンド

```ps1
# 削除前を表示する
Get-Content -Path .\Numbers3.csv -Encoding Default -TotalCount 3

# 1行目を削除してファイルに保存する
Get-Content -Path .\Numbers3.csv -Encoding Default |
Select-Object -Skip 1 |
Out-File -FilePath .\Numbers3_without_comments.csv -Encoding Default

# 削除後を表示する
Get-Content -Path .\Numbers3_without_comments.csv -Encoding Default -TotalCount 3
```

|項目|説明|
|:--|:--|
|`Out-File`|テキストファイルに書き込む|
|　`-Encoding Defalut`|文字コードを指定する。`Default` は Shift-JIS|

### 実行結果

```text:Numbers3.csv
"ナンバーズ３４","データダウンロードサービス","http://toe.jp/numbers34/"
"回別","抽選日","当せん番号","ストレート口数","ストレート金額","ボックス口数","ボックス金額","セットストレート口数","セットストレート金額","セットボックス口数","セットボックス金額","ミニ口数","ミニ金額"
"第4304回","2015/12/24(木)","187","47","116,000","508","19,300","123","67,600","1,083","9,600","419","11,600"
```

```text:Numbers3_without_comments.csv
"回別","抽選日","当せん番号","ストレート口数","ストレート金額","ボックス口数","ボックス金額","セットストレート口数","セットストレート金額","セットボックス口数","セットボックス金額","ミニ口数","ミニ金額"
"第4304回","2015/12/24(木)","187","47","116,000","508","19,300","123","67,600","1,083","9,600","419","11,600"
"第4303回","12/23(水)","447","70","116,400","204","38,800","139","77,600","198","19,400","404","11,600"
```

1行目が削除されていることが確認できます。

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

CSV ファイルをオブジェクトとして読み込みます。

### コマンド

```ps1
# CSV ファイルを読み込む
$csv = Import-Csv -Path .\Numbers3_without_comments.csv -Encoding Default

# 表形式で表示する
$csv |
Select-Object -First 10 |
Format-Table

# CSV ファイルの各列には、オブジェクトのプロパティとしてアクセスできる
$csv[0].当せん番号
```

|項目|説明|
|:--|:--|
|`Import-CSV`|CSV ファイルを読み込む|
|　`-Encoding Defalut`|文字コードを指定する。`Default` は Shift-JIS|
|`Format-Table`|表形式に整えて出力する|

`Import-CSV` で CSV ファイルを読み込むと、各列をプロパティとして持つオブジェクトの配列として読み込まれます。デフォルトでは CSV ファイルの1行目がヘッダとして解釈され、ヘッダの値がそのままオブジェクトのプロパティ名となります。

### 実行結果

```text:Format-Table
回別     抽選日           当せん番号 ストレート口数 ストレート金額 ボックス口数 ボックス金額 セットストレート口数 セットストレート金額 セットボックス口数
--     ---           ----- ------- ------- ------ ------ ---------- ---------- ---------
第4304回 2015/12/24(木) 187   47      116,000 508    19,300 123        67,600     1,083    
第4303回 12/23(水)      447   70      116,400 204    38,800 139        77,600     198      
第4302回 12/22(火)      454   83      115,300 198    38,400 133        76,800     263      
第4301回 12/21(月)      530   109     105,000 527    17,500 204        61,200     959      
第4300回 12/18(金)      966   82      114,900 270    38,300 134        76,600     294      
第4299回 12/17(木)      308   152     72,700  462    12,100 317        42,400     950      
第4298回 12/16(水)      166   93      105,400 242    35,100 130        70,200     325      
第4297回 12/15(火)      484   63      115,800 309    38,600 117        77,200     299      
第4296回 12/14(月)      671   83      99,200  583    16,500 223        57,800     1,093    
第4295回 12/11(金)      332   104     104,000 233    34,600 126        69,300     255      
```

```text:$csv[0].当せん番号
187
```

CSV ファイルをオブジェクトとして読み込むことができました。
列幅の自動調整が全角文字に対応していないダサさはありますが……（´・ω・｀）

## 値の埋まり具合を確認する

欠損値が多いと後の統計処理に影響するので、各列の値の埋まり具合を確認します。

### コマンド

```ps1
$csv |
Get-Member -MemberType NoteProperty | %{ $_.Name } |  # 列名を取得する
%{ [PSCustomObject]@{ Name = $_; Count = ($csv.$_ | ?{ $_ -ne "" }).Length } }
```

|項目|説明|
|:--|:--|
|`Get-Member`|オブジェクトのメンバー（メソッドやプロパティなど）の一覧を取得する|
|　`-MemberType NoteProperty`|メンバーのうち、プロパティだけを取得する|
|`%{}`|コレクションの各要素に対して処理を実行する|
|`@{}`|連想配列を定義する|
|`?{}`|条件に合う要素を抽出する|

`%{}` は `ForEach-Object` のエイリアスで、パイプラインで渡されたコレクションの各要素に対して `{}` 内に記述された処理を実行します。コレクションの各要素は `$_` で参照できます。

`?{}` は `Where-Object` のエイリアスで、パイプラインで渡されたコレクションの各要素を調べ `{}` 内に記述された条件に一致する要素のみを返します。`%{}` と同様、コレクションの各要素は `$_` で参照できます。

`@{}` を使うと連想配列を定義できます。一方で、PowerShell が基本としている連想配列的なオブジェクトは `PSCustomObject` なので、そちらに合わせてキャストしています。直接 `PSCustomObject` を作成するほうが処理としては素直ですが、記述が微妙に長くなるのでここでは連想配列を介しています。

```ps1:連想配列を介さない方法
$csv |
Get-Member -MemberType NoteProperty |
%{ $_.Name } |
%{
    $record = New-Object PSCustomObject | Select-Object Name, Count
    $record.Name = $_
    $record.Count = ($csv.$_ | ?{$_ -ne "" }).Length
    $record
}
```

### 実行結果

```text
Name       Count
----       -----
ストレート口数     4304
ストレート金額     4304
セットストレート口数  4304
セットストレート金額  4304
セットボックス口数   4304
セットボックス金額   4304
ボックス口数      4304
ボックス金額      4304
ミニ口数        4304
ミニ金額        4304
回別          4304
当せん番号       4304
抽選日         4304
```

行数がヘッダ含め4,305行だったので、欠損値はないようです。

## 各当せん番号のストレート口数の平均を求める

ナンバーズは賭け金を当せん者で分け合うような形になっているらしく、当せん金額も購入者や当せん者の人数に応じて変化します。そこで、

1. 各番号の当せん確率は同じ
2. 毎回の購入者数は一定

と仮定すると、賭ける人の少ない番号に（当せん者数の少ない番号に）賭ければ、当せんする確率は同じでありながら当せん金額は大きくなるので、期待値が上がるはずです。123 や 777 など誰もが連想しがちな番号が存在することからも、番号によって当せん者数にばらつきがあることは容易に想像できます。

ということで、各当せん番号のストレート口数の平均を求めます。

### コマンド

```ps1
$winner_average =
$csv |
Group-Object -Property 当せん番号 |
%{ [PSCustomObject]@{ 当せん番号 = $_.Name; ストレート口数平均 = $_.Group | %{ $_.ストレート口数 } | Measure-Object -Average | %{ $_.Average } } } |
Sort-Object -Property ストレート口数平均 -Descending

$winner_average |
%{ [PSCustomObject]@{ 当せん番号 = $_.当せん番号; ストレート口数平均 = "{0,5:n1}" -f $_.ストレート口数平均 }} |
Select-Object -First 10

$winner_average |
?{ $_.ストレート口数平均 -ne 0 } |
%{ [PSCustomObject]@{ 当せん番号 = $_.当せん番号; ストレート口数平均 = "{0,5:n1}" -f $_.ストレート口数平均 }} |
Select-Object -Last 10
```

|項目|説明|
|:--|:--|
|`Group-Object`|指定したプロパティの値ごとにグルーピングする|
|`Sort-Object`|指定したプロパティの値でソートする|

### 結果

```text:ストレート口数平均上位10件
当せん番号 ストレート口数平均
----- ---------
888   809.0    
323   792.0    
555   732.0    
777   658.7    
999   614.8    
444   541.5    
000   480.5    
222   470.7    
666   453.6    
333   364.0    
```

```text:ストレート口数平均下位10件
当せん番号 ストレート口数平均
----- ---------
167    28.7    
107    28.0    
144    26.8    
284    26.0    
450    25.0    
085    20.0    
075    16.8    
277    15.8    
882    15.0    
292    11.3    
```

想定していた通りゾロ目などわかりやすい番号が上位に来ており、その数は下位の10倍以上にもなります。
このことから、ナンバーズ3を購入する時は下位10位に載っているような番号を選ぶことで、当せん金の期待値を高めることができるかもしれません（｀・ω・´）