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

　この記事はデータサイエンスを勉強しながら、データサイエンス協会が提供する__データサイエンス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)　|

+ パーセンタイル　`quantile()`
+ データフレームの結合 `pd.merge()`
+ 欠損値を埋める　`fillna()`
+ 重複した行を検出　`duplicated()`

# P-032 `quantile()`
>P-032: レシート明細データフレーム（df_receipt）の売上金額（amount）について、25％刻みでパーセンタイル値を求めよ。

```python:p032.py
df_receipt['amount'].quantile(q=[0, 0.25, 0.5, 0.75, 1])
```

　`quantile()`：パーセンタイルを取得。（pandas.Series型）
　データを小さい順に数えて何%の値はどれくらいかを測定する。
　デフォルトでは1/2分位数・５０パーセンタイル(中央値)を返す。

　第一引数で取得したい分位数を0.0~1.0で指定する。(複数指定の場合はリスト)
　解答では`np.arange()`を使って引数を　`q=np.arange(5)/4`としている。

# P-033
>P-033: レシート明細データフレーム（df_receipt）に対し、店舗コード（store_cd）ごとに売上金額（amount）の平均を計算し、330以上のものを抽出せよ。

```python:p033.py
df_receipt.groupby('store_cd').amount.mean().reset_index().query('amount >= 330')
```

　`GroupBy`オブジェクトを`mean()`で平均を取った後、`query()`で条件を絞る。
　(今までの復習のような問題)

# P-034
>P-034: レシート明細データフレーム（df_receipt）に対し、顧客ID（customer_id）ごとに売上金額（amount）を合計して全顧客の平均を求めよ。ただし、顧客IDが"Z"から始まるのものは非会員を表すため、除外して計算すること。

```python:p034.py
df_receipt.query('not(customer_id.str.startswith("Z"))', engine='python').groupby('customer_id').amount.sum().mean()
```
　まず、`customer_id`がZから始まるのを`query()`で外し、その後グルーピング・統計。

# P-035
>P-035: レシート明細データフレーム（df_receipt）に対し、顧客ID（customer_id）ごとに売上金額（amount）を合計して全顧客の平均を求め、平均以上に買い物をしている顧客を抽出せよ。ただし、顧客IDが"Z"から始まるのものは非会員を表すため、除外して計算すること。なお、データは10件だけ表示させれば良い。

```python:myp035.py
df_receipt.query('not customer_id.str.startswith("Z")', engine='python')
.groupby('customer_id').amount.sum().reset_index().query('amount >= amount.mean()').head(10)
```

　初めにこのように書いたが、これでは`customer_id`がZから始まる会員を除外して計算はできているが、表示する時にもZから始まる会員を除外してしまう。
　解答では以下のようになっている。

```python:p035.py
amount_mean = df_receipt.query('not customer_id.str.startswith("Z")', engine='python').groupby('customer_id').amount.sum().mean()
df_amount_sum = df_receipt.groupby('customer_id').amount.sum().reset_index()
df_amount_sum[df_amount_sum['amount'] >= amount_mean].head(10)
```
　①`customer_id`がZから始まる会員を除外して、`amount`を合計して平均値を求める。
　②`customer_id`でグルーピングし、`amount`の合計をとる。
　③ ②のdfの`amount`が①で求めた`amount_mean`よりも大きいものだけを表示する。

# P-036 `pd.merge()`
>P-036: レシート明細データフレーム（df_receipt）と店舗データフレーム（df_store）を内部結合し、レシート明細データフレームの全項目と店舗データフレームの店舗名（store_name）を10件表示させよ。

```python:p036.py
pd.merge(df_receipt, df_store[['store_cd', 'store_name']], how='inner', on='store_cd')
```

　`pd.merge()`：データフレームを結合する。
　`concat()`との違いは、共通列名で結合するところ。

| パラメータ設定 | 詳細 |
|:-----------|:-----------|
| how="inner"(デフォルト) | 両方のテーブル共通してあるデータのみ残す(内部結合)|
| how="left"|左テーブル(第一引数)のデータは全て残す(左結合)|
| how="right" |右テーブル(第二引数)のデータは全て残す(右結合)|
| how="outer"|片方のテーブルにしかないデータも全て残す(外部結合)|

