データサイエンス100本ノック（構造化データ加工編）をやってみた part8(P-069~P-078)

　この記事はデータサイエンスを勉強しながら、データサイエンス協会が提供する__データサイエンス100本ノック（構造化データ加工編）__を解く過程を自分用にまとめたものです。

|　チャプター　|　リンク　|　チャプター　|　リンク　|
|:------:|:----:|:------:|:---:|
|　P-001~P-016　|　[part1](https://qiita.com/pyon/items/c208e585e0cdfabc58bd)　|　P-052~P-062　|　[part6](https://qiita.com/pyon/items/5c5926241b8e6da978f3)　|
|　P-017~P-022　|　[part2](https://qiita.com/pyon/items/73bf9e8fe75f807aeea8)　|　P-063~P-068　|　[part7](https://qiita.com/pyon/items/ac8c7fb9110dbba061f5)　|
|　P-023~P-031　|　[part3](https://qiita.com/pyon/items/948ca309c7985fa2e855)　|　P-069~P-078　|　[part8](https://qiita.com/pyon/items/60daed73e5ff670e209d)　|
|　P-032~P-039　|　[part4](https://qiita.com/pyon/items/e7146bbb728979430814)　|　P-079~P-088　|　[part9](https://qiita.com/pyon/items/911e3d236fad8df13f44)　|
|　P-040~P-051　|　[part5](https://qiita.com/pyon/items/dac17fb162c99769978b)　| P-089~P-100 |　[part10](https://qiita.com/pyon/items/70c1e222914c658a480c)　|

+ 日付の加算・減算　`relativedelta()`
+ datetime型 → UNIX時間　`timestamp()`
+ 曜日をint型で返す　`weekday()`
+ ランダムに抽出　`sample()`
+ 層化抽出　`train_test_split(stratify=)`

# P-069
>P-069: レシート明細データフレーム（df_receipt）と商品データフレーム（df_product）を結合し、顧客毎に全商品の売上金額合計と、カテゴリ大区分（category_major_cd）が"07"（瓶詰缶詰）の売上金額合計を計算の上、両者の比率を求めよ。抽出対象はカテゴリ大区分"07"（瓶詰缶詰）の購入実績がある顧客のみとし、結果は10件表示させればよい。

```python:p069.py
df_tmp_1 = pd.merge(df_receipt, df_product, how='inner', on='product_cd').groupby('customer_id').amount.sum().rename('amount_sum').reset_index()
df_tmp_2 = pd.merge(df_receipt, df_product.query('category_major_cd == "07"'), 
                    how='inner', on='product_cd').groupby('customer_id').amount.sum().rename('07_amount_sum').reset_index()
df_tmp_3 = pd.merge(df_tmp_1, df_tmp_2, how='inner', on='customer_id')
df_tmp_3['07_rate'] = df_tmp_3['07_amount_sum'] / df_tmp_3['amount_sum']
df_tmp_3.head(10)
```

# P-070 `drop_duplicates()`
>P-070: レシート明細データフレーム（df_receipt）の売上日（sales_ymd）に対し、顧客データフレーム（df_customer）の会員申込日（application_date）からの経過日数を計算し、顧客ID（customer_id）、売上日、会員申込日とともに表示せよ。結果は10件表示させれば良い（なお、sales_ymdは数値、application_dateは文字列でデータを保持している点に注意）。

```python:p070.py
df_tmp = pd.merge(df_receipt[['customer_id', 'sales_ymd']], df_customer[['customer_id', 'application_date']],
                  how='inner', on='customer_id')
df_tmp = df_tmp.drop_duplicates()
df_tmp['sales_ymd'] = pd.to_datetime(df_tmp['sales_ymd'].astype('str'))
df_tmp['application_date'] = pd.to_datetime(df_tmp['application_date'])
df_tmp['lapsed_date'] = df_tmp['sales_ymd'] - df_tmp['application_date']
df_tmp.head(10)
```

　`drop_duplicates()`：重複した行を削除

　`duplicated()`を使うなら、論理演算子を用いて2行目を以下のように変更すればいい。

```python
df_tmp = df_tmp[~df_tmp.duplicated()]
```

# P-071 `relativedelta()`
>P-071: レシート明細データフレーム（df_receipt）の売上日（sales_ymd）に対し、顧客データフレーム（df_customer）の会員申込日（application_date）からの経過月数を計算し、顧客ID（customer_id）、売上日、会員申込日とともに表示せよ。結果は10件表示させれば良い（なお、sales_ymdは数値、application_dateは文字列でデータを保持している点に注意）。1ヶ月未満は切り捨てること。

```python:p071.py
df_tmp = pd.merge(df_receipt[['customer_id', 'sales_ymd']], df_customer[['customer_id', 'application_date']],
                  how='inner', on='customer_id')
df_tmp = df_tmp.drop_duplicates()

df_tmp['sales_ymd'] = pd.to_datetime(df_tmp['sales_ymd'].astype('str'))
df_tmp['application_date'] = pd.to_datetime(df_tmp['application_date'])

df_tmp['lapsed_month'] = df_tmp[['sales_ymd', 'application_date']].apply(lambda x: 
                                    relativedelta(x[0], x[1]).years * 12 + relativedelta(x[0], x[1]).months ,axis=1)
df_tmp.head(10)
```

　`relativedelta(datetime1, datetime2)`：引数で渡したdatetime型の差分を計算してくれる。
　結果を`years`、`months`、`days`で返してくれるため、`years`には12を掛けて月に変換している。

# P-072
>P-072: レシート明細データフレーム（df_receipt）の売上日（sales_ymd）に対し、顧客データフレーム（df_customer）の会員申込日（application_date）からの経過年数を計算し、顧客ID（customer_id）、売上日、会員申込日とともに表示せよ。結果は10件表示させれば良い。（なお、sales_ymdは数値、application_dateは文字列でデータを保持している点に注意）。1年未満は切り捨てること。

```python:p072.py
df_tmp = pd.merge(df_receipt[['customer_id', 'sales_ymd']], df_customer[['customer_id', 'application_date']],
                 how='inner', on='customer_id')
df_tmp = df_tmp.drop_duplicates()

df_tmp['sales_ymd'] = pd.to_datetime(df_tmp['sales_ymd'].astype('str'))
df_tmp['application_date'] = pd.to_datetime(df_tmp['application_date'])

df_tmp['lapsed_years'] = df_tmp[['sales_ymd', 'application_date']].apply(lambda x:
                                 relativedelta(x[0], x[1]).years, axis=1)
df_tmp.head(10)
```

# P-073 `timestamp()`
>P-073: レシート明細データフレーム（df_receipt）の売上日（sales_ymd）に対し、顧客データフレーム（df_customer）の会員申込日（application_date）からのエポック秒による経過時間を計算し、顧客ID（customer_id）、売上日、会員申込日とともに表示せよ。結果は10件表示させれば良い（なお、sales_ymdは数値、application_dateは文字列でデータを保持している点に注意）。なお、時間情報は保有していないため各日付は0時0分0秒を表すものとする。

```python:p073.py
df_tmp = pd.merge(df_receipt[['customer_id', 'sales_ymd']], df_customer[['customer_id', 'application_date']],
                  how='inner', on='customer_id')
df_tmp = df_tmp.drop_duplicates()

df_tmp['sales_ymd'] = pd.to_datetime(df_tmp['sales_ymd'].astype('str'))
df_tmp['application_date'] = pd.to_datetime(df_tmp['application_date'])

df_tmp['lapsed_date_unix'] = df_tmp['sales_ymd'].apply(lambda x: x.timestamp()) - df_tmp['application_date'].apply(lambda x: x.timestamp())
df_tmp.head(10)
```

　`timestamp()`：datetimeオブジェクトをUNIX時間に変換

# P-074 `weekday()`
>P-074: レシート明細データフレーム（df_receipt）の売上日（sales_ymd）に対し、当該週の月曜日からの経過日数を計算し、売上日、当該週の月曜日付とともに表示せよ。結果は10件表示させれば良い（なお、sales_ymdは数値でデータを保持している点に注意）。

```python:074.py
df_tmp = df_receipt[['customer_id', 'sales_ymd']]
df_tmp = df_tmp.drop_duplicates()
df_tmp['sales_ymd'] = pd.to_datetime(df_receipt['sales_ymd'].astype('str'))

df_tmp['this_monday'] = df_tmp['sales_ymd'].apply(lambda x: x - relativedelta(days=x.weekday()))

df_tmp['lapsed_date'] = df_tmp['sales_ymd'] - df_tmp['this_monday']
df_tmp.head(10)
```

　`weekday()`：datetimeオブジェクトの曜日を整数値(int型)で返す。(月曜日は`0`、日曜日は`6`)


　※`weekday()`と似たような関数

+ `isoweekday()`：月曜日が`1`、日曜日が`7`の整数値を返す
+ `day_name()`：曜日を文字列で返す。('Monday', 'Tuesday',...)

# P-075 `sample()`
>P-075: 顧客データフレーム（df_customer）からランダムに1%のデータを抽出し、先頭から10件データを抽出せよ。

```python:p075.py
df_customer.sample(frac=0.01).head(10)
```
　`sample()`：行または列をランダムに抽出する。

| 引数 | 説明 |
|:---:|:------|
| n |抽出する数を指定できる。|
| frac |抽出する割合を指定(`1`なら100%、`0.01`なら1%)|
| random_state | 乱数シードを固定 |
| replace | `True`で重複を許可（デフォルトは`False`）|
| axis | `1`で列をランダムに取得（デフォルトは`0`、列） |

# P-076 `train_test_split()`
>P-076: 顧客データフレーム（df_customer）から性別（gender_cd）の割合に基づきランダムに10%のデータを層化抽出データし、性別ごとに件数を集計せよ。

```python:p076.py
_, df_tmp = train_test_split(df_customer, test_size=0.1, stratify=df_customer['gender_cd'])
df_tmp.groupby('gender_cd').customer_id.count().reset_index()
```

　`train_test_split()`：`scikit-learn`の関数で配列やリストを2分割できる。
　主に、機械学習においてデータを訓練用とテスト用に分割してホールドアウト検証を行う際に用いることが多い。

 引数`stratify`に均等に分割させたいデータを指定すると、そのデータの値の比率が元のデータと一致するように分割される。
　問題のケースではサンプル数が少ない`gender_cd`が`0`と`9`のデータを逃さないために比率を固定している。

# P-077
>P-077: レシート明細データフレーム（df_receipt）の売上金額（amount）を顧客単位に合計し、合計した売上金額の外れ値を抽出せよ。ただし、顧客IDが"Z"から始まるのものは非会員を表すため、除外して計算すること。なお、ここでは外れ値を平均から3σ以上離れたものとする。結果は10件表示させれば良い。

```python:p077.py
df_tmp = df_receipt.query('not customer_id.str.startswith("Z")', engine='python').groupby('customer_id').amount.sum().reset_index()
amount_mean = df_tmp['amount'].mean()
amount_std = np.std(df_tmp['amount'])
df_tmp[df_tmp['amount'] >= amount_mean + amount_std*3].head(10)
```

　`preprocessing.scale()`を使って平均を0、標準偏差を1に標準化して行う場合

```python:p077.py
df_tmp = df_receipt.query('not customer_id.str.startswith("Z")', engine='python').groupby('customer_id').amount.sum().reset_index()
df_tmp['amount_ss'] = preprocessing.scale(df_tmp['amount'])
df_tmp.query('abs(amount_ss) >= 3').head(10)
```

# P-078
>P-078: レシート明細データフレーム（df_receipt）の売上金額（amount）を顧客単位に合計し、合計した売上金額の外れ値を抽出せよ。ただし、顧客IDが"Z"から始まるのものは非会員を表すため、除外して計算すること。なお、ここでは外れ値を第一四分位と第三四分位の差であるIQRを用いて、「第一四分位数-1.5×IQR」よりも下回るもの、または「第三四分位数+1.5×IQR」を超えるものとする。結果は10件表示させれば良い。

```python:p078.py
df_tmp = df_receipt.query('not customer_id.str.startswith("Z")', engine='python').groupby('customer_id').amount.sum().reset_index()
amount_25 = df_tmp['amount'].quantile(0.25)
amount_75 = df_tmp['amount'].quantile(0.75)
iqr = amount_75 - amount_25

lower_lim = amount_25 - 1.5 * iqr
upper_lim = amount_75 + 1.5 * iqr

df_tmp[(lower_lim > df_tmp['amount']) | (upper_lim < df_tmp['amount'])].head(10)
```

　上のように書いたが、最後の行は`query()`を使って書いた方がスッキリする。

```python
df_tmp.query('amount < @lower_lim or @upper_lim < amount').head(10)
```