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

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

+ 欠損値の確認　`isnull()`
+ 欠損値を持つ行・列の削除　`dropna()`
+ 欠損値を含むデータの平均値を計算　`np.nanmean()`
+ 欠損値を含むデータの中央値を計算　`np.nanmedian()`
+ 欠損値に対しTrueを返す　`isnan()`
+ 関数を適用しデータフレームを整形　`transform()`

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

```python:p079.py
df_product.isnull().sum()
```

　`isnull()`：各要素に対して判定を行い、欠損値NaNであれば`True`、欠損値でなければ`False`とする。元のオブジェクトと同じサイズ（行数・列数）のオブジェクトを返す。

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

```python:p080.py
df_product_1 = df_product.dropna()
print(df_product.count())
print(df_product_1.count())
```

　`dropna()`：欠損値を含む列、または行を削除する。

# P-081 `np.nanmean()`
>P-081: 単価（unit_price）と原価（unit_cost）の欠損値について、それぞれの平均値で補完した新たなdf_product_2を作成せよ。なお、平均値について1円未満は四捨五入とし、0.5については偶数寄せでかまわない。補完実施後、各項目について欠損が生じていないことも確認すること。

```python:p081.py
df_product_2 = df_product.fillna({'unit_price':np.round(np.nanmean(df_product['unit_price'])),
                                  'unit_cost':np.round(np.nanmean(df_product['unit_cost']))})
df_product_2.isnull().sum()
```

　`np.nanmean()`：欠損値を除いた平均値を計算。
　（`df_product['unit_cost'].mean()`でもデフォルトで`NaN`を無視するため計算はできる。）
　ndarray型の場合、一つでも欠損値があれば演算できないため、`nanmean()`と同様に`nansum()`や`nanmax()`などを使う。

# P-082 `np.nanmedian()`
>P-082: 単価（unit_price）と原価（unit_cost）の欠損値について、それぞれの中央値で補完した新たなdf_product_3を作成せよ。なお、中央値について1円未満は四捨五入とし、0.5については偶数寄せでかまわない。補完実施後、各項目について欠損が生じていないことも確認すること。

```python:p082.py
df_product_3 = df_product.fillna({'unit_price':np.round(np.nanmedian(df_product['unit_price'])),
                                  'unit_cost':np.round(np.nanmedian(df_product['unit_cost']))})
df_product_3.isnull().sum()
```

　`np.nanmedian()`：欠損値を除いた中央値を計算。
　データが欠損値を含むため、`np.median()`では`nan`となるため、`nanmedian()`を使う。
　（データフレームの関数を使って`df_product['unit_price'].median()`でも同じ結果を得られる。）

# P-083 `isnan()`、`transform()`
>P-083: 単価（unit_price）と原価（unit_cost）の欠損値について、各商品の小区分（category_small_cd）ごとに算出した中央値で補完した新たなdf_product_4を作成せよ。なお、中央値について1円未満は四捨五入とし、0.5については偶数寄せでかまわない。補完実施後、各項目について欠損が生じていないことも確認すること。

```pythom:p083.py
# category_small_cd、unit_priceの中央値、unit_costの中央値をカラムに持つdf_tmpを作成
df_tmp = df_product.groupby('category_small_cd').agg({'unit_price':'median', 'unit_cost':'median'}).reset_index()
df_tmp.columns = ['category_small_cd', 'median_price', 'median_cost']
df_product_4 = pd.merge(df_product, df_tmp, how='right', on='category_small_cd')

df_product_4['unit_price'] = df_product_4[['unit_price', 'median_price']].apply(lambda x:
                                        np.round(x[1]) if np.isnan(x[0]) else x[0], axis=1)
df_product_4['unit_cost'] = df_product_4[['unit_cost', 'median_cost']].apply(lambda x:
                                        np.round(x[1]) if np.isnan(x[0]) else x[0], axis=1)
df_product_4.isnull().sum()
```

　`np.isnan()`：欠損値に対し`True`を返す。