| パラメータ設定 | 詳細 |
|:-----------|:-----------|
| on="列名"       | 指定列をキーとして結合。指定なしなら全ての共通列名がキーになる |

　`df_receipt`と`df_store`を結合して、__df_receiptの全項目__と__df_storeのstore_name列__を表示したいため、そこに結合のキー列(共通列)となる__store_cd__も合わせ、`merge()`の第二引数を`df_store[['store_cd', 'store_name']]`とした。

# P-037
>P-037: 商品データフレーム（df_product）とカテゴリデータフレーム（df_category）を内部結合し、商品データフレームの全項目とカテゴリデータフレームの小区分名（category_small_name）を10件表示させよ。

```python:p037.py
pd.merge(df_product
         , df_category[['category_small_cd','category_small_name']]
         , how='inner', on='category_small_cd').head(10)
```

　`df_product`と`df_category`の共通列を調べると
　`category_major_cd`, `category_medium_cd`, `category_small_cd`の3つがある。
　`df_category`の中身を見てみると`category_small_cd`によって`category_name`が決まっていることがわかる。
　なので`category_small_cd`をキー列に結合している。

# P-038 `fillna()`
>P-038: 顧客データフレーム（df_customer）とレシート明細データフレーム（df_receipt）から、各顧客ごとの売上金額合計を求めよ。ただし、買い物の実績がない顧客については売上金額を0として表示させること。また、顧客は性別コード（gender_cd）が女性（1）であるものを対象とし、非会員（顧客IDが'Z'から始まるもの）は除外すること。なお、結果は10件だけ表示させれば良い。

```python:p038.py
df_amount_sum = df_receipt.groupby('customer_id').amount.sum().reset_index()
df_tmp = df_customer.query('gender_cd == "1" & not(customer_id.str.startswith("Z"))', engine='python')
pd.merge(df_tmp['customer_id'], df_amount_sum, how='left', on='customer_id').fillna(0).head(10)
```
　`fillna()`：欠損値を穴埋めする。
　引数に`{'列名１':'値１', '列名2':'値2', }`のように渡すと列ごとに違う値で欠損値を埋められる。
　また、`df.fillna(df.mean())`などと書くと平均値で埋めることもできる。(平均値以外でも可能)

# P-039 `duplicated()`
>P-039: レシート明細データフレーム（df_receipt）から売上日数の多い顧客の上位20件と、売上金額合計の多い顧客の上位20件を抽出し、完全外部結合せよ。ただし、非会員（顧客IDが'Z'から始まるもの）は除外すること。

```python:p039.py
df_sales_ymd_rank = df_receipt[~df_receipt.duplicated(subset=['customer_id', 'sales_ymd'])]
df_sales_ymd_rank = df_sales_ymd_rank.query('not customer_id.str.startswith("Z")', engine='python').groupby('customer_id').sales_ymd.count().reset_index().sort_values('sales_ymd', ascending=False).head(20)
df_amount_rank = df_receipt.query('not customer_id.str.startswith("Z")', engine='python').groupby('customer_id').amount.sum().reset_index().sort_values('amount', ascending=False).head(20)
pd.merge(df_amount_rank, df_sales_ymd_rank, how='outer', on='customer_id')
```
　`duplicated()`：データフレームから重複した要素を含む行を検出・抽出。(pd.Series型)
　`subset='列名'`で判定する列を指定できる。(重複した行がTrue、それ以外はFalse)
　複数行を指定する場合はリストで渡す。(完全一致のみTrue)

　`df_receipt`で一つのcustomer_idについて見てみると、__同じ日付のデータが複数ある__のが見られる。(下の写真は`customer_id='CS017415000097'`のデータをsales_idについてソートしたもの)
![スクリーンショット 2021-08-04 23.23.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1751395/da4ff557-57a8-d71d-ec60-c4094de0911e.png)
　これは同じ日付に複数の商品を購入したからだと思われる。
　重複していない行(日数、sales_ymd)だけを計算したいため、否定`~`で反転してからカウントを取っている。