データサイエンス100本ノック解説(P021~040)

#1. はじめに
　[前回](https://qiita.com/ProgramWataru/items/42ff579a3cdb4f0ad158)に引き続き、データサイエンス100本ノックの解説を行う。
導入については[こちらの記事](https://qiita.com/karaage0703/items/1b18b1f4ab65d35afb5f)を参考に進めてください(※ MacでDockerを扱います)

基本的には解答の解説ですが別解についても記述しています。

#2. 解説編

>P-021: レシート明細データフレーム（df_receipt）に対し、件数をカウントせよ。

```python:P-021
# 件数はlen()メソッドを用いる。
len(df_receipt)
```

>P-022: レシート明細データフレーム（df_receipt）の顧客ID（customer_id）に対し、ユニーク件数をカウントせよ。

```python:P-022
# ユニーク件数はunique()メソッドを使用する。
# df_receipt['customer_id'].unique() >>> ユニークな要素の値のリストをNumPy配列ndarrayで返す
len(df_receipt['customer_id'].unique())
```

参考: https://note.nkmk.me/python-pandas-value-counts/

>P-023: レシート明細データフレーム（df_receipt）に対し、店舗コード（store_cd）ごとに売上金額（amount）と売上数量（quantity）を合計せよ。

```python:P-023
# groupbyメソッドで'store_cd'ごとにグルーピング
# agg()でデータを集計。辞書型で売上金額（amount）と売上数量（quantity）を合計（sum）
# reset_index()でインデックスを振り直す
df_receipt.groupby('store_cd').agg({'amount': 'sum', 'quantity': 'sum'}).reset_index()

#(別解)
df_receipt[['amount', 'quantity', 'store_cd']].groupby('store_cd', as_index=False).sum()
```

参考(groupby): https://note.nkmk.me/python-pandas-groupby-statistics/
参考(agg): https://note.nkmk.me/python-pandas-agg-aggregate/

>P-024: レシート明細データフレーム（df_receipt）に対し、顧客ID（customer_id）ごとに最も新しい売上日（sales_ymd）を求め、10件表示せよ。

```python:P-024
# groupbyで顧客ID（customer_id）ごとにグループ分け。
# 売上日（sales_ymd）の最も新しい(max()で取得)
df_receipt.groupby('customer_id').sales_ymd.max().reset_index().head(10)

# (別解)
# locで顧客ID（customer_id）でグループ分けした売上日（sales_ymd）の最も新しい(idxmax()で取得)
df_receipt[['customer_id', 'sales_ymd']].loc[df_receipt.groupby('customer_id').sales_ymd.idxmax()].head(10)
```
参考(groupby): https://note.nkmk.me/python-pandas-groupby-statistics/

>P-025: レシート明細データフレーム（df_receipt）に対し、顧客ID（customer_id）ごとに最も古い売上日（sales_ymd）を求め、10件表示せよ。

```python:P-025
# P-023を参考。最も古い売上日はagg({'sales_ymd': min})で表現できる
df_receipt.groupby('customer_id').agg({'sales_ymd': min}).reset_index().head(10)

# (別解)
# locで顧客ID（customer_id）でグループ分けした売上日（sales_ymd）の最も古い(idxmin()で取得)
df_receipt[['customer_id', 'sales_ymd']].loc[df_receipt.groupby('customer_id').sales_ymd.idxmin()].head(10)
```

>P-026: レシート明細データフレーム（df_receipt）に対し、顧客ID（customer_id）ごとに最も新しい売上日（sales_ymd）と古い売上日を求め、両者が異なるデータを10件表示せよ。

```python:P-026
# 顧客ID（customer_id）ごとに売上日（sales_ymd）の新しい売上日(max)と古い売上日(min)のデータフレームを作成
df_tmp = df_receipt.groupby('customer_id').agg({'sales_ymd':['max','min']}).reset_index()

# カラムを変更(詳細は下記にて説明)
df_tmp.columns = ["_".join(pair) for pair in df_tmp.columns]

# 両者が異なるデータをquery()にて探す。
df_tmp.query('sales_ymd_max != sales_ymd_min').head(10)
```

df.columnsは、MultiIndex([('customer_id',    ''),(  'sales_ymd', 'max'),( 'sales_ymd', 'min')],)となるのでfor文で一つずつ取り出し、"_".join()にてカッコの中の文字を結合する。
![スクリーンショット 2020-08-21 18.04.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621150/31d11499-2db5-7b28-5e55-78a9779da526.png)

>P-027: レシート明細データフレーム（df_receipt）に対し、店舗コード（store_cd）ごとに売上金額（amount）の平均を計算し、降順でTOP5を表示せよ。

```python:P-027
# レシート明細データフレーム（df_receipt）を店舗コード（store_cd）ごとにグループ分け。
# agg({'amount':'mean'})にて売上金額（amount）の平均を計算
# reset_index()でインデックスを振り直し、sort_values('amount', ascending=False)で売上金額（amount）でソートし降順
df_receipt.groupby('store_cd').agg({'amount':'mean'}) \
      .reset_index().sort_values('amount', ascending=False).head(5)
```

参考(groupby): https://note.nkmk.me/python-pandas-groupby-statistics/
参考(agg): https://note.nkmk.me/python-pandas-agg-aggregate/

>P-028: レシート明細データフレーム（df_receipt）に対し、店舗コード（store_cd）ごとに売上金額（amount）の中央値を計算し、降順でTOP5を表示せよ。

```python:P-028
# P-027を参考
# 中央値はmedian
df_receipt.groupby('store_cd').agg({'amount':'median'}).reset_index().sort_values('amount', ascending=False).head(5)
```

参考(median): https://note.nkmk.me/python-statistics-mean-median-mode-var-stdev/

>P-029: レシート明細データフレーム（df_receipt）に対し、店舗コード（store_cd）ごとに商品コード（product_cd）の最頻値を求めよ。

```python:P-029
# 前半部分はP-027を参考
# 商品コード（product_cd）に関数(ラムダ式)を適用する。
df_receipt.groupby('store_cd').product_cd.apply(lambda x: x.mode()).reset_index()
# df.groupby('grouping_content').最頻を求めたい対象.apply(lambda x: x.mode())

# (誤回答)
df_receipt.groupby('store_cd').agg({'product_cd':'mode'}).reset_index()
# >>>AttributeError: 'SeriesGroupBy' object has no attribute 'mode'
```

参考(lambda式): https://note.nkmk.me/python-lambda-usage/

>P-030: レシート明細データフレーム（df_receipt）に対し、店舗コード（store_cd）ごとに売上金額（amount）の標本分散を計算し、降順でTOP5を表示せよ。

```python:P-030
# 店舗コード（store_cd）の売上金額（amount）の標本分散(var(ddof=0))
# reset_indexでインデックスを振り直し、ソートする(sort_values)、降順(ascending=False)
df_receipt.groupby('store_cd').amount.var(ddof=0).reset_index().sort_values('amount', ascending=False).head(5)
```

参考: https://deepage.net/features/numpy-var.html
参考(標本分散と不偏分散): https://bellcurve.jp/statistics/course/8614.html

>P-031: レシート明細データフレーム（df_receipt）に対し、店舗コード（store_cd）ごとに売上金額（amount）の標本標準偏差を計算し、降順でTOP5を表示せよ。

```python:P-031
# 店舗コード（store_cd）の売上金額（amount）の標本標準偏差(std(ddof=0))
# reset_indexでインデックスを振り直し、ソートする(sort_values)、降順(ascending=False)
df_receipt.groupby('store_cd').amount.std(ddof=0).reset_index().sort_values('amount', ascending=False).head()
```

参考: https://deepage.net/features/numpy-var.html
参考(標本分散と不偏分散): https://bellcurve.jp/statistics/course/8614.html

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

```python:P-032
# 売上金額（amount）をパーセンタイル値(quantile)
# np.arange(5)/4 >>> [0, 0.25, 0.5, 0.75, 1]
df_receipt.amount.quantile(q=np.arange(5)/4)

# (別解)
np.percentile(df_receipt['amount'], q=[25, 50, 75,100])
```

参考(quantile): https://note.nkmk.me/python-pandas-quantile/

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

```python:P-033
# 店舗コード（store_cd）ごとにグループ分け。
# 売上金額（amount）の平均(mean)を算出し、reset_index()でインデックスを振り直す
# query()メソッドで売上金額（amount）の300以上
df_receipt.groupby('store_cd').amount.mean().reset_index().query('amount >= 330')

# (別解)
df_receipt.groupby('store_cd').amount.mean()[df_receipt.groupby('store_cd').amount.mean() >= 300]
```

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

```python:P-034
# queryを使わない書き方
# ~はNot。、顧客ID（customer_id）がZから始まる(str.startswith("Z"))
# 顧客ID（customer_id）ごとに売上金額（amount）を合計して全顧客の平均
df_receipt[~df_receipt['customer_id'].str.startswith("Z")].groupby('customer_id').amount.sum().mean()

# queryを使う書き方
df_receipt.query('not customer_id.str.startswith("Z")', engine='python').groupby('customer_id').amount.sum().mean()
```

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

```python:P-035
# 顧客IDが"Z"を除外。'customer_id'ごとにグループを分ける。（2547.742234529256）全顧客の平均
amount_mean = df_receipt[~df_receipt['customer_id'].str.startswith("Z")].groupby('customer_id').amount.sum().mean()

# 顧客ID（customer_id）ごとに売上金額（amount）を合計する（データフレーム型）
df_amount_sum = df_receipt.groupby('customer_id').amount.sum().reset_index()

# amount_mean(全顧客の平均)以上の１０件を表示
df_amount_sum[df_amount_sum['amount'] >= amount_mean].head(10)
```

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

```python:P-036
# merge(A(df), B(df), how='inner'(内部結合), on='共通する列')
pd.merge(df_receipt, df_store[['store_cd','store_name']], how='inner', on='store_cd').head(10)
```

参考: https://note.nkmk.me/python-pandas-merge-join/

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

```python:P-037
# merge(A(df), B(df), how='inner'(内部結合), on='共通する列')
pd.merge(df_product
         , df_category[['category_small_cd','category_small_name']]
         , how='inner', on='category_small_cd').head(10)
```

参考: https://note.nkmk.me/python-pandas-merge-join/

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

```python:P-038
# 各顧客ごとの売上金額合計。
# 顧客IDごとにグループ分けし、売上金額（amount）の合計(sum)
df_amount_sum = df_receipt.groupby('customer_id').amount.sum().reset_index()

# 顧客は性別コード（gender_cd）が女性（1）であるものを対象とし、非会員（顧客IDが'Z'から始まるもの）は除外
df_tmp = df_customer.query('gender_cd == "1" and not customer_id.str.startswith("Z")', engine='python')

# merge(A(df), B(df), how='inner'(内部結合), on='共通する列')
# fillna(0)で買い物の実績がない顧客については売上金額を0
pd.merge(df_tmp['customer_id'], df_amount_sum, how='left', on='customer_id').fillna(0).head(10)
```

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

```python:P-039
# 顧客ID('customer_id')でグループ分けし、売上金額（amount）の合計(sum)
df_sum = df_receipt.groupby('customer_id').amount.sum().reset_index()
# customer_id.str.startswith("Z")(顧客IDがZで始まるものを除く)
df_sum = df_sum.query('not customer_id.str.startswith("Z")', engine='python')
# 売上金額（amount）の合計をソートし、上位２０件を抽出
df_sum = df_sum.sort_values('amount', ascending=False).head(20)

# 売り上げ日数(sales_ymd)を重複を抽出する。
df_cnt = df_receipt[~df_receipt.duplicated(subset=['customer_id', 'sales_ymd'])]
# customer_id.str.startswith("Z")(顧客IDがZで始まるものを除く)
df_cnt = df_cnt.query('not customer_id.str.startswith("Z")', engine='python')
# 顧客ID('customer_id')でグループ分けし、売り上げ日数(sales_ymd)の件数(count)
df_cnt = df_cnt.groupby('customer_id').sales_ymd.count().reset_index()
# 売り上げ日数(sales_ymd)の件数でソートし、降順(ascending=False)で並べ替え上位２０件を抽出。
df_cnt = df_cnt.sort_values('sales_ymd', ascending=False).head(20)

# mergeで外部結合する
pd.merge(df_sum, df_cnt, how='outer', on='customer_id')
```

参考(duplicate): https://note.nkmk.me/python-pandas-duplicated-drop-duplicates/
参考（外部結合）: https://note.nkmk.me/python-pandas-merge-join/

>P-040: 全ての店舗と全ての商品を組み合わせると何件のデータとなるか調査したい。店舗（df_store）と商品（df_product）を直積した件数を計算せよ。

```python:P-040
# 店舗（df_store）のコピーを作成
df_store_tmp = df_store.copy()
# 商品（df_product）のコピーを作成
df_product_tmp = df_product.copy()

# 結合するためのキー(列)が必要なのでそれぞれ列を追加する。
df_store_tmp['key'] = 0
df_product_tmp['key'] = 0

# 外部結合し、lenメソッドで件数を確認する
len(pd.merge(df_store_tmp, df_product_tmp, on='key', how='outer'))
```

参考:　https://note.nkmk.me/python-pandas-merge-join/

# 3. 参考文献
[データサイエンス100本ノック](https://github.com/The-Japan-DataScientist-Society/100knocks-preprocess)
[Macでデータサイエンス100本ノックを動かす方法](https://qiita.com/karaage0703/items/1b18b1f4ab65d35afb5f)