　少しややこしく書いたが、無名関数`lambda`と`if`文を使って
　`unit_price`と`unit_cost`が`NaN`のデータには中央値を代入し、それ以外のデータはそのままにする処理を行っている。

　また、`for`文を使って下のようにも書ける。

```python
df_product_4 = df_product.copy()

for x in ['unit_price', 'unit_cost']:
    df_product_4[x] = df_product_4[x].fillna(df_product_4.groupby('category_small_cd')[x]
                                             .transform('median').round())
df_product_4.isnull().sum()
```

　`transform()`：引数に関数を渡し、その関数をデータフレームに用い整形する。
　`apply()`と使い方は似てるが、データフレームが圧縮されないのが違うところ。
　（下のページを参考にしました。）

　参考：[Pandas の transform と apply の基本的な違い](https://qiita.com/greenteabiscuit/items/132e0f9b1479926e07e0)

# P-084
>P-084: 顧客データフレーム（df_customer）の全顧客に対し、全期間の売上金額に占める2019年売上金額の割合を計算せよ。ただし、販売実績のない場合は0として扱うこと。そして計算した割合が0超のものを抽出せよ。 結果は10件表示させれば良い。また、作成したデータにNAやNANが存在しないことを確認せよ。

```python:p084.py
# df_customerの全顧客に対し、2019年の売り上げ金額の合計を求めたデータフレームを作成
# 全顧客に対するデータフレームを作成するため、結合するときdf_customerを軸にしている
df_tmp_1 = df_receipt.query('20190101 <= sales_ymd <= 20191231')
df_tmp_1 = pd.merge(df_customer['customer_id'], df_tmp_1[['customer_id', 'amount']], how='left', on='customer_id')
df_tmp_1 = df_tmp_1.groupby('customer_id').sum().reset_index().rename(columns={'amount':'amount_2019'})

# df_customerの全顧客に対し、全期間の売り上げ金額の合計を求めたデータフレームを作成
df_tmp_2 = pd.merge(df_customer['customer_id'], df_receipt[['customer_id', 'amount']], how='left', on='customer_id')
df_tmp_2 = df_tmp_2.groupby('customer_id').sum().reset_index()

# df_tmp_1とdf_tmp_2を結合し、全期間に対し2019年の占める割合を求める
# lambda関数を使い、amountが0の場合は0、それ以外の場合は割合を計算するようにする
df_tmp = pd.merge(df_tmp_1, df_tmp_2, how='inner', on='customer_id')
df_tmp['amount_rate'] = df_tmp[['amount_2019', 'amount']].apply(lambda x: 0 if x[1] == 0 else x[0]/x[1], axis=1)
df_tmp.query('not amount_rate==0').head(10)
```

　`df_tmp.isnull().sum()`で欠損値がないことを確認できる。

# P-085
>P-085: 顧客データフレーム（df_customer）の全顧客に対し、郵便番号（postal_cd）を用いて経度緯度変換用データフレーム（df_geocode）を紐付け、新たなdf_customer_1を作成せよ。ただし、複数紐づく場合は経度（longitude）、緯度（latitude）それぞれ平均を算出すること。

```python:p085.py
# df_customerと、df_geocodeのpostal_cd列を軸にして結合
# customer_idでグルーピングし、経度と緯度の平均を計算
df_tmp = pd.merge(df_customer[['customer_id', 'postal_cd']], df_geocode[['postal_cd', 'longitude', 'latitude']],
                    how='inner', on='postal_cd').groupby('customer_id').agg({'longitude':'mean' ,'latitude':'mean'}).reset_index()
df_tmp.columns = ['customer_id', 'mean_longitude', 'mean_latitude']

df_customer_1 = pd.merge(df_customer, df_tmp, how='inner', on='customer_id')
df_customer_1.head(5)
```

# P-086
>P-086: 前設問で作成した緯度経度つき顧客データフレーム（df_customer_1）に対し、申込み店舗コード（application_store_cd）をキーに店舗データフレーム（df_store）と結合せよ。そして申込み店舗の緯度（latitude）・経度情報（longitude)と顧客の緯度・経度を用いて距離（km）を求め、顧客ID（customer_id）、顧客住所（address）、店舗住所（address）とともに表示せよ。計算式は簡易式で良いものとするが、その他精度の高い方式を利用したライブラリを利用してもかまわない。結果は10件表示すれば良い。

