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

# 1. はじめに
前回に引き続き、データサイエンス100本ノックの解説を行う。
[データサイエンス100本ノック解説(P001~020)] (https://qiita.com/ProgramWataru/items/42ff579a3cdb4f0ad158)
[データサイエンス100本ノック解説(P021~040)](https://qiita.com/ProgramWataru/items/0d3a2235eabdc09f3891)
[データサイエンス100本ノック解説(P041~060)](https://qiita.com/ProgramWataru/items/4d0a1e9bebac86f52192)

導入については[こちらの記事](https://qiita.com/karaage0703/items/1b18b1f4ab65d35afb5f)を参考に進めてください(※ MacでDockerを扱います)

基本的には解答の解説ですが別解についても記述しています。
※徐々に難易度が上がってきています。

#2. 解説編

>P-061: レシート明細データフレーム（df_receipt）の売上金額（amount）を顧客ID（customer_id）ごとに合計し、合計した売上金額を常用対数化（底=10）して顧客ID、売上金額合計とともに表示せよ。ただし、顧客IDが"Z"から始まるのものは非会員を表すため、除外して計算すること。結果は10件表示させれば良い。

```python:P-061
# 顧客IDが"Z"から始まるのものを除く
# レシート明細データフレーム（df_receipt）の売上金額（amount）を顧客ID（customer_id）ごとに合計
df_sales_amount = df_receipt.query('not customer_id.str.startswith("Z")',
    engine='python').groupby('customer_id').amount.sum().reset_index()

# 売上金額（amount）を常用対数化（底=10）する。
# applyで適用するmath.log10(x+1)た常用対数化+1は必要。
df_sales_amount['amount_log10'] = df_amount_sum['amount'].apply(lambda x: math.log10(x+1))
df_sales_amount.head(10)

# (別解) np.log10を用いている
df_sales_amount = df_receipt.query('not customer_id.str.startswith("Z")', engine='python'). \
    groupby('customer_id').agg({'amount':'sum'}).reset_index()
df_sales_amount['amount_log10'] = np.log10(df_sales_amount['amount'] + 1)
df_sales_amount.head(10)
```

参考: [Pythonで指数関数・対数関数を計算（exp, log, log10, log2）](https://note.nkmk.me/python-math-exp-log/)

>P-062: レシート明細データフレーム（df_receipt）の売上金額（amount）を顧客ID（customer_id）ごとに合計し、合計した売上金額を自然対数化(底=e）して顧客ID、売上金額合計とともに表示せよ。ただし、顧客IDが"Z"から始まるのものは非会員を表すため、除外して計算すること。結果は10件表示させれば良い。

```python:P-062
# 顧客IDが"Z"から始まるのものを除く
# レシート明細データフレーム（df_receipt）の売上金額（amount）を顧客ID（customer_id）ごとに合計
df_sales_amount = df_receipt.query('not customer_id.str.startswith("Z")', engine='python') \
    .groupby('customer_id').amount.sum().reset_index()

# 売上金額（amount）を自然対数化(底=e）する（＋１を忘れない)
df_sales_amount['amount_log'] = np.log(df_sales_amount['amount'] + 1)
df_sales_amount.head()

# (別解)
df_sales_amount = df_receipt.query('not customer_id.str.startswith("Z")', engine='python'). \
    groupby('customer_id').agg({'amount':'sum'}).reset_index()
df_sales_amount['amount_loge'] = np.log(df_sales_amount['amount'] + 1)
df_sales_amount.head(10)
```

参考: [Pythonで指数関数・対数関数を計算（exp, log, log10, log2）](https://note.nkmk.me/python-math-exp-log/)

>P-063: 商品データフレーム（df_product）の単価（unit_price）と原価（unit_cost）から、各商品の利益額を算出せよ。結果は10件表示させれば良い。

```python:P-063
# 商品データフレーム（df_product）をコピーする。
df_tmp = df_product.copy()
# 単価（unit_price）から原価（unit_cost）を引いた利益額(unit_profit)を表示する
df_tmp['unit_profit'] = df_tmp['unit_price'] - df_tmp['unit_cost']
df_tmp.head(10)
```

>P-064: 商品データフレーム（df_product）の単価（unit_price）と原価（unit_cost）から、各商品の利益率の全体平均を算出せよ。 ただし、単価と原価にはNULLが存在することに注意せよ。

```python:P-64
# 商品データフレーム（df_product）をコピーする。
df_tmp = df_product.copy()
# 単価（unit_price）から原価（unit_cost）を引いた値を単価（unit_price）で割った、利益率(unit_profit_rate)を表示する
df_tmp['unit_profit_rate'] = (df_tmp['unit_price'] - df_tmp['unit_cost']) / df_tmp['unit_price']
# 利益額の平均を算出する(Nullを除く: skipna=True)
df_tmp['unit_profit_rate'].mean(skipna=True)
```

参考: [pandasで、行 or 列内に欠損値がある時に、無視して加算・無視せず加算する方法](https://qiita.com/kyoro1/items/ce89d6d444eec2b26512)

>P-065: 商品データフレーム（df_product）の各商品について、利益率が30%となる新たな単価を求めよ。ただし、1円未満は切り捨てること。そして結果を10件表示させ、利益率がおよそ30％付近であることを確認せよ。ただし、単価（unit_price）と原価（unit_cost）にはNULLが存在することに注意せよ。

```python:P-065
# 商品データフレーム（df_product）をコピーする。
df_tmp = df_product.copy()
# 原価（unit_cost）から利益率が30%となる新たな単価(new_price)を求める(1円未満は切り捨て)
# 原価（unit_cost） x 10/7 (利益率が30%となる)計算式。math.floorはエラー
df_tmp['new_price'] = np.floor(df_tmp['unit_cost'] * 10/7)

# 利益率を確認する
df_tmp['new_price_rate'] = (df_tmp['new_price'] - df_tmp['unit_cost']) / df_tmp['new_price']
# 利益額の平均を算出する(Nullを除く)
df_tmp.head(10)

# (別解)
df_tmp = df_product.copy()
df_tmp['new_price'] = df_tmp['unit_cost'].apply(lambda x: np.floor(x / 0.7))
df_tmp['new_profit_rate'] = (df_tmp['new_price'] - df_tmp['unit_cost']) / df_tmp['new_price']
df_tmp.head(10)
```

参考: [【NumPy】実数→整数（四捨五入・切り捨て・切り上げ）](https://algorithm.joho.info/programming/python/numpy-round-trunc-fllor-ceil-fix/)

>P-066: 商品データフレーム（df_product）の各商品について、利益率が30%となる新たな単価を求めよ。今回は、1円未満を四捨五入すること（0.5については偶数方向の丸めで良い）。そして結果を10件表示させ、利益率がおよそ30％付近であることを確認せよ。ただし、単価（unit_price）と原価（unit_cost）にはNULLが存在することに注意せよ。

```python:P-066
# 商品データフレーム（df_product）をコピーする。
df_tmp = df_product.copy()
# 原価（unit_cost）から利益率が30%となる新たな単価(new_price)を求める(1円未満を四捨五入0.5については偶数方向の丸め)
# 原価（unit_cost） x 10/7 (利益率が30%となる)計算式。
df_tmp['new_price'] = np.round(df_tmp['unit_cost'] * 10/7)

# 利益率を確認する
df_tmp['new_price_rate'] = (df_tmp['new_price'] - df_tmp['unit_cost']) / df_tmp['new_price']
# 利益額の平均を算出する(Nullを除く)
df_tmp.head(10)

# (別解)
# 組み込みのroundはNaNでエラーとなるが、numpy.roundはエラーとならない
df_tmp = df_product.copy()
df_tmp['new_price'] = df_tmp['unit_cost'].apply(lambda x: np.round(x / 0.7))
df_tmp['new_profit_rate'] = (df_tmp['new_price'] - df_tmp['unit_cost']) / df_tmp['new_price']
df_tmp.head(10)
```

参考: [pandasで数値を丸める（四捨五入、偶数への丸め）](https://note.nkmk.me/python-pandas-round-decimal/)

>P-067: 商品データフレーム（df_product）の各商品について、利益率が30%となる新たな単価を求めよ。今回は、1円未満を切り上げること。そして結果を10件表示させ、利益率がおよそ30％付近であることを確認せよ。ただし、単価（unit_price）と原価（unit_cost）にはNULLが存在することに注意せよ。

```python:P-067
# 商品データフレーム（df_product）をコピーする。
df_tmp = df_product.copy()
# 原価（unit_cost）から利益率が30%となる新たな単価(new_price)を求める(1円未満を切り上げ)
# 原価（unit_cost） x 10/7 (利益率が30%となる)計算式。
df_tmp['new_price'] = np.ceil(df_tmp['unit_cost'] * 10/7)

# 利益率を確認する
df_tmp['new_price_rate'] = (df_tmp['new_price'] - df_tmp['unit_cost']) / df_tmp['new_price']
# 利益額の平均を算出する(Nullを除く)
df_tmp.head(10)

# (別解)
# コード例1
# math.ceilはNaNでエラーとなるが、numpy.ceilはエラーとならない
df_tmp = df_product.copy()
df_tmp['new_price'] = df_tmp['unit_cost'].apply(lambda x: np.ceil(x / 0.7))
df_tmp['new_profit_rate'] = (df_tmp['new_price'] - df_tmp['unit_cost']) / df_tmp['new_price']
df_tmp.head(10)
```

参考: [【NumPy】実数→整数（四捨五入・切り捨て・切り上げ）](https://algorithm.joho.info/programming/python/numpy-round-trunc-fllor-ceil-fix/)

>P-068: 商品データフレーム（df_product）の各商品について、消費税率10%の税込み金額を求めよ。 1円未満の端数は切り捨てとし、結果は10件表示すれば良い。ただし、単価（unit_price）にはNULLが存在することに注意せよ。

```python:P-068
# 商品データフレーム（df_product）をコピーする。
df_tmp = df_product.copy()
# 消費税率10%の税込み金額(price_tax)を求める。(単価（unit_price)*1.1)
df_tmp['price_tax'] = np.floor(df_tmp['unit_price'] * 1.1)
df_tmp.head(10)

# (別解)
# math.floorはNaNでエラーとなるが、numpy.floorはエラーとならない
df_tmp = df_product.copy()
df_tmp['price_tax'] = df_tmp['unit_price'].apply(lambda x: np.floor(x * 1.1))
df_tmp.head(10)
```

参考: [pandasで数値を丸める（四捨五入、偶数への丸め）](https://note.nkmk.me/python-pandas-round-decimal/)

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

```python:P-069
# レシート明細データフレーム（df_receipt）と商品データフレーム（df_product）を結合(pd.merge, キーはproduct_cd)
# 顧客毎に全商品の売上金額合計を算出する
df_tmp_1 = pd.merge(df_receipt, df_product, on='product_cd', 
                    how='inner').groupby('customer_id').amount.sum().reset_index()

# カテゴリ大区分（category_major_cd）が"07"（瓶詰缶詰）の売上金額合計
# merge(df1, df2, on='キー', how='inner')で結合
# groupbyで顧客ごとにグルーピング。amount.sum()で売上金額合計
df_tmp_2 = pd.merge(df_receipt, df_product.query('category_major_cd=="07"'), on='product_cd',
                    how='inner').groupby('customer_id').amount.sum().reset_index()

# df_tmp1とdf_tmp2を結合する。
df_tmp_3 = pd.merge(df_tmp_1, df_tmp_2, on='customer_id', how='inner')

# 両者の比率を求める。
df_tmp_3['rate_07'] = df_tmp_3['amount_y'] / df_tmp_3['amount_x']
df_tmp_3.head(10)

# (別解) agg({カラム: 適用する関数})
df_tmp_1 = pd.merge(df_receipt, df_product, 
                    how='inner', on='product_cd').groupby('customer_id').agg({'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').agg({'amount':'sum'}).reset_index()

df_tmp_3 = pd.merge(df_tmp_1, df_tmp_2, how='inner', on='customer_id')
df_tmp_3['rate_07'] = df_tmp_3['amount_y'] / df_tmp_3['amount_x']
df_tmp_3.head(10)
```

参考: [pandas.DataFrameを結合するmerge, join（列・インデックス基準）](https://note.nkmk.me/python-pandas-merge-join/)

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

```python:P-70
# レシート明細データフレーム（df_receipt）と顧客データフレーム（df_customer）を結合する
df_tmp = pd.merge(df_receipt[['sales_ymd', 'customer_id']], 
                  df_customer[['application_date', 'customer_id']], how='inner', on='customer_id')

# 重複部分を削除する
df_tmp = df_tmp.drop_duplicates()

# 会員申込日（application_date）からの売上日（sales_ymd）を引く
# pandas.to_datetime()関数を使うと、日時（日付・時間）を表した文字列の列pandas.Seriesをdatetime64[ns]型に変換できる。
# sales_ymdは数値型のためastype('str')で文字列型へ変更
df_tmp['elapsed_date'] = pd.to_datetime(df_tmp['sales_ymd'].astype('str')) - pd.to_datetime(df_tmp['application_date'])
df_tmp[['sales_ymd', 'application_date', 'elapsed_date']].head(10)
```

参考: [pandas.DataFrame, Seriesの重複した行を抽出・削除](https://note.nkmk.me/python-pandas-duplicated-drop-duplicates/)
参考: [pandasで日付・時間の列を処理（文字列変換、年月日抽出など）](https://note.nkmk.me/python-pandas-datetime-timestamp/)

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

```python:P-071
# pd.mergeでレシート明細データフレーム（df_receipt）と顧客データフレーム（df_customer）を結合(キーはcustomer_id)
df_tmp = pd.merge(df_receipt[['customer_id', 'sales_ymd']], 
                  df_customer[['customer_id', 'application_date']], how='inner', on='customer_id')
# 重複を削除する(drop_duplicates())
df_tmp = df_tmp.drop_duplicates()

# pandas.to_datetime()関数を使うと、日時（日付・時間）を表した文字列の列pandas.Seriesをdatetime64[ns]型に変換できる。
df_tmp['sales_ymd'] = pd.to_datetime(df_tmp['sales_ymd'].astype('str'))
df_tmp['application_date']  = pd.to_datetime(df_tmp['application_date'])

# 月数を計算するrelativedelta(datetime1, datetime2) datetime1 - datetime2
df_tmp['elapsed_date'] = df_tmp[['sales_ymd', 'application_date']].apply(lambda x: 
                                    relativedelta(x[0], x[1]).years * 12 + relativedelta(x[0], x[1]).months, axis=1)

# 顧客ID（customer_id）でソートし、１０件表示する
df_tmp.sort_values('customer_id').head(10)
```

参考: [Python 日付・時刻の比較・計算・その他](http://hxn.blog.jp/archives/9800580.html)

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

```python:P-072
# pd.mergeでレシート明細データフレーム（df_receipt）と顧客データフレーム（df_customer）を結合(キーはcustomer_id)
df_tmp = pd.merge(df_receipt[['customer_id', 'sales_ymd']], 
                  df_customer[['customer_id', 'application_date']], how='inner', on='customer_id')
# 重複を削除する(drop_duplicates())
df_tmp = df_tmp.drop_duplicates()

# pandas.to_datetime()関数を使うと、日時（日付・時間）を表した文字列の列pandas.Seriesをdatetime64[ns]型に変換できる。
df_tmp['sales_ymd'] = pd.to_datetime(df_tmp['sales_ymd'].astype('str'))
df_tmp['application_date']  = pd.to_datetime(df_tmp['application_date'])

# 経過年数を計算する。relativedelta(datetime1, datetime2).yearsで年数差(axis=1で行に沿った処理)
df_tmp['elapsed_date'] = df_tmp[['sales_ymd', 'application_date']].apply(lambda x: 
                                            relativedelta(x[0], x[1]).years, axis=1)
df_tmp.sort_values('customer_id').head(10)
```

参考: [Python 日付・時刻の比較・計算・その他](http://hxn.blog.jp/archives/9800580.html)

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

```python:P-073
# pd.mergeでレシート明細データフレーム（df_receipt）と顧客データフレーム（df_customer）を結合(キーはcustomer_id)
df_tmp = pd.merge(df_receipt[['customer_id', 'sales_ymd']], 
                  df_customer[['customer_id', 'application_date']], how='inner', on='customer_id')
# 重複を削除する(drop_duplicates())
df_tmp = df_tmp.drop_duplicates()

# pandas.to_datetime()関数を使うと、日時（日付・時間）を表した文字列の列pandas.Seriesをdatetime64[ns]型に変換できる。
df_tmp['sales_ymd'] = pd.to_datetime(df_tmp['sales_ymd'].astype('str'))
df_tmp['application_date']  = pd.to_datetime(df_tmp['application_date'])

# エポック秒による経過時間を計算(timestamp()メソッドでdatetimeオブジェクトをUNIX時間に変換)
# 注：タイムスタンプは、ナノ秒を含むunix時間（10 ** 9で除算）
df_tmp['elapsed_date'] = (df_tmp['sales_ymd'].astype(np.int64) / 10**9) - \
        (df_tmp['application_date'].astype(np.int64) / 10 **9)

# 1０件表示する
df_tmp.head(10)
```

```python:reference
# なぜ１０の９乗で除算するのか？
nano_time = pd.to_datetime('1970-01-02')
nano_time.value
# >>>86400000000000
```

エポック時間は1970-01-01からの経過時間なので24時間x60分x60秒=86400秒
unix時間がナノ秒（10の９乗）であることがわかる

参考: [Python 日付・時刻の比較・計算・その他](http://hxn.blog.jp/archives/9800580.html)

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

```python:P-074
# レシート明細データフレーム（df_receipt）の顧客データ(customer_id),売上日（sales_ymd）
df_tmp = df_receipt[['customer_id', 'sales_ymd']]
# 重複を削除する
df_tmp = df_tmp.drop_duplicates()
# datetime64[ns]型に変換
df_tmp['sales_ymd'] = pd.to_datetime(df_tmp['sales_ymd'].astype('str'))

# 月曜のデータを追加する。(relativedelta(days=x.weekday())月曜：０, 火曜：１・・・)
df_tmp['monday'] = df_tmp['sales_ymd'].apply(lambda x: x - relativedelta(days=x.weekday()))

# 売上日（sales_ymd）から月曜日を引いて経過日数を求める
df_tmp['elapsed_weekday'] = df_tmp['sales_ymd'] - df_tmp['monday']
df_tmp.head(10)
```

参考: [pandasのdatetime型の取り扱い（pd.to_datetime、pd.offsets、タイムゾーン）
](http://hxn.blog.jp/archives/9617210.html)

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

```python:P-075
# 顧客データフレーム（df_customer）より抽出する(sample)
# ランダムに1%のデータは引数frac=0.01
df_customer.sample(frac=0.01).head(10)
```

参考: [pandasの行・列をランダムサンプリング（抽出）するsample](https://note.nkmk.me/python-pandas-sample/)

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

```python:P-076
# sklearn.model_selection.train_test_splitを使用した例
# _(訓練データ９０％)、df_tmp(テストデータ10%)とすることでランダムに１０％を抽出する(層化抽出: 引数stratify)
_, df_tmp = train_test_split(df_customer, test_size=0.1, stratify=df_customer['gender_cd'])
# 性別（gender_cd）でグループ分けし、customer_idの件数(count)を集計
df_tmp.groupby('gender_cd').agg({'customer_id' : 'count'})

# (別解)
# sampleメソッドを用いてランダムに抽出(frac=0.1で１０％データを抽出)
# 性別（gender_cd）でグループ分けし、customer_idの件数(count)を集計
df_customer.sample(frac=0.1).groupby('gender_cd').agg({'customer_id' : 'count'})
```

参考: [pandasの行・列をランダムサンプリング（抽出）するsample](https://note.nkmk.me/python-pandas-sample/)

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

```python:P-077
# 顧客IDが"Z"から始まるものを除外する
# レシート明細データフレーム（df_receipt）の売上金額（amount）を顧客単位に合計する
df_sales_amount = df_receipt.query("not customer_id.str.startswith('Z')", 
                                   engine='python').groupby('customer_id').amount.sum().reset_index()

# 正規化・標準化(平均０、標準偏差１)：preprocessing.scale()関数を使う。
df_sales_amount['amount_ss'] = preprocessing.scale(df_sales_amount['amount'])

# 外れ値を抽出する(平均(mean)の ± 3σ(std)以上離れたものを抽出する)
# queryメソッドで条件に合うものを抽出。3σ以上離れたは、絶対値(abs)を用いる
df_sales_amount.query('abs(amount_ss)>=3').head(10)


# (別解)
# 顧客IDが"Z"から始まるものを除外する
# レシート明細データフレーム（df_receipt）の売上金額（amount）を顧客単位に合計する
df_sales_amount = df_receipt.query("not customer_id.str.startswith('Z')", 
                                   engine='python').groupby('customer_id').amount.sum().reset_index()

# 各顧客の売り上げ合計の平均、標準偏差を求める
mean = df_sales_amount.describe().loc['mean']
std = df_sales_amount.describe().loc['std']

# 外れ値を抽出する(平均(mean)の ± 3σ(std)以上離れたものを抽出する)
df_sales_amount[(df_sales_amount['amount'] >= int(mean) + 3*int(std)) | \
                (df_sales_amount['amount'] <= int(mean) - 3*int(std))].head(10)
```

参考: [Pythonで正規化・標準化（リスト、NumPy配列、pandas.DataFrame）](https://note.nkmk.me/python-list-ndarray-dataframe-normalize-standardize/)

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

```python:P-078
# 顧客IDが"Z"から始まるものを除外する
# レシート明細データフレーム（df_receipt）の売上金額（amount）を顧客単位に合計する
df_sales_amount = df_receipt.query("not customer_id.str.startswith('Z')", 
                                   engine='python').groupby('customer_id').amount.sum().reset_index()

# 第一四分位と第三四分位の差であるIQRを求める
pct75 = df_sales_amount['amount'].quantile(0.75)
pct25 = df_sales_amount['amount'].quantile(0.25)
iqr = pct75 - pct25

# 第一四分位数-1.5×IQR(amount_low), 第三四分位数+1.5×IQR(amount_hight)をそれぞれ求める
amount_low = pct25 - (iqr * 1.5)
amount_hight = pct75 + (iqr * 1.5)
# 外れ値を抽出する(@で変数名)
df_sales_amount.query('amount > @amount_hight | amount < @amount_low').head(10)


# (別解) np.percentileを用いた回答
df_sales_amount = df_receipt.query('not customer_id.str.startswith("Z")', engine='python'). \
    groupby('customer_id').agg({'amount':'sum'}).reset_index()

pct75 = np.percentile(df_sales_amount['amount'], q=75)
pct25 = np.percentile(df_sales_amount['amount'], q=25)
iqr = pct75 - pct25
amount_low = pct25 - (iqr * 1.5)
amount_hight = pct75 + (iqr * 1.5)
df_sales_amount.query('amount < @amount_low or @amount_hight < amount').head(10)
```

参考: [pandasで分位数・パーセンタイルを取得するquantile](https://note.nkmk.me/python-pandas-quantile/)

>P-079: 商品データフレーム（df_product）の各項目に対し、欠損数を確認せよ。

```python:P-079
# isnull()で要素ごとに欠損値か判定
df_product.isnull().sum()
```

参考: [pandasで欠損値NaNが含まれているか判定、個数をカウント](https://note.nkmk.me/python-pandas-nan-judge-count/)

>P-080: 商品データフレーム（df_product）のいずれかの項目に欠損が発生しているレコードを全て削除した新たなdf_product_1を作成せよ。なお、削除前後の件数を表示させ、前設問で確認した件数だけ減少していることも確認すること。

```python:P-080
# 商品データフレーム（df_product）をコピーする
df_product_1 = df_product.copy()
# 欠損値削除前の件数を確認する
print('削除前: ', len(df_product_1))

# 欠損値を除外（削除）するにはdropna()メソッド
df_product_1.dropna(inplace=True)
print('削除後: ', len(df_product_1))
```

参考: [pandasで欠損値NaNを除外（削除）・置換（穴埋め）・抽出](https://note.nkmk.me/python-pandas-nan-dropna-fillna/)

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

# 4. 所感
難易度が問題によってばらつく。難しいものは難しい。