Pythonを使ってデータ分析のデモ用にそれっぽいCSVファイルを生成する

Pythonを使ってデモ用にそれっぽいCSVファイルをつくる機会があったので記事にします｡
コピペ用にコードの全体を記事末尾にまとめています｡

## ニーズの確認
- デモ用にそれっぽいCSVファイルをつくりたい
  (それっぽいデータの方が関係者への説明に使いやすいので)
- 日付､時刻､各列の選択肢もそれっぽいものにしたい

これらのニーズに応えるべく､それっぽい関数をつくりました｡

## 基本方針
- `random`を使って予め設定した範囲・選択肢から適当な値を取り出す
- CSV形式のデータを1行ずつ生成して最後にまとめて書き出す
- 日付､時刻､数値､文字列の選択肢に対してそれぞれ適した処理をする

## 1. 下準備
- 必要なライブラリをimportする
- デモ用データが生成される度にばらつくのを避けるため`random.seed()`を適当に設定する
- このあと引数として使う`namedtuple`を準備する

```Python:必要なライブラリのimportとnamedtupleの準備
import random
from random import randint
from collections import OrderedDict, namedtuple
from datetime import datetime, timedelta

random.seed(0)

DateRange = namedtuple('DateRange', ['start', 'max_range'])
TimeRange = namedtuple('TimeRange', ['start', 'end'])
IntRange = namedtuple('IntRange', ['min', 'max'])
```

## 2. 列ごとに範囲・選択肢を設定する
- 日付､時刻､数値(`Int`)の範囲を`namedtuple`に代入する
- 列(column)ごとに範囲・選択肢を指定するために`coldict`という辞書を用意する
- 文字列の選択肢はリスト型で設定する

今回は以下のような設定のもとでCSVファイルを作成します｡
  - 日付は2022年1月1日から1年間(364日後まで)
  - 時刻は午前9時(AM9)から午後18時(PM6)まで
  - 数値は0から100まで
  - 文字列の選択肢は適当に


```Python:生成するCSVファイルの範囲・選択肢の設定
Y2022 = datetime(year=2022, month=1, day=1)
ONE_YEAR = 364
date_range = DateRange(Y2022, ONE_YEAR)

AM9 = 9 * 60 * 60
PM6 = 18 * 60 * 60
time_range = TimeRange(AM9, PM6)

num_range = IntRange(0, 100)

coldict = OrderedDict()
coldict = {
    '日付':date_range,
    '時刻':time_range,
    '数値':num_range,
    '列A':['elemA1', 'elemA2', 'elemA3'],
    '列B':['elemB1', 'elemB2', 'elemB3'],
    '列C':['elemC1', 'elemC2', 'elemC3'],
}
```
:::note info
CSVを書き出す際に列名と中身がずれないようにOrderedDictを使っています｡
:::

## 4. CSVファイルの1行分を生成する関数を準備する
- `coldict`の各列に対してランダムに値を生成していく
- 列の中身の型を`isinstance`で判別して処理を分岐させる
- データ型にあわせて`randint`か`random.choice`をよしなに適用する

```Python:CSVデータの1行分を書き出す関数
def make_random_csv_row(coldict):
    """ランダムなCSVデータの1行分を生成する"""
    row = ''
    for col, elems in coldict.items():
        if isinstance(elems, DateRange):
            date = elems.start + timedelta(days=randint(0, elems.max_range))
            row += f'{date.date()},'
        elif isinstance(elems, TimeRange):
            row += f'{timedelta(seconds=randint(elems.start, elems.end))},'
        elif isinstance(elems, IntRange):
            row += f'{randint(elems.min, elems.max)},'
        else:
            row += f'{random.choice(coldict[col])},'
    row = row.rstrip(',')
    return row
```

これを実行すると筆者の環境では以下のような結果が返ってきます｡
```:ランダムに生成されたCSVファイルの1行
'2022-07-17,15:53:56,53,elemA1,elemB2,elemC3'
```
これがCSVファイルの1行になります｡
あとはこれを欲しい行数の分だけ生成して書き出せばOKです｡

## 5. CSVファイルの書き出し
- 範囲・選択肢の辞書(`coldict`)､ファイル名(`filename`)､行数(`num_rows`)を引数に設定する
- 最初に`coldict.keys()`で列名を取り出す
- `num_rows`の数だけCSVの行を生成する
- 最後に`with`文でファイルを書き出す
```Python:ランダムなCSVファイルを作成する関数
def make_random_csv(coldict, filename, num_rows=100):
    """指定した行数のランダムなCSVデータを生成する"""
    output = []
    output.append(','.join(coldict.keys()))
    for i in range(num_rows):
        output.append(make_random_csv_row(coldict))
    with open(filename, mode='w')as f:
        output = '\n'.join(output)
        f.writelines(output)
```

これを実行した結果のCSVファイルの先頭5行を抜粋します｡
```:ランダムに生成されたCSVファイル
日付,時刻,数値,列A,列B,列C
2022-07-17,15:53:56,53,elemA1,elemB2,elemC3
2022-09-06,12:41:08,100,elemA2,elemB2,elemC2
2022-10-26,17:06:57,27,elemA3,elemB1,elemC2
2022-03-13,15:52:46,12,elemA3,elemB2,elemC3
```

これでCSVファイルの完成です｡
あとは必要に応じて範囲・選択肢を設定すればOKです｡

## まとめ
- それっぽいデモ用CSVファイルがつくれるようになった
- 関係者にデモを見せる時もちょっと見栄えがよくなるかも

<details><summary>コード全体(クリックで表示)</summary>

```Python
import random
from random import randint
from collections import OrderedDict, namedtuple
from datetime import datetime, timedelta

random.seed(0)

DateRange = namedtuple('DateRange', ('start', 'max_range'))
TimeRange = namedtuple('TimeRange', ['start', 'end'])
IntRange = namedtuple('IntRange', ['min', 'max'])

def make_random_csv_row(coldict):
    """ランダムなCSVデータの1行分を生成する"""
    row = ''
    for col, elems in coldict.items():
        if isinstance(elems, DateRange):
            date = elems.start + timedelta(days=randint(0, elems.max_range))
            row += f'{date.date()},'
        elif isinstance(elems, TimeRange):
            row += f'{timedelta(seconds=randint(elems.start, elems.end))},'
        elif isinstance(elems, IntRange):
            row += f'{randint(elems.min, elems.max)},'
        else:
            row += f'{random.choice(coldict[col])},'
    row = row.rstrip(',')
    return row


def make_random_csv(coldict, filename, num_rows=100):
    """ランダムなCSVデータを指定した行数分だけ生成する"""
    output = []
    output.append(','.join(coldict.keys()))
    for i in range(num_rows):
        output.append(make_random_csv_row(coldict))
    with open(filename, mode='w')as f:
        output = '\n'.join(output)
        f.writelines(output)


# 記事内で使ったサンプル
Y2022 = datetime(year=2022, month=1, day=1)
ONE_YEAR = 364
date_range = DateRange(Y2022, ONE_YEAR)

AM9 = 9 * 60 * 60
PM6 = 18 * 60 * 60
time_range = TimeRange(AM9, PM6)

num_range = IntRange(0, 100)

coldict = OrderedDict()
coldict = {
    '日付':date_range,
    '時刻':time_range,
    '数値':num_range,
    '列A':['elemA1', 'elemA2', 'elemA3'],
    '列B':['elemB1', 'elemB2', 'elemB3'],
    '列C':['elemC1', 'elemC2', 'elemC3'],
}

make_random_csv(coldict, 'output.csv', 100)

```
</details>