```python:p086.py
# 緯度と経度から距離を計算する関数
def get_distance(lat1, lat2, lon1, lon2):
    # np.radians()を使い、緯度と経度をデグリーからラジアンに変換
    lat1, lat2, lon1, lon2 = np.radians([lat1, lat2, lon1, lon2])

    distance = 6371 * np.arccos(np.sin(lat1)*np.sin(lat2) + np.cos(lat1)*np.cos(lat2)*np.cos(lon1-lon2))
    return distance

# 結合する前にdf_customer_1の列名を変更
df_tmp = df_customer_1.copy().rename(columns={'application_store_cd':'store_cd', 'address':'customer_address'})
# df_storeのaddress列をstore_addressに変更し、df_customer_1と結合
df_tmp = pd.merge(df_tmp[['customer_id', 'customer_address', 'store_cd', 'mean_longitude', 'mean_latitude']],
                  df_store[['store_cd', 'address', 'longitude', 'latitude']].rename(columns={'address':'store_address'}), how='inner', on='store_cd')

# 店舗と顧客との距離を上で定義した関数を用いて計算　
df_tmp['distance'] = df_tmp[['mean_latitude', 'latitude', 'mean_longitude', 'longitude']].apply(lambda x: get_distance(x[0],x[1],x[2],x[3]), axis=1)
# 指定された列のみ表示
df_tmp[['customer_id', 'customer_address', 'store_address', 'distance']].head(10)
```

　`np.radians()`、`np.sin()`、'np.cos()`、`np.arccos()`など、
　度をラジアンに変換したり、三角関数・逆三角関数を計算する関数がたくさん出ました。

　参照：[NumPyで三角関数（sin, cos, tan, arcsin, arccos, arctan）](https://note.nkmk.me/python-numpy-sin-con-tan/)

# P-087
>P-087: 顧客データフレーム（df_customer）では、異なる店舗での申込みなどにより同一顧客が複数登録されている。名前（customer_name）と郵便番号（postal_cd）が同じ顧客は同一顧客とみなし、1顧客1レコードとなるように名寄せした名寄顧客データフレーム（df_customer_u）を作成せよ。ただし、同一顧客に対しては売上金額合計が最も高いものを残すものとし、売上金額合計が同一もしくは売上実績の無い顧客については顧客ID（customer_id）の番号が小さいものを残すこととする。

```python:p087.py
# customer_idと、顧客ごとのamountの合計を列にもつデータフレームを作成する
df_tmp = df_receipt.groupby('customer_id').amount.sum().reset_index()

# データフレームを結合し、欠損値を0埋めし、amountは降順、customer_idは昇順に並び替える
df_customer_u = pd.merge(df_customer, df_tmp, how='left', on='customer_id').fillna(0).sort_values(['amount','customer_id'], ascending=[False, True])
# inplace=Trueでデータフレームそのものから重複した行を削除
# (デフォルトでkeep='first'であるため省略)
df_customer_u.drop_duplicates(subset=['customer_name','postal_cd'], inplace=True)
```

# P-088
>P-088: 前設問で作成したデータを元に、顧客データフレームに統合名寄IDを付与したデータフレーム（df_customer_n）を作成せよ。ただし、統合名寄IDは以下の仕様で付与するものとする。
・重複していない顧客：顧客ID（customer_id）を設定
・重複している顧客：前設問で抽出したレコードの顧客IDを設定

```python:p088.py
df_customer_n = pd.merge(df_customer, df_customer_u[['customer_name', 'postal_cd', 'customer_id']],
                        how='inner', on =['customer_name', 'postal_cd'])
df_customer_n = df_customer_n.rename(columns={'customer_id_x':'customer_id', 'customer_id_y':'integration_id'})
```